wagenet/typescript-eslint-12708-repro

Isolated reproduction: parserOptions.projectService silently changes type-aware results once a project exceeds TypeScript's maxProgramSizeForNonTsFiles

★ 0Forks 0JavaScriptGitHub ↗Compare

README

projectService over maxProgramSizeForNonTsFiles

Isolated reproduction for typescript-eslint issue #12708.

TypeScript's ProjectService adds up the size of every program root it does not consider TypeScript. Past maxProgramSizeForNonTsFiles — 20MB, not configurable — it disables the project's language service, and a disabled project reports only the files the client currently has open as program roots. parserOptions.projectService opens one file at a time, so past that threshold every linted file gets its own ts.Program, and files the linted file does not import are no longer in it.

Nothing reports that this happened.

Setup

npm install

generate.mjs writes the two generated parts of the fixture: src/bulk/*.js, filler whose only job is to add up to a given number of megabytes, and src/app/*.ts, 100 small files for measure.mjs to parse one at a time.

Results change

src/ambient.d.ts declares a global. Nothing imports it — it is in the program because the tsconfig includes it, which is why a demoted project loses it. src/target.ts uses that global.

node generate.mjs 5    # 5MB of .js, under the limit
npx eslint src/target.ts

Clean.

node generate.mjs 25   # 25MB of .js, over the limit
npx eslint src/target.ts
src/target.ts
  1:14  error  Unsafe assignment of an error typed value                            @typescript-eslint/no-unsafe-assignment
  2:14  error  Unsafe assignment of an error typed value                            @typescript-eslint/no-unsafe-assignment
  2:24  error  Unsafe call of a type that could not be resolved                     @typescript-eslint/no-unsafe-call
  2:37  error  Unsafe member access .toUpperCase on a type that cannot be resolved  @typescript-eslint/no-unsafe-member-access

Same file, same rules, same parser. The only difference is how many bytes of unrelated JavaScript sit next to it. Uncommenting "disableSizeLimit": true in tsconfig.json makes it clean again.

Programs are rebuilt per file

measure.mjs parses each src/app/*.ts the way ESLint would, one at a time, and counts distinct ts.Program instances.

fixture distinct programs program source files ms/file
node generate.mjs 5 1 196 → 196 3.2
node generate.mjs 25 100 90 → 189 6.2
node generate.mjs 25 + disableSizeLimit 1 216 → 216 4.0

One program per file, growing as it goes. On a real app — 12,735 files — the same pattern cost 358 new programs per 400 files and about 4x per-file parse time.

Versions

package version
@typescript-eslint/parser 8.67.0
@typescript-eslint/eslint-plugin 8.67.0
typescript 5.9.3
eslint 10.8.1

This reproduction was built by Claude (Anthropic's Claude Code) while investigating a lint slowdown in a large Ember app, and is published from Peter Wagenet's account.

Contributors

wagenet

Issues