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 0b58f080fe2..baf7f747539 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1504,9 +1504,9 @@ namespace ts { return declareSymbol(container.symbol.exports, container.symbol, node, symbolFlags, symbolExcludes); case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.ObjectLiteralExpression: case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.JsxAttributes: // Interface/Object-types always have their children added to the 'members' of // their container. They are only accessible through an instance of their @@ -2104,8 +2104,9 @@ namespace ts { case SyntaxKind.ConstructorType: return bindFunctionOrConstructorType(node); case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: case SyntaxKind.MappedType: - return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode); + return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral); case SyntaxKind.ObjectLiteralExpression: return bindObjectLiteralExpression(node); case SyntaxKind.FunctionExpression: @@ -2163,13 +2164,16 @@ namespace ts { case SyntaxKind.ModuleBlock: return updateStrictModeStatementList((node).statements); + case SyntaxKind.JSDocParameterTag: + if (node.parent.kind !== SyntaxKind.JSDocTypeLiteral) { + break; + } + // falls through case SyntaxKind.JSDocPropertyTag: - return declareSymbolAndAddToSymbolTable(node as JSDocPropertyTag, - (node as JSDocPropertyTag).isBracketed || ((node as JSDocPropertyTag).typeExpression && (node as JSDocPropertyTag).typeExpression.type.kind === SyntaxKind.JSDocOptionalType) ? + return declareSymbolAndAddToSymbolTable(node as JSDocPropertyLikeTag, + (node as JSDocPropertyLikeTag).isBracketed || ((node as JSDocPropertyLikeTag).typeExpression && (node as JSDocPropertyLikeTag).typeExpression.type.kind === SyntaxKind.JSDocOptionalType) ? SymbolFlags.Property | SymbolFlags.Optional : SymbolFlags.Property, SymbolFlags.PropertyExcludes); - case SyntaxKind.JSDocTypeLiteral: - return bindAnonymousTypeWorker(node as JSDocTypeLiteral); case SyntaxKind.JSDocTypedefTag: { const { fullName } = node as JSDocTypedefTag; if (!fullName || fullName.kind === SyntaxKind.Identifier) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c2f40c169dc..c69cdaee8c2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -5074,20 +5074,9 @@ namespace ts { return unknownType; } - let declaration: JSDocTypedefTag | TypeAliasDeclaration = getDeclarationOfKind(symbol, SyntaxKind.JSDocTypedefTag); - let type: Type; - if (declaration) { - if (declaration.jsDocTypeLiteral) { - type = getTypeFromTypeNode(declaration.jsDocTypeLiteral); - } - else { - type = getTypeFromTypeNode(declaration.typeExpression.type); - } - } - else { - declaration = getDeclarationOfKind(symbol, SyntaxKind.TypeAliasDeclaration); - type = getTypeFromTypeNode(declaration.type); - } + const declaration = getDeclarationOfKind(symbol, SyntaxKind.JSDocTypedefTag) || + getDeclarationOfKind(symbol, SyntaxKind.TypeAliasDeclaration); + let type = getTypeFromTypeNode(declaration.kind === SyntaxKind.JSDocTypedefTag ? declaration.typeExpression : declaration.type); if (popTypeResolution()) { const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); @@ -7654,9 +7643,12 @@ namespace ts { links.resolvedType = emptyTypeLiteralType; } else { - const type = createObjectType(ObjectFlags.Anonymous, node.symbol); + let type = createObjectType(ObjectFlags.Anonymous, node.symbol); type.aliasSymbol = aliasSymbol; type.aliasTypeArguments = getAliasTypeArgumentsForTypeNode(node); + if (isJSDocTypeLiteral(node) && node.isArrayType) { + type = createArrayType(type); + } links.resolvedType = type; } } @@ -7890,7 +7882,8 @@ namespace ts { case SyntaxKind.ParenthesizedType: case SyntaxKind.JSDocNonNullableType: case SyntaxKind.JSDocOptionalType: - return getTypeFromTypeNode((node).type); + case SyntaxKind.JSDocTypeExpression: + return getTypeFromTypeNode((node).type); case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: case SyntaxKind.TypeLiteral: diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 0be7734968f..c67e27150aa 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -59,6 +59,9 @@ namespace ts { * @param node a given node to visit its children * @param cbNode a callback to be invoked for all child nodes * @param cbNodes a callback to be invoked for embedded array + * + * @remarks `forEachChild` must visit the children of a node in the order + * that they appear in the source code. The language service depends on this property to locate nodes by position. */ export function forEachChild(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { if (!node || node.kind <= SyntaxKind.LastToken) { @@ -407,9 +410,15 @@ namespace ts { case SyntaxKind.JSDocComment: return visitNodes(cbNode, cbNodes, (node).tags); case SyntaxKind.JSDocParameterTag: - return visitNode(cbNode, (node).preParameterName) || - visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).postParameterName); + case SyntaxKind.JSDocPropertyTag: + if ((node as JSDocPropertyLikeTag).isParameterNameFirst) { + return visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).typeExpression); + } + else { + return visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).fullName); + } case SyntaxKind.JSDocReturnTag: return visitNode(cbNode, (node).typeExpression); case SyntaxKind.JSDocTypeTag: @@ -419,15 +428,19 @@ namespace ts { case SyntaxKind.JSDocTemplateTag: return visitNodes(cbNode, cbNodes, (node).typeParameters); case SyntaxKind.JSDocTypedefTag: - return visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).fullName) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).jsDocTypeLiteral); + if ((node as JSDocTypedefTag).typeExpression && + (node as JSDocTypedefTag).typeExpression.kind === SyntaxKind.JSDocTypeExpression) { + return visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).name); + } + else { + return visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).typeExpression); + } case SyntaxKind.JSDocTypeLiteral: return visitNodes(cbNode, cbNodes, (node).jsDocPropertyTags); - case SyntaxKind.JSDocPropertyTag: - return visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).name); case SyntaxKind.PartiallyEmittedExpression: return visitNode(cbNode, (node).expression); } @@ -6457,10 +6470,10 @@ namespace ts { }); } - function parseBracketNameInPropertyAndParamTag(): { name: Identifier, isBracketed: boolean } { - // Looking for something like '[foo]' or 'foo' + function parseBracketNameInPropertyAndParamTag(): { fullName: EntityName, isBracketed: boolean } { + // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' const isBracketed = parseOptional(SyntaxKind.OpenBracketToken); - const name = parseJSDocIdentifierName(/*createIfMissing*/ true); + const fullName = parseJSDocEntityName(/*createIfMissing*/ true); if (isBracketed) { skipWhitespace(); @@ -6472,36 +6485,66 @@ namespace ts { parseExpected(SyntaxKind.CloseBracketToken); } - return { name, isBracketed }; + return { fullName, isBracketed }; } - function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: boolean): JSDocPropertyTag | JSDocParameterTag { + function isObjectOrObjectArrayTypeReference(node: TypeNode): boolean { + return node.kind === SyntaxKind.ObjectKeyword || + isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object" || + node.kind === SyntaxKind.ArrayType && isObjectOrObjectArrayTypeReference((node as ArrayTypeNode).elementType); + } + + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: true): JSDocParameterTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: false): JSDocPropertyTag; + function parseParameterOrPropertyTag(atToken: AtToken, tagName: Identifier, shouldParseParamTag: boolean): JSDocPropertyLikeTag { let typeExpression = tryParseTypeExpression(); skipWhitespace(); - const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); + const { fullName, isBracketed } = parseBracketNameInPropertyAndParamTag(); skipWhitespace(); - let preName: Identifier, postName: Identifier; + let preName: EntityName, postName: EntityName; if (typeExpression) { - postName = name; + postName = fullName; } else { - preName = name; + preName = fullName; typeExpression = tryParseTypeExpression(); } - const result = shouldParseParamTag ? + const result: JSDocPropertyLikeTag = shouldParseParamTag ? createNode(SyntaxKind.JSDocParameterTag, atToken.pos) : createNode(SyntaxKind.JSDocPropertyTag, atToken.pos); + if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: JSDocPropertyLikeTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral; + const start = scanner.getStartPos(); + while (child = tryParse(() => parseChildParameterOrPropertyTag(/*shouldParseParamTag*/ true, fullName))) { + if (!jsdocTypeLiteral) { + jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); + jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; + } + (jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray).push(child as JSDocPropertyTag); + } + if (jsdocTypeLiteral) { + if (typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; + } + typeExpression.type = finishNode(jsdocTypeLiteral); + } + } result.atToken = atToken; result.tagName = tagName; - result.preParameterName = preName; result.typeExpression = typeExpression; - result.postParameterName = postName; - result.name = postName || preName; + if (typeExpression) { + result.type = typeExpression.type; + } + result.fullName = postName || preName; + result.name = ts.isIdentifier(result.fullName) ? result.fullName : result.fullName.right; + result.isParameterNameFirst = postName ? false : !!preName; result.isBracketed = isBracketed; return finishNode(result); + } function parseReturnTag(atToken: AtToken, tagName: Identifier): JSDocReturnTag { @@ -6565,68 +6608,44 @@ namespace ts { rightNode = rightNode.body; } } - typedefTag.typeExpression = typeExpression; skipWhitespace(); - if (typeExpression) { - if (isObjectTypeReference(typeExpression.type)) { - typedefTag.jsDocTypeLiteral = scanChildTags(); + typedefTag.typeExpression = typeExpression; + if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: JSDocTypeTag | JSDocPropertyTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral; + let alreadyHasTypeTag = false; + const start = scanner.getStartPos(); + while (child = tryParse(() => parseChildParameterOrPropertyTag(/*shouldParseParamTag*/ false))) { + if (!jsdocTypeLiteral) { + jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); + } + if (child.kind === SyntaxKind.JSDocTypeTag) { + if (alreadyHasTypeTag) { + break; + } + else { + jsdocTypeLiteral.jsDocTypeTag = child; + alreadyHasTypeTag = true; + } + } + else { + if (!jsdocTypeLiteral.jsDocPropertyTags) { + jsdocTypeLiteral.jsDocPropertyTags = [] as MutableNodeArray; + } + (jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray).push(child); + } } - if (!typedefTag.jsDocTypeLiteral) { - typedefTag.jsDocTypeLiteral = typeExpression.type; + if (jsdocTypeLiteral) { + if (typeExpression && typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; + } + typedefTag.typeExpression = finishNode(jsdocTypeLiteral); } } - else { - typedefTag.jsDocTypeLiteral = scanChildTags(); - } return finishNode(typedefTag); - function isObjectTypeReference(node: TypeNode) { - return node.kind === SyntaxKind.ObjectKeyword || - isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Object"; - } - - function scanChildTags(): JSDocTypeLiteral { - const jsDocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, scanner.getStartPos()); - let resumePos = scanner.getStartPos(); - let canParseTag = true; - let seenAsterisk = false; - let parentTagTerminated = false; - - while (token() !== SyntaxKind.EndOfFileToken && !parentTagTerminated) { - nextJSDocToken(); - switch (token()) { - case SyntaxKind.AtToken: - if (canParseTag) { - parentTagTerminated = !tryParseChildTag(jsDocTypeLiteral); - if (!parentTagTerminated) { - resumePos = scanner.getStartPos(); - } - } - seenAsterisk = false; - break; - case SyntaxKind.NewLineTrivia: - resumePos = scanner.getStartPos() - 1; - canParseTag = true; - seenAsterisk = false; - break; - case SyntaxKind.AsteriskToken: - if (seenAsterisk) { - canParseTag = false; - } - seenAsterisk = true; - break; - case SyntaxKind.Identifier: - canParseTag = false; - break; - case SyntaxKind.EndOfFileToken: - break; - } - } - scanner.setTextPos(resumePos); - return finishNode(jsDocTypeLiteral); - } function parseJSDocTypeNameWithNamespace(flags: NodeFlags) { const pos = scanner.getTokenPos(); @@ -6647,8 +6666,61 @@ namespace ts { } } + function textsEqual(parent: EntityName, name: EntityName): boolean { + while (!ts.isIdentifier(parent) || !ts.isIdentifier(name)) { + if (!ts.isIdentifier(parent) && !ts.isIdentifier(name) && parent.right.text === name.right.text) { + parent = parent.left; + name = name.left; + } + else { + return false; + } + } + return parent.text === name.text; + } - function tryParseChildTag(parentTag: JSDocTypeLiteral): boolean { + function parseChildParameterOrPropertyTag(shouldParseParamTag: false): JSDocTypeTag | JSDocPropertyTag | false; + function parseChildParameterOrPropertyTag(shouldParseParamTag: true, fullName: EntityName): JSDocPropertyTag | JSDocParameterTag | false; + function parseChildParameterOrPropertyTag(shouldParseParamTag: boolean, fullName?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + let resumePos = scanner.getStartPos(); + let canParseTag = true; + let seenAsterisk = false; + while (token() !== SyntaxKind.EndOfFileToken) { + nextJSDocToken(); + switch (token()) { + case SyntaxKind.AtToken: + if (canParseTag) { + const child = tryParseChildTag(shouldParseParamTag); + if (child && child.kind === SyntaxKind.JSDocParameterTag && + (ts.isIdentifier(child.fullName) || !textsEqual(fullName, child.fullName.left))) { + break; + } + return child; + } + seenAsterisk = false; + break; + case SyntaxKind.NewLineTrivia: + resumePos = scanner.getStartPos() - 1; + canParseTag = true; + seenAsterisk = false; + break; + case SyntaxKind.AsteriskToken: + if (seenAsterisk) { + canParseTag = false; + } + seenAsterisk = true; + break; + case SyntaxKind.Identifier: + canParseTag = false; + break; + case SyntaxKind.EndOfFileToken: + break; + } + } + scanner.setTextPos(resumePos); + } + + function tryParseChildTag(shouldParseParamTag: boolean, alreadyHasTypeTag?: boolean): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { Debug.assert(token() === SyntaxKind.AtToken); const atToken = createNode(SyntaxKind.AtToken, scanner.getStartPos()); atToken.end = scanner.getTextPos(); @@ -6659,27 +6731,16 @@ namespace ts { if (!tagName) { return false; } - switch (tagName.text) { case "type": - if (parentTag.jsDocTypeTag) { - // already has a @type tag, terminate the parent tag now. - return false; - } - parentTag.jsDocTypeTag = parseTypeTag(atToken, tagName); - return true; + return !alreadyHasTypeTag && !shouldParseParamTag && parseTypeTag(atToken, tagName); case "prop": case "property": - const propertyTag = parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ false) as JSDocPropertyTag; - if (propertyTag) { - if (!parentTag.jsDocPropertyTags) { - parentTag.jsDocPropertyTags = >[]; - } - (parentTag.jsDocPropertyTags as MutableNodeArray).push(propertyTag); - return true; - } - // Error parsing property tag - return false; + return !shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ false); + case "arg": + case "argument": + case "param": + return shouldParseParamTag && parseParameterOrPropertyTag(atToken, tagName, /*shouldParseParamTag*/ true); } return false; } @@ -6728,6 +6789,26 @@ namespace ts { return currentToken = scanner.scanJSDocToken(); } + function parseJSDocEntityName(createIfMissing = false): EntityName { + let entity: EntityName = parseJSDocIdentifierName(createIfMissing); + if (parseOptional(SyntaxKind.OpenBracketToken)) { + parseExpected(SyntaxKind.CloseBracketToken); + // Note that y[] is accepted as an entity name, but the postfix brackets are not saved for checking. + // Technically usejsdoc.org requires them for specifying a property of a type equivalent to Array<{ x: ...}> + // but it's not worth it to enforce that restriction. + } + while (parseOptional(SyntaxKind.DotToken)) { + const node: QualifiedName = createNode(SyntaxKind.QualifiedName, entity.pos) as QualifiedName; + node.left = entity; + node.right = parseJSDocIdentifierName(createIfMissing); + if (parseOptional(SyntaxKind.OpenBracketToken)) { + parseExpected(SyntaxKind.CloseBracketToken); + } + entity = finishNode(node); + } + return entity; + } + function parseJSDocIdentifierName(createIfMissing = false): Identifier { return createJSDocIdentifier(tokenIsIdentifierOrKeyword(token()), createIfMissing); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8431269046c..52e9a62e6a9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -760,6 +760,7 @@ namespace ts { // SyntaxKind.ShorthandPropertyAssignment // SyntaxKind.EnumMember // SyntaxKind.JSDocPropertyTag + // SyntaxKind.JSDocParameterTag export interface VariableLikeDeclaration extends NamedDeclaration { propertyName?: PropertyName; dotDotDotToken?: DotDotDotToken; @@ -2036,7 +2037,7 @@ namespace ts { } // represents a top level: { type } expression in a JSDoc comment. - export interface JSDocTypeExpression extends Node { + export interface JSDocTypeExpression extends TypeNode { kind: SyntaxKind.JSDocTypeExpression; type: TypeNode; } @@ -2125,38 +2126,33 @@ namespace ts { kind: SyntaxKind.JSDocTypedefTag; fullName?: JSDocNamespaceDeclaration | Identifier; name?: Identifier; - typeExpression?: JSDocTypeExpression; - jsDocTypeLiteral?: JSDocTypeLiteral; + typeExpression?: JSDocTypeExpression | JSDocTypeLiteral; } - export interface JSDocPropertyTag extends JSDocTag, TypeElement { + export interface JSDocPropertyLikeTag extends JSDocTag, VariableLikeDeclaration { parent: JSDoc; - kind: SyntaxKind.JSDocPropertyTag; + fullName?: EntityName; name: Identifier; - /** the parameter name, if provided *before* the type (TypeScript-style) */ - preParameterName?: Identifier; - /** the parameter name, if provided *after* the type (JSDoc-standard) */ - postParameterName?: Identifier; typeExpression: JSDocTypeExpression; + /** Whether the property name came before the type -- non-standard for JSDoc, but Typescript-like */ + isParameterNameFirst: boolean; isBracketed: boolean; } + export interface JSDocPropertyTag extends JSDocPropertyLikeTag { + kind: SyntaxKind.JSDocPropertyTag; + } + + export interface JSDocParameterTag extends JSDocPropertyLikeTag { + kind: SyntaxKind.JSDocParameterTag; + } + export interface JSDocTypeLiteral extends JSDocType { kind: SyntaxKind.JSDocTypeLiteral; - jsDocPropertyTags?: NodeArray; + jsDocPropertyTags?: NodeArray; jsDocTypeTag?: JSDocTypeTag; - } - - export interface JSDocParameterTag extends JSDocTag { - kind: SyntaxKind.JSDocParameterTag; - /** the parameter name, if provided *before* the type (TypeScript-style) */ - preParameterName?: Identifier; - typeExpression?: JSDocTypeExpression; - /** the parameter name, if provided *after* the type (JSDoc-standard) */ - postParameterName?: Identifier; - /** the parameter name, regardless of the location it was provided */ - name: Identifier; - isBracketed: boolean; + /** If true, then this type literal represents an *array* of its type. */ + isArrayType?: boolean; } export const enum FlowFlags { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e001ec43f62..983ff8012bd 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1553,6 +1553,10 @@ namespace ts { /** Does the opposite of `getJSDocParameterTags`: given a JSDoc parameter, finds the parameter corresponding to it. */ export function getParameterFromJSDoc(node: JSDocParameterTag): ParameterDeclaration | undefined { + if (!isIdentifier(node.fullName)) { + // `@param {T} obj.prop` is not a top-level param, so it doesn't map to a top-level parameter + return undefined; + } const name = node.name.text; const grandParent = node.parent!.parent!; Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment); diff --git a/src/services/classifier.ts b/src/services/classifier.ts index 146a513ffc5..8acce0a01fa 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -755,10 +755,10 @@ namespace ts { return; function processJSDocParameterTag(tag: JSDocParameterTag) { - if (tag.preParameterName) { - pushCommentRange(pos, tag.preParameterName.pos - pos); - pushClassification(tag.preParameterName.pos, tag.preParameterName.end - tag.preParameterName.pos, ClassificationType.parameterName); - pos = tag.preParameterName.end; + if (tag.isParameterNameFirst) { + pushCommentRange(pos, tag.name.pos - pos); + pushClassification(tag.name.pos, tag.name.end - tag.name.pos, ClassificationType.parameterName); + pos = tag.name.end; } if (tag.typeExpression) { @@ -767,10 +767,10 @@ namespace ts { pos = tag.typeExpression.end; } - if (tag.postParameterName) { - pushCommentRange(pos, tag.postParameterName.pos - pos); - pushClassification(tag.postParameterName.pos, tag.postParameterName.end - tag.postParameterName.pos, ClassificationType.parameterName); - pos = tag.postParameterName.end; + if (!tag.isParameterNameFirst) { + pushCommentRange(pos, tag.name.pos - pos); + pushClassification(tag.name.pos, tag.name.end - tag.name.pos, ClassificationType.parameterName); + pos = tag.name.end; } } } diff --git a/src/services/completions.ts b/src/services/completions.ts index d7f0701caf2..84e7b375ff0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -420,7 +420,7 @@ namespace ts.Completions { if (tag.tagName.pos <= position && position <= tag.tagName.end) { request = { kind: "JsDocTagName" }; } - if (isTagWithTypeExpression(tag) && tag.typeExpression) { + if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true); if (!currentToken || (!isDeclarationName(currentToken) && diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 070b96101e2..464d251d4c2 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -120,7 +120,7 @@ namespace ts.JsDoc { } export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { - const nameThusFar = unescapeLeadingUnderscores(tag.name.text); + const nameThusFar = isIdentifier(tag.fullName) ? unescapeLeadingUnderscores(tag.name.text) : undefined; const jsdoc = tag.parent; const fn = jsdoc.parent; if (!ts.isFunctionLike(fn)) return []; @@ -129,7 +129,7 @@ namespace ts.JsDoc { if (!isIdentifier(param.name)) return undefined; const name = unescapeLeadingUnderscores(param.name.text); - if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && t.name.text === name) + if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.fullName) && t.name.text === name) || nameThusFar !== undefined && !startsWith(name, nameThusFar)) { return undefined; } 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 b47a07e8487..0a8bed8dd6d 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 cf86fda872e..f450e6d3cdb 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 506487232e0..01c2dfa9cc6 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 abc116d7452..e7469f73777 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 1df54fadcd7..b3230d6473d 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 5347b99be7a..7f08a22a723 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 9d66ca3dd55..c5dbe539faa 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 01d08a103c1..874aadf32c5 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 ab15d24f618..cd6a3ec63eb 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 5ba60030f1b..7bd46fac5b8 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 7d26097bb34..d38146ec587 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 370e139b512..c6f0d250923 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 00000000000..6add9c77cde --- /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 00000000000..80ec6edf7b0 --- /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 00000000000..2c8d4cbafb6 --- /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 c67e27150aa..799f1333730 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 baf7f747539..3c26dedea64 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 c69cdaee8c2..b7e08ea41a0 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 799f1333730..a0bd6c02c51 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 52e9a62e6a9..f608684a136 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 983ff8012bd..f20a2d422fd 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 8acce0a01fa..7c61f249dcb 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 00000000000..6d1b32b1515 --- /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 0a8bed8dd6d..0500c1d962f 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 f450e6d3cdb..40f2a47ba8e 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 01c2dfa9cc6..2fa1905fee1 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 e7469f73777..7c13bd6b12c 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 b3230d6473d..c927d2beec6 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 7f08a22a723..e7f0b8eb8c2 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 c5dbe539faa..5293bbc7363 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 874aadf32c5..204e1761454 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 cd6a3ec63eb..2b33ad391ae 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 7bd46fac5b8..4a1bc888699 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 d38146ec587..329d8848733 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 c6f0d250923..1fc8eefd8db 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 b7e08ea41a0..c4ccbd17f5c 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 a0bd6c02c51..afc7b0e97b8 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 f608684a136..f8170d62238 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 f20a2d422fd..1db25a0d211 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 464d251d4c2..2575d26cddd 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 0500c1d962f..af0215a4e9d 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 40f2a47ba8e..756c2a93be4 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 2fa1905fee1..d3111cfcc4f 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 7c13bd6b12c..2e1eebc84ee 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 c927d2beec6..3a28dc8ff37 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 e7f0b8eb8c2..73a7389ba95 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 5293bbc7363..b5d6927485f 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 204e1761454..17b45dd670d 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 2b33ad391ae..b6050ae2880 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 4a1bc888699..77f8a603f4f 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 329d8848733..23222cb7067 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 1fc8eefd8db..ecb5632402c 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 6add9c77cde..8005be7cec7 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 80ec6edf7b0..95adecafc83 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 2c8d4cbafb6..bf59ac5664d 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