Skip to content

Commit ec62747

Browse files
bradzacherJamesHenry
authored andcommitted
fix(typescript-estree): handle running out of fs watchers (typescript-eslint#1088)
1 parent 5f093ac commit ec62747

File tree

5 files changed

+137
-50
lines changed

5 files changed

+137
-50
lines changed

packages/typescript-estree/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ I work closely with the TypeScript Team and we are gradually aliging the AST of
144144
- `npm run unit-tests` - run only unit tests
145145
- `npm run ast-alignment-tests` - run only Babylon AST alignment tests
146146

147+
## Debugging
148+
149+
If you encounter a bug with the parser that you want to investigate, you can turn on the debug logging via setting the environment variable: `DEBUG=typescript-eslint:*`.
150+
I.e. in this repo you can run: `DEBUG=typescript-eslint:* yarn lint`.
151+
147152
## License
148153

149154
TypeScript ESTree inherits from the the original TypeScript ESLint Parser license, as the majority of the work began there. It is licensed under a permissive BSD 2-clause license.

packages/typescript-estree/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"dependencies": {
4242
"chokidar": "^3.0.2",
43+
"debug": "^4.1.1",
4344
"glob": "^7.1.4",
4445
"is-glob": "^4.0.1",
4546
"lodash.unescape": "4.0.1",
@@ -50,6 +51,7 @@
5051
"@babel/parser": "7.5.5",
5152
"@babel/types": "^7.3.2",
5253
"@types/babel-code-frame": "^6.20.1",
54+
"@types/debug": "^4.1.5",
5355
"@types/glob": "^7.1.1",
5456
"@types/is-glob": "^4.0.1",
5557
"@types/lodash.isplainobject": "^4.0.4",

packages/typescript-estree/src/parser.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import debug from 'debug';
12
import path from 'path';
23
import semver from 'semver';
34
import * as ts from 'typescript'; // leave this as * as ts so people using util package don't need syntheticDefaultImports
@@ -15,6 +16,8 @@ import {
1516
defaultCompilerOptions,
1617
} from './tsconfig-parser';
1718

19+
const log = debug('typescript-eslint:typescript-estree:parser');
20+
1821
/**
1922
* This needs to be kept in sync with the top-level README.md in the
2023
* typescript-eslint monorepo
@@ -41,6 +44,17 @@ function getFileName({ jsx }: { jsx?: boolean }): string {
4144
return jsx ? 'estree.tsx' : 'estree.ts';
4245
}
4346

47+
function enforceString(code: unknown): string {
48+
/**
49+
* Ensure the source code is a string
50+
*/
51+
if (typeof code !== 'string') {
52+
return String(code);
53+
}
54+
55+
return code;
56+
}
57+
4458
/**
4559
* Resets the extra config object
4660
*/
@@ -82,6 +96,8 @@ function getASTFromProject(
8296
options: TSESTreeOptions,
8397
createDefaultProgram: boolean,
8498
): ASTAndProgram | undefined {
99+
log('Attempting to get AST from project(s) for: %s', options.filePath);
100+
85101
const filePath = options.filePath || getFileName(options);
86102
const astAndProgram = firstDefined(
87103
calculateProjectParserOptions(code, filePath, extra),
@@ -139,6 +155,11 @@ function getASTAndDefaultProject(
139155
code: string,
140156
options: TSESTreeOptions,
141157
): ASTAndProgram | undefined {
158+
log(
159+
'Attempting to get AST from the default project(s): %s',
160+
options.filePath,
161+
);
162+
142163
const fileName = options.filePath || getFileName(options);
143164
const program = createProgram(code, fileName, extra);
144165
const ast = program && program.getSourceFile(fileName);
@@ -150,6 +171,8 @@ function getASTAndDefaultProject(
150171
* @returns Returns a new source file and program corresponding to the linted code
151172
*/
152173
function createNewProgram(code: string): ASTAndProgram {
174+
log('Getting AST without type information');
175+
153176
const FILENAME = getFileName(extra);
154177

155178
const compilerHost: ts.CompilerHost = {
@@ -226,6 +249,9 @@ function getProgramAndAST(
226249
}
227250

228251
function applyParserOptionsToExtra(options: TSESTreeOptions): void {
252+
/**
253+
* Turn on/off filesystem watchers
254+
*/
229255
extra.noWatch = typeof options.noWatch === 'boolean' && options.noWatch;
230256

231257
/**
@@ -378,6 +404,7 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
378404
* Reset the parse configuration
379405
*/
380406
resetExtra();
407+
381408
/**
382409
* Ensure users do not attempt to use parse() when they need parseAndGenerateServices()
383410
*/
@@ -386,24 +413,25 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
386413
`"errorOnTypeScriptSyntacticAndSemanticIssues" is only supported for parseAndGenerateServices()`,
387414
);
388415
}
416+
389417
/**
390418
* Ensure the source code is a string, and store a reference to it
391419
*/
392-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
393-
if (typeof code !== 'string' && !((code as any) instanceof String)) {
394-
code = String(code);
395-
}
420+
code = enforceString(code);
396421
extra.code = code;
422+
397423
/**
398424
* Apply the given parser options
399425
*/
400426
if (typeof options !== 'undefined') {
401427
applyParserOptionsToExtra(options);
402428
}
429+
403430
/**
404431
* Warn if the user is using an unsupported version of TypeScript
405432
*/
406433
warnAboutTSVersion();
434+
407435
/**
408436
* Create a ts.SourceFile directly, no ts.Program is needed for a simple
409437
* parse
@@ -414,6 +442,7 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
414442
ts.ScriptTarget.Latest,
415443
/* setParentNodes */ true,
416444
);
445+
417446
/**
418447
* Convert the TypeScript AST to an ESTree-compatible one
419448
*/
@@ -428,14 +457,13 @@ export function parseAndGenerateServices<
428457
* Reset the parse configuration
429458
*/
430459
resetExtra();
460+
431461
/**
432462
* Ensure the source code is a string, and store a reference to it
433463
*/
434-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
435-
if (typeof code !== 'string' && !((code as any) instanceof String)) {
436-
code = String(code);
437-
}
464+
code = enforceString(code);
438465
extra.code = code;
466+
439467
/**
440468
* Apply the given parser options
441469
*/
@@ -449,10 +477,12 @@ export function parseAndGenerateServices<
449477
extra.errorOnTypeScriptSyntacticAndSemanticIssues = true;
450478
}
451479
}
480+
452481
/**
453482
* Warn if the user is using an unsupported version of TypeScript
454483
*/
455484
warnAboutTSVersion();
485+
456486
/**
457487
* Generate a full ts.Program in order to be able to provide parser
458488
* services, such as type-checking
@@ -465,6 +495,7 @@ export function parseAndGenerateServices<
465495
shouldProvideParserServices,
466496
extra.createDefaultProgram,
467497
)!;
498+
468499
/**
469500
* Determine whether or not two-way maps of converted AST nodes should be preserved
470501
* during the conversion process
@@ -473,11 +504,13 @@ export function parseAndGenerateServices<
473504
extra.preserveNodeMaps !== undefined
474505
? extra.preserveNodeMaps
475506
: shouldProvideParserServices;
507+
476508
/**
477509
* Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve
478510
* mappings between converted and original AST nodes
479511
*/
480512
const { estree, astMaps } = astConverter(ast, extra, shouldPreserveNodeMaps);
513+
481514
/**
482515
* Even if TypeScript parsed the source code ok, and we had no problems converting the AST,
483516
* there may be other syntactic or semantic issues in the code that we can optionally report on.
@@ -488,6 +521,7 @@ export function parseAndGenerateServices<
488521
throw convertError(error);
489522
}
490523
}
524+
491525
/**
492526
* Return the converted AST and additional parser services
493527
*/

packages/typescript-estree/src/tsconfig-parser.ts

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import chokidar from 'chokidar';
2+
import debug from 'debug';
23
import path from 'path';
34
import * as ts from 'typescript'; // leave this as * as ts so people using util package don't need syntheticDefaultImports
45
import { Extra } from './parser-options';
56
import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile';
67

7-
//------------------------------------------------------------------------------
8-
// Environment calculation
9-
//------------------------------------------------------------------------------
8+
const log = debug('typescript-eslint:typescript-estree:tsconfig-parser');
109

1110
/**
1211
* Default compiler options for program generation from single root file
@@ -33,16 +32,18 @@ const knownWatchProgramMap = new Map<
3332
*/
3433
const watchCallbackTrackingMap = new Map<string, Set<ts.FileWatcherCallback>>();
3534

36-
/**
37-
* Tracks the ts.sys.watchFile watchers that we've opened for config files.
38-
* We store these so we can clean up our handles if required.
39-
*/
40-
const configSystemFileWatcherTrackingSet = new Set<ts.FileWatcher>();
35+
interface Watcher {
36+
close(): void;
37+
forceClose(): void;
38+
on(evt: 'add', listener: (file: string) => void): void;
39+
on(evt: 'change', listener: (file: string) => void): void;
40+
trackWatcher(): void;
41+
}
4142
/**
4243
* Tracks the ts.sys.watchDirectory watchers that we've opened for project folders.
4344
* We store these so we can clean up our handles if required.
4445
*/
45-
const directorySystemFileWatcherTrackingSet = new Set<ts.FileWatcher>();
46+
const fileWatcherTrackingSet = new Map<string, Watcher>();
4647

4748
const parsedFilesSeen = new Set<string>();
4849

@@ -56,12 +57,8 @@ export function clearCaches(): void {
5657
parsedFilesSeen.clear();
5758

5859
// stop tracking config files
59-
configSystemFileWatcherTrackingSet.forEach(cb => cb.close());
60-
configSystemFileWatcherTrackingSet.clear();
61-
62-
// stop tracking folders
63-
directorySystemFileWatcherTrackingSet.forEach(cb => cb.close());
64-
directorySystemFileWatcherTrackingSet.clear();
60+
fileWatcherTrackingSet.forEach(cb => cb.forceClose());
61+
fileWatcherTrackingSet.clear();
6562
}
6663

6764
/**
@@ -88,34 +85,84 @@ function getTsconfigPath(tsconfigPath: string, extra: Extra): string {
8885
: path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath);
8986
}
9087

91-
interface Watcher {
92-
close(): void;
93-
on(evt: 'add', listener: (file: string) => void): void;
94-
on(evt: 'change', listener: (file: string) => void): void;
95-
}
88+
const EMPTY_WATCHER: Watcher = {
89+
close: (): void => {},
90+
forceClose: (): void => {},
91+
on: (): void => {},
92+
trackWatcher: (): void => {},
93+
};
94+
9695
/**
9796
* Watches a file or directory for changes
9897
*/
9998
function watch(
100-
path: string,
99+
watchPath: string,
101100
options: chokidar.WatchOptions,
102101
extra: Extra,
103102
): Watcher {
104103
// an escape hatch to disable the file watchers as they can take a bit to initialise in some cases
105104
// this also supports an env variable so it's easy to switch on/off from the CLI
106-
if (process.env.PARSER_NO_WATCH === 'true' || extra.noWatch === true) {
107-
return {
108-
close: (): void => {},
109-
on: (): void => {},
110-
};
105+
const blockWatchers =
106+
process.env.PARSER_NO_WATCH === 'false'
107+
? false
108+
: process.env.PARSER_NO_WATCH === 'true' || extra.noWatch === true;
109+
if (blockWatchers) {
110+
return EMPTY_WATCHER;
111+
}
112+
113+
// reuse watchers in case typescript asks us to watch the same file/directory multiple times
114+
if (fileWatcherTrackingSet.has(watchPath)) {
115+
const watcher = fileWatcherTrackingSet.get(watchPath)!;
116+
watcher.trackWatcher();
117+
return watcher;
111118
}
112119

113-
return chokidar.watch(path, {
114-
ignoreInitial: true,
115-
persistent: false,
116-
useFsEvents: false,
117-
...options,
118-
});
120+
let fsWatcher: chokidar.FSWatcher;
121+
try {
122+
log('setting up watcher on path: %s', watchPath);
123+
fsWatcher = chokidar.watch(watchPath, {
124+
ignoreInitial: true,
125+
persistent: false,
126+
useFsEvents: false,
127+
...options,
128+
});
129+
} catch (e) {
130+
log(
131+
'error occurred using file watcher, setting up polling watcher instead: %s',
132+
watchPath,
133+
);
134+
// https://github.com/microsoft/TypeScript/blob/c9d407b52ad92370cd116105c33d618195de8070/src/compiler/sys.ts#L1232-L1237
135+
// Catch the exception and use polling instead
136+
// Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
137+
// so instead of throwing error, use fs.watchFile
138+
fsWatcher = chokidar.watch(watchPath, {
139+
ignoreInitial: true,
140+
persistent: false,
141+
useFsEvents: false,
142+
...options,
143+
usePolling: true,
144+
});
145+
}
146+
147+
let counter = 1;
148+
const watcher = {
149+
close: (): void => {
150+
counter -= 1;
151+
if (counter <= 0) {
152+
fsWatcher.close();
153+
fileWatcherTrackingSet.delete(watchPath);
154+
}
155+
},
156+
forceClose: fsWatcher.close.bind(fsWatcher),
157+
on: fsWatcher.on.bind(fsWatcher),
158+
trackWatcher: (): void => {
159+
counter += 1;
160+
},
161+
};
162+
163+
fileWatcherTrackingSet.set(watchPath, watcher);
164+
165+
return watcher;
119166
}
120167

121168
/**
@@ -219,7 +266,6 @@ export function calculateProjectParserOptions(
219266
watcher.on('change', path => {
220267
callback(path, ts.FileWatcherEventKind.Changed);
221268
});
222-
configSystemFileWatcherTrackingSet.add(watcher);
223269
}
224270

225271
const normalizedFileName = path.normalize(fileName);
@@ -239,7 +285,6 @@ export function calculateProjectParserOptions(
239285

240286
if (watcher) {
241287
watcher.close();
242-
configSystemFileWatcherTrackingSet.delete(watcher);
243288
}
244289
},
245290
};
@@ -263,13 +308,9 @@ export function calculateProjectParserOptions(
263308
watcher.on('add', path => {
264309
callback(path);
265310
});
266-
directorySystemFileWatcherTrackingSet.add(watcher);
267311

268312
return {
269-
close(): void {
270-
watcher.close();
271-
directorySystemFileWatcherTrackingSet.delete(watcher);
272-
},
313+
close: watcher.close,
273314
};
274315
};
275316

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy