From 7ff91c1e1c225a441e5d9380a48d525f30864e2a Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Fri, 21 Jul 2017 14:04:14 -0700 Subject: [PATCH 1/8] 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 0b58f080fe29d..baf7f74753970 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 c2f40c169dc3f..c69cdaee8c255 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 0be7734968fa9..c67e27150aa02 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 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: boolean): JSDocPropertyTag | JSDocParameterTag { + 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 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(parentTag: JSDocTypeLiteral): boolean { + 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 8431269046c23..52e9a62e6a99c 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 JSDocTypeLiteral extends JSDocType { - kind: SyntaxKind.JSDocTypeLiteral; - jsDocPropertyTags?: NodeArray; - jsDocTypeTag?: JSDocTypeTag; + export interface JSDocPropertyTag extends JSDocPropertyLikeTag { + kind: SyntaxKind.JSDocPropertyTag; } - export interface JSDocParameterTag extends JSDocTag { + export interface JSDocParameterTag extends JSDocPropertyLikeTag { 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; + } + + export interface JSDocTypeLiteral extends JSDocType { + kind: SyntaxKind.JSDocTypeLiteral; + jsDocPropertyTags?: NodeArray; + jsDocTypeTag?: JSDocTypeTag; + /** 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 e001ec43f624f..983ff8012bd13 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 146a513ffc5c9..8acce0a01fa4b 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 d7f0701caf2d9..84e7b375ff073 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 070b96101e254..464d251d4c279 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; } From 8d2d226aca56cc8c8c823024d76cfc10c5711cb7 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Fri, 21 Jul 2017 14:48:48 -0700 Subject: [PATCH 2/8] Update JSDocParsing unit test baselines --- ....parsesCorrectly.argSynonymForParamTag.json | 8 +++++++- ...esCorrectly.argumentSynonymForParamTag.json | 8 +++++++- ...ocComments.parsesCorrectly.oneParamTag.json | 8 +++++++- .../DocComments.parsesCorrectly.paramTag1.json | 8 +++++++- ...parsesCorrectly.paramTagBracketedName1.json | 8 +++++++- ...parsesCorrectly.paramTagBracketedName2.json | 8 +++++++- ....parsesCorrectly.paramTagNameThenType1.json | 18 ++++++++++++------ ....parsesCorrectly.paramTagNameThenType2.json | 18 ++++++++++++------ ...ments.parsesCorrectly.paramWithoutType.json | 3 ++- ...cComments.parsesCorrectly.twoParamTag2.json | 16 ++++++++++++++-- ....parsesCorrectly.twoParamTagOnSameLine.json | 16 ++++++++++++++-- ...esCorrectly.typedefTagWithChildrenTags.json | 18 +++++++++++++++--- 12 files changed, 111 insertions(+), 26 deletions(-) diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json index b47a07e8487b1..0a8bed8dd6dfb 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json @@ -28,7 +28,12 @@ "end": 20 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 14, + "end": 20 + }, + "fullName": { "kind": "Identifier", "pos": 22, "end": 27, @@ -40,6 +45,7 @@ "end": 27, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json index cf86fda872ee4..f450e6d3cdbf2 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json @@ -28,7 +28,12 @@ "end": 25 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 19, + "end": 25 + }, + "fullName": { "kind": "Identifier", "pos": 27, "end": 32, @@ -40,6 +45,7 @@ "end": 32, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json index 506487232e090..01c2dfa9cc6fe 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 24, "end": 29, @@ -40,6 +45,7 @@ "end": 29, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json index abc116d745292..e7469f7377724 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 24, "end": 29, @@ -40,6 +45,7 @@ "end": 29, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json index 1df54fadcd7b5..b3230d6473df4 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 25, "end": 30, @@ -40,6 +45,7 @@ "end": 30, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": true, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json index 5347b99be7a32..7f08a22a723de 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 26, "end": 31, @@ -40,6 +45,7 @@ "end": 31, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": true, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json index 9d66ca3dd5530..c5dbe539faab4 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json @@ -18,12 +18,6 @@ "end": 14, "text": "param" }, - "preParameterName": { - "kind": "Identifier", - "pos": 15, - "end": 20, - "text": "name1" - }, "typeExpression": { "kind": "JSDocTypeExpression", "pos": 21, @@ -34,12 +28,24 @@ "end": 28 } }, + "type": { + "kind": "NumberKeyword", + "pos": 22, + "end": 28 + }, + "fullName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, "name": { "kind": "Identifier", "pos": 15, "end": 20, "text": "name1" }, + "isParameterNameFirst": true, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json index 01d08a103c102..874aadf32c5ee 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json @@ -18,12 +18,6 @@ "end": 14, "text": "param" }, - "preParameterName": { - "kind": "Identifier", - "pos": 15, - "end": 20, - "text": "name1" - }, "typeExpression": { "kind": "JSDocTypeExpression", "pos": 21, @@ -34,12 +28,24 @@ "end": 28 } }, + "type": { + "kind": "NumberKeyword", + "pos": 22, + "end": 28 + }, + "fullName": { + "kind": "Identifier", + "pos": 15, + "end": 20, + "text": "name1" + }, "name": { "kind": "Identifier", "pos": 15, "end": 20, "text": "name1" }, + "isParameterNameFirst": true, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json index ab15d24f618bc..cd6a3ec63ebb9 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json @@ -18,7 +18,7 @@ "end": 14, "text": "param" }, - "preParameterName": { + "fullName": { "kind": "Identifier", "pos": 15, "end": 18, @@ -30,6 +30,7 @@ "end": 18, "text": "foo" }, + "isParameterNameFirst": true, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json index 5ba60030f1b84..7bd46fac5b8e0 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 24, "end": 29, @@ -40,6 +45,7 @@ "end": 29, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "" }, @@ -68,7 +74,12 @@ "end": 48 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 42, + "end": 48 + }, + "fullName": { "kind": "Identifier", "pos": 50, "end": 55, @@ -80,6 +91,7 @@ "end": 55, "text": "name2" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json index 7d26097bb3474..d38146ec5876e 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json @@ -28,7 +28,12 @@ "end": 22 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 16, + "end": 22 + }, + "fullName": { "kind": "Identifier", "pos": 24, "end": 29, @@ -40,6 +45,7 @@ "end": 29, "text": "name1" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "" }, @@ -68,7 +74,12 @@ "end": 44 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 38, + "end": 44 + }, + "fullName": { "kind": "Identifier", "pos": 46, "end": 51, @@ -80,6 +91,7 @@ "end": 51, "text": "name2" }, + "isParameterNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json index 370e139b51231..c6f0d250923a3 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json @@ -30,7 +30,7 @@ "end": 23, "text": "People" }, - "jsDocTypeLiteral": { + "typeExpression": { "kind": "JSDocTypeLiteral", "pos": 26, "end": 98, @@ -92,7 +92,12 @@ "end": 64 } }, - "postParameterName": { + "type": { + "kind": "NumberKeyword", + "pos": 58, + "end": 64 + }, + "fullName": { "kind": "Identifier", "pos": 66, "end": 69, @@ -104,6 +109,7 @@ "end": 69, "text": "age" }, + "isParameterNameFirst": false, "isBracketed": false }, { @@ -131,7 +137,12 @@ "end": 91 } }, - "postParameterName": { + "type": { + "kind": "StringKeyword", + "pos": 85, + "end": 91 + }, + "fullName": { "kind": "Identifier", "pos": 93, "end": 97, @@ -143,6 +154,7 @@ "end": 97, "text": "name" }, + "isParameterNameFirst": false, "isBracketed": false } ] From e942bbb6f24b58df997300deb5b13ea66e1f386c Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Fri, 21 Jul 2017 14:49:07 -0700 Subject: [PATCH 3/8] Test: jsdoc `@param` type literals --- .../jsdocParamTagTypeLiteral.symbols | 134 ++++++++++++++ .../reference/jsdocParamTagTypeLiteral.types | 171 ++++++++++++++++++ .../jsdoc/jsdocParamTagTypeLiteral.ts | 80 ++++++++ 3 files changed, 385 insertions(+) create mode 100644 tests/baselines/reference/jsdocParamTagTypeLiteral.symbols create mode 100644 tests/baselines/reference/jsdocParamTagTypeLiteral.types create mode 100644 tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts diff --git a/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols b/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols new file mode 100644 index 0000000000000..6add9c77cdeb0 --- /dev/null +++ b/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols @@ -0,0 +1,134 @@ +=== tests/cases/conformance/jsdoc/0.js === +/** + * @param {Object} notSpecial + * @param {string} unrelated - not actually related because it's not notSpecial.unrelated + */ +function normal(notSpecial) { +>normal : Symbol(normal, Decl(0.js, 0, 0)) +>notSpecial : Symbol(notSpecial, Decl(0.js, 4, 16)) + + notSpecial; // should just be 'any' +>notSpecial : Symbol(notSpecial, Decl(0.js, 4, 16)) +} +normal(12); +>normal : Symbol(normal, Decl(0.js, 0, 0)) + +/** + * @param {Object} opts1 doc1 + * @param {string} opts1.x doc2 + * @param {string=} opts1.y doc3 + * @param {string} [opts1.z] doc4 + * @param {string} [opts1.w="hi"] doc5 + */ +function foo1(opts1) { +>foo1 : Symbol(foo1, Decl(0.js, 7, 11)) +>opts1 : Symbol(opts1, Decl(0.js, 16, 14)) + + opts1.x; +>opts1.x : Symbol(x, Decl(0.js, 11, 3)) +>opts1 : Symbol(opts1, Decl(0.js, 16, 14)) +>x : Symbol(x, Decl(0.js, 11, 3)) +} + +foo1({x: 'abc'}); +>foo1 : Symbol(foo1, Decl(0.js, 7, 11)) +>x : Symbol(x, Decl(0.js, 20, 6)) + +/** + * @param {Object[]} opts2 + * @param {string} opts2[].anotherX + * @param {string=} opts2[].anotherY + */ +function foo2(/** @param opts2 bad idea theatre! */opts2) { +>foo2 : Symbol(foo2, Decl(0.js, 20, 17)) +>opts2 : Symbol(opts2, Decl(0.js, 27, 14)) + + opts2[0].anotherX; +>opts2[0].anotherX : Symbol(anotherX, Decl(0.js, 24, 3)) +>opts2 : Symbol(opts2, Decl(0.js, 27, 14)) +>anotherX : Symbol(anotherX, Decl(0.js, 24, 3)) +} + +foo2([{anotherX: "world"}]); +>foo2 : Symbol(foo2, Decl(0.js, 20, 17)) +>anotherX : Symbol(anotherX, Decl(0.js, 31, 7)) + +/** + * @param {object} opts3 + * @param {string} opts3.x + */ +function foo3(opts3) { +>foo3 : Symbol(foo3, Decl(0.js, 31, 28)) +>opts3 : Symbol(opts3, Decl(0.js, 37, 14)) + + opts3.x; +>opts3.x : Symbol(x, Decl(0.js, 35, 3)) +>opts3 : Symbol(opts3, Decl(0.js, 37, 14)) +>x : Symbol(x, Decl(0.js, 35, 3)) +} +foo3({x: 'abc'}); +>foo3 : Symbol(foo3, Decl(0.js, 31, 28)) +>x : Symbol(x, Decl(0.js, 40, 6)) + +/** + * @param {object[]} opts4 + * @param {string} opts4[].x + * @param {string=} opts4[].y + * @param {string} [opts4[].z] + * @param {string} [opts4[].w="hi"] + */ +function foo4(opts4) { +>foo4 : Symbol(foo4, Decl(0.js, 40, 17)) +>opts4 : Symbol(opts4, Decl(0.js, 49, 14)) + + opts4[0].x; +>opts4[0].x : Symbol(x, Decl(0.js, 44, 3)) +>opts4 : Symbol(opts4, Decl(0.js, 49, 14)) +>x : Symbol(x, Decl(0.js, 44, 3)) +} + +foo4([{ x: 'hi' }]); +>foo4 : Symbol(foo4, Decl(0.js, 40, 17)) +>x : Symbol(x, Decl(0.js, 53, 7)) + +/** + * @param {object[]} opts5 - Let's test out some multiple nesting levels + * @param {string} opts5[].help - (This one is just normal) + * @param {object} opts5[].what - Look at us go! Here's the first nest! + * @param {string} opts5[].what.a - (Another normal one) + * @param {Object[]} opts5[].what.bad - Now we're nesting inside a nested type + * @param {string} opts5[].what.bad[].idea - I don't think you can get back out of this level... + * @param {boolean} opts5[].what.bad[].oh - Oh ... that's how you do it. + * @param {number} opts5[].unnest - Here we are almost all the way back at the beginning. + */ +function foo5(opts5) { +>foo5 : Symbol(foo5, Decl(0.js, 53, 20)) +>opts5 : Symbol(opts5, Decl(0.js, 65, 14)) + + opts5[0].what.bad[0].idea; +>opts5[0].what.bad[0].idea : Symbol(idea, Decl(0.js, 61, 3)) +>opts5[0].what.bad : Symbol(bad, Decl(0.js, 60, 3)) +>opts5[0].what : Symbol(what, Decl(0.js, 58, 3)) +>opts5 : Symbol(opts5, Decl(0.js, 65, 14)) +>what : Symbol(what, Decl(0.js, 58, 3)) +>bad : Symbol(bad, Decl(0.js, 60, 3)) +>idea : Symbol(idea, Decl(0.js, 61, 3)) + + opts5[0].unnest; +>opts5[0].unnest : Symbol(unnest, Decl(0.js, 63, 3)) +>opts5 : Symbol(opts5, Decl(0.js, 65, 14)) +>unnest : Symbol(unnest, Decl(0.js, 63, 3)) +} + +foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]); +>foo5 : Symbol(foo5, Decl(0.js, 53, 20)) +>help : Symbol(help, Decl(0.js, 70, 7)) +>what : Symbol(what, Decl(0.js, 70, 21)) +>a : Symbol(a, Decl(0.js, 70, 29)) +>bad : Symbol(bad, Decl(0.js, 70, 37)) +>idea : Symbol(idea, Decl(0.js, 70, 45)) +>oh : Symbol(oh, Decl(0.js, 70, 59)) +>unnest : Symbol(unnest, Decl(0.js, 70, 75)) + +// TODO: Also write these ridiculous object[] / nested tests for @typedef as well + diff --git a/tests/baselines/reference/jsdocParamTagTypeLiteral.types b/tests/baselines/reference/jsdocParamTagTypeLiteral.types new file mode 100644 index 0000000000000..80ec6edf7b044 --- /dev/null +++ b/tests/baselines/reference/jsdocParamTagTypeLiteral.types @@ -0,0 +1,171 @@ +=== tests/cases/conformance/jsdoc/0.js === +/** + * @param {Object} notSpecial + * @param {string} unrelated - not actually related because it's not notSpecial.unrelated + */ +function normal(notSpecial) { +>normal : (notSpecial: any) => void +>notSpecial : any + + notSpecial; // should just be 'any' +>notSpecial : any +} +normal(12); +>normal(12) : void +>normal : (notSpecial: any) => void +>12 : 12 + +/** + * @param {Object} opts1 doc1 + * @param {string} opts1.x doc2 + * @param {string=} opts1.y doc3 + * @param {string} [opts1.z] doc4 + * @param {string} [opts1.w="hi"] doc5 + */ +function foo1(opts1) { +>foo1 : (opts1: { x: string; y?: string; z?: string; w?: string; }) => void +>opts1 : { x: string; y?: string; z?: string; w?: string; } + + opts1.x; +>opts1.x : string +>opts1 : { x: string; y?: string; z?: string; w?: string; } +>x : string +} + +foo1({x: 'abc'}); +>foo1({x: 'abc'}) : void +>foo1 : (opts1: { x: string; y?: string; z?: string; w?: string; }) => void +>{x: 'abc'} : { x: string; } +>x : string +>'abc' : "abc" + +/** + * @param {Object[]} opts2 + * @param {string} opts2[].anotherX + * @param {string=} opts2[].anotherY + */ +function foo2(/** @param opts2 bad idea theatre! */opts2) { +>foo2 : (opts2: { anotherX: string; anotherY?: string; }[]) => void +>opts2 : { anotherX: string; anotherY?: string; }[] + + opts2[0].anotherX; +>opts2[0].anotherX : string +>opts2[0] : { anotherX: string; anotherY?: string; } +>opts2 : { anotherX: string; anotherY?: string; }[] +>0 : 0 +>anotherX : string +} + +foo2([{anotherX: "world"}]); +>foo2([{anotherX: "world"}]) : void +>foo2 : (opts2: { anotherX: string; anotherY?: string; }[]) => void +>[{anotherX: "world"}] : { anotherX: string; }[] +>{anotherX: "world"} : { anotherX: string; } +>anotherX : string +>"world" : "world" + +/** + * @param {object} opts3 + * @param {string} opts3.x + */ +function foo3(opts3) { +>foo3 : (opts3: { x: string; }) => void +>opts3 : { x: string; } + + opts3.x; +>opts3.x : string +>opts3 : { x: string; } +>x : string +} +foo3({x: 'abc'}); +>foo3({x: 'abc'}) : void +>foo3 : (opts3: { x: string; }) => void +>{x: 'abc'} : { x: string; } +>x : string +>'abc' : "abc" + +/** + * @param {object[]} opts4 + * @param {string} opts4[].x + * @param {string=} opts4[].y + * @param {string} [opts4[].z] + * @param {string} [opts4[].w="hi"] + */ +function foo4(opts4) { +>foo4 : (opts4: { x: string; y?: string; z?: string; w?: string; }[]) => void +>opts4 : { x: string; y?: string; z?: string; w?: string; }[] + + opts4[0].x; +>opts4[0].x : string +>opts4[0] : { x: string; y?: string; z?: string; w?: string; } +>opts4 : { x: string; y?: string; z?: string; w?: string; }[] +>0 : 0 +>x : string +} + +foo4([{ x: 'hi' }]); +>foo4([{ x: 'hi' }]) : void +>foo4 : (opts4: { x: string; y?: string; z?: string; w?: string; }[]) => void +>[{ x: 'hi' }] : { x: string; }[] +>{ x: 'hi' } : { x: string; } +>x : string +>'hi' : "hi" + +/** + * @param {object[]} opts5 - Let's test out some multiple nesting levels + * @param {string} opts5[].help - (This one is just normal) + * @param {object} opts5[].what - Look at us go! Here's the first nest! + * @param {string} opts5[].what.a - (Another normal one) + * @param {Object[]} opts5[].what.bad - Now we're nesting inside a nested type + * @param {string} opts5[].what.bad[].idea - I don't think you can get back out of this level... + * @param {boolean} opts5[].what.bad[].oh - Oh ... that's how you do it. + * @param {number} opts5[].unnest - Here we are almost all the way back at the beginning. + */ +function foo5(opts5) { +>foo5 : (opts5: { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; }[]) => void +>opts5 : { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; }[] + + opts5[0].what.bad[0].idea; +>opts5[0].what.bad[0].idea : string +>opts5[0].what.bad[0] : { idea: string; oh: boolean; } +>opts5[0].what.bad : { idea: string; oh: boolean; }[] +>opts5[0].what : { a: string; bad: { idea: string; oh: boolean; }[]; } +>opts5[0] : { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; } +>opts5 : { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; }[] +>0 : 0 +>what : { a: string; bad: { idea: string; oh: boolean; }[]; } +>bad : { idea: string; oh: boolean; }[] +>0 : 0 +>idea : string + + opts5[0].unnest; +>opts5[0].unnest : number +>opts5[0] : { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; } +>opts5 : { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; }[] +>0 : 0 +>unnest : number +} + +foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]); +>foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]) : void +>foo5 : (opts5: { help: string; what: { a: string; bad: { idea: string; oh: boolean; }[]; }; unnest: number; }[]) => void +>[{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }] : { help: string; what: { a: string; bad: { idea: string; oh: false; }[]; }; unnest: number; }[] +>{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 } : { help: string; what: { a: string; bad: { idea: string; oh: false; }[]; }; unnest: number; } +>help : string +>"help" : "help" +>what : { a: string; bad: { idea: string; oh: false; }[]; } +>{ a: 'a', bad: [{ idea: 'idea', oh: false }] } : { a: string; bad: { idea: string; oh: false; }[]; } +>a : string +>'a' : "a" +>bad : { idea: string; oh: false; }[] +>[{ idea: 'idea', oh: false }] : { idea: string; oh: false; }[] +>{ idea: 'idea', oh: false } : { idea: string; oh: false; } +>idea : string +>'idea' : "idea" +>oh : boolean +>false : false +>unnest : number +>1 : 1 + +// TODO: Also write these ridiculous object[] / nested tests for @typedef as well + diff --git a/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts b/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts new file mode 100644 index 0000000000000..2c8d4cbafb63e --- /dev/null +++ b/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts @@ -0,0 +1,80 @@ +// @allowJS: true +// @checkJs: true +// @noEmit: true +// @strict: true +// @suppressOutputPathCheck: true + +// @Filename: 0.js +/** + * @param {Object} notSpecial + * @param {string} unrelated - not actually related because it's not notSpecial.unrelated + */ +function normal(notSpecial) { + notSpecial; // should just be 'any' +} +normal(12); + +/** + * @param {Object} opts1 doc1 + * @param {string} opts1.x doc2 + * @param {string=} opts1.y doc3 + * @param {string} [opts1.z] doc4 + * @param {string} [opts1.w="hi"] doc5 + */ +function foo1(opts1) { + opts1.x; +} + +foo1({x: 'abc'}); + +/** + * @param {Object[]} opts2 + * @param {string} opts2[].anotherX + * @param {string=} opts2[].anotherY + */ +function foo2(/** @param opts2 bad idea theatre! */opts2) { + opts2[0].anotherX; +} + +foo2([{anotherX: "world"}]); + +/** + * @param {object} opts3 + * @param {string} opts3.x + */ +function foo3(opts3) { + opts3.x; +} +foo3({x: 'abc'}); + +/** + * @param {object[]} opts4 + * @param {string} opts4[].x + * @param {string=} opts4[].y + * @param {string} [opts4[].z] + * @param {string} [opts4[].w="hi"] + */ +function foo4(opts4) { + opts4[0].x; +} + +foo4([{ x: 'hi' }]); + +/** + * @param {object[]} opts5 - Let's test out some multiple nesting levels + * @param {string} opts5[].help - (This one is just normal) + * @param {object} opts5[].what - Look at us go! Here's the first nest! + * @param {string} opts5[].what.a - (Another normal one) + * @param {Object[]} opts5[].what.bad - Now we're nesting inside a nested type + * @param {string} opts5[].what.bad[].idea - I don't think you can get back out of this level... + * @param {boolean} opts5[].what.bad[].oh - Oh ... that's how you do it. + * @param {number} opts5[].unnest - Here we are almost all the way back at the beginning. + */ +function foo5(opts5) { + opts5[0].what.bad[0].idea; + opts5[0].unnest; +} + +foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]); + +// TODO: Also write these ridiculous object[] / nested tests for @typedef as well From 59961394cb179e429bf155a9a9533cc3c4fbc777 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Fri, 21 Jul 2017 15:07:20 -0700 Subject: [PATCH 4/8] `@param` parsing:const enum to improve readability --- src/compiler/parser.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index c67e27150aa02..799f1333730a0 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -6168,6 +6168,11 @@ namespace ts { SavingComments, } + const enum PropertyLikeParse { + Property, + Parameter, + } + export function parseJSDocCommentWorker(start: number, length: number): JSDoc { const content = sourceText; start = start || 0; @@ -6347,7 +6352,7 @@ namespace ts { case "arg": case "argument": case "param": - tag = parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ true); + tag = parseParameterOrPropertyTag(atToken, tagName, PropertyLikeParse.Parameter); break; case "return": case "returns": @@ -6494,9 +6499,9 @@ namespace ts { 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 { + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse.Parameter): JSDocParameterTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse.Property): JSDocPropertyTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse): JSDocPropertyLikeTag { let typeExpression = tryParseTypeExpression(); skipWhitespace(); @@ -6512,14 +6517,14 @@ namespace ts { typeExpression = tryParseTypeExpression(); } - const result: JSDocPropertyLikeTag = shouldParseParamTag ? + const result: JSDocPropertyLikeTag = target ? 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))) { + while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.Parameter, fullName))) { if (!jsdocTypeLiteral) { jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; @@ -6616,7 +6621,7 @@ namespace ts { let jsdocTypeLiteral: JSDocTypeLiteral; let alreadyHasTypeTag = false; const start = scanner.getStartPos(); - while (child = tryParse(() => parseChildParameterOrPropertyTag(/*shouldParseParamTag*/ false))) { + while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.Property))) { if (!jsdocTypeLiteral) { jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); } @@ -6679,9 +6684,9 @@ namespace ts { return parent.text === name.text; } - 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 { + function parseChildParameterOrPropertyTag(target: PropertyLikeParse.Property): JSDocTypeTag | JSDocPropertyTag | false; + function parseChildParameterOrPropertyTag(target: PropertyLikeParse.Parameter, fullName: EntityName): JSDocPropertyTag | JSDocParameterTag | false; + function parseChildParameterOrPropertyTag(target: PropertyLikeParse, fullName?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { let resumePos = scanner.getStartPos(); let canParseTag = true; let seenAsterisk = false; @@ -6690,7 +6695,7 @@ namespace ts { switch (token()) { case SyntaxKind.AtToken: if (canParseTag) { - const child = tryParseChildTag(shouldParseParamTag); + const child = tryParseChildTag(target); if (child && child.kind === SyntaxKind.JSDocParameterTag && (ts.isIdentifier(child.fullName) || !textsEqual(fullName, child.fullName.left))) { break; @@ -6720,7 +6725,7 @@ namespace ts { scanner.setTextPos(resumePos); } - function tryParseChildTag(shouldParseParamTag: boolean, alreadyHasTypeTag?: boolean): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + function tryParseChildTag(target: PropertyLikeParse, alreadyHasTypeTag?: boolean): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { Debug.assert(token() === SyntaxKind.AtToken); const atToken = createNode(SyntaxKind.AtToken, scanner.getStartPos()); atToken.end = scanner.getTextPos(); @@ -6733,14 +6738,14 @@ namespace ts { } switch (tagName.text) { case "type": - return !alreadyHasTypeTag && !shouldParseParamTag && parseTypeTag(atToken, tagName); + return !alreadyHasTypeTag && target === PropertyLikeParse.Property && parseTypeTag(atToken, tagName); case "prop": case "property": - return !shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ false); + return target === PropertyLikeParse.Property && parseParameterOrPropertyTag(atToken, tagName, target); case "arg": case "argument": case "param": - return shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ true); + return target === PropertyLikeParse.Parameter && parseParameterOrPropertyTag(atToken, tagName, target); } return false; } From c55a043767d1f82cb17bfa49763a8eaf2baf4fed Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 25 Jul 2017 14:14:12 -0700 Subject: [PATCH 5/8] Address PR comments from Andy I'll take a look at Wesley's next and see if those require any changes. --- src/compiler/binder.ts | 9 ++-- src/compiler/checker.ts | 7 ++-- src/compiler/parser.ts | 41 +++++++++++-------- src/compiler/types.ts | 2 +- src/compiler/utilities.ts | 14 +++---- src/services/classifier.ts | 4 +- .../findAllReferencesJsDocTypeLiteral.ts | 22 ++++++++++ 7 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 tests/cases/fourslash/findAllReferencesJsDocTypeLiteral.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index baf7f74753970..3c26dedea64f3 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2170,10 +2170,11 @@ namespace ts { } // falls through case SyntaxKind.JSDocPropertyTag: - 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); + const propTag = node as JSDocPropertyLikeTag; + const flags = propTag.isBracketed || propTag.typeExpression.type.kind === SyntaxKind.JSDocOptionalType ? + SymbolFlags.Property | SymbolFlags.Optional : + SymbolFlags.Property; + return declareSymbolAndAddToSymbolTable(propTag, flags, SymbolFlags.PropertyExcludes); 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 c69cdaee8c255..b7e08ea41a04e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5074,8 +5074,8 @@ namespace ts { return unknownType; } - const declaration = getDeclarationOfKind(symbol, SyntaxKind.JSDocTypedefTag) || - getDeclarationOfKind(symbol, SyntaxKind.TypeAliasDeclaration); + const declaration = findDeclaration( + symbol, d => d.kind === SyntaxKind.JSDocTypedefTag || d.kind === SyntaxKind.TypeAliasDeclaration); let type = getTypeFromTypeNode(declaration.kind === SyntaxKind.JSDocTypedefTag ? declaration.typeExpression : declaration.type); if (popTypeResolution()) { @@ -22610,8 +22610,7 @@ namespace ts { } if (entityName.parent!.kind === SyntaxKind.JSDocParameterTag) { - const parameter = getParameterFromJSDoc(entityName.parent as JSDocParameterTag); - return parameter && parameter.symbol; + return getParameterSymbolFromJSDoc(entityName.parent as JSDocParameterTag); } if (entityName.parent.kind === SyntaxKind.TypeParameter && entityName.parent.parent.kind === SyntaxKind.JSDocTemplateTag) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 799f1333730a0..a0bd6c02c519c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -411,7 +411,7 @@ namespace ts { return visitNodes(cbNode, cbNodes, (node).tags); case SyntaxKind.JSDocParameterTag: case SyntaxKind.JSDocPropertyTag: - if ((node as JSDocPropertyLikeTag).isParameterNameFirst) { + if ((node as JSDocPropertyLikeTag).isNameFirst) { return visitNode(cbNode, (node).fullName) || visitNode(cbNode, (node).typeExpression); } @@ -431,12 +431,10 @@ namespace ts { 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); + visitNode(cbNode, (node).fullName); } else { return visitNode(cbNode, (node).fullName) || - visitNode(cbNode, (node).name) || visitNode(cbNode, (node).typeExpression); } case SyntaxKind.JSDocTypeLiteral: @@ -6520,7 +6518,27 @@ namespace ts { const result: JSDocPropertyLikeTag = target ? createNode(SyntaxKind.JSDocParameterTag, atToken.pos) : createNode(SyntaxKind.JSDocPropertyTag, atToken.pos); + const nestedTypeLiteral = parseNestedTypeLiteral(typeExpression, fullName); + if (nestedTypeLiteral) { + typeExpression = nestedTypeLiteral; + } + result.atToken = atToken; + result.tagName = tagName; + result.typeExpression = typeExpression; + if (typeExpression) { + result.type = typeExpression.type; + } + result.fullName = postName || preName; + result.name = ts.isIdentifier(result.fullName) ? result.fullName : result.fullName.right; + result.isNameFirst = !!nestedTypeLiteral || (postName ? false : !!preName); + result.isBracketed = isBracketed; + return finishNode(result); + + } + + function parseNestedTypeLiteral(typeExpression: JSDocTypeExpression, fullName: EntityName) { if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { + const typeLiteralExpression = createNode(SyntaxKind.JSDocTypeExpression, scanner.getTokenPos()); let child: JSDocPropertyLikeTag | false; let jsdocTypeLiteral: JSDocTypeLiteral; const start = scanner.getStartPos(); @@ -6535,21 +6553,10 @@ namespace ts { if (typeExpression.type.kind === SyntaxKind.ArrayType) { jsdocTypeLiteral.isArrayType = true; } - typeExpression.type = finishNode(jsdocTypeLiteral); + typeLiteralExpression.type = finishNode(jsdocTypeLiteral); + return finishNode(typeLiteralExpression); } } - result.atToken = atToken; - result.tagName = tagName; - result.typeExpression = typeExpression; - 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 { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 52e9a62e6a99c..f608684a13684 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2135,7 +2135,7 @@ namespace ts { name: Identifier; typeExpression: JSDocTypeExpression; /** Whether the property name came before the type -- non-standard for JSDoc, but Typescript-like */ - isParameterNameFirst: boolean; + isNameFirst: boolean; isBracketed: boolean; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 983ff8012bd13..f20a2d422fdfb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1552,19 +1552,19 @@ 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; + export function getParameterSymbolFromJSDoc(node: JSDocParameterTag): Symbol | undefined { + if (node.symbol) { + return node.symbol; } const name = node.name.text; - const grandParent = node.parent!.parent!; Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment); - if (!isFunctionLike(grandParent)) { + const func = node.parent!.parent!; + if (!isFunctionLike(func)) { return undefined; } - return find(grandParent.parameters, p => + const parameter = find(func.parameters, p => p.name.kind === SyntaxKind.Identifier && p.name.text === name); + return parameter && parameter.symbol; } export function getTypeParameterFromJsDoc(node: TypeParameterDeclaration & { parent: JSDocTemplateTag }): TypeParameterDeclaration | undefined { diff --git a/src/services/classifier.ts b/src/services/classifier.ts index 8acce0a01fa4b..7c61f249dcb93 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -755,7 +755,7 @@ namespace ts { return; function processJSDocParameterTag(tag: JSDocParameterTag) { - if (tag.isParameterNameFirst) { + if (tag.isNameFirst) { pushCommentRange(pos, tag.name.pos - pos); pushClassification(tag.name.pos, tag.name.end - tag.name.pos, ClassificationType.parameterName); pos = tag.name.end; @@ -767,7 +767,7 @@ namespace ts { pos = tag.typeExpression.end; } - if (!tag.isParameterNameFirst) { + if (!tag.isNameFirst) { 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/tests/cases/fourslash/findAllReferencesJsDocTypeLiteral.ts b/tests/cases/fourslash/findAllReferencesJsDocTypeLiteral.ts new file mode 100644 index 0000000000000..6d1b32b1515a7 --- /dev/null +++ b/tests/cases/fourslash/findAllReferencesJsDocTypeLiteral.ts @@ -0,0 +1,22 @@ +// @allowJs: true +// @checkJs: true +/// + +// @Filename: foo.js +/////** +//// * @param {object} o - very important! +//// * @param {string} o.x - a thing, its ok +//// * @param {number} o.y - another thing +//// * @param {Object} o.nested - very nested +//// * @param {boolean} o.nested.[|great|] - much greatness +//// * @param {number} o.nested.times - twice? probably!?? +//// */ +//// function f(o) { return o.nested.[|great|]; } + +verify.rangesReferenceEachOther(); + +///** +// * @param {object} [|o|] - very important! +// * @param {string} o.x - a thing, its ok +// */ +// function f([|o|]) { return [|o|].x; } From 58ad1648138939ca534cc61bff9bb7804d07215a Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 25 Jul 2017 14:14:45 -0700 Subject: [PATCH 6/8] Update baselines --- .../DocComments.parsesCorrectly.argSynonymForParamTag.json | 2 +- ...ocComments.parsesCorrectly.argumentSynonymForParamTag.json | 2 +- .../JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json | 2 +- .../JSDocParsing/DocComments.parsesCorrectly.paramTag1.json | 2 +- .../DocComments.parsesCorrectly.paramTagBracketedName1.json | 2 +- .../DocComments.parsesCorrectly.paramTagBracketedName2.json | 2 +- .../DocComments.parsesCorrectly.paramTagNameThenType1.json | 2 +- .../DocComments.parsesCorrectly.paramTagNameThenType2.json | 2 +- .../DocComments.parsesCorrectly.paramWithoutType.json | 2 +- .../DocComments.parsesCorrectly.twoParamTag2.json | 4 ++-- .../DocComments.parsesCorrectly.twoParamTagOnSameLine.json | 4 ++-- ...ocComments.parsesCorrectly.typedefTagWithChildrenTags.json | 4 ++-- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json index 0a8bed8dd6dfb..0500c1d962f16 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json @@ -45,7 +45,7 @@ "end": 27, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json index f450e6d3cdbf2..40f2a47ba8eec 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json @@ -45,7 +45,7 @@ "end": 32, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json index 01c2dfa9cc6fe..2fa1905fee14e 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json @@ -45,7 +45,7 @@ "end": 29, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json index e7469f7377724..7c13bd6b12cc6 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json @@ -45,7 +45,7 @@ "end": 29, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json index b3230d6473df4..c927d2beec60e 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json @@ -45,7 +45,7 @@ "end": 30, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": true, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json index 7f08a22a723de..e7f0b8eb8c24c 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json @@ -45,7 +45,7 @@ "end": 31, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": true, "comment": "Description text follows" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json index c5dbe539faab4..5293bbc7363e1 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json @@ -45,7 +45,7 @@ "end": 20, "text": "name1" }, - "isParameterNameFirst": true, + "isNameFirst": true, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json index 874aadf32c5ee..204e1761454cd 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json @@ -45,7 +45,7 @@ "end": 20, "text": "name1" }, - "isParameterNameFirst": true, + "isNameFirst": true, "isBracketed": false, "comment": "Description" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json index cd6a3ec63ebb9..2b33ad391ae9d 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json @@ -30,7 +30,7 @@ "end": 18, "text": "foo" }, - "isParameterNameFirst": true, + "isNameFirst": true, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json index 7bd46fac5b8e0..4a1bc8886993c 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json @@ -45,7 +45,7 @@ "end": 29, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "" }, @@ -91,7 +91,7 @@ "end": 55, "text": "name2" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json index d38146ec5876e..329d884873350 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json @@ -45,7 +45,7 @@ "end": 29, "text": "name1" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "" }, @@ -91,7 +91,7 @@ "end": 51, "text": "name2" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false, "comment": "" }, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json index c6f0d250923a3..1fc8eefd8db58 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json @@ -109,7 +109,7 @@ "end": 69, "text": "age" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false }, { @@ -154,7 +154,7 @@ "end": 97, "text": "name" }, - "isParameterNameFirst": false, + "isNameFirst": false, "isBracketed": false } ] From fde4c188ac7976b0d4500e1ccc4ef0b7b680f1b7 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 26 Jul 2017 10:57:29 -0700 Subject: [PATCH 7/8] Address more PR comments --- src/compiler/checker.ts | 4 +- src/compiler/parser.ts | 162 +++++++++++++++++++------------------- src/compiler/types.ts | 7 +- src/compiler/utilities.ts | 19 +++-- src/services/jsDoc.ts | 7 +- 5 files changed, 105 insertions(+), 94 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b7e08ea41a04e..c4ccbd17f5c93 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4493,8 +4493,8 @@ namespace ts { if (declaration.kind === SyntaxKind.ExportAssignment) { return links.type = checkExpression((declaration).expression); } - if (isInJavaScriptFile(declaration) && declaration.kind === SyntaxKind.JSDocPropertyTag && (declaration).typeExpression) { - return links.type = getTypeFromTypeNode((declaration).typeExpression.type); + if (isInJavaScriptFile(declaration) && isJSDocPropertyLikeTag(declaration) && declaration.typeExpression) { + return links.type = getTypeFromTypeNode(declaration.typeExpression.type); } // Handle variable, parameter or property if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index a0bd6c02c519c..afc7b0e97b814 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -412,12 +412,12 @@ namespace ts { case SyntaxKind.JSDocParameterTag: case SyntaxKind.JSDocPropertyTag: if ((node as JSDocPropertyLikeTag).isNameFirst) { - return visitNode(cbNode, (node).fullName) || + return visitNode(cbNode, (node).name) || visitNode(cbNode, (node).typeExpression); } else { return visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).fullName); + visitNode(cbNode, (node).name); } case SyntaxKind.JSDocReturnTag: return visitNode(cbNode, (node).typeExpression); @@ -438,7 +438,10 @@ namespace ts { visitNode(cbNode, (node).typeExpression); } case SyntaxKind.JSDocTypeLiteral: - return visitNodes(cbNode, cbNodes, (node).jsDocPropertyTags); + for (const tag of (node as JSDocTypeLiteral).jsDocPropertyTags) { + visitNode(cbNode, tag); + } + return; case SyntaxKind.PartiallyEmittedExpression: return visitNode(cbNode, (node).expression); } @@ -1943,14 +1946,18 @@ namespace ts { break; } dotPos = scanner.getStartPos(); - const node: QualifiedName = createNode(SyntaxKind.QualifiedName, entity.pos); - node.left = entity; - node.right = parseRightSideOfDot(allowReservedWords); - entity = finishNode(node); + entity = createQualifiedName(entity, parseRightSideOfDot(allowReservedWords)); } return entity; } + function createQualifiedName(entity: EntityName, name: Identifier): QualifiedName { + const node = createNode(SyntaxKind.QualifiedName, entity.pos) as QualifiedName; + node.left = entity; + node.right = name; + return finishNode(node); + } + function parseRightSideOfDot(allowIdentifierNames: boolean): Identifier { // Technically a keyword is valid here as all identifiers and keywords are identifier names. // However, often we'll encounter this in error situations when the identifier or keyword @@ -6473,10 +6480,10 @@ namespace ts { }); } - function parseBracketNameInPropertyAndParamTag(): { fullName: EntityName, isBracketed: boolean } { + function parseBracketNameInPropertyAndParamTag(): { name: EntityName, isBracketed: boolean } { // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' const isBracketed = parseOptional(SyntaxKind.OpenBracketToken); - const fullName = parseJSDocEntityName(/*createIfMissing*/ true); + const name = parseJSDocEntityName(); if (isBracketed) { skipWhitespace(); @@ -6488,68 +6495,68 @@ namespace ts { parseExpected(SyntaxKind.CloseBracketToken); } - return { fullName, isBracketed }; + return { name, isBracketed }; } 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); + switch (node.kind) { + case SyntaxKind.ObjectKeyword: + return true; + case SyntaxKind.ArrayType: + return isObjectOrObjectArrayTypeReference((node as ArrayTypeNode).elementType); + default: + return isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object"; + } } function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse.Parameter): JSDocParameterTag; function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse.Property): JSDocPropertyTag; function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, target: PropertyLikeParse): JSDocPropertyLikeTag { let typeExpression = tryParseTypeExpression(); + let isNameFirst = !typeExpression; skipWhitespace(); - const { fullName, isBracketed } = parseBracketNameInPropertyAndParamTag(); + const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); skipWhitespace(); - let preName: EntityName, postName: EntityName; - if (typeExpression) { - postName = fullName; - } - else { - preName = fullName; + if (isNameFirst) { typeExpression = tryParseTypeExpression(); } - const result: JSDocPropertyLikeTag = target ? + const result: JSDocPropertyLikeTag = target === PropertyLikeParse.Parameter ? createNode(SyntaxKind.JSDocParameterTag, atToken.pos) : createNode(SyntaxKind.JSDocPropertyTag, atToken.pos); - const nestedTypeLiteral = parseNestedTypeLiteral(typeExpression, fullName); + const nestedTypeLiteral = parseNestedTypeLiteral(typeExpression, name); if (nestedTypeLiteral) { typeExpression = nestedTypeLiteral; + isNameFirst = true; } result.atToken = atToken; result.tagName = tagName; result.typeExpression = typeExpression; - if (typeExpression) { - result.type = typeExpression.type; - } - result.fullName = postName || preName; - result.name = ts.isIdentifier(result.fullName) ? result.fullName : result.fullName.right; - result.isNameFirst = !!nestedTypeLiteral || (postName ? false : !!preName); + result.name = name; + result.isNameFirst = isNameFirst; result.isBracketed = isBracketed; return finishNode(result); } - function parseNestedTypeLiteral(typeExpression: JSDocTypeExpression, fullName: EntityName) { + function parseNestedTypeLiteral(typeExpression: JSDocTypeExpression, name: EntityName) { if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { const typeLiteralExpression = createNode(SyntaxKind.JSDocTypeExpression, scanner.getTokenPos()); - let child: JSDocPropertyLikeTag | false; + let child: JSDocParameterTag | false; let jsdocTypeLiteral: JSDocTypeLiteral; const start = scanner.getStartPos(); - while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.Parameter, fullName))) { - if (!jsdocTypeLiteral) { - jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); - jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; + let children: JSDocParameterTag[]; + while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.Parameter, name))) { + if (!children) { + children = []; } - (jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray).push(child as JSDocPropertyTag); + children.push(child); } - if (jsdocTypeLiteral) { + if (children) { + jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); + jsdocTypeLiteral.jsDocPropertyTags = children; if (typeExpression.type.kind === SyntaxKind.ArrayType) { jsdocTypeLiteral.isArrayType = true; } @@ -6678,61 +6685,58 @@ 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; + function textsEqual(a: EntityName, b: EntityName): boolean { + while (!ts.isIdentifier(a) || !ts.isIdentifier(b)) { + if (!ts.isIdentifier(a) && !ts.isIdentifier(b) && a.right.text === b.right.text) { + a = a.left; + b = b.left; } else { return false; } } - return parent.text === name.text; + return a.text === b.text; } function parseChildParameterOrPropertyTag(target: PropertyLikeParse.Property): JSDocTypeTag | JSDocPropertyTag | false; - function parseChildParameterOrPropertyTag(target: PropertyLikeParse.Parameter, fullName: EntityName): JSDocPropertyTag | JSDocParameterTag | false; - function parseChildParameterOrPropertyTag(target: PropertyLikeParse, fullName?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { - let resumePos = scanner.getStartPos(); + function parseChildParameterOrPropertyTag(target: PropertyLikeParse.Parameter, name: EntityName): JSDocParameterTag | false; + function parseChildParameterOrPropertyTag(target: PropertyLikeParse, name?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { let canParseTag = true; let seenAsterisk = false; - while (token() !== SyntaxKind.EndOfFileToken) { + while (true) { nextJSDocToken(); switch (token()) { - case SyntaxKind.AtToken: - if (canParseTag) { - const child = tryParseChildTag(target); - if (child && child.kind === SyntaxKind.JSDocParameterTag && - (ts.isIdentifier(child.fullName) || !textsEqual(fullName, child.fullName.left))) { - break; + case SyntaxKind.AtToken: + if (canParseTag) { + const child = tryParseChildTag(target); + if (child && child.kind === SyntaxKind.JSDocParameterTag && + (ts.isIdentifier(child.name) || !textsEqual(name, child.name.left))) { + return false; + } + return child; } - return child; - } - seenAsterisk = false; - break; - case SyntaxKind.NewLineTrivia: - resumePos = scanner.getStartPos() - 1; - canParseTag = true; - seenAsterisk = false; - break; - case SyntaxKind.AsteriskToken: - if (seenAsterisk) { + seenAsterisk = false; + break; + case SyntaxKind.NewLineTrivia: + canParseTag = true; + seenAsterisk = false; + break; + case SyntaxKind.AsteriskToken: + if (seenAsterisk) { + canParseTag = false; + } + seenAsterisk = true; + break; + case SyntaxKind.Identifier: canParseTag = false; - } - seenAsterisk = true; - break; - case SyntaxKind.Identifier: - canParseTag = false; - break; - case SyntaxKind.EndOfFileToken: - break; + break; + case SyntaxKind.EndOfFileToken: + return false; } } - scanner.setTextPos(resumePos); } - function tryParseChildTag(target: PropertyLikeParse, alreadyHasTypeTag?: boolean): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + function tryParseChildTag(target: PropertyLikeParse): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { Debug.assert(token() === SyntaxKind.AtToken); const atToken = createNode(SyntaxKind.AtToken, scanner.getStartPos()); atToken.end = scanner.getTextPos(); @@ -6745,7 +6749,7 @@ namespace ts { } switch (tagName.text) { case "type": - return !alreadyHasTypeTag && target === PropertyLikeParse.Property && parseTypeTag(atToken, tagName); + return target === PropertyLikeParse.Property && parseTypeTag(atToken, tagName); case "prop": case "property": return target === PropertyLikeParse.Property && parseParameterOrPropertyTag(atToken, tagName, target); @@ -6801,8 +6805,8 @@ namespace ts { return currentToken = scanner.scanJSDocToken(); } - function parseJSDocEntityName(createIfMissing = false): EntityName { - let entity: EntityName = parseJSDocIdentifierName(createIfMissing); + function parseJSDocEntityName(): EntityName { + let entity: EntityName = parseJSDocIdentifierName(/*createIfMissing*/ true); 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. @@ -6810,13 +6814,11 @@ namespace ts { // 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); + const name = parseJSDocIdentifierName(/*createIfMissing*/ true); if (parseOptional(SyntaxKind.OpenBracketToken)) { parseExpected(SyntaxKind.CloseBracketToken); } - entity = finishNode(node); + entity = createQualifiedName(entity, name); } return entity; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f608684a13684..f8170d6223849 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2129,10 +2129,9 @@ namespace ts { typeExpression?: JSDocTypeExpression | JSDocTypeLiteral; } - export interface JSDocPropertyLikeTag extends JSDocTag, VariableLikeDeclaration { + export interface JSDocPropertyLikeTag extends JSDocTag, Declaration { parent: JSDoc; - fullName?: EntityName; - name: Identifier; + name: EntityName; typeExpression: JSDocTypeExpression; /** Whether the property name came before the type -- non-standard for JSDoc, but Typescript-like */ isNameFirst: boolean; @@ -2149,7 +2148,7 @@ namespace ts { export interface JSDocTypeLiteral extends JSDocType { kind: SyntaxKind.JSDocTypeLiteral; - jsDocPropertyTags?: NodeArray; + jsDocPropertyTags?: ReadonlyArray; jsDocTypeTag?: JSDocTypeTag; /** If true, then this type literal represents an *array* of its type. */ isArrayType?: boolean; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f20a2d422fdfb..1db25a0d211a5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1542,13 +1542,10 @@ namespace ts { export function getJSDocParameterTags(param: ParameterDeclaration): JSDocParameterTag[] | undefined { if (param.name && isIdentifier(param.name)) { const name = param.name.text; - return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && tag.name.text === name) as JSDocParameterTag[]; - } - else { - // TODO: it's a destructured parameter, so it should look up an "object type" series of multiple lines - // But multi-line object types aren't supported yet either - return undefined; + return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && isIdentifier(tag.name) && tag.name.text === name) as JSDocParameterTag[]; } + // a binding pattern doesn't have a name, so it's not possible to match it a jsdoc parameter, which is identified by name + return undefined; } /** Does the opposite of `getJSDocParameterTags`: given a JSDoc parameter, finds the parameter corresponding to it. */ @@ -1556,6 +1553,9 @@ namespace ts { if (node.symbol) { return node.symbol; } + if (!isIdentifier(node.name)) { + return undefined; + } const name = node.name.text; Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment); const func = node.parent!.parent!; @@ -4049,6 +4049,9 @@ namespace ts { if (!declaration) { return undefined; } + if (isJSDocPropertyLikeTag(declaration) && declaration.name.kind === SyntaxKind.QualifiedName) { + return declaration.name.right; + } if (declaration.kind === SyntaxKind.BinaryExpression) { const expr = declaration as BinaryExpression; switch (getSpecialPropertyAssignmentKind(expr)) { @@ -4707,6 +4710,10 @@ namespace ts { return node.kind === SyntaxKind.JSDocPropertyTag; } + export function isJSDocPropertyLikeTag(node: Node): node is JSDocPropertyLikeTag { + return node.kind === SyntaxKind.JSDocPropertyTag || node.kind === SyntaxKind.JSDocParameterTag; + } + export function isJSDocTypeLiteral(node: Node): node is JSDocTypeLiteral { return node.kind === SyntaxKind.JSDocTypeLiteral; } diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 464d251d4c279..2575d26cdddb1 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -120,7 +120,10 @@ namespace ts.JsDoc { } export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { - const nameThusFar = isIdentifier(tag.fullName) ? unescapeLeadingUnderscores(tag.name.text) : undefined; + if (!isIdentifier(tag.name)) { + return emptyArray; + } + const nameThusFar = unescapeLeadingUnderscores(tag.name.text); const jsdoc = tag.parent; const fn = jsdoc.parent; if (!ts.isFunctionLike(fn)) return []; @@ -129,7 +132,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) && isIdentifier(t.fullName) && t.name.text === name) + if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.text === name) || nameThusFar !== undefined && !startsWith(name, nameThusFar)) { return undefined; } From 9e59dacbfadbc65f4f97907e32b63f727afcfc23 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 26 Jul 2017 10:59:08 -0700 Subject: [PATCH 8/8] Update baselines --- ...parsesCorrectly.argSynonymForParamTag.json | 11 ---------- ...sCorrectly.argumentSynonymForParamTag.json | 11 ---------- ...cComments.parsesCorrectly.oneParamTag.json | 11 ---------- ...DocComments.parsesCorrectly.paramTag1.json | 11 ---------- ...arsesCorrectly.paramTagBracketedName1.json | 11 ---------- ...arsesCorrectly.paramTagBracketedName2.json | 11 ---------- ...parsesCorrectly.paramTagNameThenType1.json | 11 ---------- ...parsesCorrectly.paramTagNameThenType2.json | 11 ---------- ...ents.parsesCorrectly.paramWithoutType.json | 6 ----- ...Comments.parsesCorrectly.twoParamTag2.json | 22 ------------------- ...parsesCorrectly.twoParamTagOnSameLine.json | 22 ------------------- ...sCorrectly.typedefTagWithChildrenTags.json | 22 ------------------- .../jsdocParamTagTypeLiteral.symbols | 2 -- .../reference/jsdocParamTagTypeLiteral.types | 2 -- .../jsdoc/jsdocParamTagTypeLiteral.ts | 2 -- 15 files changed, 166 deletions(-) diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json index 0500c1d962f16..af0215a4e9dc2 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json @@ -28,17 +28,6 @@ "end": 20 } }, - "type": { - "kind": "NumberKeyword", - "pos": 14, - "end": 20 - }, - "fullName": { - "kind": "Identifier", - "pos": 22, - "end": 27, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 22, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json index 40f2a47ba8eec..756c2a93be478 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json @@ -28,17 +28,6 @@ "end": 25 } }, - "type": { - "kind": "NumberKeyword", - "pos": 19, - "end": 25 - }, - "fullName": { - "kind": "Identifier", - "pos": 27, - "end": 32, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 27, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json index 2fa1905fee14e..d3111cfcc4fad 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 24, - "end": 29, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 24, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json index 7c13bd6b12cc6..2e1eebc84ee8c 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 24, - "end": 29, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 24, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json index c927d2beec60e..3a28dc8ff370e 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName1.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 25, - "end": 30, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 25, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json index e7f0b8eb8c24c..73a7389ba95fa 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagBracketedName2.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 26, - "end": 31, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 26, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json index 5293bbc7363e1..b5d6927485feb 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json @@ -28,17 +28,6 @@ "end": 28 } }, - "type": { - "kind": "NumberKeyword", - "pos": 22, - "end": 28 - }, - "fullName": { - "kind": "Identifier", - "pos": 15, - "end": 20, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 15, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json index 204e1761454cd..17b45dd670dc4 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json @@ -28,17 +28,6 @@ "end": 28 } }, - "type": { - "kind": "NumberKeyword", - "pos": 22, - "end": 28 - }, - "fullName": { - "kind": "Identifier", - "pos": 15, - "end": 20, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 15, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json index 2b33ad391ae9d..b6050ae28802c 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json @@ -18,12 +18,6 @@ "end": 14, "text": "param" }, - "fullName": { - "kind": "Identifier", - "pos": 15, - "end": 18, - "text": "foo" - }, "name": { "kind": "Identifier", "pos": 15, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json index 4a1bc8886993c..77f8a603f4fee 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 24, - "end": 29, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 24, @@ -74,17 +63,6 @@ "end": 48 } }, - "type": { - "kind": "NumberKeyword", - "pos": 42, - "end": 48 - }, - "fullName": { - "kind": "Identifier", - "pos": 50, - "end": 55, - "text": "name2" - }, "name": { "kind": "Identifier", "pos": 50, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json index 329d884873350..23222cb7067d2 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json @@ -28,17 +28,6 @@ "end": 22 } }, - "type": { - "kind": "NumberKeyword", - "pos": 16, - "end": 22 - }, - "fullName": { - "kind": "Identifier", - "pos": 24, - "end": 29, - "text": "name1" - }, "name": { "kind": "Identifier", "pos": 24, @@ -74,17 +63,6 @@ "end": 44 } }, - "type": { - "kind": "NumberKeyword", - "pos": 38, - "end": 44 - }, - "fullName": { - "kind": "Identifier", - "pos": 46, - "end": 51, - "text": "name2" - }, "name": { "kind": "Identifier", "pos": 46, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json index 1fc8eefd8db58..ecb5632402c73 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json @@ -92,17 +92,6 @@ "end": 64 } }, - "type": { - "kind": "NumberKeyword", - "pos": 58, - "end": 64 - }, - "fullName": { - "kind": "Identifier", - "pos": 66, - "end": 69, - "text": "age" - }, "name": { "kind": "Identifier", "pos": 66, @@ -137,17 +126,6 @@ "end": 91 } }, - "type": { - "kind": "StringKeyword", - "pos": 85, - "end": 91 - }, - "fullName": { - "kind": "Identifier", - "pos": 93, - "end": 97, - "text": "name" - }, "name": { "kind": "Identifier", "pos": 93, diff --git a/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols b/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols index 6add9c77cdeb0..8005be7cec766 100644 --- a/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols +++ b/tests/baselines/reference/jsdocParamTagTypeLiteral.symbols @@ -130,5 +130,3 @@ foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unne >oh : Symbol(oh, Decl(0.js, 70, 59)) >unnest : Symbol(unnest, Decl(0.js, 70, 75)) -// TODO: Also write these ridiculous object[] / nested tests for @typedef as well - diff --git a/tests/baselines/reference/jsdocParamTagTypeLiteral.types b/tests/baselines/reference/jsdocParamTagTypeLiteral.types index 80ec6edf7b044..95adecafc8364 100644 --- a/tests/baselines/reference/jsdocParamTagTypeLiteral.types +++ b/tests/baselines/reference/jsdocParamTagTypeLiteral.types @@ -167,5 +167,3 @@ foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unne >unnest : number >1 : 1 -// TODO: Also write these ridiculous object[] / nested tests for @typedef as well - diff --git a/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts b/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts index 2c8d4cbafb63e..bf59ac5664d56 100644 --- a/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts +++ b/tests/cases/conformance/jsdoc/jsdocParamTagTypeLiteral.ts @@ -76,5 +76,3 @@ function foo5(opts5) { } foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]); - -// TODO: Also write these ridiculous object[] / nested tests for @typedef as well