Skip to content

Commit 9dfbf07

Browse files
authored
Find references of a module by filename (#41805)
* Naive implementation enough to build and write a test * Add simple test * Add project references test * Add deduplication test, accept baselines * Add test for referencing a script (doesn’t do anything) * Update API baselines * Use refFileMap for non-module references * Fix find-all-refs on module specifier * Remove unused util * Don’t store text range on ts.RefFile * Ensure string literal could itself be a file reference * Remove unused utilities * Improve baseline format * Preserve old behavior of falling back to string literal references * Update baselines from master * Fix old RefFileMap code after merge * Add test for additional response info * Undo test change
1 parent 1c1cd9b commit 9dfbf07

36 files changed

+810
-45
lines changed

src/harness/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ namespace ts.server {
362362
}));
363363
}
364364

365+
getFileReferences(fileName: string): ReferenceEntry[] {
366+
const request = this.processRequest<protocol.FileReferencesRequest>(CommandNames.FileReferences, { file: fileName });
367+
const response = this.processResponse<protocol.FileReferencesResponse>(request);
368+
369+
return response.body!.refs.map(entry => ({ // TODO: GH#18217
370+
fileName: entry.file,
371+
textSpan: this.decodeSpan(entry),
372+
isWriteAccess: entry.isWriteAccess,
373+
isDefinition: entry.isDefinition,
374+
}));
375+
}
376+
365377
getEmitOutput(file: string): EmitOutput {
366378
const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
367379
const response = this.processResponse<protocol.EmitOutputResponse>(request);

src/harness/fourslashImpl.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,38 +1114,77 @@ namespace FourSlash {
11141114
}
11151115
}
11161116

1117-
public verifyBaselineFindAllReferences(markerName: string) {
1118-
const marker = this.getMarkerByName(markerName);
1119-
const references = this.languageService.findReferences(marker.fileName, marker.position);
1117+
public verifyBaselineFindAllReferences(...markerNames: string[]) {
1118+
const baseline = markerNames.map(markerName => {
1119+
const marker = this.getMarkerByName(markerName);
1120+
const references = this.languageService.findReferences(marker.fileName, marker.position);
1121+
const refsByFile = references
1122+
? ts.group(ts.sort(ts.flatMap(references, r => r.references), (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
1123+
: ts.emptyArray;
1124+
1125+
// Write input files
1126+
const baselineContent = this.getBaselineContentForGroupedReferences(refsByFile, markerName);
1127+
1128+
// Write response JSON
1129+
return baselineContent + JSON.stringify(references, undefined, 2);
1130+
}).join("\n\n");
1131+
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baseline);
1132+
}
1133+
1134+
public verifyBaselineGetFileReferences(fileName: string) {
1135+
const references = this.languageService.getFileReferences(fileName);
11201136
const refsByFile = references
1121-
? ts.group(ts.sort(ts.flatMap(references, r => r.references), (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
1137+
? ts.group(ts.sort(references, (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
11221138
: ts.emptyArray;
11231139

11241140
// Write input files
1141+
let baselineContent = this.getBaselineContentForGroupedReferences(refsByFile);
1142+
1143+
// Write response JSON
1144+
baselineContent += JSON.stringify(references, undefined, 2);
1145+
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baselineContent);
1146+
}
1147+
1148+
private getBaselineContentForGroupedReferences(refsByFile: readonly (readonly ts.ReferenceEntry[])[], markerName?: string) {
1149+
const marker = markerName !== undefined ? this.getMarkerByName(markerName) : undefined;
11251150
let baselineContent = "";
11261151
for (const group of refsByFile) {
11271152
baselineContent += getBaselineContentForFile(group[0].fileName, this.getFileContent(group[0].fileName));
11281153
baselineContent += "\n\n";
11291154
}
1130-
1131-
// Write response JSON
1132-
baselineContent += JSON.stringify(references, undefined, 2);
1133-
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baselineContent);
1155+
return baselineContent;
11341156

11351157
function getBaselineContentForFile(fileName: string, content: string) {
11361158
let newContent = `=== ${fileName} ===\n`;
11371159
let pos = 0;
11381160
for (const { textSpan } of refsByFile.find(refs => refs[0].fileName === fileName) ?? ts.emptyArray) {
1139-
if (fileName === marker.fileName && ts.textSpanContainsPosition(textSpan, marker.position)) {
1140-
newContent += "/*FIND ALL REFS*/";
1141-
}
11421161
const end = textSpan.start + textSpan.length;
11431162
newContent += content.slice(pos, textSpan.start);
1144-
newContent += "[|";
1145-
newContent += content.slice(textSpan.start, end);
1163+
pos = textSpan.start;
1164+
// It's easier to read if the /*FIND ALL REFS*/ comment is outside the range markers, which makes
1165+
// this code a bit more verbose than it would be if I were less picky about the baseline format.
1166+
if (fileName === marker?.fileName && marker.position === textSpan.start) {
1167+
newContent += "/*FIND ALL REFS*/";
1168+
newContent += "[|";
1169+
}
1170+
else if (fileName === marker?.fileName && ts.textSpanContainsPosition(textSpan, marker.position)) {
1171+
newContent += "[|";
1172+
newContent += content.slice(pos, marker.position);
1173+
newContent += "/*FIND ALL REFS*/";
1174+
pos = marker.position;
1175+
}
1176+
else {
1177+
newContent += "[|";
1178+
}
1179+
newContent += content.slice(pos, end);
11461180
newContent += "|]";
11471181
pos = end;
11481182
}
1183+
if (marker?.fileName === fileName && marker.position >= pos) {
1184+
newContent += content.slice(pos, marker.position);
1185+
newContent += "/*FIND ALL REFS*/";
1186+
pos = marker.position;
1187+
}
11491188
newContent += content.slice(pos);
11501189
return newContent.split(/\r?\n/).map(l => "// " + l).join("\n");
11511190
}

src/harness/fourslashInterfaceImpl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,12 @@ namespace FourSlashInterface {
332332
this.state.verifyTypeOfSymbolAtLocation(range, symbol, expected);
333333
}
334334

335-
public baselineFindAllReferences(markerName: string) {
336-
this.state.verifyBaselineFindAllReferences(markerName);
335+
public baselineFindAllReferences(...markerNames: string[]) {
336+
this.state.verifyBaselineFindAllReferences(...markerNames);
337+
}
338+
339+
public baselineGetFileReferences(fileName: string) {
340+
this.state.verifyBaselineGetFileReferences(fileName);
337341
}
338342

339343
public referenceGroups(starts: ArrayOrSingle<string> | ArrayOrSingle<FourSlash.Range>, parts: ReferenceGroup[]) {

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,9 @@ namespace Harness.LanguageService {
519519
findReferences(fileName: string, position: number): ts.ReferencedSymbol[] {
520520
return unwrapJSONCallResult(this.shim.findReferences(fileName, position));
521521
}
522+
getFileReferences(fileName: string): ts.ReferenceEntry[] {
523+
return unwrapJSONCallResult(this.shim.getFileReferences(fileName));
524+
}
522525
getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
523526
return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position));
524527
}

src/server/protocol.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ namespace ts.server.protocol {
3737
/* @internal */
3838
EmitOutput = "emit-output",
3939
Exit = "exit",
40+
FileReferences = "fileReferences",
41+
/* @internal */
42+
FileReferencesFull = "fileReferences-full",
4043
Format = "format",
4144
Formatonkey = "formatonkey",
4245
/* @internal */
@@ -1152,6 +1155,25 @@ namespace ts.server.protocol {
11521155
body?: ReferencesResponseBody;
11531156
}
11541157

1158+
export interface FileReferencesRequest extends FileRequest {
1159+
command: CommandTypes.FileReferences;
1160+
}
1161+
1162+
export interface FileReferencesResponseBody {
1163+
/**
1164+
* The file locations referencing the symbol.
1165+
*/
1166+
refs: readonly ReferencesResponseItem[];
1167+
/**
1168+
* The name of the symbol.
1169+
*/
1170+
symbolName: string;
1171+
}
1172+
1173+
export interface FileReferencesResponse extends Response {
1174+
body?: FileReferencesResponseBody;
1175+
}
1176+
11551177
/**
11561178
* Argument for RenameRequest request.
11571179
*/

src/server/session.ts

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,29 @@ namespace ts.server {
409409
return outputs.filter(o => o.references.length !== 0);
410410
}
411411

412+
function combineProjectOutputForFileReferences(
413+
projects: Projects,
414+
defaultProject: Project,
415+
fileName: string
416+
): readonly ReferenceEntry[] {
417+
const outputs: ReferenceEntry[] = [];
418+
419+
combineProjectOutputWorker(
420+
projects,
421+
defaultProject,
422+
/*initialLocation*/ undefined,
423+
project => {
424+
for (const referenceEntry of project.getLanguageService().getFileReferences(fileName) || emptyArray) {
425+
if (!contains(outputs, referenceEntry, documentSpansEqual)) {
426+
outputs.push(referenceEntry);
427+
}
428+
}
429+
},
430+
);
431+
432+
return outputs;
433+
}
434+
412435
interface ProjectAndLocation<TLocation extends DocumentPosition | undefined> {
413436
readonly project: Project;
414437
readonly location: TLocation;
@@ -1509,7 +1532,7 @@ namespace ts.server {
15091532
return arrayFrom(map.values());
15101533
}
15111534

1512-
private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | undefined | readonly ReferencedSymbol[] {
1535+
private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | readonly ReferencedSymbol[] {
15131536
const file = toNormalizedPath(args.file);
15141537
const projects = this.getProjects(args);
15151538
const position = this.getPositionInFile(args, file);
@@ -1528,22 +1551,28 @@ namespace ts.server {
15281551
const nameSpan = nameInfo && nameInfo.textSpan;
15291552
const symbolStartOffset = nameSpan ? scriptInfo.positionToLineOffset(nameSpan.start).offset : 0;
15301553
const symbolName = nameSpan ? scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan)) : "";
1531-
const refs: readonly protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol =>
1532-
referencedSymbol.references.map(({ fileName, textSpan, contextSpan, isWriteAccess, isDefinition }): protocol.ReferencesResponseItem => {
1533-
const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(fileName));
1534-
const span = toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo);
1535-
const lineSpan = scriptInfo.lineToTextSpan(span.start.line - 1);
1536-
const lineText = scriptInfo.getSnapshot().getText(lineSpan.start, textSpanEnd(lineSpan)).replace(/\r|\n/g, "");
1537-
return {
1538-
file: fileName,
1539-
...span,
1540-
lineText,
1541-
isWriteAccess,
1542-
isDefinition
1543-
};
1544-
}));
1554+
const refs: readonly protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol => {
1555+
return referencedSymbol.references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry));
1556+
});
15451557
return { refs, symbolName, symbolStartOffset, symbolDisplayString };
15461558
}
1559+
1560+
private getFileReferences(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.FileReferencesResponseBody | readonly ReferenceEntry[] {
1561+
const projects = this.getProjects(args);
1562+
const references = combineProjectOutputForFileReferences(
1563+
projects,
1564+
this.getDefaultProject(args),
1565+
args.file,
1566+
);
1567+
1568+
if (!simplifiedResult) return references;
1569+
const refs = references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry));
1570+
return {
1571+
refs,
1572+
symbolName: `"${args.file}"`
1573+
};
1574+
}
1575+
15471576
/**
15481577
* @param fileName is the name of the file to be opened
15491578
* @param fileContent is a version of the file content that is known to be more up to date than the one on disk
@@ -2639,6 +2668,12 @@ namespace ts.server {
26392668
[CommandNames.GetSpanOfEnclosingComment]: (request: protocol.SpanOfEnclosingCommentRequest) => {
26402669
return this.requiredResponse(this.getSpanOfEnclosingComment(request.arguments));
26412670
},
2671+
[CommandNames.FileReferences]: (request: protocol.FileReferencesRequest) => {
2672+
return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ true));
2673+
},
2674+
[CommandNames.FileReferencesFull]: (request: protocol.FileReferencesRequest) => {
2675+
return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ false));
2676+
},
26422677
[CommandNames.Format]: (request: protocol.FormatRequest) => {
26432678
return this.requiredResponse(this.getFormattingEditsForRange(request.arguments));
26442679
},
@@ -3068,4 +3103,18 @@ namespace ts.server {
30683103

30693104
return text;
30703105
}
3106+
3107+
function referenceEntryToReferencesResponseItem(projectService: ProjectService, { fileName, textSpan, contextSpan, isWriteAccess, isDefinition }: ReferenceEntry): protocol.ReferencesResponseItem {
3108+
const scriptInfo = Debug.checkDefined(projectService.getScriptInfo(fileName));
3109+
const span = toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo);
3110+
const lineSpan = scriptInfo.lineToTextSpan(span.start.line - 1);
3111+
const lineText = scriptInfo.getSnapshot().getText(lineSpan.start, textSpanEnd(lineSpan)).replace(/\r|\n/g, "");
3112+
return {
3113+
file: fileName,
3114+
...span,
3115+
lineText,
3116+
isWriteAccess,
3117+
isDefinition
3118+
};
3119+
}
30713120
}

src/services/findAllReferences.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,21 @@ namespace ts.FindAllReferences {
622622
// Could not find a symbol e.g. unknown identifier
623623
if (!symbol) {
624624
// String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial.
625-
return !options.implementations && isStringLiteralLike(node) ? getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken) : undefined;
625+
if (!options.implementations && isStringLiteralLike(node)) {
626+
if (isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ true) || isExternalModuleReference(node.parent) || isImportDeclaration(node.parent) || isImportCall(node.parent)) {
627+
const fileIncludeReasons = program.getFileIncludeReasons();
628+
const referencedFileName = node.getSourceFile().resolvedModules?.get(node.text)?.resolvedFileName;
629+
const referencedFile = referencedFileName ? program.getSourceFile(referencedFileName) : undefined;
630+
if (referencedFile) {
631+
return [{ definition: { type: DefinitionKind.String, node }, references: getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || emptyArray }];
632+
}
633+
// Fall through to string literal references. This is not very likely to return
634+
// anything useful, but I guess it's better than nothing, and there's an existing
635+
// test that expects this to happen (fourslash/cases/untypedModuleImport.ts).
636+
}
637+
return getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken);
638+
}
639+
return undefined;
626640
}
627641

628642
if (symbol.escapedName === InternalSymbolName.ExportEquals) {
@@ -642,6 +656,35 @@ namespace ts.FindAllReferences {
642656
return mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget);
643657
}
644658

659+
export function getReferencesForFileName(fileName: string, program: Program, sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlySet<string> = new Set(sourceFiles.map(f => f.fileName))): readonly Entry[] {
660+
const moduleSymbol = program.getSourceFile(fileName)?.symbol;
661+
if (moduleSymbol) {
662+
return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet)[0]?.references || emptyArray;
663+
}
664+
const fileIncludeReasons = program.getFileIncludeReasons();
665+
const referencedFile = program.getSourceFile(fileName);
666+
return referencedFile && fileIncludeReasons && getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || emptyArray;
667+
}
668+
669+
function getReferencesForNonModule(referencedFile: SourceFile, refFileMap: MultiMap<Path, FileIncludeReason>, program: Program): readonly SpanEntry[] | undefined {
670+
let entries: SpanEntry[] | undefined;
671+
const references = refFileMap.get(referencedFile.path) || emptyArray;
672+
for (const ref of references) {
673+
if (isReferencedFile(ref)) {
674+
const referencingFile = program.getSourceFileByPath(ref.file)!;
675+
const location = getReferencedFileLocation(program.getSourceFileByPath, ref);
676+
if (isReferenceFileLocation(location)) {
677+
entries = append(entries, {
678+
kind: EntryKind.Span,
679+
fileName: referencingFile.fileName,
680+
textSpan: createTextSpanFromRange(location)
681+
});
682+
}
683+
}
684+
}
685+
return entries;
686+
}
687+
645688
function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: Node, symbol: Symbol, checker: TypeChecker) {
646689
if (node.parent && isNamespaceExportDeclaration(node.parent)) {
647690
const aliasedSymbol = checker.getAliasedSymbol(symbol);

src/services/services.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,6 +1728,11 @@ namespace ts {
17281728
return FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
17291729
}
17301730

1731+
function getFileReferences(fileName: string): ReferenceEntry[] {
1732+
synchronizeHostData();
1733+
return FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(FindAllReferences.toReferenceEntry);
1734+
}
1735+
17311736
function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] {
17321737
synchronizeHostData();
17331738
const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles();
@@ -2526,6 +2531,7 @@ namespace ts {
25262531
getTypeDefinitionAtPosition,
25272532
getReferencesAtPosition,
25282533
findReferences,
2534+
getFileReferences,
25292535
getOccurrencesAtPosition,
25302536
getDocumentHighlights,
25312537
getNameOrDottedNameSpan,

0 commit comments

Comments
 (0)