Skip to content

Commit 8dcbf4c

Browse files
authored
fix(typescript-estree): better handle canonical paths (#1111)
1 parent fd39bbd commit 8dcbf4c

File tree

4 files changed

+101
-32
lines changed

4 files changed

+101
-32
lines changed

Diff for: packages/typescript-estree/src/create-program/createWatchProgram.ts

+38-25
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ import path from 'path';
44
import ts from 'typescript';
55
import { Extra } from '../parser-options';
66
import { WatchCompilerHostOfConfigFile } from '../WatchCompilerHostOfConfigFile';
7-
import { getTsconfigPath, DEFAULT_COMPILER_OPTIONS } from './shared';
7+
import {
8+
canonicalDirname,
9+
CanonicalPath,
10+
getTsconfigPath,
11+
DEFAULT_COMPILER_OPTIONS,
12+
getCanonicalFileName,
13+
} from './shared';
814

915
const log = debug('typescript-eslint:typescript-estree:createWatchProgram');
1016

1117
/**
1218
* Maps tsconfig paths to their corresponding file contents and resulting watches
1319
*/
1420
const knownWatchProgramMap = new Map<
15-
string,
21+
CanonicalPath,
1622
ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>
1723
>();
1824

@@ -21,25 +27,25 @@ const knownWatchProgramMap = new Map<
2127
* There may be more than one per file/folder if a file/folder is shared between projects
2228
*/
2329
const fileWatchCallbackTrackingMap = new Map<
24-
string,
30+
CanonicalPath,
2531
Set<ts.FileWatcherCallback>
2632
>();
2733
const folderWatchCallbackTrackingMap = new Map<
28-
string,
34+
CanonicalPath,
2935
Set<ts.FileWatcherCallback>
3036
>();
3137

3238
/**
3339
* Stores the list of known files for each program
3440
*/
35-
const programFileListCache = new Map<string, Set<string>>();
41+
const programFileListCache = new Map<CanonicalPath, Set<CanonicalPath>>();
3642

3743
/**
3844
* Caches the last modified time of the tsconfig files
3945
*/
40-
const tsconfigLsatModifiedTimestampCache = new Map<string, number>();
46+
const tsconfigLastModifiedTimestampCache = new Map<CanonicalPath, number>();
4147

42-
const parsedFilesSeen = new Set<string>();
48+
const parsedFilesSeen = new Set<CanonicalPath>();
4349

4450
/**
4551
* Clear all of the parser caches.
@@ -51,7 +57,7 @@ function clearCaches(): void {
5157
folderWatchCallbackTrackingMap.clear();
5258
parsedFilesSeen.clear();
5359
programFileListCache.clear();
54-
tsconfigLsatModifiedTimestampCache.clear();
60+
tsconfigLastModifiedTimestampCache.clear();
5561
}
5662

5763
function saveWatchCallback(
@@ -61,7 +67,7 @@ function saveWatchCallback(
6167
fileName: string,
6268
callback: ts.FileWatcherCallback,
6369
): ts.FileWatcher => {
64-
const normalizedFileName = path.normalize(fileName);
70+
const normalizedFileName = getCanonicalFileName(path.normalize(fileName));
6571
const watchers = ((): Set<ts.FileWatcherCallback> => {
6672
let watchers = trackingMap.get(normalizedFileName);
6773
if (!watchers) {
@@ -83,9 +89,9 @@ function saveWatchCallback(
8389
/**
8490
* Holds information about the file currently being linted
8591
*/
86-
const currentLintOperationState = {
92+
const currentLintOperationState: { code: string; filePath: CanonicalPath } = {
8793
code: '',
88-
filePath: '',
94+
filePath: '' as CanonicalPath,
8995
};
9096

9197
/**
@@ -101,16 +107,17 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void {
101107
/**
102108
* Calculate project environments using options provided by consumer and paths from config
103109
* @param code The code being linted
104-
* @param filePath The path of the file being parsed
110+
* @param filePathIn The path of the file being parsed
105111
* @param extra.tsconfigRootDir The root directory for relative tsconfig paths
106112
* @param extra.projects Provided tsconfig paths
107113
* @returns The programs corresponding to the supplied tsconfig paths
108114
*/
109115
function getProgramsForProjects(
110116
code: string,
111-
filePath: string,
117+
filePathIn: string,
112118
extra: Extra,
113119
): ts.Program[] {
120+
const filePath = getCanonicalFileName(filePathIn);
114121
const results = [];
115122

116123
// preserve reference to code and file being linted
@@ -145,7 +152,9 @@ function getProgramsForProjects(
145152
let updatedProgram: ts.Program | null = null;
146153
if (!fileList) {
147154
updatedProgram = existingWatch.getProgram().getProgram();
148-
fileList = new Set(updatedProgram.getRootFileNames());
155+
fileList = new Set(
156+
updatedProgram.getRootFileNames().map(f => getCanonicalFileName(f)),
157+
);
149158
programFileListCache.set(tsconfigPath, fileList);
150159
}
151160

@@ -215,7 +224,8 @@ function createWatchProgram(
215224

216225
// ensure readFile reads the code being linted instead of the copy on disk
217226
const oldReadFile = watchCompilerHost.readFile;
218-
watchCompilerHost.readFile = (filePath, encoding): string | undefined => {
227+
watchCompilerHost.readFile = (filePathIn, encoding): string | undefined => {
228+
const filePath = getCanonicalFileName(filePathIn);
219229
parsedFilesSeen.add(filePath);
220230
return path.normalize(filePath) ===
221231
path.normalize(currentLintOperationState.filePath)
@@ -297,14 +307,14 @@ function createWatchProgram(
297307
return ts.createWatchProgram(watchCompilerHost);
298308
}
299309

300-
function hasTSConfigChanged(tsconfigPath: string): boolean {
310+
function hasTSConfigChanged(tsconfigPath: CanonicalPath): boolean {
301311
const stat = fs.statSync(tsconfigPath);
302312
const lastModifiedAt = stat.mtimeMs;
303-
const cachedLastModifiedAt = tsconfigLsatModifiedTimestampCache.get(
313+
const cachedLastModifiedAt = tsconfigLastModifiedTimestampCache.get(
304314
tsconfigPath,
305315
);
306316

307-
tsconfigLsatModifiedTimestampCache.set(tsconfigPath, lastModifiedAt);
317+
tsconfigLastModifiedTimestampCache.set(tsconfigPath, lastModifiedAt);
308318

309319
if (cachedLastModifiedAt === undefined) {
310320
return false;
@@ -315,8 +325,8 @@ function hasTSConfigChanged(tsconfigPath: string): boolean {
315325

316326
function maybeInvalidateProgram(
317327
existingWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>,
318-
filePath: string,
319-
tsconfigPath: string,
328+
filePath: CanonicalPath,
329+
tsconfigPath: CanonicalPath,
320330
): ts.Program | null {
321331
/*
322332
* By calling watchProgram.getProgram(), it will trigger a resync of the program based on
@@ -355,21 +365,22 @@ function maybeInvalidateProgram(
355365
log('File was not found in program - triggering folder update. %s', filePath);
356366

357367
// Find the correct directory callback by climbing the folder tree
358-
let current: string | null = null;
359-
let next: string | null = path.dirname(filePath);
368+
const currentDir = canonicalDirname(filePath);
369+
let current: CanonicalPath | null = null;
370+
let next = currentDir;
360371
let hasCallback = false;
361372
while (current !== next) {
362373
current = next;
363374
const folderWatchCallbacks = folderWatchCallbackTrackingMap.get(current);
364375
if (folderWatchCallbacks) {
365376
folderWatchCallbacks.forEach(cb =>
366-
cb(current!, ts.FileWatcherEventKind.Changed),
377+
cb(currentDir, ts.FileWatcherEventKind.Changed),
367378
);
368379
hasCallback = true;
369380
break;
370381
}
371382

372-
next = path.dirname(current);
383+
next = canonicalDirname(current);
373384
}
374385
if (!hasCallback) {
375386
/*
@@ -410,7 +421,9 @@ function maybeInvalidateProgram(
410421
return null;
411422
}
412423

413-
const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(deletedFile);
424+
const fileWatchCallbacks = fileWatchCallbackTrackingMap.get(
425+
getCanonicalFileName(deletedFile),
426+
);
414427
if (!fileWatchCallbacks) {
415428
// shouldn't happen, but just in case
416429
log('Could not find watch callbacks for root file. %s', deletedFile);

Diff for: packages/typescript-estree/src/create-program/shared.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,29 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = {
1818
// extendedDiagnostics: true,
1919
};
2020

21-
function getTsconfigPath(tsconfigPath: string, extra: Extra): string {
22-
return path.isAbsolute(tsconfigPath)
23-
? tsconfigPath
24-
: path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath);
21+
// This narrows the type so we can be sure we're passing canonical names in the correct places
22+
type CanonicalPath = string & { __brand: unknown };
23+
const getCanonicalFileName = ts.sys.useCaseSensitiveFileNames
24+
? (path: string): CanonicalPath => path as CanonicalPath
25+
: (path: string): CanonicalPath => path.toLowerCase() as CanonicalPath;
26+
27+
function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath {
28+
return getCanonicalFileName(
29+
path.isAbsolute(tsconfigPath)
30+
? tsconfigPath
31+
: path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath),
32+
);
33+
}
34+
35+
function canonicalDirname(p: CanonicalPath): CanonicalPath {
36+
return path.dirname(p) as CanonicalPath;
2537
}
2638

27-
export { ASTAndProgram, DEFAULT_COMPILER_OPTIONS, getTsconfigPath };
39+
export {
40+
ASTAndProgram,
41+
canonicalDirname,
42+
CanonicalPath,
43+
DEFAULT_COMPILER_OPTIONS,
44+
getCanonicalFileName,
45+
getTsconfigPath,
46+
};

Diff for: packages/typescript-estree/tests/lib/persistentParse.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@ function renameFile(dirName: string, src: 'bar', dest: 'baz/bar'): void {
4040
);
4141
}
4242

43-
function setup(tsconfig: Record<string, unknown>, writeBar = true): string {
43+
function createTmpDir(): tmp.DirResult {
4444
const tmpDir = tmp.dirSync({
4545
keep: false,
4646
unsafeCleanup: true,
4747
});
4848
tmpDirs.add(tmpDir);
49+
return tmpDir;
50+
}
51+
function setup(tsconfig: Record<string, unknown>, writeBar = true): string {
52+
const tmpDir = createTmpDir();
4953

5054
writeTSConfig(tmpDir.name, tsconfig);
5155

@@ -141,4 +145,34 @@ describe('persistent lint session', () => {
141145
expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow();
142146
expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow();
143147
});
148+
149+
it('handles tsconfigs with no includes/excludes (single level)', () => {
150+
const PROJECT_DIR = setup({}, false);
151+
152+
// parse once to: assert the config as correct, and to make sure the program is setup
153+
expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow();
154+
expect(() => parseFile('bar', PROJECT_DIR)).toThrow();
155+
156+
// write a new file and attempt to parse it
157+
writeFile(PROJECT_DIR, 'bar');
158+
159+
expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow();
160+
expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow();
161+
});
162+
163+
it('handles tsconfigs with no includes/excludes (nested)', () => {
164+
const PROJECT_DIR = setup({}, false);
165+
166+
// parse once to: assert the config as correct, and to make sure the program is setup
167+
expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow();
168+
expect(() => parseFile('baz/bar', PROJECT_DIR)).toThrow();
169+
170+
// write a new file and attempt to parse it
171+
writeFile(PROJECT_DIR, 'baz/bar');
172+
173+
expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow();
174+
expect(() => parseFile('baz/bar', PROJECT_DIR)).not.toThrow();
175+
});
176+
177+
// TODO - support the complex monorepo case with a tsconfig with no include/exclude
144178
});

Diff for: packages/typescript-estree/tests/lib/semanticInfo.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ describe('semanticInfo', () => {
254254
badConfig.project = '.';
255255
expect(() =>
256256
parseCodeAndGenerateServices(readFileSync(fileName, 'utf8'), badConfig),
257-
).toThrow(/File .+semanticInfo' not found/);
257+
).toThrow(
258+
// case insensitive because unix based systems are case insensitive
259+
/File .+semanticInfo' not found/i,
260+
);
258261
});
259262

260263
it('malformed project file', () => {

0 commit comments

Comments
 (0)