diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 73a2e46be9..c295a4c956 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2102,6 +2102,7 @@ namespace ts { } export interface JSDocTag extends Node { + parent: JSDoc; atToken: AtToken; tagName: Identifier; comment: string | undefined; @@ -2132,6 +2133,7 @@ namespace ts { } export interface JSDocTypedefTag extends JSDocTag, NamedDeclaration { + parent: JSDoc; kind: SyntaxKind.JSDocTypedefTag; fullName?: JSDocNamespaceDeclaration | Identifier; name?: Identifier; @@ -2140,6 +2142,7 @@ namespace ts { } export interface JSDocPropertyTag extends JSDocTag, TypeElement { + parent: JSDoc; kind: SyntaxKind.JSDocPropertyTag; name: Identifier; typeExpression: JSDocTypeExpression; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 22210419af..134404c247 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -288,6 +288,14 @@ namespace ts { return node.kind >= SyntaxKind.FirstJSDocNode && node.kind <= SyntaxKind.LastJSDocNode; } + export function isJSDoc(node: Node): node is JSDoc { + return node.kind === SyntaxKind.JSDocComment; + } + + export function isJSDocTypedefTag(node: Node): node is JSDocTypedefTag { + return node.kind === SyntaxKind.JSDocTypedefTag; + } + export function isJSDocTag(node: Node) { return node.kind >= SyntaxKind.FirstJSDocTagNode && node.kind <= SyntaxKind.LastJSDocTagNode; } @@ -1551,6 +1559,10 @@ namespace ts { } export function getJSDocs(node: Node): (JSDoc | JSDocTag)[] { + if (isJSDocTypedefTag(node)) { + return [node.parent]; + } + let cache: (JSDoc | JSDocTag)[] = node.jsDocCache; if (!cache) { getJSDocsWorker(node); diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 7877e28b6f..df499cbd38 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -784,7 +784,8 @@ namespace ts.FindAllReferences.Core { return; } - for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container, /*fullStart*/ state.options.findInComments || container.jsDoc !== undefined)) { + const fullStart = state.options.findInComments || container.jsDoc !== undefined || forEach(search.symbol.declarations, d => d.kind === ts.SyntaxKind.JSDocTypedefTag); + for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container, fullStart)) { getReferencesAtLocation(sourceFile, position, search, state); } } diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index 9b96f339bf..ac60ba7942 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -8,7 +8,7 @@ namespace ts.GoToDefinition { if (referenceFile) { return [getDefinitionInfoForFileReference(comment.fileName, referenceFile.fileName)]; } - return undefined; + // Might still be on jsdoc, so keep looking. } // Type reference directives diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 2e771a7328..7a87c0939c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -39,13 +39,14 @@ namespace ts { case SyntaxKind.TypeLiteral: return SemanticMeaning.Type; + case SyntaxKind.JSDocTypedefTag: + // If it has no name node, it shares the name with the value declaration below it. + return (node as JSDocTypedefTag).name === undefined ? SemanticMeaning.Value | SemanticMeaning.Type : SemanticMeaning.Type; + case SyntaxKind.EnumMember: case SyntaxKind.ClassDeclaration: return SemanticMeaning.Value | SemanticMeaning.Type; - case SyntaxKind.EnumDeclaration: - return SemanticMeaning.All; - case SyntaxKind.ModuleDeclaration: if (isAmbientModule(node)) { return SemanticMeaning.Namespace | SemanticMeaning.Value; @@ -57,6 +58,7 @@ namespace ts { return SemanticMeaning.Namespace; } + case SyntaxKind.EnumDeclaration: case SyntaxKind.NamedImports: case SyntaxKind.ImportSpecifier: case SyntaxKind.ImportEqualsDeclaration: @@ -70,7 +72,7 @@ namespace ts { return SemanticMeaning.Namespace | SemanticMeaning.Value; } - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + return SemanticMeaning.All; } export function getMeaningFromLocation(node: Node): SemanticMeaning { @@ -78,7 +80,7 @@ namespace ts { return SemanticMeaning.Value; } else if (node.parent.kind === SyntaxKind.ExportAssignment) { - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + return SemanticMeaning.All; } else if (isInRightSideOfImport(node)) { return getMeaningFromRightHandSideOfImportEquals(node); @@ -162,10 +164,22 @@ namespace ts { node = node.parent; } - return node.parent.kind === SyntaxKind.TypeReference || - (node.parent.kind === SyntaxKind.ExpressionWithTypeArguments && !isExpressionWithTypeArgumentsInClassExtendsClause(node.parent)) || - (node.kind === SyntaxKind.ThisKeyword && !isPartOfExpression(node)) || - node.kind === SyntaxKind.ThisType; + switch (node.kind) { + case SyntaxKind.ThisKeyword: + return !isPartOfExpression(node); + case SyntaxKind.ThisType: + return true; + } + + switch (node.parent.kind) { + case SyntaxKind.TypeReference: + case SyntaxKind.JSDocTypeReference: + return true; + case SyntaxKind.ExpressionWithTypeArguments: + return !isExpressionWithTypeArgumentsInClassExtendsClause(node.parent); + } + + return false; } export function isCallExpressionTarget(node: Node): boolean { diff --git a/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning0.ts b/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning0.ts new file mode 100644 index 0000000000..3673c86b38 --- /dev/null +++ b/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning0.ts @@ -0,0 +1,16 @@ +/// + +// @allowJs: true +// @Filename: a.js + +/////** @typedef {number} [|{| "isWriteAccess": true, "isDefinition": true |}T|] */ + +////const [|{| "isWriteAccess": true, "isDefinition": true |}T|] = 1; + +/////** @type {[|T|]} */ +////const n = [|T|]; + +const [t0, v0, t1, v1] = test.ranges(); + +verify.singleReferenceGroup("type T = number\nconst T: 1", [t0, t1]); +verify.singleReferenceGroup("type T = number\nconst T: 1", [v0, v1]); diff --git a/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning1.ts b/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning1.ts new file mode 100644 index 0000000000..f052d4bd87 --- /dev/null +++ b/tests/cases/fourslash/jsdocTypedefTagSemanticMeaning1.ts @@ -0,0 +1,12 @@ +/// + +// @allowJs: true +// @Filename: a.js + +/////** @typedef {number} */ +////const [|{| "isWriteAccess": true, "isDefinition": true |}T|] = 1; + +/////** @type {[|T|]} */ +////const n = [|T|]; + +verify.singleReferenceGroup("type T = number\nconst T: 1"); diff --git a/tests/cases/fourslash/jsdocTypedefTagServices.ts b/tests/cases/fourslash/jsdocTypedefTagServices.ts new file mode 100644 index 0000000000..c97707e4d2 --- /dev/null +++ b/tests/cases/fourslash/jsdocTypedefTagServices.ts @@ -0,0 +1,28 @@ +/// + +// @allowJs: true +// @Filename: a.js + +/////** +//// * Doc comment +//// * @typedef /*def*/[|{| "isWriteAccess": true, "isDefinition": true |}Product|] +//// * @property {string} title +//// */ + +/////** +//// * @type {/*use*/[|Product|]} +//// */ +////const product = null; + +const desc = `type Product = { + title: string; +}`; + +verify.quickInfoAt("use", desc, "Doc comment"); + +verify.goToDefinition("use", "def"); + +verify.rangesAreOccurrences(); +verify.rangesAreDocumentHighlights(); +verify.singleReferenceGroup(desc); +verify.rangesAreRenameLocations();