Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
Description
I want to explore is building a CLI for typescript-eslint.
A big issue we run into with our tooling is that we are "at the mercy" of ESLint's CLI. ESLint controls the process - how files are selected, when files are selected, in what order they are linted.
This is a problem because we can't do efficient memory management, nor can we properly prioritise work. We don't have any control or visibility into what files are being linted so we can't "dispose" of a program when it's done: we don't know if or when all the files that belong to the project will be linted (for example it's common that a user might lint a subset of a project). This forces us to keep every single project in memory for the duration of the lint run (which leads to OOMs on any non-trivial codebase).
Additionally due to the fact that ESLint has one API for all use cases we don't know which "state" ESLint is in. ESLint just exposes theESLint
API which is used in the "single-run" CLI usecase as well as the persistent IDE usecase.
This means that we're stuck building for the worst case scenario like:
- using inefficient APIs when we could use better ones
- keeping programs in memory forever just in case we need them again (OOM when many (>10) project configs passed to the parser #1192)
For (1) we've attempted to work around by attempting to detect the CLI usecase (#3512) - which definitely helps! But it has a few unfortunate shortcomings.
For (2) we could probably build on top of the (1) solution to free up programs automatically. But that kind of feels like hacks on hacks on hacks - and we're building a super complicated system which is brittle and pushes us further into a world where we can't get new contributors due to the complexity of our system.
It's worth noting that even for the CLI usecase (which we tend to think of as "one and done") - ESLint can lint a file up to 11 times when it is calculating fixes for a file (1 initial pass and 10 fix passes). Which does cause some unique problems with optimisations we might try to apply.
So I want to explore a new direction - a new CLI which allows us to "work around" these issues from the top down instead of the middle.
My proposal would be a simple CLI along the lines of this:
$ tseslint --helpUsage: tseslint [options] file.js [file.js] [dir]Basic configuration: -c, --config path::String Use this configuration, overriding .eslintrc.* config options if presentFixing problems: --fix Automatically fix problems --fix-type Array Specify the types of fixes to apply (problem, suggestion, layout)Ignoring files: --ignore-path path::String Specify path of ignore file --ignore-pattern [String] Pattern of files to ignore (in addition to those in .eslintignore)Handling warnings: --quiet Report errors only - default: false --max-warnings Int Number of warnings to trigger nonzero exit code - default: -1Output: -o, --output-file path::String Specify file to write report to -f, --format String Use a specific output format - default: stylish --color, --no-color Force enabling/disabling of colorInline configuration comments: --no-inline-config Prevent comments from changing config or rules --report-unused-disable-directives Adds reported errors for unused eslint-disable directivesMiscellaneous: --env-info Output execution environment information - default: false --no-error-on-unmatched-pattern Prevent errors when pattern is unmatched --debug Output debugging information -h, --help Show help -v, --version Output the version number
A keen eye would notice this proposed CLI is just a subset of the ESLint CLI. This is completely intentional.
At a high level - this would be the proposed workflow for the CLI:
- Use ESLint's
FileEnumerator
to expand the passed paths/blobs to the list of actual files to lint - For each file resolve the config and bucket them into two buckets: files with type information and files without type information.
- For files without type information - we can just run ESLint over them as normal.
- For files with type information - we want to group them again. This time we want to group them by which TSConfig they belong to.
- Once we have the tsconfig buckets - we can then execute each project in turn:
- manually create the TS Program instance for the tsconfig.
- lint all files in the bucket.
- if there are fixes and
n < 10
then apply fixes,n + 1
, go to (5.iii). - dispose of the TS Program instance (solvingPurge programs once they are done #1718)
By controlling the order and grouping of files - we can know exactly when we can free up the memory - allowing us to prevent OOMs due to having all programs in memory at once.
This also unlocks other possible optimisations for us like parallelisation!
We know that each project is "isolated" from one another - so we can run each project (eg step 5) in a separate thread. We could also bucket the non-type information files (step 3) in their own separate thread. This should mean that instead of havingO(nm)
performance wheren
is the number of projects andm
is the worst-case lint time for a project, instead we would haveO(m)
performance.
If we wanted to - this would also let us rip out the "CLI detection" from the underlying CLI and thus simplify it again to reduce complexities. We could also choose to keep this so we can maintain some level of improved performance for people who continue to use the standardeslint
binary.
cc @typescript-eslint/core-team