Skip to content

Find references of a module by filename #41805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Dec 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,18 @@ namespace ts.server {
}));
}

getFileReferences(fileName: string): ReferenceEntry[] {
const request = this.processRequest<protocol.FileReferencesRequest>(CommandNames.FileReferences, { file: fileName });
const response = this.processResponse<protocol.FileReferencesResponse>(request);

return response.body!.refs.map(entry => ({ // TODO: GH#18217
fileName: entry.file,
textSpan: this.decodeSpan(entry),
isWriteAccess: entry.isWriteAccess,
isDefinition: entry.isDefinition,
}));
}

getEmitOutput(file: string): EmitOutput {
const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file });
const response = this.processResponse<protocol.EmitOutputResponse>(request);
Expand Down
65 changes: 52 additions & 13 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,38 +1114,77 @@ namespace FourSlash {
}
}

public verifyBaselineFindAllReferences(markerName: string) {
const marker = this.getMarkerByName(markerName);
const references = this.languageService.findReferences(marker.fileName, marker.position);
public verifyBaselineFindAllReferences(...markerNames: string[]) {
const baseline = markerNames.map(markerName => {
const marker = this.getMarkerByName(markerName);
const references = this.languageService.findReferences(marker.fileName, marker.position);
const refsByFile = references
? ts.group(ts.sort(ts.flatMap(references, r => r.references), (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
: ts.emptyArray;

// Write input files
const baselineContent = this.getBaselineContentForGroupedReferences(refsByFile, markerName);

// Write response JSON
return baselineContent + JSON.stringify(references, undefined, 2);
}).join("\n\n");
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baseline);
}

public verifyBaselineGetFileReferences(fileName: string) {
const references = this.languageService.getFileReferences(fileName);
const refsByFile = references
? ts.group(ts.sort(ts.flatMap(references, r => r.references), (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
? ts.group(ts.sort(references, (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
: ts.emptyArray;

// Write input files
let baselineContent = this.getBaselineContentForGroupedReferences(refsByFile);

// Write response JSON
baselineContent += JSON.stringify(references, undefined, 2);
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baselineContent);
}

private getBaselineContentForGroupedReferences(refsByFile: readonly (readonly ts.ReferenceEntry[])[], markerName?: string) {
const marker = markerName !== undefined ? this.getMarkerByName(markerName) : undefined;
let baselineContent = "";
for (const group of refsByFile) {
baselineContent += getBaselineContentForFile(group[0].fileName, this.getFileContent(group[0].fileName));
baselineContent += "\n\n";
}

// Write response JSON
baselineContent += JSON.stringify(references, undefined, 2);
Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baselineContent);
return baselineContent;

function getBaselineContentForFile(fileName: string, content: string) {
let newContent = `=== ${fileName} ===\n`;
let pos = 0;
for (const { textSpan } of refsByFile.find(refs => refs[0].fileName === fileName) ?? ts.emptyArray) {
if (fileName === marker.fileName && ts.textSpanContainsPosition(textSpan, marker.position)) {
newContent += "/*FIND ALL REFS*/";
}
const end = textSpan.start + textSpan.length;
newContent += content.slice(pos, textSpan.start);
newContent += "[|";
newContent += content.slice(textSpan.start, end);
pos = textSpan.start;
// It's easier to read if the /*FIND ALL REFS*/ comment is outside the range markers, which makes
// this code a bit more verbose than it would be if I were less picky about the baseline format.
if (fileName === marker?.fileName && marker.position === textSpan.start) {
newContent += "/*FIND ALL REFS*/";
newContent += "[|";
}
else if (fileName === marker?.fileName && ts.textSpanContainsPosition(textSpan, marker.position)) {
newContent += "[|";
newContent += content.slice(pos, marker.position);
newContent += "/*FIND ALL REFS*/";
pos = marker.position;
}
else {
newContent += "[|";
}
newContent += content.slice(pos, end);
newContent += "|]";
pos = end;
}
if (marker?.fileName === fileName && marker.position >= pos) {
newContent += content.slice(pos, marker.position);
newContent += "/*FIND ALL REFS*/";
pos = marker.position;
}
newContent += content.slice(pos);
return newContent.split(/\r?\n/).map(l => "// " + l).join("\n");
}
Expand Down
8 changes: 6 additions & 2 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,12 @@ namespace FourSlashInterface {
this.state.verifyTypeOfSymbolAtLocation(range, symbol, expected);
}

public baselineFindAllReferences(markerName: string) {
this.state.verifyBaselineFindAllReferences(markerName);
public baselineFindAllReferences(...markerNames: string[]) {
this.state.verifyBaselineFindAllReferences(...markerNames);
}

public baselineGetFileReferences(fileName: string) {
this.state.verifyBaselineGetFileReferences(fileName);
}

public referenceGroups(starts: ArrayOrSingle<string> | ArrayOrSingle<FourSlash.Range>, parts: ReferenceGroup[]) {
Expand Down
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,9 @@ namespace Harness.LanguageService {
findReferences(fileName: string, position: number): ts.ReferencedSymbol[] {
return unwrapJSONCallResult(this.shim.findReferences(fileName, position));
}
getFileReferences(fileName: string): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getFileReferences(fileName));
}
getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position));
}
Expand Down
22 changes: 22 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ namespace ts.server.protocol {
/* @internal */
EmitOutput = "emit-output",
Exit = "exit",
FileReferences = "fileReferences",
/* @internal */
FileReferencesFull = "fileReferences-full",
Format = "format",
Formatonkey = "formatonkey",
/* @internal */
Expand Down Expand Up @@ -1152,6 +1155,25 @@ namespace ts.server.protocol {
body?: ReferencesResponseBody;
}

export interface FileReferencesRequest extends FileRequest {
command: CommandTypes.FileReferences;
}

export interface FileReferencesResponseBody {
/**
* The file locations referencing the symbol.
*/
refs: readonly ReferencesResponseItem[];
/**
* The name of the symbol.
*/
symbolName: string;
}

export interface FileReferencesResponse extends Response {
body?: FileReferencesResponseBody;
}

/**
* Argument for RenameRequest request.
*/
Expand Down
79 changes: 64 additions & 15 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,29 @@ namespace ts.server {
return outputs.filter(o => o.references.length !== 0);
}

function combineProjectOutputForFileReferences(
projects: Projects,
defaultProject: Project,
fileName: string
): readonly ReferenceEntry[] {
const outputs: ReferenceEntry[] = [];

combineProjectOutputWorker(
projects,
defaultProject,
/*initialLocation*/ undefined,
project => {
for (const referenceEntry of project.getLanguageService().getFileReferences(fileName) || emptyArray) {
if (!contains(outputs, referenceEntry, documentSpansEqual)) {
outputs.push(referenceEntry);
}
}
},
);

return outputs;
}

interface ProjectAndLocation<TLocation extends DocumentPosition | undefined> {
readonly project: Project;
readonly location: TLocation;
Expand Down Expand Up @@ -1509,7 +1532,7 @@ namespace ts.server {
return arrayFrom(map.values());
}

private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | undefined | readonly ReferencedSymbol[] {
private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | readonly ReferencedSymbol[] {
const file = toNormalizedPath(args.file);
const projects = this.getProjects(args);
const position = this.getPositionInFile(args, file);
Expand All @@ -1528,22 +1551,28 @@ namespace ts.server {
const nameSpan = nameInfo && nameInfo.textSpan;
const symbolStartOffset = nameSpan ? scriptInfo.positionToLineOffset(nameSpan.start).offset : 0;
const symbolName = nameSpan ? scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan)) : "";
const refs: readonly protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol =>
referencedSymbol.references.map(({ fileName, textSpan, contextSpan, isWriteAccess, isDefinition }): protocol.ReferencesResponseItem => {
const scriptInfo = Debug.checkDefined(this.projectService.getScriptInfo(fileName));
const span = toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo);
const lineSpan = scriptInfo.lineToTextSpan(span.start.line - 1);
const lineText = scriptInfo.getSnapshot().getText(lineSpan.start, textSpanEnd(lineSpan)).replace(/\r|\n/g, "");
return {
file: fileName,
...span,
lineText,
isWriteAccess,
isDefinition
};
}));
const refs: readonly protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol => {
return referencedSymbol.references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry));
});
return { refs, symbolName, symbolStartOffset, symbolDisplayString };
}

private getFileReferences(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.FileReferencesResponseBody | readonly ReferenceEntry[] {
const projects = this.getProjects(args);
const references = combineProjectOutputForFileReferences(
projects,
this.getDefaultProject(args),
args.file,
);

if (!simplifiedResult) return references;
const refs = references.map(entry => referenceEntryToReferencesResponseItem(this.projectService, entry));
return {
refs,
symbolName: `"${args.file}"`
};
}

/**
* @param fileName is the name of the file to be opened
* @param fileContent is a version of the file content that is known to be more up to date than the one on disk
Expand Down Expand Up @@ -2639,6 +2668,12 @@ namespace ts.server {
[CommandNames.GetSpanOfEnclosingComment]: (request: protocol.SpanOfEnclosingCommentRequest) => {
return this.requiredResponse(this.getSpanOfEnclosingComment(request.arguments));
},
[CommandNames.FileReferences]: (request: protocol.FileReferencesRequest) => {
return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ true));
},
[CommandNames.FileReferencesFull]: (request: protocol.FileReferencesRequest) => {
return this.requiredResponse(this.getFileReferences(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.Format]: (request: protocol.FormatRequest) => {
return this.requiredResponse(this.getFormattingEditsForRange(request.arguments));
},
Expand Down Expand Up @@ -3068,4 +3103,18 @@ namespace ts.server {

return text;
}

function referenceEntryToReferencesResponseItem(projectService: ProjectService, { fileName, textSpan, contextSpan, isWriteAccess, isDefinition }: ReferenceEntry): protocol.ReferencesResponseItem {
const scriptInfo = Debug.checkDefined(projectService.getScriptInfo(fileName));
const span = toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo);
const lineSpan = scriptInfo.lineToTextSpan(span.start.line - 1);
const lineText = scriptInfo.getSnapshot().getText(lineSpan.start, textSpanEnd(lineSpan)).replace(/\r|\n/g, "");
return {
file: fileName,
...span,
lineText,
isWriteAccess,
isDefinition
};
}
}
45 changes: 44 additions & 1 deletion src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,21 @@ namespace ts.FindAllReferences {
// Could not find a symbol e.g. unknown identifier
if (!symbol) {
// String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial.
return !options.implementations && isStringLiteralLike(node) ? getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken) : undefined;
if (!options.implementations && isStringLiteralLike(node)) {
if (isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ true) || isExternalModuleReference(node.parent) || isImportDeclaration(node.parent) || isImportCall(node.parent)) {
const fileIncludeReasons = program.getFileIncludeReasons();
const referencedFileName = node.getSourceFile().resolvedModules?.get(node.text)?.resolvedFileName;
const referencedFile = referencedFileName ? program.getSourceFile(referencedFileName) : undefined;
if (referencedFile) {
return [{ definition: { type: DefinitionKind.String, node }, references: getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || emptyArray }];
}
// Fall through to string literal references. This is not very likely to return
// anything useful, but I guess it's better than nothing, and there's an existing
// test that expects this to happen (fourslash/cases/untypedModuleImport.ts).
}
return getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken);
}
return undefined;
}

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

export function getReferencesForFileName(fileName: string, program: Program, sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlySet<string> = new Set(sourceFiles.map(f => f.fileName))): readonly Entry[] {
const moduleSymbol = program.getSourceFile(fileName)?.symbol;
if (moduleSymbol) {
return getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet)[0]?.references || emptyArray;
}
const fileIncludeReasons = program.getFileIncludeReasons();
const referencedFile = program.getSourceFile(fileName);
return referencedFile && fileIncludeReasons && getReferencesForNonModule(referencedFile, fileIncludeReasons, program) || emptyArray;
}

function getReferencesForNonModule(referencedFile: SourceFile, refFileMap: MultiMap<Path, FileIncludeReason>, program: Program): readonly SpanEntry[] | undefined {
let entries: SpanEntry[] | undefined;
const references = refFileMap.get(referencedFile.path) || emptyArray;
for (const ref of references) {
if (isReferencedFile(ref)) {
const referencingFile = program.getSourceFileByPath(ref.file)!;
const location = getReferencedFileLocation(program.getSourceFileByPath, ref);
if (isReferenceFileLocation(location)) {
entries = append(entries, {
kind: EntryKind.Span,
fileName: referencingFile.fileName,
textSpan: createTextSpanFromRange(location)
});
}
}
}
return entries;
}

function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: Node, symbol: Symbol, checker: TypeChecker) {
if (node.parent && isNamespaceExportDeclaration(node.parent)) {
const aliasedSymbol = checker.getAliasedSymbol(symbol);
Expand Down
6 changes: 6 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,11 @@ namespace ts {
return FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
}

function getFileReferences(fileName: string): ReferenceEntry[] {
synchronizeHostData();
return FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(FindAllReferences.toReferenceEntry);
}

function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] {
synchronizeHostData();
const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles();
Expand Down Expand Up @@ -2526,6 +2531,7 @@ namespace ts {
getTypeDefinitionAtPosition,
getReferencesAtPosition,
findReferences,
getFileReferences,
getOccurrencesAtPosition,
getDocumentHighlights,
getNameOrDottedNameSpan,
Expand Down
Loading