Skip to content

fix(typescript-estree): handle running out of fs watchers #1088

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 2 commits into from
Oct 16, 2019
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
5 changes: 5 additions & 0 deletions packages/typescript-estree/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ I work closely with the TypeScript Team and we are gradually aliging the AST of
- `npm run unit-tests` - run only unit tests
- `npm run ast-alignment-tests` - run only Babylon AST alignment tests

## Debugging

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:*`.
I.e. in this repo you can run: `DEBUG=typescript-eslint:* yarn lint`.

## License

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.
2 changes: 2 additions & 0 deletions packages/typescript-estree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"dependencies": {
"chokidar": "^3.0.2",
"debug": "^4.1.1",
"glob": "^7.1.4",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
Expand All @@ -50,6 +51,7 @@
"@babel/parser": "7.5.5",
"@babel/types": "^7.3.2",
"@types/babel-code-frame": "^6.20.1",
"@types/debug": "^4.1.5",
"@types/glob": "^7.1.1",
"@types/is-glob": "^4.0.1",
"@types/lodash.isplainobject": "^4.0.4",
Expand Down
50 changes: 42 additions & 8 deletions packages/typescript-estree/src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import debug from 'debug';
import path from 'path';
import semver from 'semver';
import * as ts from 'typescript'; // leave this as * as ts so people using util package don't need syntheticDefaultImports
Expand All @@ -15,6 +16,8 @@ import {
defaultCompilerOptions,
} from './tsconfig-parser';

const log = debug('typescript-eslint:typescript-estree:parser');

/**
* This needs to be kept in sync with the top-level README.md in the
* typescript-eslint monorepo
Expand All @@ -41,6 +44,17 @@ function getFileName({ jsx }: { jsx?: boolean }): string {
return jsx ? 'estree.tsx' : 'estree.ts';
}

function enforceString(code: unknown): string {
/**
* Ensure the source code is a string
*/
if (typeof code !== 'string') {
return String(code);
}

return code;
}

/**
* Resets the extra config object
*/
Expand Down Expand Up @@ -82,6 +96,8 @@ function getASTFromProject(
options: TSESTreeOptions,
createDefaultProgram: boolean,
): ASTAndProgram | undefined {
log('Attempting to get AST from project(s) for: %s', options.filePath);

const filePath = options.filePath || getFileName(options);
const astAndProgram = firstDefined(
calculateProjectParserOptions(code, filePath, extra),
Expand Down Expand Up @@ -139,6 +155,11 @@ function getASTAndDefaultProject(
code: string,
options: TSESTreeOptions,
): ASTAndProgram | undefined {
log(
'Attempting to get AST from the default project(s): %s',
options.filePath,
);

const fileName = options.filePath || getFileName(options);
const program = createProgram(code, fileName, extra);
const ast = program && program.getSourceFile(fileName);
Expand All @@ -150,6 +171,8 @@ function getASTAndDefaultProject(
* @returns Returns a new source file and program corresponding to the linted code
*/
function createNewProgram(code: string): ASTAndProgram {
log('Getting AST without type information');

const FILENAME = getFileName(extra);

const compilerHost: ts.CompilerHost = {
Expand Down Expand Up @@ -226,6 +249,9 @@ function getProgramAndAST(
}

function applyParserOptionsToExtra(options: TSESTreeOptions): void {
/**
* Turn on/off filesystem watchers
*/
extra.noWatch = typeof options.noWatch === 'boolean' && options.noWatch;

/**
Expand Down Expand Up @@ -378,6 +404,7 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
* Reset the parse configuration
*/
resetExtra();

/**
* Ensure users do not attempt to use parse() when they need parseAndGenerateServices()
*/
Expand All @@ -386,24 +413,25 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
`"errorOnTypeScriptSyntacticAndSemanticIssues" is only supported for parseAndGenerateServices()`,
);
}

/**
* Ensure the source code is a string, and store a reference to it
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (typeof code !== 'string' && !((code as any) instanceof String)) {
code = String(code);
}
code = enforceString(code);
extra.code = code;

/**
* Apply the given parser options
*/
if (typeof options !== 'undefined') {
applyParserOptionsToExtra(options);
}

/**
* Warn if the user is using an unsupported version of TypeScript
*/
warnAboutTSVersion();

/**
* Create a ts.SourceFile directly, no ts.Program is needed for a simple
* parse
Expand All @@ -414,6 +442,7 @@ export function parse<T extends TSESTreeOptions = TSESTreeOptions>(
ts.ScriptTarget.Latest,
/* setParentNodes */ true,
);

/**
* Convert the TypeScript AST to an ESTree-compatible one
*/
Expand All @@ -428,14 +457,13 @@ export function parseAndGenerateServices<
* Reset the parse configuration
*/
resetExtra();

/**
* Ensure the source code is a string, and store a reference to it
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (typeof code !== 'string' && !((code as any) instanceof String)) {
code = String(code);
}
code = enforceString(code);
extra.code = code;

/**
* Apply the given parser options
*/
Expand All @@ -449,10 +477,12 @@ export function parseAndGenerateServices<
extra.errorOnTypeScriptSyntacticAndSemanticIssues = true;
}
}

/**
* Warn if the user is using an unsupported version of TypeScript
*/
warnAboutTSVersion();

/**
* Generate a full ts.Program in order to be able to provide parser
* services, such as type-checking
Expand All @@ -465,6 +495,7 @@ export function parseAndGenerateServices<
shouldProvideParserServices,
extra.createDefaultProgram,
)!;

/**
* Determine whether or not two-way maps of converted AST nodes should be preserved
* during the conversion process
Expand All @@ -473,11 +504,13 @@ export function parseAndGenerateServices<
extra.preserveNodeMaps !== undefined
? extra.preserveNodeMaps
: shouldProvideParserServices;

/**
* Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve
* mappings between converted and original AST nodes
*/
const { estree, astMaps } = astConverter(ast, extra, shouldPreserveNodeMaps);

/**
* Even if TypeScript parsed the source code ok, and we had no problems converting the AST,
* there may be other syntactic or semantic issues in the code that we can optionally report on.
Expand All @@ -488,6 +521,7 @@ export function parseAndGenerateServices<
throw convertError(error);
}
}

/**
* Return the converted AST and additional parser services
*/
Expand Down
119 changes: 80 additions & 39 deletions packages/typescript-estree/src/tsconfig-parser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import chokidar from 'chokidar';
import debug from 'debug';
import path from 'path';
import * as ts from 'typescript'; // leave this as * as ts so people using util package don't need syntheticDefaultImports
import { Extra } from './parser-options';
import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile';

//------------------------------------------------------------------------------
// Environment calculation
//------------------------------------------------------------------------------
const log = debug('typescript-eslint:typescript-estree:tsconfig-parser');

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

/**
* Tracks the ts.sys.watchFile watchers that we've opened for config files.
* We store these so we can clean up our handles if required.
*/
const configSystemFileWatcherTrackingSet = new Set<ts.FileWatcher>();
interface Watcher {
close(): void;
forceClose(): void;
on(evt: 'add', listener: (file: string) => void): void;
on(evt: 'change', listener: (file: string) => void): void;
trackWatcher(): void;
}
/**
* Tracks the ts.sys.watchDirectory watchers that we've opened for project folders.
* We store these so we can clean up our handles if required.
*/
const directorySystemFileWatcherTrackingSet = new Set<ts.FileWatcher>();
const fileWatcherTrackingSet = new Map<string, Watcher>();

const parsedFilesSeen = new Set<string>();

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

// stop tracking config files
configSystemFileWatcherTrackingSet.forEach(cb => cb.close());
configSystemFileWatcherTrackingSet.clear();

// stop tracking folders
directorySystemFileWatcherTrackingSet.forEach(cb => cb.close());
directorySystemFileWatcherTrackingSet.clear();
fileWatcherTrackingSet.forEach(cb => cb.forceClose());
fileWatcherTrackingSet.clear();
}

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

interface Watcher {
close(): void;
on(evt: 'add', listener: (file: string) => void): void;
on(evt: 'change', listener: (file: string) => void): void;
}
const EMPTY_WATCHER: Watcher = {
close: (): void => {},
forceClose: (): void => {},
on: (): void => {},
trackWatcher: (): void => {},
};

/**
* Watches a file or directory for changes
*/
function watch(
path: string,
watchPath: string,
options: chokidar.WatchOptions,
extra: Extra,
): Watcher {
// an escape hatch to disable the file watchers as they can take a bit to initialise in some cases
// this also supports an env variable so it's easy to switch on/off from the CLI
if (process.env.PARSER_NO_WATCH === 'true' || extra.noWatch === true) {
return {
close: (): void => {},
on: (): void => {},
};
const blockWatchers =
process.env.PARSER_NO_WATCH === 'false'
? false
: process.env.PARSER_NO_WATCH === 'true' || extra.noWatch === true;
if (blockWatchers) {
return EMPTY_WATCHER;
}

// reuse watchers in case typescript asks us to watch the same file/directory multiple times
if (fileWatcherTrackingSet.has(watchPath)) {
const watcher = fileWatcherTrackingSet.get(watchPath)!;
watcher.trackWatcher();
return watcher;
}

return chokidar.watch(path, {
ignoreInitial: true,
persistent: false,
useFsEvents: false,
...options,
});
let fsWatcher: chokidar.FSWatcher;
try {
log('setting up watcher on path: %s', watchPath);
fsWatcher = chokidar.watch(watchPath, {
ignoreInitial: true,
persistent: false,
useFsEvents: false,
...options,
});
} catch (e) {
log(
'error occurred using file watcher, setting up polling watcher instead: %s',
watchPath,
);
// https://github.com/microsoft/TypeScript/blob/c9d407b52ad92370cd116105c33d618195de8070/src/compiler/sys.ts#L1232-L1237
// Catch the exception and use polling instead
// 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
// so instead of throwing error, use fs.watchFile
fsWatcher = chokidar.watch(watchPath, {
ignoreInitial: true,
persistent: false,
useFsEvents: false,
...options,
usePolling: true,
});
}

let counter = 1;
const watcher = {
close: (): void => {
counter -= 1;
if (counter <= 0) {
fsWatcher.close();
fileWatcherTrackingSet.delete(watchPath);
}
},
forceClose: fsWatcher.close.bind(fsWatcher),
on: fsWatcher.on.bind(fsWatcher),
trackWatcher: (): void => {
counter += 1;
},
};

fileWatcherTrackingSet.set(watchPath, watcher);

return watcher;
}

/**
Expand Down Expand Up @@ -219,7 +266,6 @@ export function calculateProjectParserOptions(
watcher.on('change', path => {
callback(path, ts.FileWatcherEventKind.Changed);
});
configSystemFileWatcherTrackingSet.add(watcher);
}

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

if (watcher) {
watcher.close();
configSystemFileWatcherTrackingSet.delete(watcher);
}
},
};
Expand All @@ -263,13 +308,9 @@ export function calculateProjectParserOptions(
watcher.on('add', path => {
callback(path);
});
directorySystemFileWatcherTrackingSet.add(watcher);

return {
close(): void {
watcher.close();
directorySystemFileWatcherTrackingSet.delete(watcher);
},
close: watcher.close,
};
};

Expand Down
Loading