Skip to content

Commit 0abfb52

Browse files
authored
Handle file name casing when deduplicating results from the project (#56438)
1 parent ffc21e5 commit 0abfb52

File tree

5 files changed

+666
-11
lines changed

5 files changed

+666
-11
lines changed

src/harness/fourslashImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,7 @@ export class TestState {
15691569
details.push({ location: contextSpanEnd, locationMarker: "|>", span, type: "contextEnd" });
15701570
}
15711571

1572-
if (additionalSpan && ts.documentSpansEqual(additionalSpan, span)) {
1572+
if (additionalSpan && ts.documentSpansEqual(additionalSpan, span, this.languageServiceAdapterHost.useCaseSensitiveFileNames())) {
15731573
// This span is same as text span
15741574
groupedSpanForAdditionalSpan = span;
15751575
}

src/server/session.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
formatting,
5454
getDeclarationFromName,
5555
getDeclarationOfKind,
56+
getDocumentSpansEqualityComparer,
5657
getEmitDeclarations,
5758
getEntrypointsFromPackageJsonInfo,
5859
getLineAndCharacterOfPosition,
@@ -498,8 +499,8 @@ interface ProjectNavigateToItems {
498499
navigateToItems: readonly NavigateToItem[];
499500
}
500501

501-
function createDocumentSpanSet(): Set<DocumentSpan> {
502-
return createSet(({ textSpan }) => textSpan.start + 100003 * textSpan.length, documentSpansEqual);
502+
function createDocumentSpanSet(useCaseSensitiveFileNames: boolean): Set<DocumentSpan> {
503+
return createSet(({ textSpan }) => textSpan.start + 100003 * textSpan.length, getDocumentSpansEqualityComparer(useCaseSensitiveFileNames));
503504
}
504505

505506
function getRenameLocationsWorker(
@@ -509,6 +510,7 @@ function getRenameLocationsWorker(
509510
findInStrings: boolean,
510511
findInComments: boolean,
511512
preferences: protocol.UserPreferences,
513+
useCaseSensitiveFileNames: boolean,
512514
): readonly RenameLocation[] {
513515
const perProjectResults = getPerProjectReferences(
514516
projects,
@@ -525,7 +527,7 @@ function getRenameLocationsWorker(
525527
}
526528

527529
const results: RenameLocation[] = [];
528-
const seen = createDocumentSpanSet();
530+
const seen = createDocumentSpanSet(useCaseSensitiveFileNames);
529531

530532
perProjectResults.forEach((projectResults, project) => {
531533
for (const result of projectResults) {
@@ -552,6 +554,7 @@ function getReferencesWorker(
552554
projects: Projects,
553555
defaultProject: Project,
554556
initialLocation: DocumentPosition,
557+
useCaseSensitiveFileNames: boolean,
555558
logger: Logger,
556559
): readonly ReferencedSymbol[] {
557560
const perProjectResults = getPerProjectReferences(
@@ -593,7 +596,7 @@ function getReferencesWorker(
593596
}
594597
else {
595598
// Correct isDefinition properties from projects other than defaultProject
596-
const knownSymbolSpans = createDocumentSpanSet();
599+
const knownSymbolSpans = createDocumentSpanSet(useCaseSensitiveFileNames);
597600
for (const referencedSymbol of defaultProjectResults) {
598601
for (const ref of referencedSymbol.references) {
599602
if (ref.isDefinition) {
@@ -632,7 +635,7 @@ function getReferencesWorker(
632635
// of each definition and merging references from all the projects where they appear.
633636

634637
const results: ReferencedSymbol[] = [];
635-
const seenRefs = createDocumentSpanSet(); // It doesn't make sense to have a reference in two definition lists, so we de-dup globally
638+
const seenRefs = createDocumentSpanSet(useCaseSensitiveFileNames); // It doesn't make sense to have a reference in two definition lists, so we de-dup globally
636639

637640
// TODO: We might end up with a more logical allocation of refs to defs if we pre-sorted the defs by descending ref-count.
638641
// Otherwise, it just ends up attached to the first corresponding def we happen to process. The others may or may not be
@@ -649,7 +652,7 @@ function getReferencesWorker(
649652
contextSpan: getMappedContextSpanForProject(referencedSymbol.definition, project),
650653
};
651654

652-
let symbolToAddTo = find(results, o => documentSpansEqual(o.definition, definition));
655+
let symbolToAddTo = find(results, o => documentSpansEqual(o.definition, definition, useCaseSensitiveFileNames));
653656
if (!symbolToAddTo) {
654657
symbolToAddTo = { definition, references: [] };
655658
results.push(symbolToAddTo);
@@ -1542,7 +1545,10 @@ export class Session<TMessage = string> implements EventSender {
15421545
);
15431546

15441547
if (needsJsResolution) {
1545-
const definitionSet = createSet<DefinitionInfo>(d => d.textSpan.start, documentSpansEqual);
1548+
const definitionSet = createSet<DefinitionInfo>(
1549+
d => d.textSpan.start,
1550+
getDocumentSpansEqualityComparer(this.host.useCaseSensitiveFileNames),
1551+
);
15461552
definitions?.forEach(d => definitionSet.add(d));
15471553
const noDtsProject = project.getNoDtsResolutionProject(file);
15481554
const ls = noDtsProject.getLanguageService();
@@ -1993,6 +1999,7 @@ export class Session<TMessage = string> implements EventSender {
19931999
!!args.findInStrings,
19942000
!!args.findInComments,
19952001
preferences,
2002+
this.host.useCaseSensitiveFileNames,
19962003
);
19972004
if (!simplifiedResult) return locations;
19982005
return { info: renameInfo, locs: this.toSpanGroups(locations) };
@@ -2029,6 +2036,7 @@ export class Session<TMessage = string> implements EventSender {
20292036
projects,
20302037
this.getDefaultProject(args),
20312038
{ fileName: args.file, pos: position },
2039+
this.host.useCaseSensitiveFileNames,
20322040
this.logger,
20332041
);
20342042

@@ -2054,7 +2062,7 @@ export class Session<TMessage = string> implements EventSender {
20542062
const preferences = this.getPreferences(toNormalizedPath(fileName));
20552063

20562064
const references: ReferenceEntry[] = [];
2057-
const seen = createDocumentSpanSet();
2065+
const seen = createDocumentSpanSet(this.host.useCaseSensitiveFileNames);
20582066

20592067
forEachProjectInProjects(projects, /*path*/ undefined, project => {
20602068
if (project.getCancellationToken().isCancellationRequested()) return;

src/services/utilities.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ import {
5959
EndOfFileToken,
6060
endsWith,
6161
ensureScriptKind,
62+
EqualityComparer,
6263
EqualityOperator,
64+
equateStringsCaseInsensitive,
65+
equateStringsCaseSensitive,
6366
escapeString,
6467
ExportAssignment,
6568
ExportDeclaration,
@@ -2663,8 +2666,14 @@ export function textSpansEqual(a: TextSpan | undefined, b: TextSpan | undefined)
26632666
return !!a && !!b && a.start === b.start && a.length === b.length;
26642667
}
26652668
/** @internal */
2666-
export function documentSpansEqual(a: DocumentSpan, b: DocumentSpan): boolean {
2667-
return a.fileName === b.fileName && textSpansEqual(a.textSpan, b.textSpan);
2669+
export function documentSpansEqual(a: DocumentSpan, b: DocumentSpan, useCaseSensitiveFileNames: boolean): boolean {
2670+
return (useCaseSensitiveFileNames ? equateStringsCaseSensitive : equateStringsCaseInsensitive)(a.fileName, b.fileName) &&
2671+
textSpansEqual(a.textSpan, b.textSpan);
2672+
}
2673+
2674+
/** @internal */
2675+
export function getDocumentSpansEqualityComparer(useCaseSensitiveFileNames: boolean): EqualityComparer<DocumentSpan> {
2676+
return (a, b) => documentSpansEqual(a, b, useCaseSensitiveFileNames);
26682677
}
26692678

26702679
/**

src/testRunner/unittests/tsserver/rename.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import * as ts from "../../_namespaces/ts";
2+
import {
3+
dedent,
4+
} from "../../_namespaces/Utils";
5+
import {
6+
jsonToReadableText,
7+
} from "../helpers";
8+
import {
9+
libContent,
10+
} from "../helpers/contents";
211
import {
312
baselineTsserverLogs,
413
openFilesForSession,
@@ -8,6 +17,7 @@ import {
817
import {
918
createServerHost,
1019
File,
20+
libFile,
1121
} from "../helpers/virtualFileSystemWithWatch";
1222

1323
describe("unittests:: tsserver:: rename", () => {
@@ -131,4 +141,56 @@ describe("unittests:: tsserver:: rename", () => {
131141
});
132142
baselineTsserverLogs("rename", "rename behavior is based on file of rename initiation", session);
133143
});
144+
145+
it("with symlinks and case difference", () => {
146+
const file: File = {
147+
path: "C:/temp/test/project1/index.ts",
148+
content: dedent`
149+
export function myFunc() {
150+
}
151+
`,
152+
};
153+
const host = createServerHost({
154+
[file.path]: file.content,
155+
"C:/temp/test/project1/tsconfig.json": jsonToReadableText({
156+
compilerOptions: {
157+
composite: true,
158+
},
159+
}),
160+
"C:/temp/test/project1/package.json": jsonToReadableText({
161+
name: "project1",
162+
version: "1.0.0",
163+
main: "index.js",
164+
}),
165+
"C:/temp/test/project2/index.ts": dedent`
166+
import { myFunc } from 'project1'
167+
myFunc();
168+
`,
169+
"C:/temp/test/project2/tsconfig.json": jsonToReadableText({
170+
compilerOptions: {
171+
composite: true,
172+
},
173+
references: [
174+
{ path: "../project1" },
175+
],
176+
}),
177+
"C:/temp/test/tsconfig.json": jsonToReadableText({
178+
references: [
179+
{ path: "./project1" },
180+
{ path: "./project2" },
181+
],
182+
files: [],
183+
include: [],
184+
}),
185+
"C:/temp/test/node_modules/project1": { symLink: "c:/temp/test/project1" },
186+
[libFile.path]: libContent,
187+
}, { windowsStyleRoot: "C:/" });
188+
const session = new TestSession(host);
189+
openFilesForSession([file.path.toLowerCase()], session);
190+
session.executeCommandSeq<ts.server.protocol.RenameRequest>({
191+
command: ts.server.protocol.CommandTypes.Rename,
192+
arguments: protocolFileLocationFromSubstring(file, "myFunc"),
193+
});
194+
baselineTsserverLogs("rename", "with symlinks and case difference", session);
195+
});
134196
});

0 commit comments

Comments
 (0)