Skip to content

Commit 12ed012

Browse files
authored
Clean up FAR aggregation (#48619)
* Clean up FAR and RenameLocations This change had two goals: 1. Make the code easier to understand, primarily by simplifying the callback structure and minimizing side-effects 2. Improve performance by reducing repeated work, both FAR searches of individual projects and default tsconfig searches This implementation attempts to preserve the merging order found in the original code (someone less relevant in the present state of using syntactic isDefinition). * Stop enforcing search and aggregation order ...in preparation for implementing isDefinition explicitly. Also restore convention of referring to `DocumentPosition`s as "locations". * Introduce LanguageService.updateIsDefinitionOfReferencedSymbols ...to allow use of the checker when computing isDefinition across projects. * Update baselines * Tidy diff * De-dup simplified results * Baseline cross-project isDefinition results * Move de-duping upstream to fix Full output * Add server baseline test to confirm searches are not repeated * Manually merge #48758 * Update baseline for newer fix to #48963
1 parent e56a067 commit 12ed012

File tree

36 files changed

+4226
-221
lines changed

36 files changed

+4226
-221
lines changed

src/harness/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,10 @@ namespace ts.server {
880880
throw new Error("Program objects are not serializable through the server protocol.");
881881
}
882882

883+
updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ReferencedSymbol[], _knownSymbolSpans: Set<DocumentSpan>): boolean {
884+
return notImplemented();
885+
}
886+
883887
getNonBoundSourceFile(_fileName: string): SourceFile {
884888
throw new Error("SourceFile objects are not serializable through the server protocol.");
885889
}

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,9 @@ namespace Harness.LanguageService {
630630
getAutoImportProvider(): ts.Program | undefined {
631631
throw new Error("Program can not be marshaled across the shim layer.");
632632
}
633+
updateIsDefinitionOfReferencedSymbols(_referencedSymbols: readonly ts.ReferencedSymbol[], _knownSymbolSpans: ts.Set<ts.DocumentSpan>): boolean {
634+
return ts.notImplemented();
635+
}
633636
getNonBoundSourceFile(): ts.SourceFile {
634637
throw new Error("SourceFile can not be marshaled across the shim layer.");
635638
}

src/server/session.ts

Lines changed: 262 additions & 169 deletions
Large diffs are not rendered by default.

src/services/findAllReferences.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ namespace ts.FindAllReferences {
564564
}
565565

566566
/** Whether a reference, `node`, is a definition of the `target` symbol */
567-
function isDeclarationOfSymbol(node: Node, target: Symbol | undefined): boolean {
567+
export function isDeclarationOfSymbol(node: Node, target: Symbol | undefined): boolean {
568568
if (!target) return false;
569569
const source = getDeclarationFromName(node) ||
570570
(node.kind === SyntaxKind.DefaultKeyword ? node.parent

src/services/services.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,62 @@ namespace ts {
16011601
return host.getPackageJsonAutoImportProvider?.();
16021602
}
16031603

1604+
function updateIsDefinitionOfReferencedSymbols(referencedSymbols: readonly ReferencedSymbol[], knownSymbolSpans: Set<DocumentSpan>): boolean {
1605+
const checker = program.getTypeChecker();
1606+
const symbol = getSymbolForProgram();
1607+
1608+
if (!symbol) return false;
1609+
1610+
for (const referencedSymbol of referencedSymbols) {
1611+
for (const ref of referencedSymbol.references) {
1612+
const refNode = getNodeForSpan(ref);
1613+
Debug.assertIsDefined(refNode);
1614+
if (knownSymbolSpans.has(ref) || FindAllReferences.isDeclarationOfSymbol(refNode, symbol)) {
1615+
knownSymbolSpans.add(ref);
1616+
ref.isDefinition = true;
1617+
const mappedSpan = getMappedDocumentSpan(ref, sourceMapper, maybeBind(host, host.fileExists));
1618+
if (mappedSpan) {
1619+
knownSymbolSpans.add(mappedSpan);
1620+
}
1621+
}
1622+
else {
1623+
ref.isDefinition = false;
1624+
}
1625+
}
1626+
}
1627+
1628+
return true;
1629+
1630+
function getSymbolForProgram(): Symbol | undefined {
1631+
for (const referencedSymbol of referencedSymbols) {
1632+
for (const ref of referencedSymbol.references) {
1633+
if (knownSymbolSpans.has(ref)) {
1634+
const refNode = getNodeForSpan(ref);
1635+
Debug.assertIsDefined(refNode);
1636+
return checker.getSymbolAtLocation(refNode);
1637+
}
1638+
const mappedSpan = getMappedDocumentSpan(ref, sourceMapper, maybeBind(host, host.fileExists));
1639+
if (mappedSpan && knownSymbolSpans.has(mappedSpan)) {
1640+
const refNode = getNodeForSpan(mappedSpan);
1641+
if (refNode) {
1642+
return checker.getSymbolAtLocation(refNode);
1643+
}
1644+
}
1645+
}
1646+
}
1647+
1648+
return undefined;
1649+
}
1650+
1651+
function getNodeForSpan(docSpan: DocumentSpan): Node | undefined {
1652+
const sourceFile = program.getSourceFile(docSpan.fileName);
1653+
if (!sourceFile) return undefined;
1654+
const rawNode = getTouchingPropertyName(sourceFile, docSpan.textSpan.start);
1655+
const adjustedNode = FindAllReferences.Core.getAdjustedNode(rawNode, { use: FindAllReferences.FindReferencesUse.References });
1656+
return adjustedNode;
1657+
}
1658+
}
1659+
16041660
function cleanupSemanticCache(): void {
16051661
program = undefined!; // TODO: GH#18217
16061662
}
@@ -2716,6 +2772,7 @@ namespace ts {
27162772
getNonBoundSourceFile,
27172773
getProgram,
27182774
getAutoImportProvider,
2775+
updateIsDefinitionOfReferencedSymbols,
27192776
getApplicableRefactors,
27202777
getEditsForRefactor,
27212778
toLineColumnOffset,

src/services/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,11 @@ namespace ts {
552552
/* @internal */ getNonBoundSourceFile(fileName: string): SourceFile;
553553
/* @internal */ getAutoImportProvider(): Program | undefined;
554554

555+
/// Returns true if a suitable symbol was found in the project.
556+
/// May set isDefinition properties in `referencedSymbols` to false.
557+
/// May add elements to `knownSymbolSpans`.
558+
/* @internal */ updateIsDefinitionOfReferencedSymbols(referencedSymbols: readonly ReferencedSymbol[], knownSymbolSpans: Set<DocumentSpan>): boolean;
559+
555560
toggleLineComment(fileName: string, textRange: TextRange): TextChange[];
556561
toggleMultilineComment(fileName: string, textRange: TextRange): TextChange[];
557562
commentSelection(fileName: string, textRange: TextRange): TextChange[];

src/services/utilities.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,6 +2117,48 @@ namespace ts {
21172117
return true;
21182118
}
21192119

2120+
export function getMappedLocation(location: DocumentPosition, sourceMapper: SourceMapper, fileExists: ((path: string) => boolean) | undefined): DocumentPosition | undefined {
2121+
const mapsTo = sourceMapper.tryGetSourcePosition(location);
2122+
return mapsTo && (!fileExists || fileExists(normalizePath(mapsTo.fileName)) ? mapsTo : undefined);
2123+
}
2124+
2125+
export function getMappedDocumentSpan(documentSpan: DocumentSpan, sourceMapper: SourceMapper, fileExists?: (path: string) => boolean): DocumentSpan | undefined {
2126+
const { fileName, textSpan } = documentSpan;
2127+
const newPosition = getMappedLocation({ fileName, pos: textSpan.start }, sourceMapper, fileExists);
2128+
if (!newPosition) return undefined;
2129+
const newEndPosition = getMappedLocation({ fileName, pos: textSpan.start + textSpan.length }, sourceMapper, fileExists);
2130+
const newLength = newEndPosition
2131+
? newEndPosition.pos - newPosition.pos
2132+
: textSpan.length; // This shouldn't happen
2133+
return {
2134+
fileName: newPosition.fileName,
2135+
textSpan: {
2136+
start: newPosition.pos,
2137+
length: newLength,
2138+
},
2139+
originalFileName: documentSpan.fileName,
2140+
originalTextSpan: documentSpan.textSpan,
2141+
contextSpan: getMappedContextSpan(documentSpan, sourceMapper, fileExists),
2142+
originalContextSpan: documentSpan.contextSpan
2143+
};
2144+
}
2145+
2146+
export function getMappedContextSpan(documentSpan: DocumentSpan, sourceMapper: SourceMapper, fileExists?: (path: string) => boolean): TextSpan | undefined {
2147+
const contextSpanStart = documentSpan.contextSpan && getMappedLocation(
2148+
{ fileName: documentSpan.fileName, pos: documentSpan.contextSpan.start },
2149+
sourceMapper,
2150+
fileExists
2151+
);
2152+
const contextSpanEnd = documentSpan.contextSpan && getMappedLocation(
2153+
{ fileName: documentSpan.fileName, pos: documentSpan.contextSpan.start + documentSpan.contextSpan.length },
2154+
sourceMapper,
2155+
fileExists
2156+
);
2157+
return contextSpanStart && contextSpanEnd ?
2158+
{ start: contextSpanStart.pos, length: contextSpanEnd.pos - contextSpanStart.pos } :
2159+
undefined;
2160+
}
2161+
21202162
// #endregion
21212163

21222164
// Display-part writer helpers

src/testRunner/unittests/tsserver/declarationFileMaps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ namespace ts.projectSystem {
400400

401401
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()"));
402402
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
403-
refs: [...referencesUserTs(userTs, /*isDefinition*/ undefined), referenceATs(aTs, /*isDefinition*/ true)], // Presently inconsistent across projects
403+
refs: [...referencesUserTs(userTs, /*isDefinition*/ undefined), referenceATs(aTs, /*isDefinition*/ undefined)],
404404
symbolName: "fnA",
405405
symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset,
406406
symbolDisplayString: "function fnA(): void",
@@ -455,7 +455,7 @@ namespace ts.projectSystem {
455455
},
456456
references: [
457457
makeReferencedSymbolEntry({ file: userTs, text: "fnA" }),
458-
makeReferencedSymbolEntry({ file: aTs, text: "fnA", isDefinition: true, isWriteAccess: true, contextText: "export function fnA() {}" }),
458+
makeReferencedSymbolEntry({ file: aTs, text: "fnA", isWriteAccess: true, contextText: "export function fnA() {}" }),
459459
],
460460
},
461461
]);

src/testRunner/unittests/tsserver/projectReferences.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,126 @@ testCompositeFunction('why hello there', 42);`
581581
baselineTsserverLogs("projectReferences", `finding local reference doesnt load ancestor/sibling projects`, session);
582582
});
583583

584+
it("when finding references in overlapping projects", () => {
585+
const solutionLocation = "/user/username/projects/solution";
586+
const solutionConfig: File = {
587+
path: `${solutionLocation}/tsconfig.json`,
588+
content: JSON.stringify({
589+
files: [],
590+
include: [],
591+
references: [
592+
{ path: "./a" },
593+
{ path: "./b" },
594+
{ path: "./c" },
595+
{ path: "./d" },
596+
]
597+
})
598+
};
599+
const aConfig: File = {
600+
path: `${solutionLocation}/a/tsconfig.json`,
601+
content: JSON.stringify({
602+
compilerOptions: {
603+
composite: true,
604+
module: "none"
605+
},
606+
files: ["./index.ts"]
607+
})
608+
};
609+
const aFile: File = {
610+
path: `${solutionLocation}/a/index.ts`,
611+
content: `
612+
export interface I {
613+
M(): void;
614+
}`
615+
};
616+
617+
const bConfig: File = {
618+
path: `${solutionLocation}/b/tsconfig.json`,
619+
content: JSON.stringify({
620+
compilerOptions: {
621+
composite: true
622+
},
623+
files: ["./index.ts"],
624+
references: [
625+
{ path: "../a" }
626+
]
627+
})
628+
};
629+
const bFile: File = {
630+
path: `${solutionLocation}/b/index.ts`,
631+
content: `
632+
import { I } from "../a";
633+
634+
export class B implements I {
635+
M() {}
636+
}`
637+
};
638+
639+
const cConfig: File = {
640+
path: `${solutionLocation}/c/tsconfig.json`,
641+
content: JSON.stringify({
642+
compilerOptions: {
643+
composite: true
644+
},
645+
files: ["./index.ts"],
646+
references: [
647+
{ path: "../b" }
648+
]
649+
})
650+
};
651+
const cFile: File = {
652+
path: `${solutionLocation}/c/index.ts`,
653+
content: `
654+
import { I } from "../a";
655+
import { B } from "../b";
656+
657+
export const C: I = new B();
658+
`
659+
};
660+
661+
const dConfig: File = {
662+
path: `${solutionLocation}/d/tsconfig.json`,
663+
content: JSON.stringify({
664+
compilerOptions: {
665+
composite: true
666+
},
667+
files: ["./index.ts"],
668+
references: [
669+
{ path: "../c" }
670+
]
671+
})
672+
};
673+
const dFile: File = {
674+
path: `${solutionLocation}/d/index.ts`,
675+
content: `
676+
import { I } from "../a";
677+
import { C } from "../c";
678+
679+
export const D: I = C;
680+
`
681+
};
682+
683+
const files = [libFile, solutionConfig, aConfig, aFile, bConfig, bFile, cConfig, cFile, dConfig, dFile, libFile];
684+
const host = createServerHost(files);
685+
const session = createSession(host, { logger: createLoggerWithInMemoryLogs() });
686+
openFilesForSession([bFile], session);
687+
688+
// The first search will trigger project loads
689+
session.executeCommandSeq<protocol.ReferencesRequest>({
690+
command: protocol.CommandTypes.References,
691+
arguments: protocolFileLocationFromSubstring(bFile, "I", { index: 1 })
692+
});
693+
694+
// The second search starts with the projects already loaded
695+
// Formerly, this would search some projects multiple times
696+
session.executeCommandSeq<protocol.ReferencesRequest>({
697+
command: protocol.CommandTypes.References,
698+
arguments: protocolFileLocationFromSubstring(bFile, "I", { index: 1 })
699+
});
700+
701+
baselineTsserverLogs("projectReferences", `finding references in overlapping projects`, session);
702+
});
703+
584704
describe("special handling of localness of the definitions for findAllRefs", () => {
585705
function verify(scenario: string, definition: string, usage: string, referenceTerm: string) {
586706
it(scenario, () => {

0 commit comments

Comments
 (0)