diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 2734d05900070..dc5e5a1a9d8c4 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -577,10 +577,10 @@ namespace ts.FindAllReferences { // If a reference is a class expression, the exported node would be its parent. // If a reference is a variable declaration, the exported node would be the variable statement. function getExportNode(parent: Node, node: Node): Node | undefined { - if (parent.kind === SyntaxKind.VariableDeclaration) { - const p = parent as VariableDeclaration; - return p.name !== node ? undefined : - p.parent.kind === SyntaxKind.CatchClause ? undefined : p.parent.parent.kind === SyntaxKind.VariableStatement ? p.parent.parent : undefined; + const declaration = isVariableDeclaration(parent) ? parent : isBindingElement(parent) ? walkUpBindingElementsAndPatterns(parent) : undefined; + if (declaration) { + return (parent as VariableDeclaration | BindingElement).name !== node ? undefined : + isCatchClause(declaration.parent) ? undefined : isVariableStatement(declaration.parent.parent) ? declaration.parent.parent : undefined; } else { return parent; diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 659dd426b79bc..90e16bd52f67e 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -130,6 +130,7 @@ "unittests/tsserver/formatSettings.ts", "unittests/tsserver/getApplicableRefactors.ts", "unittests/tsserver/getEditsForFileRename.ts", + "unittests/tsserver/getExportReferences.ts", "unittests/tsserver/importHelpers.ts", "unittests/tsserver/inferredProjects.ts", "unittests/tsserver/languageService.ts", diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts index 0f8af3c414eab..734af2a68fe29 100644 --- a/src/testRunner/unittests/tsserver/declarationFileMaps.ts +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -1,11 +1,4 @@ namespace ts.projectSystem { - interface DocumentSpanFromSubstring { - file: File; - text: string; - options?: SpanFromSubstringOptions; - contextText?: string; - contextOptions?: SpanFromSubstringOptions; - } function documentSpanFromSubstring({ file, text, contextText, options, contextOptions }: DocumentSpanFromSubstring): DocumentSpan { const contextSpan = contextText !== undefined ? documentSpanFromSubstring({ file, text: contextText, options: contextOptions }) : undefined; return { @@ -19,19 +12,6 @@ namespace ts.projectSystem { return documentSpanFromSubstring(input); } - interface MakeReferenceItem extends DocumentSpanFromSubstring { - isDefinition: boolean; - lineText: string; - } - function makeReferenceItem({ isDefinition, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { - return { - ...protocolFileSpanWithContextFromSubstring(rest), - isDefinition, - isWriteAccess: isDefinition, - lineText, - }; - } - interface MakeReferenceEntry extends DocumentSpanFromSubstring { isDefinition: boolean; } diff --git a/src/testRunner/unittests/tsserver/getExportReferences.ts b/src/testRunner/unittests/tsserver/getExportReferences.ts new file mode 100644 index 0000000000000..45bd08e39b8de --- /dev/null +++ b/src/testRunner/unittests/tsserver/getExportReferences.ts @@ -0,0 +1,185 @@ +namespace ts.projectSystem { + describe("unittests:: tsserver:: getExportReferences", () => { + const exportVariable = "export const value = 0;"; + const exportArrayDestructured = "export const [valueA, valueB] = [0, 1];"; + const exportObjectDestructured = "export const { valueC, valueD: renamedD } = { valueC: 0, valueD: 1 };"; + const exportNestedObject = "export const { nest: [valueE, { valueF }] } = { nest: [0, { valueF: 1 }] };"; + + const mainTs: File = { + path: "/main.ts", + content: 'import { value, valueA, valueB, valueC, renamedD, valueE, valueF } from "./mod";', + }; + const modTs: File = { + path: "/mod.ts", + content: `${exportVariable} +${exportArrayDestructured} +${exportObjectDestructured} +${exportNestedObject} +`, + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + + function makeSampleSession() { + const host = createServerHost([mainTs, modTs, tsconfig]); + const session = createSession(host); + openFilesForSession([mainTs, modTs], session); + return session; + } + + const referenceMainTs = (mainTs: File, text: string): protocol.ReferencesResponseItem => + makeReferenceItem({ + file: mainTs, + isDefinition: true, + lineText: mainTs.content, + contextText: mainTs.content, + text, + }); + + const referenceModTs = ( + texts: { text: string; lineText: string; contextText?: string }, + override: Partial = {}, + ): protocol.ReferencesResponseItem => + makeReferenceItem({ + file: modTs, + isDefinition: true, + ...texts, + ...override, + }); + + it("should get const variable declaration references", () => { + const session = makeSampleSession(); + + const response = executeSessionRequest( + session, + protocol.CommandTypes.References, + protocolFileLocationFromSubstring(modTs, "value"), + ); + + const expectResponse = { + refs: [ + referenceModTs({ text: "value", lineText: exportVariable, contextText: exportVariable }), + referenceMainTs(mainTs, "value"), + ], + symbolDisplayString: "const value: 0", + symbolName: "value", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "value").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + + it("should get array destructuring declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest( + session, + protocol.CommandTypes.References, + protocolFileLocationFromSubstring(modTs, "valueA"), + ); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueA", + lineText: exportArrayDestructured, + contextText: exportArrayDestructured, + }), + referenceMainTs(mainTs, "valueA"), + ], + symbolDisplayString: "const valueA: number", + symbolName: "valueA", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueA").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + + it("should get object destructuring declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest( + session, + protocol.CommandTypes.References, + protocolFileLocationFromSubstring(modTs, "valueC"), + ); + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueC", + lineText: exportObjectDestructured, + contextText: exportObjectDestructured, + }), + referenceMainTs(mainTs, "valueC"), + referenceModTs( + { text: "valueC", lineText: exportObjectDestructured, contextText: "valueC: 0" }, + { options: { index: 1 } }, + ), + ], + symbolDisplayString: "const valueC: number", + symbolName: "valueC", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueC").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + + it("should get object declaration references that renames destructured property", () => { + const session = makeSampleSession(); + const response = executeSessionRequest( + session, + protocol.CommandTypes.References, + protocolFileLocationFromSubstring(modTs, "renamedD"), + ); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "renamedD", + lineText: exportObjectDestructured, + contextText: exportObjectDestructured, + }), + referenceMainTs(mainTs, "renamedD"), + ], + symbolDisplayString: "const renamedD: number", + symbolName: "renamedD", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "renamedD").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + + it("should get nested object declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest( + session, + protocol.CommandTypes.References, + protocolFileLocationFromSubstring(modTs, "valueF"), + ); + + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueF", + lineText: exportNestedObject, + contextText: exportNestedObject, + }), + referenceMainTs(mainTs, "valueF"), + referenceModTs( + { + text: "valueF", + lineText: exportNestedObject, + contextText: "valueF: 1", + }, + { options: { index: 1 } }, + ), + ], + symbolDisplayString: "const valueF: number", + symbolName: "valueF", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueF").offset, + }; + + assert.deepEqual(response, expectResponse); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index 63b854e8ce84e..e6baba932f59f 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -519,6 +519,8 @@ namespace ts.projectSystem { file: File; text: string; options?: SpanFromSubstringOptions; + contextText?: string; + contextOptions?: SpanFromSubstringOptions; } export function protocolFileSpanFromSubstring({ file, text, options }: DocumentSpanFromSubstring): protocol.FileSpan { return { file: file.path, ...protocolTextSpanFromSubstring(file.content, text, options) }; @@ -727,4 +729,18 @@ namespace ts.projectSystem { assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); } } + + export interface MakeReferenceItem extends DocumentSpanFromSubstring { + isDefinition: boolean; + lineText: string; + } + + export function makeReferenceItem({ isDefinition, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { + return { + ...protocolFileSpanWithContextFromSubstring(rest), + isDefinition, + isWriteAccess: isDefinition, + lineText, + }; + } }