/* * @fileoverview Type expression parser. * @author Yusuke Suzuki <utatane.tea@gmail.com> * @author Dan Tao <daniel.tao@gmail.com> * @author Andrew Eisenberg <andrew@eisenberg.as> */ // "typed", the Type Expression Parser for doctrine. (function () { 'use strict'; var Syntax, Token, source, length, index, previous, token, value, esutils, utility, rangeOffset, addRange; esutils = require('esutils'); utility = require('./utility'); Syntax = { NullableLiteral: 'NullableLiteral', AllLiteral: 'AllLiteral', NullLiteral: 'NullLiteral', UndefinedLiteral: 'UndefinedLiteral', VoidLiteral: 'VoidLiteral', UnionType: 'UnionType', ArrayType: 'ArrayType', RecordType: 'RecordType', FieldType: 'FieldType', FunctionType: 'FunctionType', ParameterType: 'ParameterType', RestType: 'RestType', NonNullableType: 'NonNullableType', OptionalType: 'OptionalType', NullableType: 'NullableType', NameExpression: 'NameExpression', TypeApplication: 'TypeApplication', StringLiteralType: 'StringLiteralType', NumericLiteralType: 'NumericLiteralType', BooleanLiteralType: 'BooleanLiteralType' }; Token = { ILLEGAL: 0, // ILLEGAL DOT_LT: 1, // .< REST: 2, // ... LT: 3, // < GT: 4, // > LPAREN: 5, // ( RPAREN: 6, // ) LBRACE: 7, // { RBRACE: 8, // } LBRACK: 9, // [ RBRACK: 10, // ] COMMA: 11, // , COLON: 12, // : STAR: 13, // * PIPE: 14, // | QUESTION: 15, // ? BANG: 16, // ! EQUAL: 17, // = NAME: 18, // name token STRING: 19, // string NUMBER: 20, // number EOF: 21 }; function isTypeName(ch) { return '><(){}[],:*|?!='.indexOf(String.fromCharCode(ch)) === -1 && !esutils.code.isWhiteSpace(ch) && !esutils.code.isLineTerminator(ch); } function Context(previous, index, token, value) { this._previous = previous; this._index = index; this._token = token; this._value = value; } Context.prototype.restore = function () { previous = this._previous; index = this._index; token = this._token; value = this._value; }; Context.save = function () { return new Context(previous, index, token, value); }; function maybeAddRange(node, range) { if (addRange) { node.range = [range[0] + rangeOffset, range[1] + rangeOffset]; } return node; } function advance() { var ch = source.charAt(index); index += 1; return ch; } function scanHexEscape(prefix) { var i, len, ch, code = 0; len = (prefix === 'u') ? 4 : 2; for (i = 0; i < len; ++i) { if (index < length && esutils.code.isHexDigit(source.charCodeAt(index))) { ch = advance(); code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); } else { return ''; } } return String.fromCharCode(code); } function scanString() { var str = '', quote, ch, code, unescaped, restore; //TODO review removal octal = false quote = source.charAt(index); ++index; while (index < length) { ch = advance(); if (ch === quote) { quote = ''; break; } else if (ch === '\\') { ch = advance(); if (!esutils.code.isLineTerminator(ch.charCodeAt(0))) { switch (ch) { case 'n': str += '\n'; break; case 'r': str += '\r'; break; case 't': str += '\t'; break; case 'u': case 'x': restore = index; unescaped = scanHexEscape(ch); if (unescaped) { str += unescaped; } else { index = restore; str += ch; } break; case 'b': str += '\b'; break; case 'f': str += '\f'; break; case 'v': str += '\v'; break; default: if (esutils.code.isOctalDigit(ch.charCodeAt(0))) { code = '01234567'.indexOf(ch); // \0 is not octal escape sequence // Deprecating unused code. TODO review removal //if (code !== 0) { // octal = true; //} if (index < length && esutils.code.isOctalDigit(source.charCodeAt(index))) { //TODO Review Removal octal = true; code = code * 8 + '01234567'.indexOf(advance()); // 3 digits are only allowed when string starts // with 0, 1, 2, 3 if ('0123'.indexOf(ch) >= 0 && index < length && esutils.code.isOctalDigit(source.charCodeAt(index))) { code = code * 8 + '01234567'.indexOf(advance()); } } str += String.fromCharCode(code); } else { str += ch; } break; } } else { if (ch === '\r' && source.charCodeAt(index) === 0x0A /* '\n' */) { ++index; } } } else if (esutils.code.isLineTerminator(ch.charCodeAt(0))) { break; } else { str += ch; } } if (quote !== '') { utility.throwError('unexpected quote'); } value = str; return Token.STRING; } function scanNumber() { var number, ch; number = ''; ch = source.charCodeAt(index); if (ch !== 0x2E /* '.' */) { number = advance(); ch = source.charCodeAt(index); if (number === '0') { if (ch === 0x78 /* 'x' */ || ch === 0x58 /* 'X' */) { number += advance(); while (index < length) { ch = source.charCodeAt(index); if (!esutils.code.isHexDigit(ch)) { break; } number += advance(); } if (number.length <= 2) { // only 0x utility.throwError('unexpected token'); } if (index < length) { ch = source.charCodeAt(index); if (esutils.code.isIdentifierStartES5(ch)) { utility.throwError('unexpected token'); } } value = parseInt(number, 16); return Token.NUMBER; } if (esutils.code.isOctalDigit(ch)) { number += advance(); while (index < length) { ch = source.charCodeAt(index); if (!esutils.code.isOctalDigit(ch)) { break; } number += advance(); } if (index < length) { ch = source.charCodeAt(index); if (esutils.code.isIdentifierStartES5(ch) || esutils.code.isDecimalDigit(ch)) { utility.throwError('unexpected token'); } } value = parseInt(number, 8); return Token.NUMBER; } if (esutils.code.isDecimalDigit(ch)) { utility.throwError('unexpected token'); } } while (index < length) { ch = source.charCodeAt(index); if (!esutils.code.isDecimalDigit(ch)) { break; } number += advance(); } } if (ch === 0x2E /* '.' */) { number += advance(); while (index < length) { ch = source.charCodeAt(index); if (!esutils.code.isDecimalDigit(ch)) { break; } number += advance(); } } if (ch === 0x65 /* 'e' */ || ch === 0x45 /* 'E' */) { number += advance(); ch = source.charCodeAt(index); if (ch === 0x2B /* '+' */ || ch === 0x2D /* '-' */) { number += advance(); } ch = source.charCodeAt(index); if (esutils.code.isDecimalDigit(ch)) { number += advance(); while (index < length) { ch = source.charCodeAt(index); if (!esutils.code.isDecimalDigit(ch)) { break; } number += advance(); } } else { utility.throwError('unexpected token'); } } if (index < length) { ch = source.charCodeAt(index); if (esutils.code.isIdentifierStartES5(ch)) { utility.throwError('unexpected token'); } } value = parseFloat(number); return Token.NUMBER; } function scanTypeName() { var ch, ch2; value = advance(); while (index < length && isTypeName(source.charCodeAt(index))) { ch = source.charCodeAt(index); if (ch === 0x2E /* '.' */) { if ((index + 1) >= length) { return Token.ILLEGAL; } ch2 = source.charCodeAt(index + 1); if (ch2 === 0x3C /* '<' */) { break; } } value += advance(); } return Token.NAME; } function next() { var ch; previous = index; while (index < length && esutils.code.isWhiteSpace(source.charCodeAt(index))) { advance(); } if (index >= length) { token = Token.EOF; return token; } ch = source.charCodeAt(index); switch (ch) { case 0x27: /* ''' */ case 0x22: /* '"' */ token = scanString(); return token; case 0x3A: /* ':' */ advance(); token = Token.COLON; return token; case 0x2C: /* ',' */ advance(); token = Token.COMMA; return token; case 0x28: /* '(' */ advance(); token = Token.LPAREN; return token; case 0x29: /* ')' */ advance(); token = Token.RPAREN; return token; case 0x5B: /* '[' */ advance(); token = Token.LBRACK; return token; case 0x5D: /* ']' */ advance(); token = Token.RBRACK; return token; case 0x7B: /* '{' */ advance(); token = Token.LBRACE; return token; case 0x7D: /* '}' */ advance(); token = Token.RBRACE; return token; case 0x2E: /* '.' */ if (index + 1 < length) { ch = source.charCodeAt(index + 1); if (ch === 0x3C /* '<' */) { advance(); // '.' advance(); // '<' token = Token.DOT_LT; return token; } if (ch === 0x2E /* '.' */ && index + 2 < length && source.charCodeAt(index + 2) === 0x2E /* '.' */) { advance(); // '.' advance(); // '.' advance(); // '.' token = Token.REST; return token; } if (esutils.code.isDecimalDigit(ch)) { token = scanNumber(); return token; } } token = Token.ILLEGAL; return token; case 0x3C: /* '<' */ advance(); token = Token.LT; return token; case 0x3E: /* '>' */ advance(); token = Token.GT; return token; case 0x2A: /* '*' */ advance(); token = Token.STAR; return token; case 0x7C: /* '|' */ advance(); token = Token.PIPE; return token; case 0x3F: /* '?' */ advance(); token = Token.QUESTION; return token; case 0x21: /* '!' */ advance(); token = Token.BANG; return token; case 0x3D: /* '=' */ advance(); token = Token.EQUAL; return token; case 0x2D: /* '-' */ token = scanNumber(); return token; default: if (esutils.code.isDecimalDigit(ch)) { token = scanNumber(); return token; } // type string permits following case, // // namespace.module.MyClass // // this reduced 1 token TK_NAME utility.assert(isTypeName(ch)); token = scanTypeName(); return token; } } function consume(target, text) { utility.assert(token === target, text || 'consumed token not matched'); next(); } function expect(target, message) { if (token !== target) { utility.throwError(message || 'unexpected token'); } next(); } // UnionType := '(' TypeUnionList ')' // // TypeUnionList := // <<empty>> // | NonemptyTypeUnionList // // NonemptyTypeUnionList := // TypeExpression // | TypeExpression '|' NonemptyTypeUnionList function parseUnionType() { var elements, startIndex = index - 1; consume(Token.LPAREN, 'UnionType should start with ('); elements = []; if (token !== Token.RPAREN) { while (true) { elements.push(parseTypeExpression()); if (token === Token.RPAREN) { break; } expect(Token.PIPE); } } consume(Token.RPAREN, 'UnionType should end with )'); return maybeAddRange({ type: Syntax.UnionType, elements: elements }, [startIndex, previous]); } // ArrayType := '[' ElementTypeList ']' // // ElementTypeList := // <<empty>> // | TypeExpression // | '...' TypeExpression // | TypeExpression ',' ElementTypeList function parseArrayType() { var elements, startIndex = index - 1, restStartIndex; consume(Token.LBRACK, 'ArrayType should start with ['); elements = []; while (token !== Token.RBRACK) { if (token === Token.REST) { restStartIndex = index - 3; consume(Token.REST); elements.push(maybeAddRange({ type: Syntax.RestType, expression: parseTypeExpression() }, [restStartIndex, previous])); break; } else { elements.push(parseTypeExpression()); } if (token !== Token.RBRACK) { expect(Token.COMMA); } } expect(Token.RBRACK); return maybeAddRange({ type: Syntax.ArrayType, elements: elements }, [startIndex, previous]); } function parseFieldName() { var v = value; if (token === Token.NAME || token === Token.STRING) { next(); return v; } if (token === Token.NUMBER) { consume(Token.NUMBER); return String(v); } utility.throwError('unexpected token'); } // FieldType := // FieldName // | FieldName ':' TypeExpression // // FieldName := // NameExpression // | StringLiteral // | NumberLiteral // | ReservedIdentifier function parseFieldType() { var key, rangeStart = previous; key = parseFieldName(); if (token === Token.COLON) { consume(Token.COLON); return maybeAddRange({ type: Syntax.FieldType, key: key, value: parseTypeExpression() }, [rangeStart, previous]); } return maybeAddRange({ type: Syntax.FieldType, key: key, value: null }, [rangeStart, previous]); } // RecordType := '{' FieldTypeList '}' // // FieldTypeList := // <<empty>> // | FieldType // | FieldType ',' FieldTypeList function parseRecordType() { var fields, rangeStart = index - 1, rangeEnd; consume(Token.LBRACE, 'RecordType should start with {'); fields = []; if (token === Token.COMMA) { consume(Token.COMMA); } else { while (token !== Token.RBRACE) { fields.push(parseFieldType()); if (token !== Token.RBRACE) { expect(Token.COMMA); } } } rangeEnd = index; expect(Token.RBRACE); return maybeAddRange({ type: Syntax.RecordType, fields: fields }, [rangeStart, rangeEnd]); } // NameExpression := // Identifier // | TagIdentifier ':' Identifier // // Tag identifier is one of "module", "external" or "event" // Identifier is the same as Token.NAME, including any dots, something like // namespace.module.MyClass function parseNameExpression() { var name = value, rangeStart = index - name.length; expect(Token.NAME); if (token === Token.COLON && ( name === 'module' || name === 'external' || name === 'event')) { consume(Token.COLON); name += ':' + value; expect(Token.NAME); } return maybeAddRange({ type: Syntax.NameExpression, name: name }, [rangeStart, previous]); } // TypeExpressionList := // TopLevelTypeExpression // | TopLevelTypeExpression ',' TypeExpressionList function parseTypeExpressionList() { var elements = []; elements.push(parseTop()); while (token === Token.COMMA) { consume(Token.COMMA); elements.push(parseTop()); } return elements; } // TypeName := // NameExpression // | NameExpression TypeApplication // // TypeApplication := // '.<' TypeExpressionList '>' // | '<' TypeExpressionList '>' // this is extension of doctrine function parseTypeName() { var expr, applications, startIndex = index - value.length; expr = parseNameExpression(); if (token === Token.DOT_LT || token === Token.LT) { next(); applications = parseTypeExpressionList(); expect(Token.GT); return maybeAddRange({ type: Syntax.TypeApplication, expression: expr, applications: applications }, [startIndex, previous]); } return expr; } // ResultType := // <<empty>> // | ':' void // | ':' TypeExpression // // BNF is above // but, we remove <<empty>> pattern, so token is always TypeToken::COLON function parseResultType() { consume(Token.COLON, 'ResultType should start with :'); if (token === Token.NAME && value === 'void') { consume(Token.NAME); return { type: Syntax.VoidLiteral }; } return parseTypeExpression(); } // ParametersType := // RestParameterType // | NonRestParametersType // | NonRestParametersType ',' RestParameterType // // RestParameterType := // '...' // '...' Identifier // // NonRestParametersType := // ParameterType ',' NonRestParametersType // | ParameterType // | OptionalParametersType // // OptionalParametersType := // OptionalParameterType // | OptionalParameterType, OptionalParametersType // // OptionalParameterType := ParameterType= // // ParameterType := TypeExpression | Identifier ':' TypeExpression // // Identifier is "new" or "this" function parseParametersType() { var params = [], optionalSequence = false, expr, rest = false, startIndex, restStartIndex = index - 3, nameStartIndex; while (token !== Token.RPAREN) { if (token === Token.REST) { // RestParameterType consume(Token.REST); rest = true; } startIndex = previous; expr = parseTypeExpression(); if (expr.type === Syntax.NameExpression && token === Token.COLON) { nameStartIndex = previous - expr.name.length; // Identifier ':' TypeExpression consume(Token.COLON); expr = maybeAddRange({ type: Syntax.ParameterType, name: expr.name, expression: parseTypeExpression() }, [nameStartIndex, previous]); } if (token === Token.EQUAL) { consume(Token.EQUAL); expr = maybeAddRange({ type: Syntax.OptionalType, expression: expr }, [startIndex, previous]); optionalSequence = true; } else { if (optionalSequence) { utility.throwError('unexpected token'); } } if (rest) { expr = maybeAddRange({ type: Syntax.RestType, expression: expr }, [restStartIndex, previous]); } params.push(expr); if (token !== Token.RPAREN) { expect(Token.COMMA); } } return params; } // FunctionType := 'function' FunctionSignatureType // // FunctionSignatureType := // | TypeParameters '(' ')' ResultType // | TypeParameters '(' ParametersType ')' ResultType // | TypeParameters '(' 'this' ':' TypeName ')' ResultType // | TypeParameters '(' 'this' ':' TypeName ',' ParametersType ')' ResultType function parseFunctionType() { var isNew, thisBinding, params, result, fnType, startIndex = index - value.length; utility.assert(token === Token.NAME && value === 'function', 'FunctionType should start with \'function\''); consume(Token.NAME); // Google Closure Compiler is not implementing TypeParameters. // So we do not. if we don't get '(', we see it as error. expect(Token.LPAREN); isNew = false; params = []; thisBinding = null; if (token !== Token.RPAREN) { // ParametersType or 'this' if (token === Token.NAME && (value === 'this' || value === 'new')) { // 'this' or 'new' // 'new' is Closure Compiler extension isNew = value === 'new'; consume(Token.NAME); expect(Token.COLON); thisBinding = parseTypeName(); if (token === Token.COMMA) { consume(Token.COMMA); params = parseParametersType(); } } else { params = parseParametersType(); } } expect(Token.RPAREN); result = null; if (token === Token.COLON) { result = parseResultType(); } fnType = maybeAddRange({ type: Syntax.FunctionType, params: params, result: result }, [startIndex, previous]); if (thisBinding) { // avoid adding null 'new' and 'this' properties fnType['this'] = thisBinding; if (isNew) { fnType['new'] = true; } } return fnType; } // BasicTypeExpression := // '*' // | 'null' // | 'undefined' // | TypeName // | FunctionType // | UnionType // | RecordType // | ArrayType function parseBasicTypeExpression() { var context, startIndex; switch (token) { case Token.STAR: consume(Token.STAR); return maybeAddRange({ type: Syntax.AllLiteral }, [previous - 1, previous]); case Token.LPAREN: return parseUnionType(); case Token.LBRACK: return parseArrayType(); case Token.LBRACE: return parseRecordType(); case Token.NAME: startIndex = index - value.length; if (value === 'null') { consume(Token.NAME); return maybeAddRange({ type: Syntax.NullLiteral }, [startIndex, previous]); } if (value === 'undefined') { consume(Token.NAME); return maybeAddRange({ type: Syntax.UndefinedLiteral }, [startIndex, previous]); } if (value === 'true' || value === 'false') { consume(Token.NAME); return maybeAddRange({ type: Syntax.BooleanLiteralType, value: value === 'true' }, [startIndex, previous]); } context = Context.save(); if (value === 'function') { try { return parseFunctionType(); } catch (e) { context.restore(); } } return parseTypeName(); case Token.STRING: next(); return maybeAddRange({ type: Syntax.StringLiteralType, value: value }, [previous - value.length - 2, previous]); case Token.NUMBER: next(); return maybeAddRange({ type: Syntax.NumericLiteralType, value: value }, [previous - String(value).length, previous]); default: utility.throwError('unexpected token'); } } // TypeExpression := // BasicTypeExpression // | '?' BasicTypeExpression // | '!' BasicTypeExpression // | BasicTypeExpression '?' // | BasicTypeExpression '!' // | '?' // | BasicTypeExpression '[]' function parseTypeExpression() { var expr, rangeStart; if (token === Token.QUESTION) { rangeStart = index - 1; consume(Token.QUESTION); if (token === Token.COMMA || token === Token.EQUAL || token === Token.RBRACE || token === Token.RPAREN || token === Token.PIPE || token === Token.EOF || token === Token.RBRACK || token === Token.GT) { return maybeAddRange({ type: Syntax.NullableLiteral }, [rangeStart, previous]); } return maybeAddRange({ type: Syntax.NullableType, expression: parseBasicTypeExpression(), prefix: true }, [rangeStart, previous]); } else if (token === Token.BANG) { rangeStart = index - 1; consume(Token.BANG); return maybeAddRange({ type: Syntax.NonNullableType, expression: parseBasicTypeExpression(), prefix: true }, [rangeStart, previous]); } else { rangeStart = previous; } expr = parseBasicTypeExpression(); if (token === Token.BANG) { consume(Token.BANG); return maybeAddRange({ type: Syntax.NonNullableType, expression: expr, prefix: false }, [rangeStart, previous]); } if (token === Token.QUESTION) { consume(Token.QUESTION); return maybeAddRange({ type: Syntax.NullableType, expression: expr, prefix: false }, [rangeStart, previous]); } if (token === Token.LBRACK) { consume(Token.LBRACK); expect(Token.RBRACK, 'expected an array-style type declaration (' + value + '[])'); return maybeAddRange({ type: Syntax.TypeApplication, expression: maybeAddRange({ type: Syntax.NameExpression, name: 'Array' }, [rangeStart, previous]), applications: [expr] }, [rangeStart, previous]); } return expr; } // TopLevelTypeExpression := // TypeExpression // | TypeUnionList // // This rule is Google Closure Compiler extension, not ES4 // like, // { number | string } // If strict to ES4, we should write it as // { (number|string) } function parseTop() { var expr, elements; expr = parseTypeExpression(); if (token !== Token.PIPE) { return expr; } elements = [expr]; consume(Token.PIPE); while (true) { elements.push(parseTypeExpression()); if (token !== Token.PIPE) { break; } consume(Token.PIPE); } return maybeAddRange({ type: Syntax.UnionType, elements: elements }, [0, index]); } function parseTopParamType() { var expr; if (token === Token.REST) { consume(Token.REST); return maybeAddRange({ type: Syntax.RestType, expression: parseTop() }, [0, index]); } expr = parseTop(); if (token === Token.EQUAL) { consume(Token.EQUAL); return maybeAddRange({ type: Syntax.OptionalType, expression: expr }, [0, index]); } return expr; } function parseType(src, opt) { var expr; source = src; length = source.length; index = 0; previous = 0; addRange = opt && opt.range; rangeOffset = opt && opt.startIndex || 0; next(); expr = parseTop(); if (opt && opt.midstream) { return { expression: expr, index: previous }; } if (token !== Token.EOF) { utility.throwError('not reach to EOF'); } return expr; } function parseParamType(src, opt) { var expr; source = src; length = source.length; index = 0; previous = 0; addRange = opt && opt.range; rangeOffset = opt && opt.startIndex || 0; next(); expr = parseTopParamType(); if (opt && opt.midstream) { return { expression: expr, index: previous }; } if (token !== Token.EOF) { utility.throwError('not reach to EOF'); } return expr; } function stringifyImpl(node, compact, topLevel) { var result, i, iz; switch (node.type) { case Syntax.NullableLiteral: result = '?'; break; case Syntax.AllLiteral: result = '*'; break; case Syntax.NullLiteral: result = 'null'; break; case Syntax.UndefinedLiteral: result = 'undefined'; break; case Syntax.VoidLiteral: result = 'void'; break; case Syntax.UnionType: if (!topLevel) { result = '('; } else { result = ''; } for (i = 0, iz = node.elements.length; i < iz; ++i) { result += stringifyImpl(node.elements[i], compact); if ((i + 1) !== iz) { result += compact ? '|' : ' | '; } } if (!topLevel) { result += ')'; } break; case Syntax.ArrayType: result = '['; for (i = 0, iz = node.elements.length; i < iz; ++i) { result += stringifyImpl(node.elements[i], compact); if ((i + 1) !== iz) { result += compact ? ',' : ', '; } } result += ']'; break; case Syntax.RecordType: result = '{'; for (i = 0, iz = node.fields.length; i < iz; ++i) { result += stringifyImpl(node.fields[i], compact); if ((i + 1) !== iz) { result += compact ? ',' : ', '; } } result += '}'; break; case Syntax.FieldType: if (node.value) { result = node.key + (compact ? ':' : ': ') + stringifyImpl(node.value, compact); } else { result = node.key; } break; case Syntax.FunctionType: result = compact ? 'function(' : 'function ('; if (node['this']) { if (node['new']) { result += (compact ? 'new:' : 'new: '); } else { result += (compact ? 'this:' : 'this: '); } result += stringifyImpl(node['this'], compact); if (node.params.length !== 0) { result += compact ? ',' : ', '; } } for (i = 0, iz = node.params.length; i < iz; ++i) { result += stringifyImpl(node.params[i], compact); if ((i + 1) !== iz) { result += compact ? ',' : ', '; } } result += ')'; if (node.result) { result += (compact ? ':' : ': ') + stringifyImpl(node.result, compact); } break; case Syntax.ParameterType: result = node.name + (compact ? ':' : ': ') + stringifyImpl(node.expression, compact); break; case Syntax.RestType: result = '...'; if (node.expression) { result += stringifyImpl(node.expression, compact); } break; case Syntax.NonNullableType: if (node.prefix) { result = '!' + stringifyImpl(node.expression, compact); } else { result = stringifyImpl(node.expression, compact) + '!'; } break; case Syntax.OptionalType: result = stringifyImpl(node.expression, compact) + '='; break; case Syntax.NullableType: if (node.prefix) { result = '?' + stringifyImpl(node.expression, compact); } else { result = stringifyImpl(node.expression, compact) + '?'; } break; case Syntax.NameExpression: result = node.name; break; case Syntax.TypeApplication: result = stringifyImpl(node.expression, compact) + '.<'; for (i = 0, iz = node.applications.length; i < iz; ++i) { result += stringifyImpl(node.applications[i], compact); if ((i + 1) !== iz) { result += compact ? ',' : ', '; } } result += '>'; break; case Syntax.StringLiteralType: result = '"' + node.value + '"'; break; case Syntax.NumericLiteralType: result = String(node.value); break; case Syntax.BooleanLiteralType: result = String(node.value); break; default: utility.throwError('Unknown type ' + node.type); } return result; } function stringify(node, options) { if (options == null) { options = {}; } return stringifyImpl(node, options.compact, options.topLevel); } exports.parseType = parseType; exports.parseParamType = parseParamType; exports.stringify = stringify; exports.Syntax = Syntax; }()); /* vim: set sw=4 ts=4 et tw=80 : */