From 9aab98419b5f8cb54cdcb781854292aa722ee08a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 28 May 2015 10:14:18 -0700 Subject: [PATCH] Parsing support for jsDocComments. --- Jakefile.js | 1 + src/compiler/core.ts | 7 +- .../diagnosticInformationMap.generated.ts | 2 +- src/compiler/diagnosticMessages.json | 9 +- src/compiler/parser.ts | 878 ++++++- src/compiler/scanner.ts | 26 +- src/compiler/types.ts | 153 +- src/compiler/utilities.ts | 94 +- src/harness/harness.ts | 16 +- src/services/services.ts | 2 +- src/services/utilities.ts | 4 - .../getJavaScriptSemanticDiagnostics16.ts | 2 +- tests/cases/unittests/jsDocParsing.ts | 2204 +++++++++++++++++ 13 files changed, 3326 insertions(+), 72 deletions(-) create mode 100644 tests/cases/unittests/jsDocParsing.ts diff --git a/Jakefile.js b/Jakefile.js index cb53b47950..afe0ffd3ce 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -123,6 +123,7 @@ var harnessSources = [ return path.join(harnessDirectory, f); }).concat([ "incrementalParser.ts", + "jsDocParsing.ts", "services/colorization.ts", "services/documentRegistry.ts", "services/preProcessFile.ts", diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ef7c892be9..f5719033a2 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -312,8 +312,11 @@ module ts { Debug.assert(start >= 0, "start must be non-negative, is " + start); Debug.assert(length >= 0, "length must be non-negative, is " + length); - Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${ start } > ${ file.text.length }`); - Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${ end } > ${ file.text.length }`); + + if (file) { + Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${ start } > ${ file.text.length }`); + Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${ end } > ${ file.text.length }`); + } let text = getLocaleSpecificMessage(message.key); diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 2a85271684..61a55dde55 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -174,6 +174,7 @@ module ts { Type_expected_0_is_a_reserved_word_in_strict_mode: { code: 1215, category: DiagnosticCategory.Error, key: "Type expected. '{0}' is a reserved word in strict mode" }, Type_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode: { code: 1216, category: DiagnosticCategory.Error, key: "Type expected. '{0}' is a reserved word in strict mode. Class definitions are automatically in strict mode." }, Export_assignment_is_not_supported_when_module_flag_is_system: { code: 1218, category: DiagnosticCategory.Error, key: "Export assignment is not supported when '--module' flag is 'system'." }, + _0_tag_already_specified: { code: 1219, category: DiagnosticCategory.Error, key: "'{0}' tag already specified." }, Duplicate_identifier_0: { code: 2300, category: DiagnosticCategory.Error, key: "Duplicate identifier '{0}'." }, Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor: { code: 2301, category: DiagnosticCategory.Error, key: "Initializer of instance member variable '{0}' cannot reference identifier '{1}' declared in the constructor." }, Static_members_cannot_reference_class_type_parameters: { code: 2302, category: DiagnosticCategory.Error, key: "Static members cannot reference class type parameters." }, @@ -535,7 +536,6 @@ module ts { types_can_only_be_used_in_a_ts_file: { code: 8010, category: DiagnosticCategory.Error, key: "'types' can only be used in a .ts file." }, type_arguments_can_only_be_used_in_a_ts_file: { code: 8011, category: DiagnosticCategory.Error, key: "'type arguments' can only be used in a .ts file." }, parameter_modifiers_can_only_be_used_in_a_ts_file: { code: 8012, category: DiagnosticCategory.Error, key: "'parameter modifiers' can only be used in a .ts file." }, - can_only_be_used_in_a_ts_file: { code: 8013, category: DiagnosticCategory.Error, key: "'?' can only be used in a .ts file." }, property_declarations_can_only_be_used_in_a_ts_file: { code: 8014, category: DiagnosticCategory.Error, key: "'property declarations' can only be used in a .ts file." }, enum_declarations_can_only_be_used_in_a_ts_file: { code: 8015, category: DiagnosticCategory.Error, key: "'enum declarations' can only be used in a .ts file." }, type_assertion_expressions_can_only_be_used_in_a_ts_file: { code: 8016, category: DiagnosticCategory.Error, key: "'type assertion expressions' can only be used in a .ts file." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cf1645b140..a4d7e3168d 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -683,7 +683,10 @@ "category": "Error", "code": 1218 }, - + "'{0}' tag already specified.": { + "category": "Error", + "code": 1219 + }, "Duplicate identifier '{0}'.": { "category": "Error", "code": 2300 @@ -2132,10 +2135,6 @@ "category": "Error", "code": 8012 }, - "'?' can only be used in a .ts file.": { - "category": "Error", - "code": 8013 - }, "'property declarations' can only be used in a .ts file.": { "category": "Error", "code": 8014 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index af9b486a1d..28fd8184d7 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -2,6 +2,8 @@ /// module ts { + export var throwOnJSDocErrors = false; + let nodeConstructors = new Array Node>(SyntaxKind.Count); /* @internal */ export let parseTime = 0; @@ -315,6 +317,49 @@ module ts { return visitNode(cbNode, (node).expression); case SyntaxKind.MissingDeclaration: return visitNodes(cbNodes, node.decorators); + case SyntaxKind.JSDocTypeExpression: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocUnionType: + return visitNodes(cbNodes, (node).types); + case SyntaxKind.JSDocTupleType: + return visitNodes(cbNodes, (node).types); + case SyntaxKind.JSDocArrayType: + return visitNode(cbNode, (node).elementType); + case SyntaxKind.JSDocNonNullableType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocNullableType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocRecordType: + return visitNodes(cbNodes, (node).members); + case SyntaxKind.JSDocTypeReference: + return visitNode(cbNode, (node).name) || + visitNodes(cbNodes, (node).typeArguments); + case SyntaxKind.JSDocOptionalType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocFunctionType: + return visitNodes(cbNodes, (node).parameters) || + visitNode(cbNode, (node).type); + case SyntaxKind.JSDocVariadicType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocConstructorType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocThisType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocRecordMember: + return visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).type); + case SyntaxKind.JSDocComment: + return visitNodes(cbNodes, (node).tags); + case SyntaxKind.JSDocParameterTag: + return visitNode(cbNode, (node).preParameterName) || + visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).postParameterName); + case SyntaxKind.JSDocReturnTag: + return visitNode(cbNode, (node).typeExpression); + case SyntaxKind.JSDocTypeTag: + return visitNode(cbNode, (node).typeExpression); + case SyntaxKind.JSDocTemplateTag: + return visitNodes(cbNodes, (node).typeParameters); } } @@ -338,6 +383,17 @@ module ts { export function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile { return IncrementalParser.updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks); } + + /* @internal */ + export function parseIsolatedJSDocComment(content: string, start?: number, length?: number) { + return Parser.JSDocParser.parseIsolatedJSDocComment(content, start, length); + } + + /* @internal */ + // Exposed only for testing. + export function parseJSDocTypeExpressionForTests(content: string, start?: number, length?: number) { + return Parser.JSDocParser.parseJSDocTypeExpressionForTests(content, start, length); + } // Implement the parser as a singleton module. We do this for perf reasons because creating // parser instances can actually be expensive enough to impact us on projects with many source @@ -349,6 +405,7 @@ module ts { const disallowInAndDecoratorContext = ParserContextFlags.DisallowIn | ParserContextFlags.Decorator; let sourceFile: SourceFile; + let parseDiagnostics: Diagnostic[]; let syntaxCursor: IncrementalParser.SyntaxCursor; let token: SyntaxKind; @@ -405,7 +462,7 @@ module ts { // Note: it should not be necessary to save/restore these flags during speculative/lookahead // parsing. These context flags are naturally stored and restored through normal recursive // descent parsing and unwinding. - let contextFlags: ParserContextFlags = 0; + let contextFlags: ParserContextFlags; // Whether or not we've had a parse error since creating the last AST node. If we have // encountered an error, it will be stored on the next AST node we create. Parse errors @@ -437,25 +494,52 @@ module ts { let parseErrorBeforeNextFinishedNode: boolean = false; export function parseSourceFile(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): SourceFile { + initializeState(fileName, _sourceText, languageVersion, _syntaxCursor); + + let result = parseSourceFileWorker(fileName, languageVersion, setParentNodes); + + clearState(); + + return result; + } + + function initializeState(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor) { sourceText = _sourceText; syntaxCursor = _syntaxCursor; + parseDiagnostics = []; parsingContext = 0; identifiers = {}; identifierCount = 0; nodeCount = 0; - contextFlags = 0; + contextFlags = isJavaScript(fileName) ? ParserContextFlags.JavaScriptFile : ParserContextFlags.None; parseErrorBeforeNextFinishedNode = false; - createSourceFile(fileName, languageVersion); - // Initialize and prime the scanner before parsing the source elements. scanner.setText(sourceText); scanner.setOnError(scanError); scanner.setScriptTarget(languageVersion); - token = nextToken(); + } + function clearState() { + // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. + scanner.setText(""); + scanner.setOnError(undefined); + + // Clear any data. We don't want to accidently hold onto it for too long. + parseDiagnostics = undefined; + sourceFile = undefined; + identifiers = undefined; + syntaxCursor = undefined; + sourceText = undefined; + } + + function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean): SourceFile { + sourceFile = createSourceFile(fileName, languageVersion); + + // Prime the scanner. + token = nextToken(); processReferenceComments(sourceFile); sourceFile.statements = parseList(ParsingContext.SourceElements, /*checkForStrictMode*/ true, parseSourceElement); @@ -467,29 +551,53 @@ module ts { sourceFile.nodeCount = nodeCount; sourceFile.identifierCount = identifierCount; sourceFile.identifiers = identifiers; + sourceFile.parseDiagnostics = parseDiagnostics; if (setParentNodes) { fixupParentReferences(sourceFile); } - syntaxCursor = undefined; + // If this is a javascript file, proactively see if we can get JSDoc comments for + // relevant nodes in the file. We'll use these to provide typing informaion if they're + // available. + if (isJavaScript(fileName)) { + addJSDocComments(); + } - // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. - scanner.setText(""); - scanner.setOnError(undefined); - - let result = sourceFile; - - // Clear any data. We don't want to accidently hold onto it for too long. - sourceFile = undefined; - identifiers = undefined; - syntaxCursor = undefined; - sourceText = undefined; - - return result; + return sourceFile; } - function fixupParentReferences(sourceFile: SourceFile) { + function addJSDocComments() { + forEachChild(sourceFile, visit); + return; + + function visit(node: Node) { + // Add additional cases as necessary depending on how we see JSDoc comments used + // in the wild. + switch (node.kind) { + case SyntaxKind.VariableStatement: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.Parameter: + addJSDocComment(node); + } + + forEachChild(node, visit); + } + } + + function addJSDocComment(node: Node) { + let comments = getLeadingCommentRangesOfNode(node, sourceFile); + if (comments) { + for (let comment of comments) { + let jsDocComment = JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos); + if (jsDocComment) { + node.jsDocComment = jsDocComment; + } + } + } + } + + export function fixupParentReferences(sourceFile: Node) { // normally parent references are set during binding. However, for clients that only need // a syntax tree, and no semantic features, then the binding process is an unnecessary // overhead. This functions allows us to set all the parents, without all the expense of @@ -514,18 +622,18 @@ module ts { } } - function createSourceFile(fileName: string, languageVersion: ScriptTarget) { - sourceFile = createNode(SyntaxKind.SourceFile, /*pos*/ 0); + function createSourceFile(fileName: string, languageVersion: ScriptTarget): SourceFile { + let sourceFile = createNode(SyntaxKind.SourceFile, /*pos*/ 0); sourceFile.pos = 0; sourceFile.end = sourceText.length; sourceFile.text = sourceText; - - sourceFile.parseDiagnostics = []; sourceFile.bindDiagnostics = []; sourceFile.languageVersion = languageVersion; sourceFile.fileName = normalizePath(fileName); sourceFile.flags = fileExtensionIs(sourceFile.fileName, ".d.ts") ? NodeFlags.DeclarationFile : 0; + + return sourceFile; } function setContextFlag(val: Boolean, flag: ParserContextFlags) { @@ -659,9 +767,9 @@ module ts { function parseErrorAtPosition(start: number, length: number, message: DiagnosticMessage, arg0?: any): void { // Don't report another error if it would just be at the same position as the last error. - let lastError = lastOrUndefined(sourceFile.parseDiagnostics); + let lastError = lastOrUndefined(parseDiagnostics); if (!lastError || start !== lastError.start) { - sourceFile.parseDiagnostics.push(createFileDiagnostic(sourceFile, start, length, message, arg0)); + parseDiagnostics.push(createFileDiagnostic(sourceFile, start, length, message, arg0)); } // Mark that we've encountered an error. We'll set an appropriate bit on the next @@ -706,7 +814,7 @@ module ts { // Keep track of the state we'll need to rollback to if lookahead fails (or if the // caller asked us to always reset our state). let saveToken = token; - let saveParseDiagnosticsLength = sourceFile.parseDiagnostics.length; + let saveParseDiagnosticsLength = parseDiagnostics.length; let saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; // Note: it is not actually necessary to save/restore the context flags here. That's @@ -728,7 +836,7 @@ module ts { // then unconditionally restore us to where we were. if (!result || isLookAhead) { token = saveToken; - sourceFile.parseDiagnostics.length = saveParseDiagnosticsLength; + parseDiagnostics.length = saveParseDiagnosticsLength; parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; } @@ -843,8 +951,8 @@ module ts { return node; } - function finishNode(node: T): T { - node.end = scanner.getStartPos(); + function finishNode(node: T, end?: number): T { + node.end = end === undefined ? scanner.getStartPos() : end; if (contextFlags) { node.parserContextFlags = contextFlags; @@ -913,16 +1021,28 @@ module ts { token === SyntaxKind.NumericLiteral; } - function parsePropertyName(): DeclarationName { + function parsePropertyNameWorker(allowComputedPropertyNames: boolean): DeclarationName { if (token === SyntaxKind.StringLiteral || token === SyntaxKind.NumericLiteral) { return parseLiteralNode(/*internName:*/ true); } - if (token === SyntaxKind.OpenBracketToken) { + if (allowComputedPropertyNames && token === SyntaxKind.OpenBracketToken) { return parseComputedPropertyName(); } return parseIdentifierName(); } + function parsePropertyName(): DeclarationName { + return parsePropertyNameWorker(/*allowComputedPropertyNames:*/ true); + } + + function parseSimplePropertyName(): Identifier | LiteralExpression { + return parsePropertyNameWorker(/*allowComputedPropertyNames:*/ false); + } + + function isSimplePropertyName() { + return token === SyntaxKind.StringLiteral || token === SyntaxKind.NumericLiteral || isIdentifierOrKeyword(); + } + function parseComputedPropertyName(): ComputedPropertyName { // PropertyName[Yield,GeneratorParameter] : // LiteralPropertyName @@ -1057,6 +1177,12 @@ module ts { return isHeritageClause(); case ParsingContext.ImportOrExportSpecifiers: return isIdentifierOrKeyword(); + case ParsingContext.JSDocFunctionParameters: + case ParsingContext.JSDocTypeArguments: + case ParsingContext.JSDocTupleTypes: + return JSDocParser.isJSDocType(); + case ParsingContext.JSDocRecordMembers: + return isSimplePropertyName(); } Debug.fail("Non-exhaustive case in 'isListElement'."); @@ -1142,6 +1268,14 @@ module ts { return token === SyntaxKind.GreaterThanToken || token === SyntaxKind.OpenParenToken; case ParsingContext.HeritageClauses: return token === SyntaxKind.OpenBraceToken || token === SyntaxKind.CloseBraceToken; + case ParsingContext.JSDocFunctionParameters: + return token === SyntaxKind.CloseParenToken || token === SyntaxKind.ColonToken || token === SyntaxKind.CloseBraceToken; + case ParsingContext.JSDocTypeArguments: + return token === SyntaxKind.GreaterThanToken || token === SyntaxKind.CloseBraceToken; + case ParsingContext.JSDocTupleTypes: + return token === SyntaxKind.CloseBracketToken || token === SyntaxKind.CloseBraceToken; + case ParsingContext.JSDocRecordMembers: + return token === SyntaxKind.CloseBraceToken; } } @@ -1199,7 +1333,7 @@ module ts { // test elements only if we are not already in strict mode if (checkForStrictMode && !inStrictModeContext()) { if (isPrologueDirective(element)) { - if (isUseStrictPrologueDirective(sourceFile, element)) { + if (isUseStrictPrologueDirective(element)) { setStrictModeContext(true); checkForStrictMode = false; } @@ -1224,9 +1358,9 @@ module ts { } /// Should be called only on prologue directives (isPrologueDirective(node) should be true) - function isUseStrictPrologueDirective(sourceFile: SourceFile, node: Node): boolean { + function isUseStrictPrologueDirective(node: Node): boolean { Debug.assert(isPrologueDirective(node)); - let nodeText = getSourceTextOfNodeFromSourceFile(sourceFile, (node).expression); + let nodeText = getTextOfNodeFromSourceText(sourceText, (node).expression); // Note: the node text must be exactly "use strict" or 'use strict'. It is not ok for the // string to contain unicode escapes (as per ES5). @@ -1547,6 +1681,10 @@ module ts { case ParsingContext.TupleElementTypes: return Diagnostics.Type_expected; case ParsingContext.HeritageClauses: return Diagnostics.Unexpected_token_expected; case ParsingContext.ImportOrExportSpecifiers: return Diagnostics.Identifier_expected; + case ParsingContext.JSDocFunctionParameters: return Diagnostics.Parameter_declaration_expected; + case ParsingContext.JSDocTypeArguments: return Diagnostics.Type_argument_expected; + case ParsingContext.JSDocTupleTypes: return Diagnostics.Type_expected; + case ParsingContext.JSDocRecordMembers: return Diagnostics.Property_assignment_expected; } }; @@ -4785,7 +4923,7 @@ module ts { referencedFiles.push(fileReference); } if (diagnosticMessage) { - sourceFile.parseDiagnostics.push(createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, diagnosticMessage)); + parseDiagnostics.push(createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, diagnosticMessage)); } } else { @@ -4793,7 +4931,7 @@ module ts { let amdModuleNameMatchResult = amdModuleNameRegEx.exec(comment); if (amdModuleNameMatchResult) { if (amdModuleName) { - sourceFile.parseDiagnostics.push(createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments)); + parseDiagnostics.push(createFileDiagnostic(sourceFile, range.pos, range.end - range.pos, Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments)); } amdModuleName = amdModuleNameMatchResult[2]; } @@ -4851,6 +4989,10 @@ module ts { TupleElementTypes, // Element types in tuple element type list HeritageClauses, // Heritage clauses for a class or interface declaration. ImportOrExportSpecifiers, // Named import clause's import specifier list + JSDocFunctionParameters, + JSDocTypeArguments, + JSDocRecordMembers, + JSDocTupleTypes, Count // Number of parsing contexts } @@ -4859,6 +5001,659 @@ module ts { True, Unknown } + + export module JSDocParser { + export function isJSDocType() { + switch (token) { + case SyntaxKind.AsteriskToken: + case SyntaxKind.QuestionToken: + case SyntaxKind.OpenParenToken: + case SyntaxKind.OpenBracketToken: + case SyntaxKind.ExclamationToken: + case SyntaxKind.OpenBraceToken: + case SyntaxKind.FunctionKeyword: + case SyntaxKind.DotDotDotToken: + case SyntaxKind.NewKeyword: + case SyntaxKind.ThisKeyword: + return true; + } + + return isIdentifierOrKeyword(); + } + + export function parseJSDocTypeExpressionForTests(content: string, start: number, length: number) { + initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined); + let jsDocTypeExpression = parseJSDocTypeExpression(start, length); + let diagnostics = parseDiagnostics; + clearState(); + + return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; + } + + // Parses out a JSDoc type expression. The starting position should be right at the open + // curly in the type expression. Returns 'undefined' if it encounters any errors while parsing. + /* @internal */ + export function parseJSDocTypeExpression(start: number, length: number): JSDocTypeExpression { + scanner.setText(sourceText, start, length); + + // Prime the first token for us to start processing. + token = nextToken(); + + let result = createNode(SyntaxKind.JSDocTypeExpression); + + parseExpected(SyntaxKind.OpenBraceToken); + result.type = parseJSDocTopLevelType(); + parseExpected(SyntaxKind.CloseBraceToken); + + fixupParentReferences(result); + return finishNode(result); + } + + function setError(message: DiagnosticMessage) { + parseErrorAtCurrentToken(message); + if (throwOnJSDocErrors) { + throw new Error(message.key); + } + } + + function parseJSDocTopLevelType(): JSDocType { + var type = parseJSDocType(); + if (token === SyntaxKind.BarToken) { + var unionType = createNode(SyntaxKind.JSDocUnionType, type.pos); + unionType.types = parseJSDocTypeList(type); + type = finishNode(unionType); + } + + if (token === SyntaxKind.EqualsToken) { + var optionalType = createNode(SyntaxKind.JSDocOptionalType, type.pos); + nextToken(); + optionalType.type = type; + type = finishNode(optionalType); + } + + return type; + } + + function parseJSDocType(): JSDocType { + let type = parseBasicTypeExpression(); + + while (true) { + if (token === SyntaxKind.OpenBracketToken) { + let arrayType = createNode(SyntaxKind.JSDocArrayType, type.pos); + arrayType.elementType = type; + + nextToken(); + parseExpected(SyntaxKind.CloseBracketToken); + + type = finishNode(arrayType); + } + else if (token === SyntaxKind.QuestionToken) { + let nullableType = createNode(SyntaxKind.JSDocNullableType, type.pos); + nullableType.type = type; + + nextToken(); + type = finishNode(nullableType); + } + else if (token === SyntaxKind.ExclamationToken) { + let nonNullableType = createNode(SyntaxKind.JSDocNonNullableType, type.pos); + nonNullableType.type = type; + + nextToken(); + type = finishNode(nonNullableType); + } + else { + break; + } + } + + return type; + } + + function parseBasicTypeExpression(): JSDocType { + switch (token) { + case SyntaxKind.AsteriskToken: + return parseJSDocAllType(); + case SyntaxKind.QuestionToken: + return parseJSDocUnknownOrNullableType(); + case SyntaxKind.OpenParenToken: + return parseJSDocUnionType(); + case SyntaxKind.OpenBracketToken: + return parseJSDocTupleType(); + case SyntaxKind.ExclamationToken: + return parseJSDocNonNullableType(); + case SyntaxKind.OpenBraceToken: + return parseJSDocRecordType(); + case SyntaxKind.FunctionKeyword: + return parseJSDocFunctionType(); + case SyntaxKind.DotDotDotToken: + return parseJSDocVariadicType(); + case SyntaxKind.NewKeyword: + return parseJSDocConstructorType(); + case SyntaxKind.ThisKeyword: + return parseJSDocThisType(); + case SyntaxKind.AnyKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.VoidKeyword: + return parseTokenNode(); + } + + return parseJSDocTypeReference(); + } + + function parseJSDocThisType(): JSDocThisType { + let result = createNode(SyntaxKind.JSDocThisType); + nextToken(); + parseExpected(SyntaxKind.ColonToken); + result.type = parseJSDocType(); + return finishNode(result); + } + + function parseJSDocConstructorType(): JSDocConstructorType { + let result = createNode(SyntaxKind.JSDocConstructorType); + nextToken(); + parseExpected(SyntaxKind.ColonToken); + result.type = parseJSDocType(); + return finishNode(result); + } + + function parseJSDocVariadicType(): JSDocVariadicType { + let result = createNode(SyntaxKind.JSDocVariadicType); + nextToken(); + result.type = parseJSDocType(); + return finishNode(result); + } + + function parseJSDocFunctionType(): JSDocFunctionType { + let result = createNode(SyntaxKind.JSDocFunctionType); + nextToken(); + + parseExpected(SyntaxKind.OpenParenToken); + result.parameters = parseDelimitedList(ParsingContext.JSDocFunctionParameters, parseJSDocParameter); + checkForTrailingComma(result.parameters); + parseExpected(SyntaxKind.CloseParenToken); + + if (token === SyntaxKind.ColonToken) { + nextToken(); + result.type = parseJSDocType(); + } + + return finishNode(result); + } + + function parseJSDocParameter(): ParameterDeclaration { + let parameter = createNode(SyntaxKind.Parameter); + parameter.type = parseJSDocType(); + return finishNode(parameter); + } + + function parseJSDocOptionalType(type: JSDocType): JSDocOptionalType { + let result = createNode(SyntaxKind.JSDocOptionalType, type.pos); + nextToken(); + result.type = type; + return finishNode(result); + } + + function parseJSDocTypeReference(): JSDocTypeReference { + let result = createNode(SyntaxKind.JSDocTypeReference); + result.name = parseSimplePropertyName(); + + while (parseOptional(SyntaxKind.DotToken)) { + if (token === SyntaxKind.LessThanToken) { + result.typeArguments = parseTypeArguments(); + break; + } + else { + result.name = parseQualifiedName(result.name); + } + } + + return finishNode(result); + } + + function parseTypeArguments() { + // Move past the < + nextToken(); + let typeArguments = parseDelimitedList(ParsingContext.JSDocTypeArguments, parseJSDocType); + checkForTrailingComma(typeArguments); + checkForEmptyTypeArgumentList(typeArguments); + parseExpected(SyntaxKind.GreaterThanToken); + + return typeArguments; + } + + function checkForEmptyTypeArgumentList(typeArguments: NodeArray) { + if (parseDiagnostics.length === 0 && typeArguments && typeArguments.length === 0) { + let start = typeArguments.pos - "<".length; + let end = skipTrivia(sourceText, typeArguments.end) + ">".length; + return parseErrorAtPosition(start, end - start, Diagnostics.Type_argument_list_cannot_be_empty); + } + } + + function parseQualifiedName(left: EntityName): QualifiedName { + let result = createNode(SyntaxKind.QualifiedName, left.pos); + result.left = left; + result.right = parseIdentifierName(); + + return finishNode(result); + } + + function parseJSDocRecordType(): JSDocRecordType { + let result = createNode(SyntaxKind.JSDocRecordType); + nextToken(); + result.members = parseDelimitedList(ParsingContext.JSDocRecordMembers, parseJSDocRecordMember); + checkForTrailingComma(result.members); + parseExpected(SyntaxKind.CloseBraceToken); + return finishNode(result); + } + + function parseJSDocRecordMember(): JSDocRecordMember { + let result = createNode(SyntaxKind.JSDocRecordMember); + result.name = parseSimplePropertyName(); + + if (token === SyntaxKind.ColonToken) { + nextToken(); + result.type = parseJSDocType(); + } + + return finishNode(result); + } + + function parseJSDocNonNullableType(): JSDocNonNullableType { + let result = createNode(SyntaxKind.JSDocNonNullableType); + nextToken(); + result.type = parseJSDocType(); + return finishNode(result); + } + + function parseJSDocTupleType(): JSDocTupleType { + let result = createNode(SyntaxKind.JSDocTupleType); + nextToken(); + result.types = parseDelimitedList(ParsingContext.JSDocTupleTypes, parseJSDocType); + checkForTrailingComma(result.types); + parseExpected(SyntaxKind.CloseBracketToken); + + return finishNode(result); + } + + function checkForTrailingComma(list: NodeArray) { + if (parseDiagnostics.length === 0 && list.hasTrailingComma) { + let start = list.end - ",".length; + parseErrorAtPosition(start, ",".length, Diagnostics.Trailing_comma_not_allowed); + } + } + + function parseJSDocUnionType(): JSDocUnionType { + let result = createNode(SyntaxKind.JSDocUnionType); + nextToken(); + result.types = parseJSDocTypeList(parseJSDocType()); + + parseExpected(SyntaxKind.CloseParenToken); + + return finishNode(result); + } + + function parseJSDocTypeList(firstType: JSDocType) { + Debug.assert(!!firstType); + + let types = >[]; + types.pos = firstType.pos; + + types.push(firstType); + while (parseOptional(SyntaxKind.BarToken)) { + types.push(parseJSDocType()); + } + + types.end = scanner.getStartPos(); + return types; + } + + function parseJSDocAllType(): JSDocAllType { + let result = createNode(SyntaxKind.JSDocAllType); + nextToken(); + return finishNode(result); + } + + function parseJSDocUnknownOrNullableType(): JSDocUnknownType | JSDocNullableType { + let pos = scanner.getStartPos(); + // skip the ? + nextToken(); + + // Need to lookahead to decide if this is a nullable or unknown type. + + // Here are cases where we'll pick the unknown type: + // + // Foo(?, + // { a: ? } + // Foo(?) + // Foo + // Foo(?= + // (?| + if (token === SyntaxKind.CommaToken || + token === SyntaxKind.CloseBraceToken || + token === SyntaxKind.CloseParenToken || + token === SyntaxKind.GreaterThanToken || + token === SyntaxKind.EqualsToken || + token === SyntaxKind.BarToken) { + + let result = createNode(SyntaxKind.JSDocUnknownType, pos); + return finishNode(result); + } + else { + let result = createNode(SyntaxKind.JSDocNullableType, pos); + result.type = parseJSDocType(); + return finishNode(result); + } + } + + export function parseIsolatedJSDocComment(content: string, start: number, length: number) { + initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined); + let jsDocComment = parseJSDocComment(/*parent:*/ undefined, start, length); + let diagnostics = parseDiagnostics; + clearState(); + + return jsDocComment ? { jsDocComment, diagnostics } : undefined; + } + + export function parseJSDocComment(parent: Node, start: number, length: number): JSDocComment { + let comment = parseJSDocCommentWorker(start, length); + if (comment) { + fixupParentReferences(comment); + comment.parent = parent; + } + + return comment; + } + + export function parseJSDocCommentWorker(start: number, length: number): JSDocComment { + let content = sourceText; + start = start || 0; + let end = length === undefined ? content.length : start + length; + length = end - start; + + Debug.assert(start >= 0); + Debug.assert(start <= end); + Debug.assert(end <= content.length); + + let tags: NodeArray; + let pos: number; + + // NOTE(cyrusn): This is essentially a handwritten scanner for JSDocComments. I + // considered using an actual Scanner, but this would complicate things. The + // scanner would need to know it was in a Doc Comment. Otherwise, it would then + // produce comments *inside* the doc comment. In the end it was just easier to + // write a simple scanner rather than go that route. + if (length >= "/** */".length) { + if (content.charCodeAt(start) === CharacterCodes.slash && + content.charCodeAt(start + 1) === CharacterCodes.asterisk && + content.charCodeAt(start + 2) === CharacterCodes.asterisk && + content.charCodeAt(start + 3) !== CharacterCodes.asterisk) { + + // Initially we can parse out a tag. We also have seen a starting asterisk. + // This is so that /** * @type */ doesn't parse. + let canParseTag = true; + let seenAsterisk = true; + + for (pos = start + "/**".length; pos < end;) { + let ch = content.charCodeAt(pos); + pos++; + + if (ch === CharacterCodes.at && canParseTag) { + parseTag(); + + // Once we parse out a tag, we cannot keep parsing out tags on this line. + canParseTag = false; + continue; + } + + if (isLineBreak(ch)) { + // After a line break, we can parse a tag, and we haven't seen as asterisk + // on the next line yet. + canParseTag = true; + seenAsterisk = false; + continue; + } + + if (isWhiteSpace(ch)) { + // Whitespace doesn't affect any of our parsing. + continue; + } + + // Ignore the first asterisk on a line. + if (ch === CharacterCodes.asterisk) { + if (seenAsterisk) { + // If we've already seen an asterisk, then we can no longer parse a tag + // on this line. + canParseTag = false; + } + seenAsterisk = true; + continue; + } + + // Anything else is doc comment text. We can't do anything with it. Because it + // wasn't a tag, we can no longer parse a tag on this line until we hit the next + // line break. + canParseTag = false; + } + } + } + + return createJSDocComment(); + + function createJSDocComment(): JSDocComment { + if (!tags) { + return undefined; + } + + let result = createNode(SyntaxKind.JSDocComment, start); + result.tags = tags; + return finishNode(result, end); + } + + function skipWhitespace(): void { + while (pos < end && isWhiteSpace(content.charCodeAt(pos))) { + pos++; + } + } + + function parseTag(): void { + Debug.assert(content.charCodeAt(pos - 1) === CharacterCodes.at); + let atToken = createNode(SyntaxKind.AtToken, pos - 1); + atToken.end = pos; + + let startPos = pos; + let tagName = scanIdentifier(); + if (!tagName) { + return; + } + + let tag = handleTag(atToken, tagName) || handleUnknownTag(atToken, tagName); + addTag(tag); + } + + function handleTag(atToken: Node, tagName: Identifier): JSDocTag { + if (tagName) { + switch (tagName.text) { + case "param": + return handleParamTag(atToken, tagName); + case "return": + case "returns": + return handleReturnTag(atToken, tagName); + case "template": + return handleTemplateTag(atToken, tagName); + case "type": + return handleTypeTag(atToken, tagName); + } + } + + return undefined; + } + + function handleUnknownTag(atToken: Node, tagName: Identifier) { + let result = createNode(SyntaxKind.JSDocTag, atToken.pos); + result.atToken = atToken; + result.tagName = tagName; + return finishNode(result, pos); + } + + function addTag(tag: JSDocTag): void { + if (tag) { + if (!tags) { + tags = >[]; + tags.pos = tag.pos; + } + + tags.push(tag); + tags.end = tag.end; + } + } + + function tryParseTypeExpression(): JSDocTypeExpression { + skipWhitespace(); + + if (content.charCodeAt(pos) !== CharacterCodes.openBrace) { + return undefined; + } + + let typeExpression = parseJSDocTypeExpression(pos, end - pos); + pos = typeExpression.end; + return typeExpression; + } + + function handleParamTag(atToken: Node, tagName: Identifier) { + let typeExpression = tryParseTypeExpression(); + + skipWhitespace(); + let name: Identifier; + let isBracketed: boolean; + if (content.charCodeAt(pos) === CharacterCodes.openBracket) { + pos++; + skipWhitespace(); + name = scanIdentifier(); + isBracketed = true; + } + else { + name = scanIdentifier(); + } + + if (!name) { + parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected); + return undefined; + } + + let preName: Identifier, postName: Identifier; + if (typeExpression) { + postName = name; + } + else { + preName = name; + } + + if (!typeExpression) { + typeExpression = tryParseTypeExpression(); + } + + let result = createNode(SyntaxKind.JSDocParameterTag, atToken.pos); + result.atToken = atToken; + result.tagName = tagName; + result.preParameterName = preName; + result.typeExpression = typeExpression; + result.postParameterName = postName; + result.isBracketed = isBracketed; + return finishNode(result, pos); + } + + function handleReturnTag(atToken: Node, tagName: Identifier): JSDocReturnTag { + if (forEach(tags, t => t.kind === SyntaxKind.JSDocReturnTag)) { + parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + } + + let result = createNode(SyntaxKind.JSDocReturnTag, atToken.pos); + result.atToken = atToken; + result.tagName = tagName; + result.typeExpression = tryParseTypeExpression(); + return finishNode(result, pos); + } + + function handleTypeTag(atToken: Node, tagName: Identifier): JSDocTypeTag { + if (forEach(tags, t => t.kind === SyntaxKind.JSDocTypeTag)) { + parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + } + + let result = createNode(SyntaxKind.JSDocTypeTag, atToken.pos); + result.atToken = atToken; + result.tagName = tagName; + result.typeExpression = tryParseTypeExpression(); + return finishNode(result, pos); + } + + function handleTemplateTag(atToken: Node, tagName: Identifier): JSDocTemplateTag { + if (forEach(tags, t => t.kind === SyntaxKind.JSDocTemplateTag)) { + parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + } + + let typeParameters = >[]; + typeParameters.pos = pos; + + while (true) { + skipWhitespace(); + + let startPos = pos; + let name = scanIdentifier(); + if (!name) { + parseErrorAtPosition(startPos, 0, Diagnostics.Identifier_expected); + return undefined; + } + + let typeParameter = createNode(SyntaxKind.TypeParameter, name.pos); + typeParameter.name = name; + finishNode(typeParameter, pos); + + typeParameters.push(typeParameter); + + skipWhitespace(); + if (content.charCodeAt(pos) !== CharacterCodes.comma) { + break; + } + + pos++; + } + + typeParameters.end = pos; + + let result = createNode(SyntaxKind.JSDocTemplateTag, atToken.pos); + result.atToken = atToken; + result.tagName = tagName; + result.typeParameters = typeParameters; + return finishNode(result, pos); + } + + function scanIdentifier(): Identifier { + let startPos = pos; + for (; pos < end; pos++) { + let ch = content.charCodeAt(pos); + if (pos === startPos && isIdentifierStart(ch, ScriptTarget.Latest)) { + continue; + } + else if (pos > startPos && isIdentifierPart(ch, ScriptTarget.Latest)) { + continue; + } + + break; + } + + if (startPos === pos) { + return undefined; + } + + let result = createNode(SyntaxKind.Identifier, startPos); + result.text = content.substring(startPos, pos); + return finishNode(result, pos); + } + } + } } module IncrementalParser { @@ -4959,7 +5754,14 @@ module ts { // Ditch any existing LS children we may have created. This way we can avoid // moving them forward. - node._children = undefined; + if (node._children) { + node._children = undefined; + } + + if (node.jsDocComment) { + node.jsDocComment = undefined; + } + node.pos += delta; node.end += delta; diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 1fc8bff92e..89ac837ab2 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -598,12 +598,26 @@ module ts { ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); } - /** Creates a scanner over a (possibly unspecified) range of a piece of text. */ - export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, text?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner { - let pos: number; // Current position (end position of text of current token) - let end: number; // end of text - let startPos: number; // Start position of whitespace before current token - let tokenPos: number; // Start position of text of current token + /* @internal */ + // Creates a scanner over a (possibly unspecified) range of a piece of text. + export function createScanner(languageVersion: ScriptTarget, + skipTrivia: boolean, + text?: string, + onError?: ErrorCallback, + start?: number, + length?: number): Scanner { + // Current position (end position of text of current token) + let pos: number; + + // end of text + let end: number; + + // Start position of whitespace before current token + let startPos: number; + + // Start position of text of current token + let tokenPos: number; + let token: SyntaxKind; let tokenValue: string; let precedingLineBreak: boolean; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index c8d22b2672..85fa461daa 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -270,6 +270,32 @@ module ts { // Top-level nodes SourceFile, + // JSDoc nodes. + JSDocTypeExpression, + // The * type. + JSDocAllType, + // The ? type. + JSDocUnknownType, + JSDocArrayType, + JSDocUnionType, + JSDocTupleType, + JSDocNullableType, + JSDocNonNullableType, + JSDocRecordType, + JSDocRecordMember, + JSDocTypeReference, + JSDocOptionalType, + JSDocFunctionType, + JSDocVariadicType, + JSDocConstructorType, + JSDocThisType, + JSDocComment, + JSDocTag, + JSDocParameterTag, + JSDocReturnTag, + JSDocTypeTag, + JSDocTemplateTag, + // Synthesized list SyntaxList, // Enum value count @@ -324,6 +350,8 @@ module ts { /* @internal */ export const enum ParserContextFlags { + None = 0, + // Set if this node was parsed in strict mode. Used for grammar error checks, as well as // checking if the node can be reused in incremental settings. StrictMode = 1 << 0, @@ -345,6 +373,10 @@ module ts { // error. ThisNodeHasError = 1 << 5, + // This node was parsed in a JavaScript file and can be processed differently. For example + // its type can be specified usign a JSDoc comment. + JavaScriptFile = 1 << 6, + // Context flags set directly by the parser. ParserGeneratedFlags = StrictMode | DisallowIn | Yield | GeneratorParameter | Decorator | ThisNodeHasError, @@ -352,10 +384,10 @@ module ts { // Used during incremental parsing to determine if this node or any of its children had an // error. Computed only once and then cached. - ThisNodeOrAnySubNodesHasError = 1 << 6, + ThisNodeOrAnySubNodesHasError = 1 << 7, // Used to know if we've computed data from children and cached it in this node. - HasAggregatedChildData = 1 << 7 + HasAggregatedChildData = 1 << 8 } /* @internal */ @@ -371,14 +403,15 @@ module ts { // Specific context the parser was in when this node was created. Normally undefined. // Only set when the parser was in some interesting context (like async/yield). /* @internal */ parserContextFlags?: ParserContextFlags; - decorators?: NodeArray; // Array of decorators (in document order) - modifiers?: ModifiersArray; // Array of modifiers - /* @internal */ id?: number; // Unique id (used to look up NodeLinks) - parent?: Node; // Parent node (initialized by binding) - /* @internal */ symbol?: Symbol; // Symbol declared by node (initialized by binding) - /* @internal */ locals?: SymbolTable; // Locals associated with node (initialized by binding) - /* @internal */ nextContainer?: Node; // Next container in declaration order (initialized by binding) - /* @internal */ localSymbol?: Symbol; // Local symbol declared by node (initialized by binding only for exported nodes) + decorators?: NodeArray; // Array of decorators (in document order) + modifiers?: ModifiersArray; // Array of modifiers + /* @internal */ id?: number; // Unique id (used to look up NodeLinks) + parent?: Node; // Parent node (initialized by binding + /* @internal */ jsDocComment?: JSDocComment; // JSDoc for the node, if it has any. Only for .js files. + /* @internal */ symbol?: Symbol; // Symbol declared by node (initialized by binding) + /* @internal */ locals?: SymbolTable; // Locals associated with node (initialized by binding) + /* @internal */ nextContainer?: Node; // Next container in declaration order (initialized by binding) + /* @internal */ localSymbol?: Symbol; // Local symbol declared by node (initialized by binding only for exported nodes) } export interface NodeArray extends Array, TextRange { @@ -992,6 +1025,106 @@ module ts { kind: SyntaxKind; } + // represents a top level: { type } expression in a JSDoc comment. + export interface JSDocTypeExpression extends Node { + type: JSDocType; + } + + export interface JSDocType extends TypeNode { + _jsDocTypeBrand: any; + } + + export interface JSDocAllType extends JSDocType { + _JSDocAllTypeBrand: any; + } + + export interface JSDocUnknownType extends JSDocType { + _JSDocUnknownTypeBrand: any; + } + + export interface JSDocArrayType extends JSDocType { + elementType: JSDocType; + } + + export interface JSDocUnionType extends JSDocType { + types: NodeArray; + } + + export interface JSDocTupleType extends JSDocType { + types: NodeArray; + } + + export interface JSDocNonNullableType extends JSDocType { + type: JSDocType; + } + + export interface JSDocNullableType extends JSDocType { + type: JSDocType; + } + + export interface JSDocRecordType extends JSDocType, TypeLiteralNode { + members: NodeArray; + } + + export interface JSDocTypeReference extends JSDocType { + name: EntityName; + typeArguments: NodeArray + } + + export interface JSDocOptionalType extends JSDocType { + type: JSDocType; + } + + export interface JSDocFunctionType extends JSDocType, SignatureDeclaration { + parameters: NodeArray; + type: JSDocType; + } + + export interface JSDocVariadicType extends JSDocType { + type: JSDocType; + } + + export interface JSDocConstructorType extends JSDocType { + type: JSDocType; + } + + export interface JSDocThisType extends JSDocType { + type: JSDocType; + } + + export interface JSDocRecordMember extends PropertyDeclaration { + name: Identifier | LiteralExpression, + type?: JSDocType + } + + export interface JSDocComment extends Node { + tags: NodeArray; + } + + export interface JSDocTag extends Node { + atToken: Node; + tagName: Identifier; + } + + export interface JSDocTemplateTag extends JSDocTag { + typeParameters: NodeArray; + } + + export interface JSDocReturnTag extends JSDocTag { + typeExpression: JSDocTypeExpression; + } + + export interface JSDocTypeTag extends JSDocTag { + typeExpression: JSDocTypeExpression; + } + + export interface JSDocParameterTag extends JSDocTag { + preParameterName?: Identifier; + typeExpression?: JSDocTypeExpression; + postParameterName?: Identifier; + isBracketed: boolean; + } + // Source files are declarations when they are external modules. export interface SourceFile extends Declaration { statements: NodeArray; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 356337e131..4aca0859ce 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -468,6 +468,12 @@ module ts { return false; } + export function isClassLike(node: Node): boolean { + if (node) { + return node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression; + } + } + export function isFunctionLike(node: Node): boolean { if (node) { switch (node.kind) { @@ -830,7 +836,7 @@ module ts { } export function hasDotDotDotToken(node: Node) { - return node && node.kind === SyntaxKind.Parameter && (node).dotDotDotToken !== undefined; + return node && node.kind === SyntaxKind.Parameter && (node).dotDotDotToken !== undefined; } export function hasQuestionToken(node: Node) { @@ -856,6 +862,78 @@ module ts { return s.parameters.length > 0 && lastOrUndefined(s.parameters).dotDotDotToken !== undefined; } + export function isJSDocConstructSignature(node: Node) { + return node.kind === SyntaxKind.JSDocFunctionType && + (node).parameters.length > 0 && + (node).parameters[0].type.kind === SyntaxKind.JSDocConstructorType; + } + + function getJSDocTag(node: Node, kind: SyntaxKind): JSDocTag { + if (node && node.jsDocComment) { + for (let tag of node.jsDocComment.tags) { + if (tag.kind === kind) { + return tag; + } + } + } + } + + export function getJSDocTypeTag(node: Node): JSDocTypeTag { + return getJSDocTag(node, SyntaxKind.JSDocTypeTag); + } + + export function getJSDocReturnTag(node: Node): JSDocReturnTag { + return getJSDocTag(node, SyntaxKind.JSDocReturnTag); + } + + export function getJSDocTemplateTag(node: Node): JSDocTemplateTag { + return getJSDocTag(node, SyntaxKind.JSDocTemplateTag); + } + + export function getCorrespondingJSDocParameterTag(parameter: ParameterDeclaration): JSDocParameterTag { + if (parameter.name && parameter.name.kind === SyntaxKind.Identifier) { + // If it's a parameter, see if the parent has a jsdoc comment with an @param + // annotation. + let parameterName = (parameter.name).text; + + let docComment = parameter.parent.jsDocComment; + if (docComment) { + return forEach(docComment.tags, t => { + if (t.kind === SyntaxKind.JSDocParameterTag) { + let parameterTag = t; + let name = parameterTag.preParameterName || parameterTag.postParameterName; + if (name.text === parameterName) { + return t; + } + } + }); + } + } + } + + export function hasRestParameter(s: SignatureDeclaration): boolean { + return isRestParameter(lastOrUndefined(s.parameters)); + } + + export function isRestParameter(node: ParameterDeclaration) { + if (node) { + if (node.parserContextFlags & ParserContextFlags.JavaScriptFile) { + if (node.type && node.type.kind === SyntaxKind.JSDocVariadicType) { + return true; + } + + let paramTag = getCorrespondingJSDocParameterTag(node); + if (paramTag && paramTag.typeExpression) { + return paramTag.typeExpression.type.kind === SyntaxKind.JSDocVariadicType; + } + } + + return node.dotDotDotToken !== undefined; + } + + return false; + } + export function isLiteralKind(kind: SyntaxKind): boolean { return SyntaxKind.FirstLiteralToken <= kind && kind <= SyntaxKind.LastLiteralToken; } @@ -1707,6 +1785,10 @@ module ts { return symbol && symbol.valueDeclaration && (symbol.valueDeclaration.flags & NodeFlags.Default) ? symbol.valueDeclaration.localSymbol : undefined; } + export function isJavaScript(fileName: string) { + return fileExtensionIs(fileName, ".js"); + } + /** * Replace each instance of non-ascii characters by one, two, three, or four escape sequences * representing the UTF-8 encoding of the character, and return the expanded char code list. @@ -2000,4 +2082,14 @@ module ts { return createTextChangeRange(createTextSpanFromBounds(oldStartN, oldEndN), /*newLength:*/ newEndN - oldStartN); } + + export function getTypeParameterOwner(d: Declaration): Declaration { + if (d && d.kind === SyntaxKind.TypeParameter) { + for (let current: Node = d; current; current = current.parent) { + if (isFunctionLike(current) || isClassLike(current) || current.kind === SyntaxKind.InterfaceDeclaration) { + return current; + } + } + } + } } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 23cdfdfb7c..90afab029e 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -193,12 +193,16 @@ module Utils { }; } - export function sourceFileToJSON(file: ts.SourceFile): string { + export function sourceFileToJSON(file: ts.Node): string { return JSON.stringify(file,(k, v) => { return isNodeOrArray(v) ? serializeNode(v) : v; }, " "); - function getKindName(k: number): string { + function getKindName(k: number | string): string { + if (typeof k === "string") { + return k; + } + return (ts).SyntaxKind[k] } @@ -231,7 +235,9 @@ module Utils { function serializeNode(n: ts.Node): any { var o: any = { kind: getKindName(n.kind) }; - o.containsParseError = ts.containsParseError(n); + if (ts.containsParseError(n)) { + o.containsParseError = true; + } ts.forEach(Object.getOwnPropertyNames(n), propertyName => { switch (propertyName) { @@ -249,6 +255,10 @@ module Utils { // Blacklist of items we never put in the baseline file. break; + case "originalKeywordKind": + o[propertyName] = getKindName((n)[propertyName]); + break; + case "flags": // Print out flags with their enum names. o[propertyName] = getNodeFlagName(n.flags); diff --git a/src/services/services.ts b/src/services/services.ts index a6a26f0b60..1019c53219 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2698,7 +2698,7 @@ module ts { return true; } if (parameter.questionToken) { - diagnostics.push(createDiagnosticForNode(parameter.questionToken, Diagnostics.can_only_be_used_in_a_ts_file)); + diagnostics.push(createDiagnosticForNode(parameter.questionToken, Diagnostics._0_can_only_be_used_in_a_ts_file, '?')); return true; } if (parameter.type) { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index f2074d43ec..da0a0aa96c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -652,8 +652,4 @@ module ts { typechecker.getSymbolDisplayBuilder().buildSignatureDisplay(signature, writer, enclosingDeclaration, flags); }); } - - export function isJavaScript(fileName: string) { - return fileExtensionIs(fileName, ".js"); - } } \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptSemanticDiagnostics16.ts b/tests/cases/fourslash/getJavaScriptSemanticDiagnostics16.ts index 2ccac13dbf..1e114532c7 100644 --- a/tests/cases/fourslash/getJavaScriptSemanticDiagnostics16.ts +++ b/tests/cases/fourslash/getJavaScriptSemanticDiagnostics16.ts @@ -10,6 +10,6 @@ verify.getSemanticDiagnostics(`[ "start": 12, "length": 1, "category": "error", - "code": 8013 + "code": 8009 } ]`); \ No newline at end of file diff --git a/tests/cases/unittests/jsDocParsing.ts b/tests/cases/unittests/jsDocParsing.ts new file mode 100644 index 0000000000..ebba6e8a1a --- /dev/null +++ b/tests/cases/unittests/jsDocParsing.ts @@ -0,0 +1,2204 @@ +/// +/// +/// + +module ts { + describe("JSDocParsing", () => { + describe("TypeExpressions", () => { + function parsesCorrectly(content: string, expected: string) { + let typeAndDiagnostics = ts.parseJSDocTypeExpressionForTests(content); + assert.isTrue(typeAndDiagnostics && typeAndDiagnostics.diagnostics.length === 0); + + let result = Utils.sourceFileToJSON(typeAndDiagnostics.jsDocTypeExpression.type); + assert.equal(result, expected); + } + + function parsesIncorrectly(content: string) { + let type = ts.parseJSDocTypeExpressionForTests(content); + assert.isTrue(!type || type.diagnostics.length > 0); + } + + describe("parseCorrectly", () => { + it("unknownType", () => { + parsesCorrectly("{?}", + `{ + "kind": "JSDocUnknownType", + "pos": 1, + "end": 2 +}`); + }); + + it("allType", () => { + parsesCorrectly("{*}", + `{ + "kind": "JSDocAllType", + "pos": 1, + "end": 2 +}`); + }); + + it("nullableType", () => { + parsesCorrectly("{?number}", + `{ + "kind": "JSDocNullableType", + "pos": 1, + "end": 8, + "type": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + } +}`) + }); + + it("nullableType2", () => { + parsesCorrectly("{number?}", + `{ + "kind": "JSDocNullableType", + "pos": 1, + "end": 8, + "type": { + "kind": "NumberKeyword", + "pos": 1, + "end": 7 + } +}`) + }); + + it("nonNullableType", () => { + parsesCorrectly("{!number}", + `{ + "kind": "JSDocNonNullableType", + "pos": 1, + "end": 8, + "type": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + } +}`) + }); + + it("nonNullableType2", () => { + parsesCorrectly("{number!}", + `{ + "kind": "JSDocNonNullableType", + "pos": 1, + "end": 8, + "type": { + "kind": "NumberKeyword", + "pos": 1, + "end": 7 + } +}`) + }); + + it("recordType1", () => { + parsesCorrectly("{{}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 3, + "members": { + "length": 0, + "pos": 2, + "end": 2 + } +}`) + }); + + it("recordType2", () => { + parsesCorrectly("{{foo}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 6, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 5, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } + }, + "length": 1, + "pos": 2, + "end": 5 + } +}`) + }); + + it("recordType3", () => { + parsesCorrectly("{{foo: number}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 14, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 13, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } + }, + "length": 1, + "pos": 2, + "end": 13 + } +}`) + }); + + it("recordType4", () => { + parsesCorrectly("{{foo, bar}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 11, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 5, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } + }, + "1": { + "kind": "JSDocRecordMember", + "pos": 6, + "end": 10, + "name": { + "kind": "Identifier", + "pos": 6, + "end": 10, + "text": "bar" + } + }, + "length": 2, + "pos": 2, + "end": 10 + } +}`) + }); + + it("recordType5", () => { + parsesCorrectly("{{foo: number, bar}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 19, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 13, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } + }, + "1": { + "kind": "JSDocRecordMember", + "pos": 14, + "end": 18, + "name": { + "kind": "Identifier", + "pos": 14, + "end": 18, + "text": "bar" + } + }, + "length": 2, + "pos": 2, + "end": 18 + } +}`) + }); + + it("recordType6", () => { + parsesCorrectly("{{foo, bar: number}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 19, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 5, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + } + }, + "1": { + "kind": "JSDocRecordMember", + "pos": 6, + "end": 18, + "name": { + "kind": "Identifier", + "pos": 6, + "end": 10, + "text": "bar" + }, + "type": { + "kind": "NumberKeyword", + "pos": 11, + "end": 18 + } + }, + "length": 2, + "pos": 2, + "end": 18 + } +}`) + }); + + it("recordType7", () => { + parsesCorrectly("{{foo: number, bar: number}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 27, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 13, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 5, + "text": "foo" + }, + "type": { + "kind": "NumberKeyword", + "pos": 6, + "end": 13 + } + }, + "1": { + "kind": "JSDocRecordMember", + "pos": 14, + "end": 26, + "name": { + "kind": "Identifier", + "pos": 14, + "end": 18, + "text": "bar" + }, + "type": { + "kind": "NumberKeyword", + "pos": 19, + "end": 26 + } + }, + "length": 2, + "pos": 2, + "end": 26 + } +}`) + }); + + it("recordType8", () => { + parsesCorrectly("{{function}}", + `{ + "kind": "JSDocRecordType", + "pos": 1, + "end": 11, + "members": { + "0": { + "kind": "JSDocRecordMember", + "pos": 2, + "end": 10, + "name": { + "kind": "Identifier", + "pos": 2, + "end": 10, + "originalKeywordKind": "FunctionKeyword", + "text": "function" + } + }, + "length": 1, + "pos": 2, + "end": 10 + } +}`) + }); + + it("unionType", () => { + parsesCorrectly("{(number|string)}", + `{ + "kind": "JSDocUnionType", + "pos": 1, + "end": 16, + "types": { + "0": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + }, + "1": { + "kind": "StringKeyword", + "pos": 9, + "end": 15 + }, + "length": 2, + "pos": 2, + "end": 15 + } +}`); + }); + + it("topLevelNoParenUnionType", () => { + parsesCorrectly("{number|string}", + `{ + "kind": "JSDocUnionType", + "pos": 1, + "end": 14, + "types": { + "0": { + "kind": "NumberKeyword", + "pos": 1, + "end": 7 + }, + "1": { + "kind": "StringKeyword", + "pos": 8, + "end": 14 + }, + "length": 2, + "pos": 1, + "end": 14 + } +}`); + }); + + it("functionType1", () => { + parsesCorrectly("{function()}", + `{ + "kind": "JSDocFunctionType", + "pos": 1, + "end": 11, + "parameters": { + "length": 0, + "pos": 10, + "end": 10 + } +}`); + }); + + it("functionType2", () => { + parsesCorrectly("{function(string, boolean)}", + `{ + "kind": "JSDocFunctionType", + "pos": 1, + "end": 26, + "parameters": { + "0": { + "kind": "Parameter", + "pos": 10, + "end": 16, + "type": { + "kind": "StringKeyword", + "pos": 10, + "end": 16 + } + }, + "1": { + "kind": "Parameter", + "pos": 17, + "end": 25, + "type": { + "kind": "BooleanKeyword", + "pos": 17, + "end": 25 + } + }, + "length": 2, + "pos": 10, + "end": 25 + } +}`); + }); + + it("functionReturnType1", () => { + parsesCorrectly("{function(string, boolean)}", + `{ + "kind": "JSDocFunctionType", + "pos": 1, + "end": 26, + "parameters": { + "0": { + "kind": "Parameter", + "pos": 10, + "end": 16, + "type": { + "kind": "StringKeyword", + "pos": 10, + "end": 16 + } + }, + "1": { + "kind": "Parameter", + "pos": 17, + "end": 25, + "type": { + "kind": "BooleanKeyword", + "pos": 17, + "end": 25 + } + }, + "length": 2, + "pos": 10, + "end": 25 + } +}`); + }); + + it("thisType1", () => { + parsesCorrectly("{this:a.b}", + `{ + "kind": "JSDocThisType", + "pos": 1, + "end": 9, + "type": { + "kind": "JSDocTypeReference", + "pos": 6, + "end": 9, + "name": { + "kind": "FirstNode", + "pos": 6, + "end": 9, + "left": { + "kind": "Identifier", + "pos": 6, + "end": 7, + "text": "a" + }, + "right": { + "kind": "Identifier", + "pos": 8, + "end": 9, + "text": "b" + } + } + } +}`); + }); + + it("newType1", () => { + parsesCorrectly("{new:a.b}", + `{ + "kind": "JSDocConstructorType", + "pos": 1, + "end": 8, + "type": { + "kind": "JSDocTypeReference", + "pos": 5, + "end": 8, + "name": { + "kind": "FirstNode", + "pos": 5, + "end": 8, + "left": { + "kind": "Identifier", + "pos": 5, + "end": 6, + "text": "a" + }, + "right": { + "kind": "Identifier", + "pos": 7, + "end": 8, + "text": "b" + } + } + } +}`); + }); + + it("variadicType", () => { + parsesCorrectly("{...number}", + `{ + "kind": "JSDocVariadicType", + "pos": 1, + "end": 10, + "type": { + "kind": "NumberKeyword", + "pos": 4, + "end": 10 + } +}`); + }); + + it("optionalType", () => { + parsesCorrectly("{number=}", + `{ + "kind": "JSDocOptionalType", + "pos": 1, + "end": 8, + "type": { + "kind": "NumberKeyword", + "pos": 1, + "end": 7 + } +}`); + }); + + it("optionalNullable", () => { + parsesCorrectly("{?=}", + `{ + "kind": "JSDocOptionalType", + "pos": 1, + "end": 3, + "type": { + "kind": "JSDocUnknownType", + "pos": 1, + "end": 2 + } +}`); + }); + + it("typeReference1", () => { + parsesCorrectly("{a.}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 11, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + }, + "typeArguments": { + "0": { + "kind": "NumberKeyword", + "pos": 4, + "end": 10 + }, + "length": 1, + "pos": 4, + "end": 10 + } +}`); + }); + + it("typeReference2", () => { + parsesCorrectly("{a.}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 18, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + }, + "typeArguments": { + "0": { + "kind": "NumberKeyword", + "pos": 4, + "end": 10 + }, + "1": { + "kind": "StringKeyword", + "pos": 11, + "end": 17 + }, + "length": 2, + "pos": 4, + "end": 17 + } +}`); + }); + + it("typeReference3", () => { + parsesCorrectly("{a.function}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 11, + "name": { + "kind": "FirstNode", + "pos": 1, + "end": 11, + "left": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + }, + "right": { + "kind": "Identifier", + "pos": 3, + "end": 11, + "originalKeywordKind": "FunctionKeyword", + "text": "function" + } + } +}`); + }); + + it("arrayType1", () => { + parsesCorrectly( + "{a[]}", + `{ + "kind": "JSDocArrayType", + "pos": 1, + "end": 4, + "elementType": { + "kind": "JSDocTypeReference", + "pos": 1, + "end": 2, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + } + } +}`); + }); + + it("arrayType2", () => { + parsesCorrectly( + "{a[][]}", + `{ + "kind": "JSDocArrayType", + "pos": 1, + "end": 6, + "elementType": { + "kind": "JSDocArrayType", + "pos": 1, + "end": 4, + "elementType": { + "kind": "JSDocTypeReference", + "pos": 1, + "end": 2, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + } + } + } +}`); + }); + + it("arrayType3", () => { + parsesCorrectly( + "{a[][]=}", + `{ + "kind": "JSDocOptionalType", + "pos": 1, + "end": 7, + "type": { + "kind": "JSDocArrayType", + "pos": 1, + "end": 6, + "elementType": { + "kind": "JSDocArrayType", + "pos": 1, + "end": 4, + "elementType": { + "kind": "JSDocTypeReference", + "pos": 1, + "end": 2, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 2, + "text": "a" + } + } + } + } +}`); + }); + + it("keyword1", () => { + parsesCorrectly( + "{var}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 4, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 4, + "originalKeywordKind": "VarKeyword", + "text": "var" + } +}`); + }); + + it("keyword2", () => { + parsesCorrectly( + "{null}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 5, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 5, + "originalKeywordKind": "NullKeyword", + "text": "null" + } +}`); + }); + + it("keyword3", () => { + parsesCorrectly( + "{undefined}", + `{ + "kind": "JSDocTypeReference", + "pos": 1, + "end": 10, + "name": { + "kind": "Identifier", + "pos": 1, + "end": 10, + "text": "undefined" + } +}`); + }); + + it("tupleType0", () => { + parsesCorrectly( + "{[]}", + `{ + "kind": "JSDocTupleType", + "pos": 1, + "end": 3, + "types": { + "length": 0, + "pos": 2, + "end": 2 + } +}`); + }); + + it("tupleType1", () => { + parsesCorrectly( + "{[number]}", + `{ + "kind": "JSDocTupleType", + "pos": 1, + "end": 9, + "types": { + "0": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + }, + "length": 1, + "pos": 2, + "end": 8 + } +}`); + }); + + it("tupleType2", () => { + parsesCorrectly( + "{[number,string]}", + `{ + "kind": "JSDocTupleType", + "pos": 1, + "end": 16, + "types": { + "0": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + }, + "1": { + "kind": "StringKeyword", + "pos": 9, + "end": 15 + }, + "length": 2, + "pos": 2, + "end": 15 + } +}`); + }); + + it("tupleType3", () => { + parsesCorrectly( + "{[number,string,boolean]}", + `{ + "kind": "JSDocTupleType", + "pos": 1, + "end": 24, + "types": { + "0": { + "kind": "NumberKeyword", + "pos": 2, + "end": 8 + }, + "1": { + "kind": "StringKeyword", + "pos": 9, + "end": 15 + }, + "2": { + "kind": "BooleanKeyword", + "pos": 16, + "end": 23 + }, + "length": 3, + "pos": 2, + "end": 23 + } +}`); + }); + }); + + describe("parsesIncorrectly", () => { + it("emptyType", () => { + parsesIncorrectly("{}"); + }); + + it("trailingCommaInRecordType", () => { + parsesIncorrectly("{{a,}}"); + }); + + it("unionTypeWithTrailingBar", () => { + parsesIncorrectly("{(a|)}"); + }); + + it("unionTypeWithoutTypes", () => { + parsesIncorrectly("{()}"); + }); + + it("nullableTypeWithoutType", () => { + parsesIncorrectly("{!}"); + }); + + it("functionTypeWithTrailingComma", () => { + parsesIncorrectly("{function(a,)}"); + }); + + it("thisWithoutType", () => { + parsesIncorrectly("{this:}"); + }); + + it("newWithoutType", () => { + parsesIncorrectly("{new:}"); + }); + + it("variadicWithoutType", () => { + parsesIncorrectly("{...}"); + }); + + it("optionalWithoutType", () => { + parsesIncorrectly("{=}"); + }); + + it("allWithType", () => { + parsesIncorrectly("{*foo}"); + }); + + it("typeArgumentsNotFollowingDot", () => { + parsesIncorrectly("{a<>}"); + }); + + it("emptyTypeArguments", () => { + parsesIncorrectly("{a.<>}"); + }); + + it("typeArgumentsWithTrailingComma", () => { + parsesIncorrectly("{a.}"); + }); + + it("tsFunctionType", () => { + parsesIncorrectly("{() => string}"); + }); + + it("tsConstructoType", () => { + parsesIncorrectly("{new () => string}"); + }); + + it("typeOfType", () => { + parsesIncorrectly("{typeof M}"); + }); + + it("namedParameter", () => { + parsesIncorrectly("{function(a: number)}"); + }); + + it("callSignatureInRecordType", () => { + parsesIncorrectly("{{(): number}}"); + }); + + it("methodInRecordType", () => { + parsesIncorrectly("{{foo(): number}}"); + }); + + it("tupleTypeWithComma", () => { + parsesIncorrectly( "{[,]}"); + }); + + it("tupleTypeWithTrailingComma", () => { + parsesIncorrectly("{[number,]}"); + }); + + it("tupleTypeWithLeadingComma", () => { + parsesIncorrectly("{[,number]}"); + }); + }); + }); + + describe("DocComments", () => { + function parsesCorrectly(content: string, expected: string) { + let comment = parseIsolatedJSDocComment(content); + Debug.assert(comment && comment.diagnostics.length === 0); + + let result = JSON.stringify(comment.jsDocComment, (k, v) => { + return v && v.pos !== undefined + ? JSON.parse(Utils.sourceFileToJSON(v)) + : v; + }, " "); + + assert.equal(result, expected); + } + + function parsesIncorrectly(content: string) { + let type = parseIsolatedJSDocComment(content); + assert.isTrue(!type || type.diagnostics.length > 0); + } + + describe("parsesIncorrectly", () => { + it("emptyComment", () => { + parsesIncorrectly("/***/"); + }); + + it("threeAsterisks", () => { + parsesIncorrectly("/*** */"); + }); + + it("asteriskAfterPreamble", () => { + parsesIncorrectly("/** * @type {number} */"); + }); + + it("multipleTypes", () => { + parsesIncorrectly( +`/** + * @type {number} + * @type {string} + */`); + }); + + it("multipleReturnTypes", () => { + parsesIncorrectly( +`/** + * @return {number} + * @return {string} + */`); + }); + + it("noTypeParameters", () => { + parsesIncorrectly( +`/** + * @template + */`); + }); + + it("trailingTypeParameterComma", () => { + parsesIncorrectly( +`/** + * @template T, + */`); + }); + + it("paramWithoutName", () => { + parsesIncorrectly( +`/** + * @param {number} + */`); + }); + + it("paramWithoutTypeOrName", () => { + parsesIncorrectly( +`/** + * @param + */`); + }); + }); + + describe("parsesCorrectly", () => { + it("noLeadingAsterisk", () => { + parsesCorrectly( +`/** + @type {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 27, + "tags": { + "0": { + "kind": "JSDocTypeTag", + "pos": 8, + "end": 22, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 13, + "text": "type" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 14, + "end": 22, + "type": { + "kind": "NumberKeyword", + "pos": 15, + "end": 21 + } + } + }, + "length": 1, + "pos": 8, + "end": 22 + } +}`); + }); + + it("noType", () => { + parsesCorrectly( + `/** + * @type + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 18, + "tags": { + "0": { + "kind": "JSDocTypeTag", + "pos": 8, + "end": 13, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 13, + "text": "type" + } + }, + "length": 1, + "pos": 8, + "end": 13 + } +}`); + }); + + it("noReturnType", () => { + parsesCorrectly( + `/** + * @return + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 20, + "tags": { + "0": { + "kind": "JSDocReturnTag", + "pos": 8, + "end": 15, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 15, + "text": "return" + } + }, + "length": 1, + "pos": 8, + "end": 15 + } +}`); + }); + + it("leadingAsterisk", () => { + parsesCorrectly( +`/** + * @type {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 27, + "tags": { + "0": { + "kind": "JSDocTypeTag", + "pos": 8, + "end": 22, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 13, + "text": "type" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 14, + "end": 22, + "type": { + "kind": "NumberKeyword", + "pos": 15, + "end": 21 + } + } + }, + "length": 1, + "pos": 8, + "end": 22 + } +}`); + }); + + it("typeTag", () => { + parsesCorrectly( +`/** + * @type {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 27, + "tags": { + "0": { + "kind": "JSDocTypeTag", + "pos": 8, + "end": 22, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 13, + "text": "type" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 14, + "end": 22, + "type": { + "kind": "NumberKeyword", + "pos": 15, + "end": 21 + } + } + }, + "length": 1, + "pos": 8, + "end": 22 + } +}`); + }); + + it("returnTag1", () => { + parsesCorrectly( +`/** + * @return {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 29, + "tags": { + "0": { + "kind": "JSDocReturnTag", + "pos": 8, + "end": 24, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 15, + "text": "return" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 16, + "end": 24, + "type": { + "kind": "NumberKeyword", + "pos": 17, + "end": 23 + } + } + }, + "length": 1, + "pos": 8, + "end": 24 + } +}`); + }); + + it("returnTag2", () => { + parsesCorrectly( + `/** + * @return {number} Description text follows + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 54, + "tags": { + "0": { + "kind": "JSDocReturnTag", + "pos": 8, + "end": 24, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 15, + "text": "return" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 16, + "end": 24, + "type": { + "kind": "NumberKeyword", + "pos": 17, + "end": 23 + } + } + }, + "length": 1, + "pos": 8, + "end": 24 + } +}`); + }); + + it("returnsTag1", () => { + parsesCorrectly( + `/** + * @returns {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 30, + "tags": { + "0": { + "kind": "JSDocReturnTag", + "pos": 8, + "end": 25, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 16, + "text": "returns" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 17, + "end": 25, + "type": { + "kind": "NumberKeyword", + "pos": 18, + "end": 24 + } + } + }, + "length": 1, + "pos": 8, + "end": 25 + } +}`); + }); + + it("oneParamTag", () => { + parsesCorrectly( +`/** + * @param {number} name1 + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 34, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" + } + }, + "length": 1, + "pos": 8, + "end": 29 + } +}`); + }); + + it("twoParamTag2", () => { + parsesCorrectly( +`/** + * @param {number} name1 + * @param {number} name2 + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 60, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" + } + }, + "1": { + "kind": "JSDocParameterTag", + "pos": 34, + "end": 55, + "atToken": { + "kind": "AtToken", + "pos": 34, + "end": 35 + }, + "tagName": { + "kind": "Identifier", + "pos": 35, + "end": 40, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 41, + "end": 49, + "type": { + "kind": "NumberKeyword", + "pos": 42, + "end": 48 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 50, + "end": 55, + "text": "name2" + } + }, + "length": 2, + "pos": 8, + "end": 55 + } +}`); + }); + + it("paramTag1", () => { + parsesCorrectly( + `/** + * @param {number} name1 Description text follows + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 59, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" + } + }, + "length": 1, + "pos": 8, + "end": 29 + } +}`); + }); + + it("paramTagBracketedName1", () => { + parsesCorrectly( + `/** + * @param {number} [name1] Description text follows + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 61, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 30, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 25, + "end": 30, + "text": "name1" + }, + "isBracketed": true + }, + "length": 1, + "pos": 8, + "end": 30 + } +}`); + }); + + it("paramTagBracketedName2", () => { + parsesCorrectly( + `/** + * @param {number} [ name1 = 1] Description text follows + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 66, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 31, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 26, + "end": 31, + "text": "name1" + }, + "isBracketed": true + }, + "length": 1, + "pos": 8, + "end": 31 + } +}`); + }); + + it("twoParamTagOnSameLine", () => { + parsesCorrectly( +`/** + * @param {number} name1 @param {number} name2 + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 56, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 15, + "end": 23, + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + } + }, + "postParameterName": { + "kind": "Identifier", + "pos": 24, + "end": 29, + "text": "name1" + } + }, + "length": 1, + "pos": 8, + "end": 29 + } +}`); + }); + + it("paramTagNameThenType1", () => { + parsesCorrectly( + `/** + * @param name1 {number} + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 34, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "preParameterName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 21, + "end": 29, + "type": { + "kind": "NumberKeyword", + "pos": 22, + "end": 28 + } + } + }, + "length": 1, + "pos": 8, + "end": 29 + } +}`); + }); + + it("paramTagNameThenType2", () => { + parsesCorrectly( + `/** + * @param name1 {number} Description + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 46, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 29, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "preParameterName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, + "typeExpression": { + "kind": "JSDocTypeExpression", + "pos": 21, + "end": 29, + "type": { + "kind": "NumberKeyword", + "pos": 22, + "end": 28 + } + } + }, + "length": 1, + "pos": 8, + "end": 29 + } +}`); + }); + + it("templateTag", () => { + parsesCorrectly( +`/** + * @template T + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 24, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 19, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "T" + } + }, + "length": 1, + "pos": 17, + "end": 19 + } + }, + "length": 1, + "pos": 8, + "end": 19 + } +}`); + }); + + it("templateTag2", () => { + parsesCorrectly( + `/** + * @template K,V + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 26, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 21, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "K" + } + }, + "1": { + "kind": "TypeParameter", + "pos": 20, + "end": 21, + "name": { + "kind": "Identifier", + "pos": 20, + "end": 21, + "text": "V" + } + }, + "length": 2, + "pos": 17, + "end": 21 + } + }, + "length": 1, + "pos": 8, + "end": 21 + } +}`); + }); + + it("templateTag3", () => { + parsesCorrectly( + `/** + * @template K ,V + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 27, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 22, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "K" + } + }, + "1": { + "kind": "TypeParameter", + "pos": 21, + "end": 22, + "name": { + "kind": "Identifier", + "pos": 21, + "end": 22, + "text": "V" + } + }, + "length": 2, + "pos": 17, + "end": 22 + } + }, + "length": 1, + "pos": 8, + "end": 22 + } +}`); + }); + + it("templateTag4", () => { + parsesCorrectly( + `/** + * @template K, V + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 27, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 22, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "K" + } + }, + "1": { + "kind": "TypeParameter", + "pos": 21, + "end": 22, + "name": { + "kind": "Identifier", + "pos": 21, + "end": 22, + "text": "V" + } + }, + "length": 2, + "pos": 17, + "end": 22 + } + }, + "length": 1, + "pos": 8, + "end": 22 + } +}`); + }); + + it("templateTag5", () => { + parsesCorrectly( + `/** + * @template K , V + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 28, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 23, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "K" + } + }, + "1": { + "kind": "TypeParameter", + "pos": 22, + "end": 23, + "name": { + "kind": "Identifier", + "pos": 22, + "end": 23, + "text": "V" + } + }, + "length": 2, + "pos": 17, + "end": 23 + } + }, + "length": 1, + "pos": 8, + "end": 23 + } +}`); + }); + + it("templateTag6", () => { + parsesCorrectly( + `/** + * @template K , V Description of type parameters. + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 60, + "tags": { + "0": { + "kind": "JSDocTemplateTag", + "pos": 8, + "end": 24, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 17, + "text": "template" + }, + "typeParameters": { + "0": { + "kind": "TypeParameter", + "pos": 18, + "end": 19, + "name": { + "kind": "Identifier", + "pos": 18, + "end": 19, + "text": "K" + } + }, + "1": { + "kind": "TypeParameter", + "pos": 22, + "end": 23, + "name": { + "kind": "Identifier", + "pos": 22, + "end": 23, + "text": "V" + } + }, + "length": 2, + "pos": 17, + "end": 24 + } + }, + "length": 1, + "pos": 8, + "end": 24 + } +}`); + }); + + it("paramWithoutType", () => { + parsesCorrectly( + `/** + * @param foo + */`, + `{ + "kind": "JSDocComment", + "pos": 0, + "end": 23, + "tags": { + "0": { + "kind": "JSDocParameterTag", + "pos": 8, + "end": 18, + "atToken": { + "kind": "AtToken", + "pos": 8, + "end": 9 + }, + "tagName": { + "kind": "Identifier", + "pos": 9, + "end": 14, + "text": "param" + }, + "preParameterName": { + "kind": "Identifier", + "pos": 15, + "end": 18, + "text": "foo" + } + }, + "length": 1, + "pos": 8, + "end": 18 + } +}`); + }); + }); + }); + }); +} \ No newline at end of file