From 7ff91c1e1c225a441e5d9380a48d525f30864e2a Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Fri, 21 Jul 2017 14:04:14 -0700 Subject: [PATCH] Parse jsdoc type literals in params Now Typescript supports the creation of anonymous types using successive `@param` lines in JSDoc: ```js /** * @param {object} o - has a string and a number * @param {string} o.s - the string * @param {number} o.n - the number */ function f(o) { return o.s.length + o.n; } ``` This is equivalent to the Typescript syntax `{ s: string, n: number }`, but it allows per-property documentation, even for types that only need to be used in one place. (`@typedef` can be used for reusable types.) If the type of the initial `@param` is `{object[]}`, then the resulting type is an array of the specified anonymous type: ```js /** * @param {Object[]} os - has a string and a number * @param {string} os[].s - the string * @param {number} os[].n - the number */ function f(os) { return os[0].s; } ``` Finally, nested anonymous types can be created by nesting the pattern: ```js /** * @param {Object[]} os - has a string and a number * @param {string} os[].s - the string * @param {object} os[].nested - it's nested because of the object type * @param {number} os[].nested.length - it's a number */ function f(os) { return os[0].nested.length; } ``` Implementation notes: 1. I refactored JSDocParameterTag and JSDocPropertyTag to JSDocPropertyLikeTag and modified its parsing to be more succinct. These changes make the overall change easier to read but are not strictly required. 2. parseJSDocEntityName accepts postfix[] as in `os[].nested.length`, but it doesn't check that usages are correct. Such checking would be easy to add but tedious and low-value. 3. `@typedef` doesn't support nested `@property` tags, but does support `object[]` types. This is mostly a practical decision, backed up by the fact that usejsdoc.org doesn't document nested types for `@typedef`. --- src/compiler/binder.ts | 16 ++- src/compiler/checker.ts | 25 ++-- src/compiler/parser.ts | 271 +++++++++++++++++++++++------------- src/compiler/types.ts | 40 +++--- src/compiler/utilities.ts | 4 + src/services/classifier.ts | 16 +-- src/services/completions.ts | 2 +- src/services/jsDoc.ts | 4 +- 8 files changed, 228 insertions(+), 150 deletions(-) diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 0b58f080fe2..baf7f747539 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1504,9 +1504,9 @@ namespace ts { return declareSymbol(container.symbol.exports, container.symbol, node, symbolFlags, symbolExcludes); case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.ObjectLiteralExpression: case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.JsxAttributes: // Interface/Object-types always have their children added to the 'members' of // their container. They are only accessible through an instance of their @@ -2104,8 +2104,9 @@ namespace ts { case SyntaxKind.ConstructorType: return bindFunctionOrConstructorType(node); case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.MappedType: - return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode); + return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral); case SyntaxKind.ObjectLiteralExpression: return bindObjectLiteralExpression(node); case SyntaxKind.FunctionExpression: @@ -2163,13 +2164,16 @@ namespace ts { case SyntaxKind.ModuleBlock: return updateStrictModeStatementList((node).statements); + case SyntaxKind.JSDocParameterTag: + if (node.parent.kind !== SyntaxKind.JSDocTypeLiteral) { + break; + } + // falls through case SyntaxKind.JSDocPropertyTag: - return declareSymbolAndAddToSymbolTable(node as JSDocPropertyTag, - (node as JSDocPropertyTag).isBracketed || ((node as JSDocPropertyTag).typeExpression && (node as JSDocPropertyTag).typeExpression.type.kind === SyntaxKind.JSDocOptionalType) ? + return declareSymbolAndAddToSymbolTable(node as JSDocPropertyLikeTag, + (node as JSDocPropertyLikeTag).isBracketed || ((node as JSDocPropertyLikeTag).typeExpression && (node as JSDocPropertyLikeTag).typeExpression.type.kind === SyntaxKind.JSDocOptionalType) ? SymbolFlags.Property | SymbolFlags.Optional : SymbolFlags.Property, SymbolFlags.PropertyExcludes); - case SyntaxKind.JSDocTypeLiteral: - return bindAnonymousTypeWorker(node as JSDocTypeLiteral); case SyntaxKind.JSDocTypedefTag: { const { fullName } = node as JSDocTypedefTag; if (!fullName || fullName.kind === SyntaxKind.Identifier) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c2f40c169dc..c69cdaee8c2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5074,20 +5074,9 @@ namespace ts { return unknownType; } - let declaration: JSDocTypedefTag | TypeAliasDeclaration = getDeclarationOfKind(symbol, SyntaxKind.JSDocTypedefTag); - let type: Type; - if (declaration) { - if (declaration.jsDocTypeLiteral) { - type = getTypeFromTypeNode(declaration.jsDocTypeLiteral); - } - else { - type = getTypeFromTypeNode(declaration.typeExpression.type); - } - } - else { - declaration = getDeclarationOfKind(symbol, SyntaxKind.TypeAliasDeclaration); - type = getTypeFromTypeNode(declaration.type); - } + const declaration = getDeclarationOfKind(symbol, SyntaxKind.JSDocTypedefTag) || + getDeclarationOfKind(symbol, SyntaxKind.TypeAliasDeclaration); + let type = getTypeFromTypeNode(declaration.kind === SyntaxKind.JSDocTypedefTag ? declaration.typeExpression : declaration.type); if (popTypeResolution()) { const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); @@ -7654,9 +7643,12 @@ namespace ts { links.resolvedType = emptyTypeLiteralType; } else { - const type = createObjectType(ObjectFlags.Anonymous, node.symbol); + let type = createObjectType(ObjectFlags.Anonymous, node.symbol); type.aliasSymbol = aliasSymbol; type.aliasTypeArguments = getAliasTypeArgumentsForTypeNode(node); + if (isJSDocTypeLiteral(node) && node.isArrayType) { + type = createArrayType(type); + } links.resolvedType = type; } } @@ -7890,7 +7882,8 @@ namespace ts { case SyntaxKind.ParenthesizedType: case SyntaxKind.JSDocNonNullableType: case SyntaxKind.JSDocOptionalType: - return getTypeFromTypeNode((node).type); + case SyntaxKind.JSDocTypeExpression: + return getTypeFromTypeNode((node).type); case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: case SyntaxKind.TypeLiteral: diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 0be7734968f..c67e27150aa 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -59,6 +59,9 @@ namespace ts { * @param node a given node to visit its children * @param cbNode a callback to be invoked for all child nodes * @param cbNodes a callback to be invoked for embedded array + * + * @remarks `forEachChild` must visit the children of a node in the order + * that they appear in the source code. The language service depends on this property to locate nodes by position. */ export function forEachChild(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { if (!node || node.kind <= SyntaxKind.LastToken) { @@ -407,9 +410,15 @@ namespace ts { case SyntaxKind.JSDocComment: return visitNodes(cbNode, cbNodes, (node).tags); case SyntaxKind.JSDocParameterTag: - return visitNode(cbNode, (node).preParameterName) || - visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).postParameterName); + case SyntaxKind.JSDocPropertyTag: + if ((node as JSDocPropertyLikeTag).isParameterNameFirst) { + return visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).typeExpression); + } + else { + return visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).fullName); + } case SyntaxKind.JSDocReturnTag: return visitNode(cbNode, (node).typeExpression); case SyntaxKind.JSDocTypeTag: @@ -419,15 +428,19 @@ namespace ts { case SyntaxKind.JSDocTemplateTag: return visitNodes(cbNode, cbNodes, (node).typeParameters); case SyntaxKind.JSDocTypedefTag: - return visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).fullName) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).jsDocTypeLiteral); + if ((node as JSDocTypedefTag).typeExpression && + (node as JSDocTypedefTag).typeExpression.kind === SyntaxKind.JSDocTypeExpression) { + return visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).name); + } + else { + return visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).typeExpression); + } case SyntaxKind.JSDocTypeLiteral: return visitNodes(cbNode, cbNodes, (node).jsDocPropertyTags); - case SyntaxKind.JSDocPropertyTag: - return visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).name); case SyntaxKind.PartiallyEmittedExpression: return visitNode(cbNode, (node).expression); } @@ -6457,10 +6470,10 @@ namespace ts { }); } - function parseBracketNameInPropertyAndParamTag(): { name: Identifier, isBracketed: boolean } { - // Looking for something like '[foo]' or 'foo' + function parseBracketNameInPropertyAndParamTag(): { fullName: EntityName, isBracketed: boolean } { + // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' const isBracketed = parseOptional(SyntaxKind.OpenBracketToken); - const name = parseJSDocIdentifierName(/*createIfMissing*/ true); + const fullName = parseJSDocEntityName(/*createIfMissing*/ true); if (isBracketed) { skipWhitespace(); @@ -6472,36 +6485,66 @@ namespace ts { parseExpected(SyntaxKind.CloseBracketToken); } - return { name, isBracketed }; + return { fullName, isBracketed }; } - function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: boolean): JSDocPropertyTag | JSDocParameterTag { + function isObjectOrObjectArrayTypeReference(node: TypeNode): boolean { + return node.kind === SyntaxKind.ObjectKeyword || + isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object" || + node.kind === SyntaxKind.ArrayType && isObjectOrObjectArrayTypeReference((node as ArrayTypeNode).elementType); + } + + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: true): JSDocParameterTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: false): JSDocPropertyTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: boolean): JSDocPropertyLikeTag { let typeExpression = tryParseTypeExpression(); skipWhitespace(); - const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); + const { fullName, isBracketed } = parseBracketNameInPropertyAndParamTag(); skipWhitespace(); - let preName: Identifier, postName: Identifier; + let preName: EntityName, postName: EntityName; if (typeExpression) { - postName = name; + postName = fullName; } else { - preName = name; + preName = fullName; typeExpression = tryParseTypeExpression(); } - const result = shouldParseParamTag ? + const result: JSDocPropertyLikeTag = shouldParseParamTag ? createNode(SyntaxKind.JSDocParameterTag, atToken.pos) : createNode(SyntaxKind.JSDocPropertyTag, atToken.pos); + if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: JSDocPropertyLikeTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral; + const start = scanner.getStartPos(); + while (child = tryParse(() => parseChildParameterOrPropertyTag(/*shouldParseParamTag*/ true, fullName))) { + if (!jsdocTypeLiteral) { + jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); + jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; + } + (jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray).push(child as JSDocPropertyTag); + } + if (jsdocTypeLiteral) { + if (typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; + } + typeExpression.type = finishNode(jsdocTypeLiteral); + } + } result.atToken = atToken; result.tagName = tagName; - result.preParameterName = preName; result.typeExpression = typeExpression; - result.postParameterName = postName; - result.name = postName || preName; + if (typeExpression) { + result.type = typeExpression.type; + } + result.fullName = postName || preName; + result.name = ts.isIdentifier(result.fullName) ? result.fullName : result.fullName.right; + result.isParameterNameFirst = postName ? false : !!preName; result.isBracketed = isBracketed; return finishNode(result); + } function parseReturnTag(atToken: AtToken, tagName: Identifier): JSDocReturnTag { @@ -6565,68 +6608,44 @@ namespace ts { rightNode = rightNode.body; } } - typedefTag.typeExpression = typeExpression; skipWhitespace(); - if (typeExpression) { - if (isObjectTypeReference(typeExpression.type)) { - typedefTag.jsDocTypeLiteral = scanChildTags(); + typedefTag.typeExpression = typeExpression; + if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: JSDocTypeTag | JSDocPropertyTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral; + let alreadyHasTypeTag = false; + const start = scanner.getStartPos(); + while (child = tryParse(() => parseChildParameterOrPropertyTag(/*shouldParseParamTag*/ false))) { + if (!jsdocTypeLiteral) { + jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); + } + if (child.kind === SyntaxKind.JSDocTypeTag) { + if (alreadyHasTypeTag) { + break; + } + else { + jsdocTypeLiteral.jsDocTypeTag = child; + alreadyHasTypeTag = true; + } + } + else { + if (!jsdocTypeLiteral.jsDocPropertyTags) { + jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; + } + (jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray).push(child); + } } - if (!typedefTag.jsDocTypeLiteral) { - typedefTag.jsDocTypeLiteral = typeExpression.type; + if (jsdocTypeLiteral) { + if (typeExpression && typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; + } + typedefTag.typeExpression = finishNode(jsdocTypeLiteral); } } - else { - typedefTag.jsDocTypeLiteral = scanChildTags(); - } return finishNode(typedefTag); - function isObjectTypeReference(node: TypeNode) { - return node.kind === SyntaxKind.ObjectKeyword || - isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object"; - } - - function scanChildTags(): JSDocTypeLiteral { - const jsDocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, scanner.getStartPos()); - let resumePos = scanner.getStartPos(); - let canParseTag = true; - let seenAsterisk = false; - let parentTagTerminated = false; - - while (token() !== SyntaxKind.EndOfFileToken && !parentTagTerminated) { - nextJSDocToken(); - switch (token()) { - case SyntaxKind.AtToken: - if (canParseTag) { - parentTagTerminated = !tryParseChildTag(jsDocTypeLiteral); - if (!parentTagTerminated) { - resumePos = scanner.getStartPos(); - } - } - seenAsterisk = false; - break; - case SyntaxKind.NewLineTrivia: - resumePos = scanner.getStartPos() - 1; - canParseTag = true; - seenAsterisk = false; - break; - case SyntaxKind.AsteriskToken: - if (seenAsterisk) { - canParseTag = false; - } - seenAsterisk = true; - break; - case SyntaxKind.Identifier: - canParseTag = false; - break; - case SyntaxKind.EndOfFileToken: - break; - } - } - scanner.setTextPos(resumePos); - return finishNode(jsDocTypeLiteral); - } function parseJSDocTypeNameWithNamespace(flags: NodeFlags) { const pos = scanner.getTokenPos(); @@ -6647,8 +6666,61 @@ namespace ts { } } + function textsEqual(parent: EntityName, name: EntityName): boolean { + while (!ts.isIdentifier(parent) || !ts.isIdentifier(name)) { + if (!ts.isIdentifier(parent) && !ts.isIdentifier(name) && parent.right.text === name.right.text) { + parent = parent.left; + name = name.left; + } + else { + return false; + } + } + return parent.text === name.text; + } - function tryParseChildTag(parentTag: JSDocTypeLiteral): boolean { + function parseChildParameterOrPropertyTag(shouldParseParamTag: false): JSDocTypeTag | JSDocPropertyTag | false; + function parseChildParameterOrPropertyTag(shouldParseParamTag: true, fullName: EntityName): JSDocPropertyTag | JSDocParameterTag | false; + function parseChildParameterOrPropertyTag(shouldParseParamTag: boolean, fullName?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + let resumePos = scanner.getStartPos(); + let canParseTag = true; + let seenAsterisk = false; + while (token() !== SyntaxKind.EndOfFileToken) { + nextJSDocToken(); + switch (token()) { + case SyntaxKind.AtToken: + if (canParseTag) { + const child = tryParseChildTag(shouldParseParamTag); + if (child && child.kind === SyntaxKind.JSDocParameterTag && + (ts.isIdentifier(child.fullName) || !textsEqual(fullName, child.fullName.left))) { + break; + } + return child; + } + seenAsterisk = false; + break; + case SyntaxKind.NewLineTrivia: + resumePos = scanner.getStartPos() - 1; + canParseTag = true; + seenAsterisk = false; + break; + case SyntaxKind.AsteriskToken: + if (seenAsterisk) { + canParseTag = false; + } + seenAsterisk = true; + break; + case SyntaxKind.Identifier: + canParseTag = false; + break; + case SyntaxKind.EndOfFileToken: + break; + } + } + scanner.setTextPos(resumePos); + } + + function tryParseChildTag(shouldParseParamTag: boolean, alreadyHasTypeTag?: boolean): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { Debug.assert(token() === SyntaxKind.AtToken); const atToken = createNode(SyntaxKind.AtToken, scanner.getStartPos()); atToken.end = scanner.getTextPos(); @@ -6659,27 +6731,16 @@ namespace ts { if (!tagName) { return false; } - switch (tagName.text) { case "type": - if (parentTag.jsDocTypeTag) { - // already has a @type tag, terminate the parent tag now. - return false; - } - parentTag.jsDocTypeTag = parseTypeTag(atToken, tagName); - return true; + return !alreadyHasTypeTag && !shouldParseParamTag && parseTypeTag(atToken, tagName); case "prop": case "property": - const propertyTag = parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ false) as JSDocPropertyTag; - if (propertyTag) { - if (!parentTag.jsDocPropertyTags) { - parentTag.jsDocPropertyTags = >[]; - } - (parentTag.jsDocPropertyTags as MutableNodeArray).push(propertyTag); - return true; - } - // Error parsing property tag - return false; + return !shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ false); + case "arg": + case "argument": + case "param": + return shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ true); } return false; } @@ -6728,6 +6789,26 @@ namespace ts { return currentToken = scanner.scanJSDocToken(); } + function parseJSDocEntityName(createIfMissing = false): EntityName { + let entity: EntityName = parseJSDocIdentifierName(createIfMissing); + if (parseOptional(SyntaxKind.OpenBracketToken)) { + parseExpected(SyntaxKind.CloseBracketToken); + // Note that y[] is accepted as an entity name, but the postfix brackets are not saved for checking. + // Technically usejsdoc.org requires them for specifying a property of a type equivalent to Array<{ x: ...}> + // but it's not worth it to enforce that restriction. + } + while (parseOptional(SyntaxKind.DotToken)) { + const node: QualifiedName = createNode(SyntaxKind.QualifiedName, entity.pos) as QualifiedName; + node.left = entity; + node.right = parseJSDocIdentifierName(createIfMissing); + if (parseOptional(SyntaxKind.OpenBracketToken)) { + parseExpected(SyntaxKind.CloseBracketToken); + } + entity = finishNode(node); + } + return entity; + } + function parseJSDocIdentifierName(createIfMissing = false): Identifier { return createJSDocIdentifier(tokenIsIdentifierOrKeyword(token()), createIfMissing); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8431269046c..52e9a62e6a9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -760,6 +760,7 @@ namespace ts { // SyntaxKind.ShorthandPropertyAssignment // SyntaxKind.EnumMember // SyntaxKind.JSDocPropertyTag + // SyntaxKind.JSDocParameterTag export interface VariableLikeDeclaration extends NamedDeclaration { propertyName?: PropertyName; dotDotDotToken?: DotDotDotToken; @@ -2036,7 +2037,7 @@ namespace ts { } // represents a top level: { type } expression in a JSDoc comment. - export interface JSDocTypeExpression extends Node { + export interface JSDocTypeExpression extends TypeNode { kind: SyntaxKind.JSDocTypeExpression; type: TypeNode; } @@ -2125,38 +2126,33 @@ namespace ts { kind: SyntaxKind.JSDocTypedefTag; fullName?: JSDocNamespaceDeclaration | Identifier; name?: Identifier; - typeExpression?: JSDocTypeExpression; - jsDocTypeLiteral?: JSDocTypeLiteral; + typeExpression?: JSDocTypeExpression | JSDocTypeLiteral; } - export interface JSDocPropertyTag extends JSDocTag, TypeElement { + export interface JSDocPropertyLikeTag extends JSDocTag, VariableLikeDeclaration { parent: JSDoc; - kind: SyntaxKind.JSDocPropertyTag; + fullName?: EntityName; name: Identifier; - /** the parameter name, if provided *before* the type (TypeScript-style) */ - preParameterName?: Identifier; - /** the parameter name, if provided *after* the type (JSDoc-standard) */ - postParameterName?: Identifier; typeExpression: JSDocTypeExpression; + /** Whether the property name came before the type -- non-standard for JSDoc, but Typescript-like */ + isParameterNameFirst: boolean; isBracketed: boolean; } + export interface JSDocPropertyTag extends JSDocPropertyLikeTag { + kind: SyntaxKind.JSDocPropertyTag; + } + + export interface JSDocParameterTag extends JSDocPropertyLikeTag { + kind: SyntaxKind.JSDocParameterTag; + } + export interface JSDocTypeLiteral extends JSDocType { kind: SyntaxKind.JSDocTypeLiteral; - jsDocPropertyTags?: NodeArray; + jsDocPropertyTags?: NodeArray; jsDocTypeTag?: JSDocTypeTag; - } - - export interface JSDocParameterTag extends JSDocTag { - kind: SyntaxKind.JSDocParameterTag; - /** the parameter name, if provided *before* the type (TypeScript-style) */ - preParameterName?: Identifier; - typeExpression?: JSDocTypeExpression; - /** the parameter name, if provided *after* the type (JSDoc-standard) */ - postParameterName?: Identifier; - /** the parameter name, regardless of the location it was provided */ - name: Identifier; - isBracketed: boolean; + /** If true, then this type literal represents an *array* of its type. */ + isArrayType?: boolean; } export const enum FlowFlags { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e001ec43f62..983ff8012bd 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1553,6 +1553,10 @@ namespace ts { /** Does the opposite of `getJSDocParameterTags`: given a JSDoc parameter, finds the parameter corresponding to it. */ export function getParameterFromJSDoc(node: JSDocParameterTag): ParameterDeclaration | undefined { + if (!isIdentifier(node.fullName)) { + // `@param {T} obj.prop` is not a top-level param, so it doesn't map to a top-level parameter + return undefined; + } const name = node.name.text; const grandParent = node.parent!.parent!; Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment); diff --git a/src/services/classifier.ts b/src/services/classifier.ts index 146a513ffc5..8acce0a01fa 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -755,10 +755,10 @@ namespace ts { return; function processJSDocParameterTag(tag: JSDocParameterTag) { - if (tag.preParameterName) { - pushCommentRange(pos, tag.preParameterName.pos - pos); - pushClassification(tag.preParameterName.pos, tag.preParameterName.end - tag.preParameterName.pos, ClassificationType.parameterName); - pos = tag.preParameterName.end; + if (tag.isParameterNameFirst) { + pushCommentRange(pos, tag.name.pos - pos); + pushClassification(tag.name.pos, tag.name.end - tag.name.pos, ClassificationType.parameterName); + pos = tag.name.end; } if (tag.typeExpression) { @@ -767,10 +767,10 @@ namespace ts { pos = tag.typeExpression.end; } - if (tag.postParameterName) { - pushCommentRange(pos, tag.postParameterName.pos - pos); - pushClassification(tag.postParameterName.pos, tag.postParameterName.end - tag.postParameterName.pos, ClassificationType.parameterName); - pos = tag.postParameterName.end; + if (!tag.isParameterNameFirst) { + pushCommentRange(pos, tag.name.pos - pos); + pushClassification(tag.name.pos, tag.name.end - tag.name.pos, ClassificationType.parameterName); + pos = tag.name.end; } } } diff --git a/src/services/completions.ts b/src/services/completions.ts index d7f0701caf2..84e7b375ff0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -420,7 +420,7 @@ namespace ts.Completions { if (tag.tagName.pos <= position && position <= tag.tagName.end) { request = { kind: "JsDocTagName" }; } - if (isTagWithTypeExpression(tag) && tag.typeExpression) { + if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true); if (!currentToken || (!isDeclarationName(currentToken) && diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 070b96101e2..464d251d4c2 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -120,7 +120,7 @@ namespace ts.JsDoc { } export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { - const nameThusFar = unescapeLeadingUnderscores(tag.name.text); + const nameThusFar = isIdentifier(tag.fullName) ? unescapeLeadingUnderscores(tag.name.text) : undefined; const jsdoc = tag.parent; const fn = jsdoc.parent; if (!ts.isFunctionLike(fn)) return []; @@ -129,7 +129,7 @@ namespace ts.JsDoc { if (!isIdentifier(param.name)) return undefined; const name = unescapeLeadingUnderscores(param.name.text); - if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && t.name.text === name) + if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.fullName) && t.name.text === name) || nameThusFar !== undefined && !startsWith(name, nameThusFar)) { return undefined; }