`.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-int-cm-')));
+ writeMapperProject(
+ dir,
+ mapperServer(`
+function transform(content) {
+ const open = ''));
+ const mappings = [[0, body.length, start, body.length, 0, 1048575]];
+ const directives = [];
+ let text = body + '\\nexport {};\\nfunction ';
+ mappings.push([text.length, 14, 1, 8, 1, 0]);
+ text += '__VLS_template() {\\n';
+ directives.push([0, 0, body.length, text.length, 0]);
+ for (const m of content.matchAll(/(?:\\s*)?\\{\\{ (.*?) \\}\\}<\\/p>/g)) {
+ const exprStart = m.index + m[0].indexOf('{{ ') + 3;
+ const virtualStart = text.length;
+ mappings.push([virtualStart, m[2].length, exprStart, m[2].length, 0, 1048575]);
+ text += m[2] + ';\\n';
+ const commentLength = m[0].indexOf('-->') + 3;
+ if (m[1] === '@vue-ignore') directives.push([m.index, commentLength, virtualStart, text.length, 0]);
+ if (m[1] === '@vue-expect-error') directives.push([m.index, commentLength, virtualStart, text.length, 1, 0]);
+ }
+ text += '}\\n';
+ return {
+ text,
+ extension: '.ts',
+ mappings,
+ diagnosticDirectives: { unusedExpectDirectiveDiagnostics: [{ code: 2578, messageText: 'Unused directive.' }], directives },
+ };
+}
+`),
+ );
+ fs.writeFileSync(
+ path.join(dir, 'tsslint.config.ts'),
+ `import { defineConfig, defineRule } from ${importPath(configModule)};\n`
+ + `export default defineConfig({\n`
+ + ` rules: {\n`
+ + ` 'no-console': (await import(${importPath(noConsoleRule)})),\n`
+ + ` 'generated': defineRule(({ file, report }) => {\n`
+ + ` const start = file.text.indexOf('__VLS_template');\n`
+ + ` report('generated identifier', start, start + 14);\n`
+ + ` }),\n`
+ + ` },\n`
+ + `});\n`,
+ );
+ fs.writeFileSync(
+ path.join(dir, 'App.vue'),
+ [
+ '',
+ '\t',
+ `\t{{ console.log('ignored') }}
`,
+ '\t',
+ `\t{{ console.log('expected') }}
`,
+ `\t{{ console.log('kept') }}
`,
+ ' ',
+ '',
+ '',
+ ].join('\n'),
+ );
+ try {
+ const out = stripAnsi(runCli(dir).stdout);
+ check('report outside directives is kept', out.includes('App.vue:6:8') && out.includes('App.vue:9:1'), out);
+ check('@vue-ignore suppresses the report in its range', !out.includes('App.vue:3:'), out);
+ check('@vue-expect-error suppresses the report in its range', !out.includes('App.vue:5:'), out);
+ check('synthesized Ignore suppresses the generated-code report', !out.includes('generated identifier'), out);
+ check('no unused report for directives', !/unused/i.test(out), out);
+ check('exactly the two kept reports', out.includes('2 messages'), out);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// ── Test 19 (content mapper): one fix per original range ──────────────
+//
+// The script body is copied twice; the second copy carries only a
+// navigation feature, like Vue's navigation-only copies. The same
+// insertion reported on both copies must land in App.vue once.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-int-cm-')));
+ writeMapperProject(
+ dir,
+ mapperServer(`
+function transform(content) {
+ const open = ''));
+ return {
+ text: body + '\\n' + body + '\\nexport {};\\n',
+ extension: '.ts',
+ mappings: [[0, body.length, start, body.length, 0, 1048575], [body.length + 1, body.length, start, body.length, 0, 8]],
+ };
+}
+`),
+ );
+ fs.writeFileSync(
+ path.join(dir, 'tsslint.config.ts'),
+ `import { defineConfig, defineRule } from ${importPath(configModule)};\n`
+ + `export default defineConfig({\n`
+ + ` rules: {\n`
+ + ` 'mark': defineRule(({ file, report }) => {\n`
+ + ` for (let i = file.text.indexOf('console'); i >= 0; i = file.text.indexOf('console', i + 1)) {\n`
+ + ` if (file.text.slice(0, i).endsWith('/* checked */ ')) continue;\n`
+ + ` report('unchecked', i, i + 7).withFix('mark', () => [\n`
+ + ` { pos: i, end: i, newText: '/* checked */ ' },\n`
+ + ` ]);\n`
+ + ` }\n`
+ + ` }),\n`
+ + ` },\n`
+ + `});\n`,
+ );
+ const vue = path.join(dir, 'App.vue');
+ fs.writeFileSync(vue, ['', ''].join('\n'));
+ try {
+ runCli(dir, '--fix');
+ const after = fs.readFileSync(vue, 'utf8');
check(
- 'warm after ambient edit cache-hits again',
- markerLineCount(markerPath) === 2,
+ 'insertion from both copies applied once',
+ after === ['', ''].join('\n'),
+ JSON.stringify(after),
);
}
finally {
@@ -332,22 +1155,71 @@ function markerLineCount(markerPath: string): number {
}
}
-// ── Test 9 (--force): type-aware rule re-runs every time ────────────────
+// ── Test 20 (content mapper): compiler diagnostics are mapped once ─────
//
-// `--force` is the opt-out from layer 2. It skips the cache load, so
-// the linter starts cold every invocation — the type-aware rule has
-// no prev state to cache-hit against and runs fresh.
+// `getSemanticDiagnostics` on a content-mapped file already returns
+// original positions.
{
- const { dir, markerPath } = makeTypeAwareFixture();
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-int-cm-')));
+ writeMapperProject(dir, MAPPER_SERVER);
+ fs.writeFileSync(
+ path.join(dir, 'tsslint.config.ts'),
+ `import { defineConfig, createDiagnosticsPlugin } from ${importPath(configModule)};\n`
+ + `export default defineConfig({ plugins: [createDiagnosticsPlugin('semantic')] });\n`,
+ );
+ fs.writeFileSync(
+ path.join(dir, 'App.vue'),
+ ['', '
', ' ', '', ''].join(
+ '\n',
+ ),
+ );
try {
- runCli(dir, '--force');
- check('first --force ran rule once', markerLineCount(markerPath) === 1);
+ const out = stripAnsi(runCli(dir).stdout);
+ check(
+ 'compiler diagnostic points at App.vue:5:7',
+ out.includes('App.vue:5:7') && out.includes(`Type 'number' is not assignable to type 'string'.`),
+ out,
+ );
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
- runCli(dir, '--force');
+// ── Test 21 (content mapper): a rule crash is reported without a position
+//
+// The transformed text starts with generated code, so a crash placed at
+// offset 0 would have no original position and be dropped.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-int-cm-')));
+ writeMapperProject(
+ dir,
+ mapperServer(`
+function transform(content) {
+ const open = ''));
+ const prefix = 'export {};\\n';
+ return { text: prefix + body, extension: '.ts', mappings: [[prefix.length, body.length, start, body.length, 0, 1048575]] };
+}
+`),
+ );
+ fs.writeFileSync(
+ path.join(dir, 'tsslint.config.ts'),
+ `import { defineConfig, defineRule } from ${importPath(configModule)};\n`
+ + `export default defineConfig({\n`
+ + ` rules: {\n`
+ + ` 'crash': defineRule(() => { throw new Error('rule exploded'); }),\n`
+ + ` },\n`
+ + `});\n`,
+ );
+ fs.writeFileSync(path.join(dir, 'App.vue'), ['', ''].join('\n'));
+ try {
+ const out = stripAnsi(runCli(dir).stdout);
check(
- 'second --force re-ran type-aware rule',
- markerLineCount(markerPath) === 2,
- `expected 2 marker lines, got ${markerLineCount(markerPath)} — --force should bypass layer 2`,
+ 'crash is reported for App.vue without a position',
+ out.includes('App.vue - message crash: Error: rule exploded') && !/App\.vue:\d+:\d+/.test(out),
+ out,
);
}
finally {
@@ -355,6 +1227,85 @@ function markerLineCount(markerPath: string): number {
}
}
+// ── Test 22 (content mapper): editing the mapper re-lints everything ───
+//
+// A cache hit shows the `--force` hint.
+{
+ const { dir } = makeContentMapperFixture('\n');
+ const serverPath = path.join(dir, 'node_modules', 'test-mapper', 'server.js');
+ try {
+ runCli(dir);
+ check('unchanged mapper → cache hit', runCli(dir).stdout.includes('--force'));
+ editFile(serverPath, fs.readFileSync(serverPath, 'utf8') + '\n// changed\n');
+ const out = runCli(dir).stdout;
+ check('mapper edit → no cache hit', !out.includes('--force'), out);
+ check('mapper edit still reports', out.includes('no-console'), out);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// ── Test 23 (content mapper): mapped files compare declaration signatures
+//
+// Editing a function body in A.vue doesn't re-lint B.vue, which imports
+// it; changing its return type does.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-int-cm-')));
+ writeMapperProject(dir, MAPPER_SERVER);
+ const markerPath = path.join(dir, 'marker.log');
+ fs.writeFileSync(
+ path.join(dir, 'tsslint.config.ts'),
+ `import { defineConfig, defineRule } from ${importPath(configModule)};\n`
+ + `import * as fs from 'fs';\n`
+ + `export default defineConfig({\n`
+ + ` rules: {\n`
+ + ` 'marker': defineRule(({ file }) => {\n`
+ + ` fs.appendFileSync(${JSON.stringify(markerPath)}, file.fileName + '\\n');\n`
+ + ` }),\n`
+ + ` },\n`
+ + `});\n`,
+ );
+ const a = path.join(dir, 'A.vue');
+ const vue = (script: string) =>
+ ['', '
', ' ', '', ''].join('\n');
+ fs.writeFileSync(a, vue(`export function f() { return 1; }`));
+ fs.writeFileSync(path.join(dir, 'B.vue'), vue(`import { f } from './A.vue';\nexport const v = f();`));
+ try {
+ check('cold run lints both files', lintedDuring(markerPath, () => runCli(dir)) === 'A.vue,B.vue');
+ editFile(a, vue(`export function f() { return 2; }`));
+ const body = lintedDuring(markerPath, () => runCli(dir));
+ check('mapped body edit re-lints only A.vue', body === 'A.vue', body);
+ editFile(a, vue(`export function f() { return 'x'; }`));
+ const type = lintedDuring(markerPath, () => runCli(dir));
+ check('mapped type change re-lints B.vue', type === 'A.vue,B.vue', type);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// ── Test 24 (content mapper): a lockfile change re-lints everything ────
+//
+// Upgrading a dependency of the mapper (e.g. `@vue/language-core`) changes
+// neither the mapper's package.json nor its `exec` files, only the lockfile.
+{
+ const { dir } = makeContentMapperFixture('\n');
+ const lockPath = path.join(dir, 'pnpm-lock.yaml');
+ fs.writeFileSync(lockPath, `lockfileVersion: '9.0'\n`);
+ try {
+ runCli(dir);
+ check('unchanged lockfile → cache hit', runCli(dir).stdout.includes('--force'));
+ editFile(lockPath, `lockfileVersion: '9.0'\n# upgraded\n`);
+ const out = runCli(dir).stdout;
+ check('lockfile edit → no cache hit', !out.includes('--force'), out);
+ check('lockfile edit still reports', out.includes('no-console'), out);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
// ── Done ────────────────────────────────────────────────────────────────
process.stdout.write('\n');
if (failures.length) {
diff --git a/packages/cli/test/invalidation.test.ts b/packages/cli/test/invalidation.test.ts
new file mode 100644
index 00000000..b624cee8
--- /dev/null
+++ b/packages/cli/test/invalidation.test.ts
@@ -0,0 +1,258 @@
+// Invalidation tests against a stubbed TypeScript API, for program states
+// that are hard to reproduce with a real project.
+//
+// Run via:
+// node packages/cli/test/invalidation.test.js
+
+import fs = require('fs');
+import os = require('os');
+import path = require('path');
+import { API } from 'typescript/sync';
+import { emptyCache } from '../lib/cache.js';
+import { invalidate } from '../lib/invalidation.js';
+
+// Parses declaration texts for the stub.
+const realApi = new API({ cwd: process.cwd() });
+
+const failures: string[] = [];
+function check(name: string, cond: boolean, detail?: string) {
+ if (cond) {
+ process.stdout.write('.');
+ }
+ else {
+ failures.push(name + (detail ? ' — ' + detail : ''));
+ process.stdout.write('F');
+ }
+}
+
+interface StubFile {
+ text: string;
+ imports: string[];
+ declaration: string;
+}
+
+// `resolve` maps a module specifier to the file name the resolver returns;
+// `getSourceFile` accepts every spelling in `aliases` like the program does.
+function createStub(
+ files: Map,
+ resolve: (specifier: string) => string,
+ aliases: Map,
+) {
+ const call = (fn: (...args: T) => R) =>
+ Object.assign(fn, { gen: (...args: T) => ({ run: () => fn(...args) }) });
+ const getSourceFile = call((fileName: string) => {
+ const name = aliases.get(fileName) ?? fileName;
+ const file = files.get(name);
+ return file && {
+ fileName: name,
+ text: file.text,
+ imports: file.imports.map(text => ({ text })),
+ typeReferenceDirectives: [],
+ referencedFiles: [],
+ externalModuleIndicator: {},
+ statements: [],
+ moduleAugmentations: [],
+ ambientModuleNames: [],
+ isDeclarationFile: false,
+ };
+ });
+ const program = {
+ getCompilerOptions: () => ({}),
+ getConfigFileNames: () => [],
+ getSourceFileNames: () => [...files.keys()],
+ getSourceFile,
+ getResolvedModuleFromModuleSpecifier: call((specifier: { text: string }) => ({
+ resolvedFileName: resolve(specifier.text),
+ })),
+ getResolvedTypeReferenceDirectiveFromTypeReferenceDirective: call(() => undefined),
+ getDeclarationEmit: (fileNames: string[]) => ({
+ outputFiles: new Map(fileNames.map(fileName => [
+ fileName.replace(/\.ts$/, '.d.ts'),
+ { text: files.get(fileName)!.declaration, sourceFileName: fileName },
+ ])),
+ }),
+ };
+ const api = {
+ batch: (...requests: ({ run(): unknown } | Generator)[]) =>
+ requests.map(request => 'run' in request ? request.run() : realApi.batch(request)[0]),
+ createSourceFile: realApi.createSourceFile,
+ };
+ return { api: api as any, project: { program } as any };
+}
+
+function bumpMtime(fileName: string) {
+ const time = new Date(fs.statSync(fileName).mtimeMs + 2000);
+ fs.utimesSync(fileName, time, time);
+}
+
+// ── Test 1: a resolved module spelled differently than the program file ─
+//
+// With project references through symlinked directories, the resolver can
+// return a path (e.g. `/tmp/...`) for a file the program lists under
+// another spelling (`/private/tmp/...`); the program's lookup accepts both.
+// The importer must still depend on the file.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-inv-alias-')));
+ try {
+ const a = path.join(dir, 'a.ts');
+ const b = path.join(dir, 'b.ts');
+ const aAlias = path.join(dir + '-alias', 'a.ts');
+ fs.writeFileSync(a, 'export const x = 1;\n');
+ fs.writeFileSync(b, `import { x } from 'a';\n`);
+ const files = new Map([
+ [a, { text: fs.readFileSync(a, 'utf8'), imports: [], declaration: 'export declare const x = 1;\n' }],
+ [b, { text: fs.readFileSync(b, 'utf8'), imports: ['a'], declaration: 'export {};\n' }],
+ ]);
+ const { api, project } = createStub(files, () => aAlias, new Map([[aAlias, a]]));
+
+ const cache = emptyCache();
+ invalidate(api, project, cache, '', new Map());
+ check(
+ 'importer depends on the program spelling',
+ cache.graph[b]?.deps.includes(a) === true,
+ JSON.stringify(cache.graph[b]),
+ );
+
+ cache.files[a] = { hasFix: false, diagnostics: [], touched: {} };
+ cache.files[b] = { hasFix: false, diagnostics: [], touched: {} };
+ fs.writeFileSync(a, 'export const x = 2;\n');
+ bumpMtime(a);
+ files.get(a)!.declaration = 'export declare const x = 2;\n';
+ invalidate(api, project, cache, '', new Map());
+ check('signature change re-lints the importer', !cache.files[b]);
+ check('signature change re-lints the file', !cache.files[a]);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// ── Test 2: an import attribute is not a module specifier ──────────────
+//
+// `'import'` in `with { 'resolution-mode': 'import' }` must not start a
+// side-effect import that spans to the next quote, which would leave an
+// unknown specifier and count the signature as changed.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-inv-attr-')));
+ try {
+ const b = path.join(dir, 'b.ts');
+ const c = path.join(dir, 'c.ts');
+ fs.writeFileSync(b, 'export const v = 1;\n');
+ fs.writeFileSync(c, `import { v } from './b';\n`);
+ const declaration = `import type * as A from 'a' with { 'resolution-mode': 'import' };\n`
+ + `import type { X } from 'x';\n`
+ + `export declare const v: A.T | X;\n`;
+ const files = new Map([
+ [b, { text: fs.readFileSync(b, 'utf8'), imports: ['a', 'x'], declaration }],
+ [c, { text: fs.readFileSync(c, 'utf8'), imports: ['./b'], declaration: 'export {};\n' }],
+ ]);
+ const resolve = (specifier: string) => specifier === './b' ? b : `/node_modules/${specifier}/index.d.ts`;
+ const { api, project } = createStub(files, resolve, new Map());
+
+ const cache = emptyCache();
+ invalidate(api, project, cache, '', new Map());
+ cache.files[b] = { hasFix: false, diagnostics: [], touched: {} };
+ cache.files[c] = { hasFix: false, diagnostics: [], touched: {} };
+ fs.writeFileSync(b, 'export const v = 2;\n');
+ bumpMtime(b);
+ invalidate(api, project, cache, '', new Map());
+ check('body edit re-lints the file', !cache.files[b]);
+ check('unchanged declaration with an import attribute keeps the importer', !!cache.files[c]);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// b.ts's declaration names a.ts only through `reference`; when a.ts's
+// signature changes, b.ts's declaration text stays the same, but c.ts,
+// which imports b.ts, must be re-linted.
+function checkReference(name: string, reference: string, declaration: string) {
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-inv-ref-')));
+ try {
+ const a = path.join(dir, 'a.ts');
+ const b = path.join(dir, 'b.ts');
+ const c = path.join(dir, 'c.ts');
+ fs.writeFileSync(a, 'export const x = 1;\n');
+ fs.writeFileSync(b, 'export {};\n');
+ fs.writeFileSync(c, `import { v } from './b';\n`);
+ const files = new Map([
+ [a, { text: '', imports: [], declaration: 'export declare const x = 1;\n' }],
+ [b, { text: '', imports: [reference], declaration }],
+ [c, { text: '', imports: ['./b'], declaration: 'export {};\n' }],
+ ]);
+ const { api, project } = createStub(files, specifier => specifier === './b' ? b : a, new Map());
+
+ const cache = emptyCache();
+ invalidate(api, project, cache, '', new Map());
+ for (const fileName of [a, b, c]) {
+ cache.files[fileName] = { hasFix: false, diagnostics: [], touched: {} };
+ }
+ fs.writeFileSync(a, 'export const x = 2;\n');
+ bumpMtime(a);
+ files.get(a)!.declaration = 'export declare const x = 2;\n';
+ invalidate(api, project, cache, '', new Map());
+ check(name, !cache.files[c]);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+// ── Test 3: an import type with attributes ─────────────────────────────
+checkReference(
+ '`import("./a", { with })` is a reference',
+ './a',
+ `export declare const v: typeof import("./a", { with: { "resolution-mode": "import" } }).x;\n`,
+);
+
+// ── Test 4: a type reference with `resolution-mode` first ──────────────
+checkReference(
+ '`` is a reference',
+ './a',
+ `/// \nexport declare const v: number;\n`,
+);
+
+// ── Test 5: specifiers in comments and strings are not references ──────
+//
+// b.ts imports nothing; its declaration only spells module specifiers in a
+// comment and a string literal type, so a body edit keeps c.ts.
+{
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-inv-text-')));
+ try {
+ const b = path.join(dir, 'b.ts');
+ const c = path.join(dir, 'c.ts');
+ fs.writeFileSync(b, 'export const v = 1;\n');
+ fs.writeFileSync(c, `import { v } from './b';\n`);
+ const declaration =
+ `/**\n * Like \`import { y } from './x'\` or \`require('./y')\`.\n * /// \n */\n`
+ + `export declare const v: "from './x'" | 'import("./w")';\n`;
+ const files = new Map([
+ [b, { text: fs.readFileSync(b, 'utf8'), imports: [], declaration }],
+ [c, { text: fs.readFileSync(c, 'utf8'), imports: ['./b'], declaration: 'export {};\n' }],
+ ]);
+ const { api, project } = createStub(files, () => b, new Map());
+
+ const cache = emptyCache();
+ invalidate(api, project, cache, '', new Map());
+ cache.files[b] = { hasFix: false, diagnostics: [], touched: {} };
+ cache.files[c] = { hasFix: false, diagnostics: [], touched: {} };
+ fs.writeFileSync(b, 'export const v = 2;\n');
+ bumpMtime(b);
+ invalidate(api, project, cache, '', new Map());
+ check('body edit re-lints the file', !cache.files[b]);
+ check('specifiers in a comment or a string keep the importer', !!cache.files[c]);
+ }
+ finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+realApi.close();
+process.stdout.write('\n');
+if (failures.length) {
+ console.error(`\n${failures.length} failure(s):`);
+ for (const f of failures) console.error(' - ' + f);
+ process.exit(1);
+}
+console.log('OK');
diff --git a/packages/cli/test/project.ts b/packages/cli/test/project.ts
new file mode 100644
index 00000000..14095180
--- /dev/null
+++ b/packages/cli/test/project.ts
@@ -0,0 +1,52 @@
+// A real on-disk TypeScript 7 project for core tests. One API process
+// per test run; temp dirs are removed when the process exits.
+import type { LinterContext, LintTarget } from '@tsslint/types';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import * as ts from 'typescript/ast';
+import { API } from 'typescript/sync';
+
+let api: API | undefined;
+const tempDirs: string[] = [];
+
+function getApi(): API {
+ if (!api) {
+ api = new API({ cwd: process.cwd() });
+ process.once('exit', () => {
+ // The child may already be gone when exiting via process.exit().
+ try {
+ api?.close();
+ }
+ catch {}
+ for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true });
+ });
+ }
+ return api;
+}
+
+export function createProject(files: Record) {
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tsslint-cli-test-')));
+ tempDirs.push(dir);
+ for (const [name, text] of Object.entries(files)) {
+ fs.writeFileSync(path.join(dir, name), text);
+ }
+ const tsconfig = path.join(dir, 'tsconfig.json');
+ fs.writeFileSync(
+ tsconfig,
+ JSON.stringify({
+ compilerOptions: { target: 'esnext', lib: ['esnext'], noEmit: true },
+ files: Object.keys(files),
+ }),
+ );
+ const snapshot = getApi().createSnapshot({ openProjects: [tsconfig] });
+ const { program, checker } = snapshot.getConfiguredProject(tsconfig)!;
+ const ctx: LinterContext = { typescript: ts };
+ return {
+ ctx,
+ rootDir: dir,
+ target(name: string): LintTarget {
+ return { program, checker, file: program.getSourceFile(path.join(dir, name))! };
+ },
+ };
+}
diff --git a/packages/cli/test/render.test.ts b/packages/cli/test/render.test.ts
index df86b34e..c98f905a 100644
--- a/packages/cli/test/render.test.ts
+++ b/packages/cli/test/render.test.ts
@@ -6,7 +6,7 @@
// the previous emission was "content" (diagnostic or summary).
// - `summary()` always prepends a leading newline (even non-TTY) and
// marks itself as content so any follow-up `info()` (e.g. the
-// `--list-rules` block) gets the same separator a diagnostic block
+// `--debug-estree` block) gets the same separator a diagnostic block
// would have produced.
// - `diagnostic()` indents only in TTY mode, and prepends a newline.
//
@@ -97,15 +97,15 @@ function withCapture(isTTY: boolean, fn: (renderer: import('../lib/render').Rend
// ── Test 5: summary→info in TTY: blank-line separator ──────────────────
// This is the recently fixed invariant. summary() must mark itself as
-// content so --list-rules block reads visually as its own section.
+// content so the --debug-estree block reads visually as its own section.
{
const out = withCapture(true, r => {
r.summary(['5 passed']);
- r.info('type-aware (1)');
+ r.info('estree node types');
});
check(
'summary→info: separator in TTY',
- out === '\n5 passed\n\ntype-aware (1)\n',
+ out === '\n5 passed\n\nestree node types\n',
`got: ${JSON.stringify(out)}`,
);
}
@@ -114,11 +114,11 @@ function withCapture(isTTY: boolean, fn: (renderer: import('../lib/render').Rend
{
const out = withCapture(false, r => {
r.summary(['5 passed']);
- r.info('type-aware (1)');
+ r.info('estree node types');
});
check(
'summary→info: no separator in non-TTY',
- out === '\n5 passed\ntype-aware (1)\n',
+ out === '\n5 passed\nestree node types\n',
`got: ${JSON.stringify(out)}`,
);
}
diff --git a/packages/compat-eslint/README.md b/packages/compat-eslint/README.md
index 708cf6fc..2b7a8fd9 100644
--- a/packages/compat-eslint/README.md
+++ b/packages/compat-eslint/README.md
@@ -6,7 +6,6 @@ Adapter that lets ESLint rules run under TSSLint. Consumed by `importESLintRules
```bash
npm install @tsslint/compat-eslint --save-dev
-npm install @typescript-eslint/eslint-plugin --save-dev # for @typescript-eslint/* rules
```
```ts
@@ -16,7 +15,7 @@ export default defineConfig({
rules: {
...await importESLintRules({
'no-unused-vars': true,
- '@typescript-eslint/no-explicit-any': 'warn',
+ 'eqeqeq': 'warn',
}),
},
});
@@ -24,7 +23,7 @@ export default defineConfig({
For each `/` you reference, install the matching `eslint-plugin-` (or `@scope/eslint-plugin`) package.
-See the [root README](../../README.md) for full interop notes, including TSLint and TSL.
+`@typescript-eslint/eslint-plugin` does not load with `typescript@7` yet. See the [root README](../../README.md) for full interop notes.
---
diff --git a/packages/compat-eslint/index.ts b/packages/compat-eslint/index.ts
index 5d495dd7..79cad069 100644
--- a/packages/compat-eslint/index.ts
+++ b/packages/compat-eslint/index.ts
@@ -1,6 +1,16 @@
import type * as TSSLint from '@tsslint/types';
+import { analyze } from '@typescript-eslint/scope-manager';
import type * as ESLint from 'eslint';
-import * as ts from 'typescript';
+import {
+ type Checker,
+ type CompilerOptions,
+ DiagnosticCategory,
+ type Program,
+ ScriptKind,
+ type TextEdit,
+} from 'typescript/sync';
+import { ESLINT_BUILTIN_GLOBALS } from './lib/globals';
+import * as ts from './lib/ts';
import CodePathAnalyzer = require('./lib/code-path-analysis/code-path-analyzer.js');
import { convertLazy, LISTENER_PRE_MATERIALIZE } from './lib/lazy-estree';
@@ -11,7 +21,6 @@ import { LazySourceCode } from './lib/lazy-source-code';
import { decomposeSimple, isCodePathListener } from './lib/selector-analysis';
import { convertComments, convertTokens } from './lib/tokens';
import { predicateAllKinds, predicateForTriggerSet, tsScanTraverse } from './lib/ts-ast-scan';
-import { applyEslintGlobals, TsScopeManager } from './lib/ts-scope-manager';
import { visitorKeys } from './lib/visitor-keys';
// Build a parse-time-named wrapper around a rule listener. V8's CPU
@@ -92,7 +101,7 @@ interface RuleEntry {
eslintRule: ESLint.Rule.RuleModule;
options: any[];
context: Partial;
- category: ts.DiagnosticCategory;
+ category: DiagnosticCategory;
}
interface DeferredReport {
@@ -100,9 +109,9 @@ interface DeferredReport {
message: string;
start: number;
end: number;
- category: ts.DiagnosticCategory;
- textChanges?: ts.TextChange[];
- suggestions?: { message: string; textChanges: ts.TextChange[] }[];
+ category: DiagnosticCategory;
+ textChanges?: TextEdit[];
+ suggestions?: { message: string; textChanges: TextEdit[] }[];
}
// Module-level state — populated by convertRule, queried at lint time.
@@ -116,8 +125,8 @@ const ruleRegistry = new Map* eslintRule */ ESLint.Rule.RuleModule, RuleEntry>
// - `reports`: deferred per-rule reports collected during a single
// shared traversal; each tsslintRule call replays its own bucket.
// - `errors`: per-rule listener throws, captured so a rule's tsslintRule
-// can rethrow at replay time (preserving TSSLint core's per-rule
-// type-aware retry semantics).
+// can rethrow at replay time, so core reports the crash under that
+// rule.
//
// WeakMap (vs prior single-slot module variable) means concurrent files
// don't thrash, and ts.SourceFile GC cleans up automatically.
@@ -128,16 +137,11 @@ const fileStates = new WeakMap;
}>();
-// `isExternalModule` checks `externalModuleIndicator` only (ESM-only).
-// `commonJsModuleIndicator` flags TS files that look like CJS — those
-// runtime-wrap in a function scope, so top-level vars are NOT global,
-// matching module semantics for our purposes. Fold both flags into one
-// classification; pure script (neither flag) is rare in real TS code
-// but classifying it correctly avoids `no-var` rewriting a genuinely
-// global `var` to `const` (which would change runtime visibility).
+// Pure script (no module indicator) is rare in real TS code but
+// classifying it correctly avoids `no-var` rewriting a genuinely global
+// `var` to `const` (which would change runtime visibility).
function classifySourceType(file: ts.SourceFile): 'module' | 'script' {
- const f = file as { externalModuleIndicator?: unknown; commonJsModuleIndicator?: unknown };
- return (f.externalModuleIndicator || f.commonJsModuleIndicator) ? 'module' : 'script';
+ return file.externalModuleIndicator ? 'module' : 'script';
}
// Caller may pass an ESLint-style `Partial` that includes
@@ -165,7 +169,7 @@ function stripDerivedFields(
console.warn(
`[@tsslint/compat-eslint] Dropping caller-supplied ${
dropped.join(', ')
- }. tsslint derives these per-file from TS (sourceType from externalModuleIndicator/commonJsModuleIndicator; ecmaVersion fixed at 'latest'). Remove these from the convertRule context to silence this warning.`,
+ }. tsslint derives these per-file from TS (sourceType from externalModuleIndicator; ecmaVersion fixed at 'latest'). Remove these from the convertRule context to silence this warning.`,
);
}
const cleaned: Record = { ...(context as Record) };
@@ -196,7 +200,7 @@ export function convertRule(
eslintRule: ESLint.Rule.RuleModule,
options: any[] = [],
context: Partial = {},
- category: ts.DiagnosticCategory = 3 satisfies ts.DiagnosticCategory.Message,
+ category: DiagnosticCategory = DiagnosticCategory.Message,
): TSSLint.Rule {
// ESLint deep-merges `meta.defaultOptions` into user options so each
// rule sees the FULL options object (with all defaults filled in).
@@ -226,10 +230,11 @@ export function convertRule(
const entry: RuleEntry = { id, eslintRule, options, context: cleanedContext, category };
ruleRegistry.set(eslintRule, entry);
- const tsslintRule: TSSLint.Rule = ({ file, report, program }) => {
+ const tsslintRule: TSSLint.Rule = ctx => {
+ const { file, report, program } = ctx;
let perFileState = fileStates.get(file);
if (!perFileState) {
- const { sourceCode, convertContext } = buildEstree(file, program, classifySourceType(file));
+ const { sourceCode, convertContext } = buildEstree(file, program, () => ctx.checker, classifySourceType(file));
perFileState = {
sourceCode,
convertContext,
@@ -252,20 +257,20 @@ export function convertRule(
for (const r of myReports) {
const reporter = report(r.message, r.start, r.end).at(r.stackErr, 1);
- if (r.category === 0 satisfies ts.DiagnosticCategory.Warning) {
+ if (r.category === DiagnosticCategory.Warning) {
reporter.asWarning();
}
- else if (r.category === 1 satisfies ts.DiagnosticCategory.Error) {
+ else if (r.category === DiagnosticCategory.Error) {
reporter.asError();
}
- else if (r.category === 2 satisfies ts.DiagnosticCategory.Suggestion) {
+ else if (r.category === DiagnosticCategory.Suggestion) {
reporter.asSuggestion();
}
if (r.textChanges) {
const tc = r.textChanges;
reporter.withFix(
getTextChangeMessage(file, tc),
- () => [{ fileName: file.fileName, textChanges: tc }],
+ () => tc,
);
}
if (r.suggestions) {
@@ -273,7 +278,7 @@ export function convertRule(
const tc = s.textChanges;
reporter.withRefactor(
s.message,
- () => [{ fileName: file.fileName, textChanges: tc }],
+ () => tc,
);
}
}
@@ -285,7 +290,7 @@ export function convertRule(
function runSharedTraversal(
file: ts.SourceFile,
- program: ts.Program,
+ program: Program,
state: NonNullable>,
) {
const { sourceCode, convertContext, reports, errors } = state;
@@ -306,12 +311,8 @@ function runSharedTraversal(
// files allow top-level `return`. Built once and reused for both the
// legacy `parserOptions` shape and modern `languageOptions.parserOptions`
// so rules reading either path see the same view.
- // `scriptKind` isn't in TS's public SourceFile typing but is set at
- // runtime when the file is parsed; same access pattern as
- // `externalModuleIndicator` in classifySourceType.
- const scriptKind = (file as { scriptKind?: ts.ScriptKind }).scriptKind;
const ecmaFeatures = {
- jsx: scriptKind === ts.ScriptKind.TSX || scriptKind === ts.ScriptKind.JSX,
+ jsx: isJsxFile(file),
globalReturn: sourceType === 'script',
impliedStrict: sourceType === 'module',
} as const;
@@ -758,26 +759,26 @@ function runEntries(
}
}
-function getTextChangeMessage(file: ts.SourceFile, textChanges: ts.TextChange[]) {
+function getTextChangeMessage(file: ts.SourceFile, textChanges: TextEdit[]) {
if (textChanges.length === 1) {
const change = textChanges[0];
- const originalText = file.text.substring(change.span.start, change.span.start + change.span.length);
+ const originalText = file.text.substring(change.pos, change.end);
if (change.newText.length === 0) {
return `Remove \`${originalText}\`.`;
}
- else if (change.span.length === 0) {
- const line = file.getLineAndCharacterOfPosition(change.span.start).line;
+ else if (change.pos === change.end) {
+ const line = file.getLineAndCharacterOfPosition(change.pos).line;
const lineStart = file.getPositionOfLineAndCharacter(line, 0);
- const lineText = file.text.substring(lineStart, change.span.start).trimStart();
+ const lineText = file.text.substring(lineStart, change.pos).trimStart();
return `Insert \`${change.newText}\` after \`${lineText}\`.`;
}
}
- const changes = [...textChanges].sort((a, b) => a.span.start - b.span.start);
+ const changes = [...textChanges].sort((a, b) => a.pos - b.pos);
let text = '';
let newText = '';
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
- text += file.text.substring(change.span.start, change.span.start + change.span.length);
+ text += file.text.substring(change.pos, change.end);
newText += change.newText;
if (i !== changes.length - 1) {
text += '…';
@@ -831,7 +832,7 @@ function getTextChangeMessage(file: ts.SourceFile, textChanges: ts.TextChange[])
function getTextChanges(
_file: ts.SourceFile,
fix: ESLint.Rule.ReportFixer | null | undefined,
-): ts.TextChange[] {
+): TextEdit[] {
if (!fix) {
return [];
}
@@ -866,10 +867,11 @@ function getTextChanges(
return [];
}
const fixes = isIterable(result) ? [...result] : [result];
- const textChanges: ts.TextChange[] = [];
+ const textChanges: TextEdit[] = [];
for (const f of fixes) {
textChanges.push({
- span: { start: f.range[0], length: f.range[1] - f.range[0] },
+ pos: f.range[0],
+ end: f.range[1],
newText: f.text,
});
}
@@ -880,6 +882,30 @@ function isIterable(obj: unknown): obj is Iterable {
return obj != null && typeof (obj as { [Symbol.iterator]?: unknown })[Symbol.iterator] === 'function';
}
+function isJsxFile(file: ts.SourceFile): boolean {
+ return file.scriptKind === ScriptKind.TSX || file.scriptKind === ScriptKind.JSX;
+}
+
+type Lib = NonNullable[1]>['lib']>[number];
+
+// Mirrors @typescript-eslint/parser's `getLib`: explicit `lib` entries
+// (`lib.es2022.d.ts`) win; otherwise the default lib of `target`.
+function getLib(options: CompilerOptions): Lib[] {
+ if (options.lib) {
+ const libs: Lib[] = [];
+ for (const lib of options.lib) {
+ const match = /^lib\.(.+)\.d\.[cm]?ts$/.exec(lib.toLowerCase());
+ if (match) libs.push(match[1] as Lib);
+ }
+ return libs;
+ }
+ const target = options.target;
+ if (target === undefined || target === ts.ScriptTarget.ESNext) return ['esnext.full'];
+ if (target === ts.ScriptTarget.ES2015) return ['es6'];
+ const name = ts.ScriptTarget[target]?.toLowerCase();
+ return name?.startsWith('es20') ? [`${name}.full` as Lib] : ['esnext.full'];
+}
+
// Build a fresh `LazySourceCode` + lazy-estree convert context for `file`.
// Pure: no module-level caching here — callers cache via `perFileState`.
//
@@ -889,7 +915,8 @@ function isIterable(obj: unknown): obj is Iterable {
// ts.SourceFile for. Calling our own converter directly avoids the require.
function buildEstree(
file: ts.SourceFile,
- program: ts.Program,
+ program: Program,
+ getChecker: () => Checker,
sourceType: 'module' | 'script',
): {
sourceCode: ESLint.SourceCode;
@@ -938,19 +965,24 @@ function buildEstree(
// ESM syntax is present); that's intentional, since it reflects how
// the file is actually emitted/run.
estree.sourceType = sourceType;
- const scopeManager = new TsScopeManager(file, program, estree, astMaps, sourceType);
- // Inject ECMAScript built-ins + TS lib type globals so `no-undef`
- // doesn't fire on `undefined` / `Math` / `Record` / etc.
- // `TsScopeManager` itself stays free of this lint-pipeline policy
- // (upstream eslint-scope parity tests rely on the un-injected
- // shape); the names + de-dupe logic live next to `addGlobals` in
- // `ts-scope-manager.ts`.
- applyEslintGlobals(scopeManager);
const sourceCode = new LazySourceCode({
text: file.text,
ast: estree,
tsFile: file,
- scopeManager,
+ createScopeManager: () => {
+ const compilerOptions = program.getCompilerOptions();
+ const scopeManager = analyze(estree, {
+ childVisitorKeys: visitorKeys as Record,
+ globalReturn: false,
+ impliedStrict: sourceType === 'module',
+ jsxPragma: compilerOptions.jsxFactory ?? 'React',
+ jsxFragmentName: compilerOptions.jsxFragmentFactory ?? null,
+ lib: getLib(compilerOptions),
+ sourceType,
+ });
+ scopeManager.addGlobals([...ESLINT_BUILTIN_GLOBALS]);
+ return scopeManager;
+ },
visitorKeys: visitorKeys as Record,
parserServices: {
...astMaps,
@@ -959,10 +991,8 @@ function buildEstree(
emitDecoratorMetadata: undefined,
experimentalDecorators: undefined,
isolatedDeclarations: undefined,
- getSymbolAtLocation: (node: any) =>
- program.getTypeChecker().getSymbolAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)),
- getTypeAtLocation: (node: any) =>
- program.getTypeChecker().getTypeAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)),
+ getSymbolAtLocation: (node: any) => getChecker().getSymbolAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)),
+ getTypeAtLocation: (node: any) => getChecker().getTypeAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)),
},
}) as unknown as ESLint.SourceCode;
return { sourceCode, convertContext };
diff --git a/packages/compat-eslint/lib/globals.ts b/packages/compat-eslint/lib/globals.ts
new file mode 100644
index 00000000..ff7b1770
--- /dev/null
+++ b/packages/compat-eslint/lib/globals.ts
@@ -0,0 +1,79 @@
+// ECMAScript built-in globals, vendored from `eslint/conf/globals.js`'s
+// `es2026` (the latest superset). ESLint registers these before rules
+// run (`addDeclaredGlobals`) so `no-undef` doesn't fire on `undefined`,
+// `Math`, etc. Update if ESLint adds a new ES year.
+export const ESLINT_BUILTIN_GLOBALS: readonly string[] = [
+ 'AggregateError',
+ 'Array',
+ 'ArrayBuffer',
+ 'AsyncDisposableStack',
+ 'Atomics',
+ 'BigInt',
+ 'BigInt64Array',
+ 'BigUint64Array',
+ 'Boolean',
+ 'DataView',
+ 'Date',
+ 'DisposableStack',
+ 'Error',
+ 'EvalError',
+ 'FinalizationRegistry',
+ 'Float16Array',
+ 'Float32Array',
+ 'Float64Array',
+ 'Function',
+ 'Infinity',
+ 'Int16Array',
+ 'Int32Array',
+ 'Int8Array',
+ 'Intl',
+ 'Iterator',
+ 'JSON',
+ 'Map',
+ 'Math',
+ 'NaN',
+ 'Number',
+ 'Object',
+ 'Promise',
+ 'Proxy',
+ 'RangeError',
+ 'ReferenceError',
+ 'Reflect',
+ 'RegExp',
+ 'Set',
+ 'SharedArrayBuffer',
+ 'String',
+ 'SuppressedError',
+ 'Symbol',
+ 'SyntaxError',
+ 'Temporal',
+ 'TypeError',
+ 'URIError',
+ 'Uint16Array',
+ 'Uint32Array',
+ 'Uint8Array',
+ 'Uint8ClampedArray',
+ 'WeakMap',
+ 'WeakRef',
+ 'WeakSet',
+ 'constructor',
+ 'decodeURI',
+ 'decodeURIComponent',
+ 'encodeURI',
+ 'encodeURIComponent',
+ 'escape',
+ 'eval',
+ 'globalThis',
+ 'hasOwnProperty',
+ 'isFinite',
+ 'isNaN',
+ 'isPrototypeOf',
+ 'parseFloat',
+ 'parseInt',
+ 'propertyIsEnumerable',
+ 'toLocaleString',
+ 'toString',
+ 'undefined',
+ 'unescape',
+ 'valueOf',
+];
diff --git a/packages/compat-eslint/lib/lazy-estree.ts b/packages/compat-eslint/lib/lazy-estree.ts
index 5f2cc302..cc95fd1a 100644
--- a/packages/compat-eslint/lib/lazy-estree.ts
+++ b/packages/compat-eslint/lib/lazy-estree.ts
@@ -50,7 +50,7 @@
// findWrapperRoute as needed. Adding a new "real ESTree class" goes through
// `defineShape` — see the table starting around the SHAPE_CLASSES const.
-import * as ts from 'typescript';
+import * as ts from './ts';
import { xhtmlEntities } from './xhtml-entities';
const SK = ts.SyntaxKind;
@@ -1587,7 +1587,7 @@ function maybeFixExports(
// `@dec export class` has `modifiers[0]` = the Decorator. getModifiers
// excludes decorators (matching typescript-estree's fixExports), so the
// `export` keyword is first whatever the decorator position.
- const modifiers = ts.canHaveModifiers(tsNode) ? ts.getModifiers(tsNode) : undefined;
+ const modifiers = ts.getModifiers(tsNode);
if (!modifiers?.length || modifiers[0].kind !== SK.ExportKeyword) return inner;
const exportKeyword = modifiers[0];
const next = modifiers[1];
@@ -1897,13 +1897,28 @@ defineShape(SK.IndexedAccessType, {
});
// SK.LiteralType NOT migrated: convertLiteralType has a special case
// for `null` (wraps NullKeyword as bare TSNullKeyword to match eager).
-defineShape(SK.QualifiedName, {
+const qualifiedNameShape = makeShapeClass({
type: 'TSQualifiedName',
slots: {
left: { tsField: 'left' },
right: { tsField: 'right' },
},
});
+const heritageQualifiedNameShape = makeShapeClass({
+ type: 'MemberExpression',
+ defaults: { computed: false, optional: false },
+ slots: {
+ object: { tsField: 'left' },
+ property: { tsField: 'right' },
+ },
+});
+defineShapeRouter(
+ SK.QualifiedName,
+ (tsNode, parent) =>
+ ts.isHeritageQualifiedName(tsNode)
+ ? new heritageQualifiedNameShape(tsNode, parent)
+ : new qualifiedNameShape(tsNode, parent),
+);
defineShape(SK.TypeAssertionExpression, {
type: 'TSTypeAssertion',
slots: {
@@ -2010,10 +2025,17 @@ defineShape(SK.ConditionalExpression, {
alternate: { tsField: 'whenFalse' },
},
});
-defineShape(SK.NonNullExpression, {
+// `a?.b!` is itself the outermost chain link, so it runs through
+// wrapChainIfNeeded like Member/CallExpression.
+const nonNullExpressionShape = makeShapeClass({
type: 'TSNonNullExpression',
slots: { expression: { tsField: 'expression' } },
});
+defineShapeRouter(
+ SK.NonNullExpression,
+ (tsNode, parent) =>
+ wrapChainIfNeeded(new nonNullExpressionShape(tsNode, parent), tsNode as ts.NonNullExpression, parent),
+);
defineShape(SK.ExternalModuleReference, {
type: 'TSExternalModuleReference',
slots: { expression: { tsField: 'expression' } },
@@ -2036,7 +2058,7 @@ defineShape(SK.TypeParameter, {
slots: {
name: { tsField: 'name' },
constraint: { tsField: 'constraint' },
- default: { tsField: 'default' },
+ default: { tsField: 'defaultType' },
},
});
defineShape(SK.YieldExpression, {
@@ -2090,13 +2112,30 @@ defineShape(SK.TypeOfExpression, {
});
// Shapes that use custom converter funcs + whenAbsent='undefined' for
// type-position slots:
-defineShape(SK.TypeReference, {
+const typeReferenceShape = makeShapeClass({
type: 'TSTypeReference',
slots: {
typeName: { tsField: 'typeName' },
typeArguments: { tsField: 'typeArguments', via: convertTypeArguments, whenAbsent: 'undefined' },
},
});
+// TS 7 parses `implements A.B` and interface `extends A.B` as
+// TypeReferences; typescript-estree reads them as expressions.
+const heritageTypeReferenceShape = makeShapeClass({
+ type: tn =>
+ (tn.parent as ts.HeritageClause).token === SK.ImplementsKeyword ? 'TSClassImplements' : 'TSInterfaceHeritage',
+ slots: {
+ expression: { tsField: 'typeName' },
+ typeArguments: { tsField: 'typeArguments', via: convertTypeArguments, whenAbsent: 'undefined' },
+ },
+});
+defineShapeRouter(
+ SK.TypeReference,
+ (tsNode, parent) =>
+ ts.isHeritageTypeReference(tsNode)
+ ? new heritageTypeReferenceShape(tsNode, parent)
+ : new typeReferenceShape(tsNode, parent),
+);
defineShape(SK.ConstructorType, {
type: 'TSConstructorType',
consts: tn => ({
@@ -2112,7 +2151,7 @@ defineShape(SK.ModuleDeclaration, {
type: 'TSModuleDeclaration',
consts: tn => ({
declare: hasMod(tn, SK.DeclareKeyword),
- global: !!(tn.flags & ts.NodeFlags.GlobalAugmentation),
+ global: ts.isGlobalAugmentation(tn, tn.getSourceFile().text),
// `declare global { ... }` is `kind: 'global'` per typescript-estree.
// Distinct from `namespace` and `module` — scope-manager's
// Referencer.TSModuleDeclaration skips defineIdentifier for
@@ -2124,9 +2163,9 @@ defineShape(SK.ModuleDeclaration, {
// declaration selector that would have marked inner var/fn
// declarations as used. Caught on prisma matched-type-aware
// (4 false fires across runtime/getPrismaClient.ts and friends).
- kind: tn.flags & ts.NodeFlags.GlobalAugmentation
+ kind: ts.isGlobalAugmentation(tn, tn.getSourceFile().text)
? 'global'
- : tn.flags & ts.NodeFlags.Namespace
+ : tn.keyword === SK.NamespaceKeyword
? 'namespace'
: 'module',
}),
@@ -2399,12 +2438,12 @@ defineShape(SK.RegularExpressionLiteral, {
void tn;
},
});
-defineShape(SK.PropertySignature, {
+defineShape(SK.PropertySignature, {
type: 'TSPropertySignature',
defaults: { accessibility: undefined, static: false },
consts: tn => ({
computed: tn.name.kind === SK.ComputedPropertyName,
- optional: !!tn.questionToken,
+ optional: ts.hasQuestionToken(tn),
readonly: hasMod(tn, SK.ReadonlyKeyword),
}),
slots: {
@@ -2412,12 +2451,12 @@ defineShape(SK.PropertySignature, {
typeAnnotation: { tsField: 'type', via: convertTypeAnnotation, whenAbsent: 'undefined' },
},
});
-defineShape(SK.MethodSignature, {
+defineShape(SK.MethodSignature, {
type: 'TSMethodSignature',
defaults: { accessibility: undefined, kind: 'method', static: false },
consts: tn => ({
computed: tn.name.kind === SK.ComputedPropertyName,
- optional: !!tn.questionToken,
+ optional: ts.hasQuestionToken(tn),
readonly: hasMod(tn, SK.ReadonlyKeyword),
}),
slots: {
@@ -2581,7 +2620,7 @@ defineShape(SK.ArrayBindingPattern, {
elements: {
tsField: 'elements',
via: (els, parent) =>
- els.map((e: ts.ArrayBindingElement) => e.kind === SK.OmittedExpression ? null : convertChild(e, parent)),
+ els.map((e: ts.ArrayBindingElement) => isArrayBindingHole(e) ? null : convertChild(e, parent)),
},
},
});
@@ -3437,7 +3476,7 @@ const classShape = makeShapeClass({
superClass: {
compute: (tn: ts.ClassDeclaration | ts.ClassExpression, parent) => {
const ext = tn.heritageClauses?.find(c => c.token === SK.ExtendsKeyword);
- return ext ? convertChild(ext.types[0]?.expression, parent) : null;
+ return ext ? convertChild(ext.types[0] && ts.heritageElementExpression(ext.types[0]), parent) : null;
},
},
superTypeArguments: {
@@ -3451,7 +3490,11 @@ const classShape = makeShapeClass({
compute: (tn: ts.ClassDeclaration | ts.ClassExpression, parent) => {
const impl = tn.heritageClauses?.find(c => c.token === SK.ImplementsKeyword);
if (!impl) return [];
- return impl.types.map(t => new classImplementsShape(t, parent));
+ return impl.types.map(t =>
+ t.kind === SK.TypeReference
+ ? new heritageTypeReferenceShape(t, parent)
+ : new classImplementsShape(t, parent)
+ );
},
},
decorators: { compute: (tn, parent) => convertDecorators(tn, parent) },
@@ -3517,8 +3560,8 @@ defineShape(SK.PropertyDeclaration, {
declare: hasMod(tn, SK.DeclareKeyword),
accessibility: accessibilityOf(tn),
computed: tn.name.kind === SK.ComputedPropertyName,
- optional: !!tn.questionToken,
- definite: !!tn.exclamationToken,
+ optional: ts.hasQuestionToken(tn),
+ definite: ts.hasExclamationToken(tn),
};
},
slots: {
@@ -3581,7 +3624,7 @@ const objectMethodShape = makeShapeClass({
defaults: { kind: 'init', method: true, shorthand: false },
consts: tn => ({
computed: tn.name.kind === SK.ComputedPropertyName,
- optional: !!tn.questionToken,
+ optional: ts.hasQuestionToken(tn),
}),
slots: {
key: { tsField: 'name' },
@@ -3629,7 +3672,7 @@ const methodDefinitionShape = makeShapeClass<
computed: tn.kind !== SK.Constructor
&& !!(tn as ts.MethodDeclaration).name
&& (tn as ts.MethodDeclaration).name.kind === SK.ComputedPropertyName,
- optional: !!(tn as ts.MethodDeclaration).questionToken,
+ optional: ts.hasQuestionToken(tn as ts.MethodDeclaration),
};
},
slots: {
@@ -3705,7 +3748,7 @@ defineShapeRouter(SK.JsxSelfClosingElement, (tsNode, parent) => new jsxElementSh
defineShape(SK.ImportDeclaration, {
type: 'ImportDeclaration',
- consts: tn => ({ importKind: tn.importClause?.isTypeOnly ? 'type' : 'value' }),
+ consts: tn => ({ importKind: ts.isTypeOnlyImportClause(tn.importClause) ? 'type' : 'value' }),
slots: {
source: { tsField: 'moduleSpecifier' },
specifiers: {
@@ -3905,21 +3948,11 @@ const NO_COUNTERPART_NODE_KINDS = new Set([
SK.JsxAttributes, // → JSXOpeningElement.attributes
SK.CaseBlock, // → SwitchStatement.cases
SK.HeritageClause, // → ClassDeclaration.superClass / implements
- // SK.AssertClause === SK.ImportAttributes (kind 301) — the import-
- // attributes CONTAINER. Upstream's `convertImportAttributes` flattens
- // `assertClause.elements` directly into `ImportDeclaration.attributes`
- // without a wrapper node — so the container has no standalone ESTree
- // type. Distinct from SK.ImportAttribute (kind 302, the individual
- // `key: value` entry) which DOES have a defineShape and emits
- // `'ImportAttribute'`. Confusion potential because TS renamed both at
- // the same time (4.5: AssertClause/AssertEntry; 5.x:
- // ImportAttributes/ImportAttribute).
- SK.AssertClause,
+ SK.ImportAttributes, // flattened into ImportDeclaration.attributes
SK.OmittedExpression, // sparse array hole — represented as null
SK.TemplateSpan, // wrapped in TemplateExpression
SK.TemplateLiteralTypeSpan, // wrapped in TSTemplateLiteralType
SK.MissingDeclaration, // parse error
- SK.Bundle, // multi-file container
// (UnparsedSource / InputFiles existed in older TS; removed in 5.x —
// not listed because the SyntaxKind enum no longer has them)
// (SK.SourceFile is intentionally NOT here — `convertLazy` pre-
@@ -4016,7 +4049,7 @@ export function hasNoEstreeCounterpart(kind: ts.SyntaxKind): boolean {
// Trivia (comments, whitespace, shebang, conflict markers).
if (kind >= SK.FirstTriviaToken && kind <= SK.LastTriviaToken) return true;
// File boundary token.
- if (kind === SK.EndOfFileToken) return true;
+ if (kind === SK.EndOfFile) return true;
// Punctuators / operators (`+`, `?`, `===`, `?.`, `&&`, `=>`, etc.).
// ESLint's SourceCode exposes these as Token objects (`{type:
// 'Punctuator', value, loc, range}`) — they're NOT ESTree nodes.
@@ -4126,6 +4159,7 @@ export function isOutermostOptionalChain(n: ts.Node): boolean {
n.kind !== SK.PropertyAccessExpression
&& n.kind !== SK.ElementAccessExpression
&& n.kind !== SK.CallExpression
+ && n.kind !== SK.NonNullExpression
) {
return false;
}
@@ -4142,16 +4176,16 @@ export function isOutermostOptionalChain(n: ts.Node): boolean {
// that the eventual outer build would have to unwrap and re-point.
function wrapChainIfNeeded(
result: LazyNode,
- tsNode: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression,
+ tsNode: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression | ts.NonNullExpression,
parent: LazyNode | null | undefined,
): LazyNode {
if (!isOutermostOptionalChain(tsNode)) return result;
- // Force the inner `object`/`callee` getter before the wrapper claims the TS
- // slot, so the inner access parents to `result` (the Member/CallExpression),
- // matching eager — without it a bottom-up `materialize()` of an inner node
- // resolves its parent up to the cache-claimed wrapper instead.
- const r = result as unknown as { type: string; object?: LazyNode; callee?: LazyNode };
- void (r.type === 'MemberExpression' ? r.object : r.callee);
+ // Force the inner `object`/`callee`/`expression` getter before the wrapper
+ // claims the TS slot, so the inner access parents to `result`, matching
+ // eager — without it a bottom-up `materialize()` of an inner node resolves
+ // its parent up to the cache-claimed wrapper instead.
+ const r = result as unknown as { type: string; object?: LazyNode; callee?: LazyNode; expression?: LazyNode };
+ void (r.type === 'MemberExpression' ? r.object : r.type === 'CallExpression' ? r.callee : r.expression);
return new ChainExpressionWrappingNode(tsNode, parent, result);
}
@@ -4256,7 +4290,8 @@ const ProgramNode = makeShapeClass({
comments: [],
tokens: [],
}),
- range: (tn, ctx) => [tn.getStart(ctx.ast), tn.endOfFileToken.end],
+ // typescript-estree's Program starts at 0, leading trivia included.
+ range: tn => [tn.pos, tn.endOfFileToken.end],
slots: {
body: {
tsField: 'statements',
@@ -4469,7 +4504,7 @@ const BindingAssignmentPatternNode = makeShapeClass({
type: 'AssignmentPattern',
defaults: { decorators: EMPTY_ARRAY, optional: false, typeAnnotation: undefined },
registersInMaps: () => false,
- range: (tn, ctx) => [tn.name.getStart(ctx.ast), tn.initializer!.end],
+ range: (tn, ctx) => [tn.name!.getStart(ctx.ast), tn.initializer!.end],
slots: {
left: { tsField: 'name', via: 'convertChildAsPattern' },
right: { tsField: 'initializer' },
@@ -4501,6 +4536,11 @@ const TSEnumBodyNode = makeShapeClass({
// Pull `@dec` decorators out of a node's `modifiers` array. TS folds
// decorators and modifiers into one list since 4.8; typescript-estree
// emits them as a separate `decorators` slot on the owning ESTree node.
+// TS 7 parses a hole in an array binding pattern (`[, a]`) as a nameless
+// BindingElement rather than an OmittedExpression.
+function isArrayBindingHole(e: ts.Node): boolean {
+ return e.kind === SK.OmittedExpression || (e.kind === SK.BindingElement && !(e as ts.BindingElement).name);
+}
function hasMod(tsNode: ts.Node, kind: ts.SyntaxKind): boolean {
const ms = (tsNode as { modifiers?: ts.NodeArray }).modifiers;
return !!ms?.some(m => m.kind === kind);
diff --git a/packages/compat-eslint/lib/lazy-source-code.ts b/packages/compat-eslint/lib/lazy-source-code.ts
index 01231a76..466b3511 100644
--- a/packages/compat-eslint/lib/lazy-source-code.ts
+++ b/packages/compat-eslint/lib/lazy-source-code.ts
@@ -25,9 +25,9 @@
// method that's missing here, throw a clear error rather than silently
// returning undefined.
-import * as ts from 'typescript';
import { materialize } from './lazy-estree';
import { buildCommentObject, walkInnerCommentsOf } from './tokens';
+import * as ts from './ts';
const SK = ts.SyntaxKind;
@@ -132,7 +132,9 @@ interface LazyConfig {
text: string;
ast: any; // LazyESTree Program
tsFile: ts.SourceFile;
- scopeManager: any;
+ // Scope analysis walks the whole ESTree, so it only runs once a rule
+ // actually asks for scopes.
+ createScopeManager: () => any;
parserServices: any;
visitorKeys: any;
}
@@ -140,7 +142,6 @@ interface LazyConfig {
export class LazySourceCode {
readonly text: string;
readonly ast: any;
- readonly scopeManager: any;
readonly parserServices: any;
readonly visitorKeys: any;
readonly hasBOM: boolean;
@@ -154,17 +155,23 @@ export class LazySourceCode {
private _lines?: string[];
private _lineStartIndices?: number[];
private _scopeCache?: WeakMap;
+ private _scopeManager: any;
+ private _createScopeManager: () => any;
constructor(config: LazyConfig) {
this.text = config.text;
this.ast = config.ast;
this._tsFile = config.tsFile;
- this.scopeManager = config.scopeManager ?? null;
+ this._createScopeManager = config.createScopeManager;
this.parserServices = config.parserServices ?? {};
this.visitorKeys = config.visitorKeys;
this.hasBOM = config.text.charCodeAt(0) === 0xfeff;
}
+ get scopeManager(): any {
+ return this._scopeManager ??= this._createScopeManager();
+ }
+
// ─── Lazy collections ──────────────────────────────────────────────────
get tokens(): Token[] {
@@ -460,11 +467,7 @@ export class LazySourceCode {
const out: Comment[] = [];
const text = this.text;
const ast = this._tsFile;
- const scanner = this._innerScanner ??= ts.createScanner(
- ast.languageVersion,
- /*skipTrivia*/ true,
- ast.languageVariant,
- );
+ const scanner = this._innerScanner ??= ts.createScanner(/*skipTrivia*/ true, ast.languageVariant);
scanner.setText(text);
const collect = (pos: number, end: number, kind: ts.CommentKind): void => {
out.push(buildCommentObject(text, pos, end, kind, ast));
@@ -485,7 +488,7 @@ export class LazySourceCode {
else {
const start = tsNode.getStart(ast);
if (start >= tsNode.end) return out;
- scanner.setTextPos(start);
+ scanner.resetTokenState(start);
scanner.scan();
innerStart = scanner.getTokenEnd();
}
diff --git a/packages/compat-eslint/lib/tokens.ts b/packages/compat-eslint/lib/tokens.ts
index c0aaea15..94a9e6e6 100644
--- a/packages/compat-eslint/lib/tokens.ts
+++ b/packages/compat-eslint/lib/tokens.ts
@@ -8,7 +8,7 @@
// `getTokenBefore` etc. gets the same data ESLint+typescript-estree would
// produce.
-import * as ts from 'typescript';
+import * as ts from './ts';
const SK = ts.SyntaxKind;
@@ -56,9 +56,7 @@ function isComment(node: ts.Node): boolean {
return node.kind === SK.SingleLineCommentTrivia || node.kind === SK.MultiLineCommentTrivia;
}
function isJSDocComment(node: ts.Node): boolean {
- // JSDocComment was added in TS 4.7; alias as JSDoc for older versions.
- return node.kind === SK.JSDocComment
- || (node as { kind: number }).kind === (SK as unknown as { JSDoc?: number }).JSDoc;
+ return node.kind === SK.JSDoc;
}
function isJSXToken(node: ts.Node): boolean {
return node.kind >= SK.JsxElement && node.kind <= SK.JsxAttribute;
@@ -153,19 +151,15 @@ function buildToken(token: ts.Node, start: number, end: number, ast: ts.SourceFi
export function convertTokens(ast: ts.SourceFile): Token[] {
const result: Token[] = [];
const text = ast.text;
- const scanner = ts.createScanner(
- ast.languageVersion,
- /*skipTrivia*/ true,
- ast.languageVariant,
- );
+ const scanner = ts.createScanner(/*skipTrivia*/ true, ast.languageVariant);
scanner.setText(text);
let pos = 0;
function emitScanned(targetEnd: number): void {
while (pos < targetEnd) {
- scanner.setTextPos(pos);
+ scanner.resetTokenState(pos);
const k = scanner.scan();
- if (k === SK.EndOfFileToken) break;
+ if (k === SK.EndOfFile) break;
const start = scanner.getTokenStart();
if (start >= targetEnd) break;
pos = scanner.getTokenEnd();
@@ -185,7 +179,7 @@ export function convertTokens(ast: ts.SourceFile): Token[] {
const nodeStart = node.kind === SK.JsxText ? node.getFullStart() : node.getStart(ast);
// Scanner sweeps through trivia / punctuators preceding this node.
emitScanned(nodeStart);
- if (isToken(node) && node.kind !== SK.EndOfFileToken) {
+ if (isToken(node) && node.kind !== SK.EndOfFile) {
result.push(convertToken(node, ast));
pos = node.getEnd();
return;
@@ -280,7 +274,7 @@ export function walkInnerCommentsOf(
emitCommentsFrom: (scanFrom: number) => void,
): void {
if (isComment(node) || isJSDocComment(node)) return;
- if (isToken(node) && node.kind !== SK.EndOfFileToken) return;
+ if (isToken(node) && node.kind !== SK.EndOfFile) return;
let firstSeen = false;
let lastEnd = -1;
ts.forEachChild(node, child => {
@@ -298,7 +292,7 @@ export function walkInnerCommentsOf(
else {
const start = node.getStart(ast);
if (start >= node.end) return;
- scanner.setTextPos(start);
+ scanner.resetTokenState(start);
scanner.scan();
tailFrom = scanner.getTokenEnd();
}
@@ -328,11 +322,7 @@ export function convertComments(ast: ts.SourceFile): Comment[] {
const collect = (pos: number, end: number, kind: ts.CommentKind): void => {
out.push(buildCommentObject(text, pos, end, kind, ast));
};
- const scanner = ts.createScanner(
- ast.languageVersion,
- /*skipTrivia*/ true,
- ast.languageVariant,
- );
+ const scanner = ts.createScanner(/*skipTrivia*/ true, ast.languageVariant);
scanner.setText(text);
const emitCommentsFrom = (scanFrom: number): void => {
if (scanFrom > 0) {
diff --git a/packages/compat-eslint/lib/ts-ast-scan.ts b/packages/compat-eslint/lib/ts-ast-scan.ts
index a69f4912..57be3ed8 100644
--- a/packages/compat-eslint/lib/ts-ast-scan.ts
+++ b/packages/compat-eslint/lib/ts-ast-scan.ts
@@ -17,7 +17,6 @@
// already lazy-builds the parent chain when a rule reads `.parent`, so we
// don't pre-build ancestors either.
-import * as ts from 'typescript';
import {
type ConvertContext,
GENERIC_TS_NODE_MARKER,
@@ -26,6 +25,7 @@ import {
NoESTreeCounterpartError,
} from './lazy-estree';
import { UnsupportedSelectorError } from './selector-analysis';
+import * as ts from './ts';
const SK = ts.SyntaxKind;
// `import * as ts` lowers to a namespace object guarded by a getter on
@@ -522,14 +522,15 @@ const PREDICATES: Record = {
'TSInstantiationExpression': n =>
n.kind === SK.ExpressionWithTypeArguments
&& n.parent?.kind !== SK.HeritageClause,
+ // TS 7 parses `implements X` / interface `extends X` as a TypeReference.
'TSClassImplements': n =>
- n.kind === SK.ExpressionWithTypeArguments
+ (n.kind === SK.ExpressionWithTypeArguments || n.kind === SK.TypeReference)
&& n.parent?.kind === SK.HeritageClause
&& (n.parent as ts.HeritageClause).token === SK.ImplementsKeyword
&& (n.parent.parent?.kind === SK.ClassDeclaration
|| n.parent.parent?.kind === SK.ClassExpression),
'TSInterfaceHeritage': n =>
- n.kind === SK.ExpressionWithTypeArguments
+ (n.kind === SK.ExpressionWithTypeArguments || n.kind === SK.TypeReference)
&& n.parent?.kind === SK.HeritageClause
&& (n.parent as ts.HeritageClause).token === SK.ExtendsKeyword
&& n.parent.parent?.kind === SK.InterfaceDeclaration,
@@ -549,6 +550,10 @@ const PREDICATES: Record = {
// Context-dependent — see isInJSXIdentifierPosition / chain helpers.
'JSXIdentifier': n => n.kind === SK.Identifier && isInJSXIdentifierPosition(n),
'JSXMemberExpression': n => n.kind === SK.PropertyAccessExpression && isInJSXMemberExpressionChain(n),
+ 'MemberExpression': n =>
+ n.kind === SK.PropertyAccessExpression || n.kind === SK.ElementAccessExpression || ts.isHeritageQualifiedName(n),
+ 'TSTypeReference': n => n.kind === SK.TypeReference && !ts.isHeritageTypeReference(n),
+ 'TSQualifiedName': n => n.kind === SK.QualifiedName && !ts.isHeritageQualifiedName(n),
};
// Sidecar table: ESTree types whose predicate is EXACTLY a TS-kind check
@@ -600,7 +605,6 @@ const SIMPLE_KINDS: Record = {
'ClassDeclaration': [SK.ClassDeclaration],
'ClassExpression': [SK.ClassExpression],
'NewExpression': [SK.NewExpression],
- 'MemberExpression': [SK.PropertyAccessExpression, SK.ElementAccessExpression],
'ConditionalExpression': [SK.ConditionalExpression],
'AwaitExpression': [SK.AwaitExpression],
'YieldExpression': [SK.YieldExpression],
@@ -647,7 +651,6 @@ const SIMPLE_KINDS: Record = {
'TSTypeAssertion': [SK.TypeAssertionExpression],
'TSNonNullExpression': [SK.NonNullExpression],
'TSSatisfiesExpression': [SK.SatisfiesExpression],
- 'TSTypeReference': [SK.TypeReference],
'TSUnionType': [SK.UnionType],
'TSIntersectionType': [SK.IntersectionType],
'TSArrayType': [SK.ArrayType],
@@ -664,7 +667,6 @@ const SIMPLE_KINDS: Record = {
'TSTemplateLiteralType': [SK.TemplateLiteralType],
'TSTypePredicate': [SK.TypePredicate],
'TSTypeLiteral': [SK.TypeLiteral],
- 'TSQualifiedName': [SK.QualifiedName],
'TSOptionalType': [SK.OptionalType],
'TSRestType': [SK.RestType],
'TSTypeParameter': [SK.TypeParameter],
@@ -843,12 +845,8 @@ export function tsScanTraverse(
const enterCb = (target: unknown) => visitor.enterNode(target);
const leaveCb = (target: unknown) => visitor.leaveNode(target);
// `parentTarget` is the most recent ESTree target we entered on the
- // way down. ParenthesizedExpression / ComputedPropertyName / ParenthesizedType
- // are pass-through in convertChildInner — materialise on those returns
- // the INNER ESTree, not a wrapper. Visit then recurses into the
- // parens' child (the same inner) and would fire enter/leave a second
- // time on the same target. Skip when materialise yields the same
- // ESTree we just entered.
+ // way down. A node whose materialise yields that same target (a TS
+ // kind that maps back onto its parent's ESTree) is not re-entered.
// `embedAfter`: an extra TS child to visit INSIDE this node's enter/
// leave bracket, just before leaving. Used to inject typeAnnotation
// from a parent ts.ParameterDeclaration / ts.VariableDeclaration into
@@ -895,6 +893,16 @@ export function tsScanTraverse(
// Recurse through with the CatchClause as parentTarget so the
// inner Identifier visit lands with the right ESTree parent.
|| (k === SK.VariableDeclaration && node.parent?.kind === SK.CatchClause)
+ // Pass-through in convertChildInner: materialise returns the
+ // INNER ESTree, which the child visit enters anyway. When that
+ // inner is a wrapper head (`(a?.b)` → ChainExpression,
+ // `(typeof import('x'))` → TSTypeQuery) this level would enter
+ // the whole chain and leave its tail as parentTarget, so the
+ // child's own visit — same head, different tail — entered the
+ // chain a second time.
+ || k === SK.ParenthesizedExpression
+ || k === SK.ParenthesizedType
+ || k === SK.ComputedPropertyName
) {
tsForEachChild(node, child => visit(child, parentTarget));
return;
diff --git a/packages/compat-eslint/lib/ts-scope-manager.ts b/packages/compat-eslint/lib/ts-scope-manager.ts
deleted file mode 100644
index 9879f281..00000000
--- a/packages/compat-eslint/lib/ts-scope-manager.ts
+++ /dev/null
@@ -1,2936 +0,0 @@
-// TS-backed reimplementation of @typescript-eslint/scope-manager's surface.
-//
-// Approach: walk the TS AST once at construction time, build a scope tree that
-// matches @typescript-eslint/scope-manager's shape (so ESLint rules see the
-// same structure they would from the upstream analyzer). Variables and refs
-// are populated lazily per scope. The TS type checker is queried lazily for
-// reference resolution.
-//
-// Scope kind dispatch is keyed off TS SyntaxKind directly — not `node.locals`,
-// which only contains a subset of ES bindings (var hoisting + let/const) and
-// would miss block-scopes-without-decls that ESLint expects.
-
-import type { TSESTree } from '@typescript-eslint/types';
-import * as ts from 'typescript';
-import { materialize, NoESTreeCounterpartError } from './lazy-estree';
-
-type AstMaps = {
- esTreeNodeToTSNodeMap: WeakMap;
- tsNodeToESTreeNodeMap: WeakMap;
-};
-
-// ECMAScript built-in globals, vendored from `eslint/conf/globals.js`'s
-// `es2026` (the latest superset). ESLint core registers these via
-// `addDeclaredGlobals` before rules run; the constructor below mirrors
-// that step so `no-undef` doesn't fire on `undefined`, `Math`, `String`,
-// `Array`, etc. — names that `@typescript-eslint/scope-manager`'s lib
-// data marks TYPE-only after merging (`es2015.core` re-declares es5's
-// `TYPE_VALUE Math` as `TYPE`, etc.). Update if ESLint adds a new ES
-// year.
-const ESLINT_BUILTIN_GLOBALS: readonly string[] = [
- 'AggregateError',
- 'Array',
- 'ArrayBuffer',
- 'AsyncDisposableStack',
- 'Atomics',
- 'BigInt',
- 'BigInt64Array',
- 'BigUint64Array',
- 'Boolean',
- 'DataView',
- 'Date',
- 'DisposableStack',
- 'Error',
- 'EvalError',
- 'FinalizationRegistry',
- 'Float16Array',
- 'Float32Array',
- 'Float64Array',
- 'Function',
- 'Infinity',
- 'Int16Array',
- 'Int32Array',
- 'Int8Array',
- 'Intl',
- 'Iterator',
- 'JSON',
- 'Map',
- 'Math',
- 'NaN',
- 'Number',
- 'Object',
- 'Promise',
- 'Proxy',
- 'RangeError',
- 'ReferenceError',
- 'Reflect',
- 'RegExp',
- 'Set',
- 'SharedArrayBuffer',
- 'String',
- 'SuppressedError',
- 'Symbol',
- 'SyntaxError',
- 'Temporal',
- 'TypeError',
- 'URIError',
- 'Uint16Array',
- 'Uint32Array',
- 'Uint8Array',
- 'Uint8ClampedArray',
- 'WeakMap',
- 'WeakRef',
- 'WeakSet',
- 'constructor',
- 'decodeURI',
- 'decodeURIComponent',
- 'encodeURI',
- 'encodeURIComponent',
- 'escape',
- 'eval',
- 'globalThis',
- 'hasOwnProperty',
- 'isFinite',
- 'isNaN',
- 'isPrototypeOf',
- 'parseFloat',
- 'parseInt',
- 'propertyIsEnumerable',
- 'toLocaleString',
- 'toString',
- 'undefined',
- 'unescape',
- 'valueOf',
-];
-
-// TypeScript built-in lib type globals. Vendored union of every TYPE-tagged
-// name across `@typescript-eslint/scope-manager`'s `dist/lib/es*.js`
-// (es5 / es2015–es2025 / esnext, including sub-libs like
-// `es2015.collection`, `es2024.regexp`, etc.). Excludes DOM / decorators /
-// scripthost (environment-specific — opting in would silence undef on
-// browser-only names in pure-Node code, which over-silences). Registering
-// these via `addGlobals` makes type-position references to lib utility
-// types (`Record`, `Promise`, `Awaited`, …) resolve cleanly
-// even when only a partial lib is loaded — without this, our type-position
-// guard had to silence ALL type-position freeRefs, hiding ESLint's
-// no-undef on real undeclared type names (`NodeJS.ErrnoException`,
-// `Visitor`).
-const TS_LIB_TYPE_GLOBALS: readonly string[] = [
- 'AggregateError',
- 'AggregateErrorConstructor',
- 'Array',
- 'ArrayBuffer',
- 'ArrayBufferConstructor',
- 'ArrayBufferLike',
- 'ArrayBufferTypes',
- 'ArrayBufferView',
- 'ArrayConstructor',
- 'ArrayIterator',
- 'ArrayLike',
- 'AsyncDisposable',
- 'AsyncDisposableStack',
- 'AsyncDisposableStackConstructor',
- 'AsyncGenerator',
- 'AsyncGeneratorFunction',
- 'AsyncGeneratorFunctionConstructor',
- 'AsyncIterable',
- 'AsyncIterableIterator',
- 'AsyncIterator',
- 'AsyncIteratorObject',
- 'Atomics',
- 'Awaited',
- 'BigInt',
- 'BigInt64Array',
- 'BigInt64ArrayConstructor',
- 'BigIntConstructor',
- 'BigIntToLocaleStringOptions',
- 'BigUint64Array',
- 'BigUint64ArrayConstructor',
- 'Boolean',
- 'BooleanConstructor',
- 'BuiltinIteratorReturn',
- 'CallableFunction',
- 'Capitalize',
- 'ConcatArray',
- 'ConstructorParameters',
- 'DataView',
- 'DataViewConstructor',
- 'Date',
- 'DateConstructor',
- 'Disposable',
- 'DisposableStack',
- 'DisposableStackConstructor',
- 'Error',
- 'ErrorConstructor',
- 'ErrorOptions',
- 'EvalError',
- 'EvalErrorConstructor',
- 'Exclude',
- 'Extract',
- 'FinalizationRegistry',
- 'FinalizationRegistryConstructor',
- 'FlatArray',
- 'Float16Array',
- 'Float16ArrayConstructor',
- 'Float32Array',
- 'Float32ArrayConstructor',
- 'Float64Array',
- 'Float64ArrayConstructor',
- 'Function',
- 'FunctionConstructor',
- 'Generator',
- 'GeneratorFunction',
- 'GeneratorFunctionConstructor',
- 'IArguments',
- 'ImportAssertions',
- 'ImportAttributes',
- 'ImportCallOptions',
- 'ImportMeta',
- 'InstanceType',
- 'Int16Array',
- 'Int16ArrayConstructor',
- 'Int32Array',
- 'Int32ArrayConstructor',
- 'Int8Array',
- 'Int8ArrayConstructor',
- 'Intl',
- 'Iterable',
- 'IterableIterator',
- 'Iterator',
- 'IteratorObject',
- 'IteratorObjectConstructor',
- 'IteratorResult',
- 'IteratorReturnResult',
- 'IteratorYieldResult',
- 'JSON',
- 'Lowercase',
- 'Map',
- 'MapConstructor',
- 'MapIterator',
- 'Math',
- 'NewableFunction',
- 'NoInfer',
- 'NonNullable',
- 'Number',
- 'NumberConstructor',
- 'Object',
- 'ObjectConstructor',
- 'Omit',
- 'OmitThisParameter',
- 'Parameters',
- 'Partial',
- 'Pick',
- 'Promise',
- 'PromiseConstructor',
- 'PromiseConstructorLike',
- 'PromiseFulfilledResult',
- 'PromiseLike',
- 'PromiseRejectedResult',
- 'PromiseSettledResult',
- 'PromiseWithResolvers',
- 'PropertyDescriptor',
- 'PropertyDescriptorMap',
- 'PropertyKey',
- 'ProxyConstructor',
- 'ProxyHandler',
- 'RangeError',
- 'RangeErrorConstructor',
- 'Readonly',
- 'ReadonlyArray',
- 'ReadonlyMap',
- 'ReadonlySet',
- 'ReadonlySetLike',
- 'Record',
- 'ReferenceError',
- 'ReferenceErrorConstructor',
- 'Reflect',
- 'RegExp',
- 'RegExpConstructor',
- 'RegExpExecArray',
- 'RegExpIndicesArray',
- 'RegExpMatchArray',
- 'RegExpStringIterator',
- 'Required',
- 'ReturnType',
- 'Set',
- 'SetConstructor',
- 'SetIterator',
- 'SharedArrayBuffer',
- 'SharedArrayBufferConstructor',
- 'String',
- 'StringConstructor',
- 'StringIterator',
- 'SuppressedError',
- 'SuppressedErrorConstructor',
- 'Symbol',
- 'SymbolConstructor',
- 'SyntaxError',
- 'SyntaxErrorConstructor',
- 'TemplateStringsArray',
- 'Temporal',
- 'ThisParameterType',
- 'ThisType',
- 'TypeError',
- 'TypeErrorConstructor',
- 'TypedPropertyDescriptor',
- 'URIError',
- 'URIErrorConstructor',
- 'Uint16Array',
- 'Uint16ArrayConstructor',
- 'Uint32Array',
- 'Uint32ArrayConstructor',
- 'Uint8Array',
- 'Uint8ArrayConstructor',
- 'Uint8ClampedArray',
- 'Uint8ClampedArrayConstructor',
- 'Uncapitalize',
- 'Uppercase',
- 'WeakKey',
- 'WeakKeyTypes',
- 'WeakMap',
- 'WeakMapConstructor',
- 'WeakRef',
- 'WeakRefConstructor',
- 'WeakSet',
- 'WeakSetConstructor',
- // Decorator types (from `lib.decorators*.d.ts`). Includes both stage-3
- // (ClassMethodDecoratorContext etc.) and legacy (ClassDecorator,
- // MethodDecorator, PropertyDecorator, ParameterDecorator).
- 'ClassMemberDecoratorContext',
- 'DecoratorContext',
- 'DecoratorMetadataObject',
- 'DecoratorMetadata',
- 'ClassDecoratorContext',
- 'ClassMethodDecoratorContext',
- 'ClassGetterDecoratorContext',
- 'ClassSetterDecoratorContext',
- 'ClassAccessorDecoratorContext',
- 'ClassAccessorDecoratorTarget',
- 'ClassAccessorDecoratorResult',
- 'ClassFieldDecoratorContext',
- 'ClassDecorator',
- 'PropertyDecorator',
- 'MethodDecorator',
- 'ParameterDecorator',
-];
-
-// Lib globals that upstream's `analyze({ lib: ['esnext'] })` exposes as values
-// (i.e. `isValueVariable === true`) after resolving the lib chain. Most lib
-// names are type-only because later libs (e.g. `esnext.collection`) override
-// es5/es2015's TYPE_VALUE entries with TYPE. Captured empirically from a
-// minimal analyze() run; refresh if upstream's lib data changes.
-const LIB_VALUE_GLOBALS = new Set([
- 'AggregateError',
- 'AsyncDisposableStack',
- 'BigInt',
- 'Boolean',
- 'DisposableStack',
- 'EvalError',
- 'FinalizationRegistry',
- 'Float16Array',
- 'Intl',
- 'Iterator',
- 'Object',
- 'RangeError',
- 'ReferenceError',
- 'Reflect',
- 'SuppressedError',
- 'SyntaxError',
- 'Temporal',
- 'TypeError',
- 'URIError',
- 'WeakRef',
-]);
-
-// Scope kinds we tag. Names match upstream casing (e.g. 'tsEnum', 'tsModule').
-type ScopeType =
- | 'global'
- | 'module'
- | 'function'
- | 'block'
- | 'switch'
- | 'for'
- | 'catch'
- | 'with'
- | 'class'
- | 'class-field-initializer'
- | 'function-expression-name'
- | 'tsEnum'
- | 'tsModule'
- | 'type'
- | 'function-type'
- | 'mapped-type'
- | 'conditional-type';
-
-type DefinitionType =
- | 'CatchClause'
- | 'ClassName'
- | 'FunctionName'
- | 'ImplicitGlobalVariable'
- | 'ImportBinding'
- | 'Parameter'
- | 'TDZ'
- | 'Variable'
- | 'Type'
- | 'TSEnumName'
- | 'TSEnumMember'
- | 'TSModuleName';
-
-// _classifyIdentifier dispatch bitmaps. Hot path: ~120k identifiers/file
-// run through this method. Most identifiers (~90%) have a parent.kind that
-// falls through the switch's `default` returning `0b11` (both flags true)
-// — a single Uint8Array lookup short-circuits before entering the switch.
-// A second bitmap covers the large fall-through case body that just does
-// `name === id ? 0 : 0b11` — also collapses out of the switch.
-const _SK_CLASSIFY_BITMAP_SIZE = 400;
-const _CLASSIFY_HANDLED_KINDS = new Uint8Array(_SK_CLASSIFY_BITMAP_SIZE);
-const _CLASSIFY_NAME_SLOT_KINDS = new Uint8Array(_SK_CLASSIFY_BITMAP_SIZE);
-(() => {
- const SK = ts.SyntaxKind;
- // Fall-through "name slot" cases — body is `(p as { name }).name === id ? 0 : 0b11`.
- const nameSlotKinds = [
- SK.PropertyDeclaration,
- SK.PropertySignature,
- SK.PropertyAssignment,
- SK.MethodDeclaration,
- SK.MethodSignature,
- SK.GetAccessor,
- SK.SetAccessor,
- SK.EnumMember,
- SK.FunctionDeclaration,
- SK.FunctionExpression,
- SK.ClassDeclaration,
- SK.ClassExpression,
- SK.EnumDeclaration,
- SK.ModuleDeclaration,
- SK.TypeAliasDeclaration,
- SK.InterfaceDeclaration,
- SK.TypeParameter,
- SK.ImportClause,
- SK.NamespaceImport,
- SK.ImportEqualsDeclaration,
- SK.NamedTupleMember,
- SK.JsxAttribute,
- ];
- for (const k of nameSlotKinds) {
- _CLASSIFY_NAME_SLOT_KINDS[k] = 1;
- _CLASSIFY_HANDLED_KINDS[k] = 1;
- }
- // All other kinds with a dedicated case body in the switch.
- const otherHandled = [
- SK.PropertyAccessExpression,
- SK.QualifiedName,
- SK.LabeledStatement,
- SK.BreakStatement,
- SK.ContinueStatement,
- SK.MetaProperty,
- SK.ImportSpecifier,
- SK.VariableDeclaration,
- SK.BindingElement,
- SK.Parameter,
- SK.ExportSpecifier,
- SK.TypeReference,
- SK.ImportType,
- ];
- for (const k of otherHandled) _CLASSIFY_HANDLED_KINDS[k] = 1;
-})();
-
-// Module-level caches keyed by ts.Program. Lib symbols (`Object`, `Array`,
-// `String` …) are stable per-program — IDEs share one program across all
-// files, so the same set of lib symbols gets queried over and over. Caching
-// per-instance (per-file) means every file rebuilds the same decisions.
-// WeakMap lets entries fall away when a program is GC'd.
-const _libDecisionByProgram = new WeakMap>();
-const _libSourceFileByProgram = new WeakMap>();
-
-function _getLibDecisionMap(program: ts.Program): Map {
- let m = _libDecisionByProgram.get(program);
- if (!m) _libDecisionByProgram.set(program, m = new Map());
- return m;
-}
-function _getLibSourceFileMap(program: ts.Program): WeakMap {
- let m = _libSourceFileByProgram.get(program);
- if (!m) _libSourceFileByProgram.set(program, m = new WeakMap());
- return m;
-}
-
-// Module-level cache of fake `ts.Symbol` placeholders for ESLint declared
-// globals. The shape `{ name, declarations: [], flags: 0 }` is completely
-// stateless — same fake works for every file's `TsScopeManager`. Without
-// this, every `getEstree` would alloc 250+ throwaway plain objects.
-//
-// `_variableBySymbol` is still per-manager, so the wrapper `TsVariable`
-// is per-file (it carries `manager`, `references` are file-local). But
-// the symbol that keys it can — and should — be shared.
-const _SHARED_FAKE_GLOBAL_SYMBOLS = new Map();
-function _sharedFakeGlobalSymbol(name: string): ts.Symbol {
- let s = _SHARED_FAKE_GLOBAL_SYMBOLS.get(name);
- if (!s) {
- s = { name, declarations: [], flags: 0 } as unknown as ts.Symbol;
- _SHARED_FAKE_GLOBAL_SYMBOLS.set(name, s);
- }
- return s;
-}
-
-// Public surface mirrors `@typescript-eslint/scope-manager`'s
-// `ScopeManager` class (`scope-manager/dist/ScopeManager.js`):
-// `scopes`, `globalScope`, `getDeclaredVariables`, `acquire`,
-// plus the boolean accessors (`isGlobalReturn`, `isModule`, …)
-// that ESLint's `Linter` reads at scope-construction time.
-//
-// Construction is intentionally LAZY: the upstream `Referencer`
-// walks the entire AST eagerly to build the scope tree and
-// classify every reference. We split that into a single-pass
-// scope-tree walk (`_buildScopeTree`) plus a deferred reference
-// classification (`_ensureRefIndex`) triggered by the first
-// `scope.through` / `var.references` / `getReferencesFor` read.
-// Most rules only ever read a small subset of refs.
-export class TsScopeManager {
- scopes: TsScope[] = [];
- globalScope!: TsScope;
- moduleScope: TsScope | null = null;
- declaredVariables = new WeakMap();
- nodeToScope = new Map();
- currentScope: TsScope | null = null;
-
- _variableBySymbol = new Map();
- _libVariableBySymbol = new Map();
- _libDecisionBySymbol: Map;
- _libSourceFileCache: WeakMap;
- _syntheticArguments = new Map();
- readonly checker: ts.TypeChecker;
-
- constructor(
- readonly tsFile: ts.SourceFile,
- readonly program: ts.Program,
- readonly estree: TSESTree.Program,
- readonly astMaps: AstMaps,
- readonly sourceType: 'module' | 'script',
- ) {
- this.checker = program.getTypeChecker();
- // Per-program caches — share lib decisions across every file in the
- // same ts.Program (typical IDE setup is one program for the whole
- // project). Per-file managers all hit the same Map / WeakMap.
- this._libDecisionBySymbol = _getLibDecisionMap(program);
- this._libSourceFileCache = _getLibSourceFileMap(program);
- // Build scope tree by walking the TS AST. ESLint built-in globals
- // are NOT injected here — that's a lint-pipeline policy, not a
- // scope-analysis fact. `applyEslintGlobals(manager)` is the
- // pipeline-side hook (see compat-eslint/index.ts).
- this._buildScopeTree();
- }
-
- // Identifiers picked up during the scope-tree walk; classified once after
- // scope variables are populated (so we know which symbols are "ours").
- _pendingIdentifiers: ts.Identifier[] = [];
-
- _buildScopeTree() {
- this.globalScope = new TsScope(this, this.tsFile, 'global', null);
- this._registerScope(this.tsFile, this.globalScope);
-
- let topScope: TsScope = this.globalScope;
- if (this.sourceType === 'module') {
- this.moduleScope = new TsScope(this, this.tsFile, 'module', this.globalScope);
- this._registerScope(this.tsFile, this.moduleScope);
- topScope = this.moduleScope;
- }
- this.currentScope = topScope;
-
- // Single AST walk: build scope tree + collect identifiers for later
- // classification. We thread `currentParent` through a stack-style
- // save/restore around the recursion so `forEachChild` can take a single
- // stable callback (no per-call arrow allocation).
- const SK_Identifier = ts.SyntaxKind.Identifier;
- const SK_ConditionalType = ts.SyntaxKind.ConditionalType;
- const idents = this._pendingIdentifiers;
- let currentParent: TsScope = topScope;
- const walk = (n: ts.Node) => {
- const prevParent = currentParent;
- const created = this._createScopesFor(n, prevParent);
- if (created) currentParent = created;
- if (n.kind === SK_Identifier) idents.push(n as ts.Identifier);
- // Conditional type: only `trueType` lives inside the conditional-type
- // scope (where `infer X` is accessible). `checkType`, `extendsType`,
- // `falseType` walk in the OUTER scope. Without this split, a nested
- // conditional in `falseType` becomes a child of the outer conditional-
- // type scope, and its `infer X` shadows the outer's `infer X`.
- // Repro: `type U = T extends Array ? U : T extends Promise ? U : T;`
- // — no-shadow incorrectly reported the second `infer U`.
- if (n.kind === SK_ConditionalType) {
- const cond = n as ts.ConditionalTypeNode;
- currentParent = prevParent;
- walk(cond.checkType);
- walk(cond.extendsType);
- if (created) currentParent = created;
- walk(cond.trueType);
- currentParent = prevParent;
- walk(cond.falseType);
- }
- else {
- ts.forEachChild(n, walk);
- }
- currentParent = prevParent;
- };
- ts.forEachChild(this.tsFile, walk);
- }
-
- _registerScope(tsNode: ts.Node, scope: TsScope) {
- this.scopes.push(scope);
- let arr = this.nodeToScope.get(tsNode);
- if (!arr) {
- this.nodeToScope.set(tsNode, arr = []);
- }
- arr.push(scope);
- }
-
- // For nodes that are themselves scope-creating, build the appropriate scope
- // (or pair of scopes, for named function expressions). Returns the innermost
- // scope created, so its descendants live in it.
- _createScopesFor(n: ts.Node, parent: TsScope): TsScope | undefined {
- const SK = ts.SyntaxKind;
- // Class field initializer: `class C { f = expr }` — `expr` is in its
- // own scope so `this` and (in the future) other class-field-only
- // constructs resolve correctly. Detected before the kind-switch
- // because the scope's boundary is the initializer node itself, which
- // can be any expression kind.
- const np = n.parent;
- if (
- np
- && np.kind === SK.PropertyDeclaration
- && (np as ts.PropertyDeclaration).initializer === n
- ) {
- return new TsScope(this, n, 'class-field-initializer', parent, true);
- }
- switch (n.kind) {
- case SK.FunctionDeclaration:
- case SK.ArrowFunction:
- case SK.MethodDeclaration:
- case SK.Constructor:
- case SK.GetAccessor:
- case SK.SetAccessor:
- return new TsScope(this, n, 'function', parent, true);
- case SK.FunctionExpression: {
- // Named function expressions get a wrapping
- // `function-expression-name` scope; inner function scope is its
- // child.
- const name = (n as ts.FunctionExpression).name;
- const outer = name
- ? new TsScope(this, n, 'function-expression-name', parent, true)
- : parent;
- return new TsScope(this, n, 'function', outer, true);
- }
- case SK.ClassDeclaration:
- case SK.ClassExpression:
- return new TsScope(this, n, 'class', parent, true);
- case SK.Block: {
- // Block as a function/method body is NOT its own scope; the
- // function scope already covers it. Free-standing blocks are.
- const pk = n.parent?.kind;
- if (
- pk === SK.FunctionDeclaration
- || pk === SK.FunctionExpression
- || pk === SK.ArrowFunction
- || pk === SK.MethodDeclaration
- || pk === SK.Constructor
- || pk === SK.GetAccessor
- || pk === SK.SetAccessor
- ) return undefined;
- return new TsScope(this, n, 'block', parent, true);
- }
- case SK.ForStatement:
- case SK.ForInStatement:
- case SK.ForOfStatement: {
- // Upstream nests a 'for' scope only when the init clause is a
- // let/const declaration (block-scoped binding). For
- // `for (a in xs)` or `for (var x = …)` the loop binding lives
- // in the enclosing scope.
- const init = n.kind === SK.ForStatement
- ? (n as ts.ForStatement).initializer
- : (n as ts.ForInStatement | ts.ForOfStatement).initializer;
- if (
- init
- && init.kind === SK.VariableDeclarationList
- && ((init as ts.VariableDeclarationList).flags
- & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0
- ) {
- return new TsScope(this, n, 'for', parent, true);
- }
- return undefined;
- }
- case SK.CatchClause:
- return new TsScope(this, n, 'catch', parent, true);
- case SK.SwitchStatement: {
- // Use CaseBlock (not SwitchStatement) as the scope's boundary
- // node, so the discriminant `switch (ok)` resolves to the
- // outer scope rather than the switch scope.
- return new TsScope(this, (n as ts.SwitchStatement).caseBlock, 'switch', parent, true);
- }
- case SK.WithStatement: {
- // `with (obj) { stmt }` — `obj` resolves to the outer scope
- // (visited before entering with scope). Use the body statement
- // as the with scope boundary; `block` getter steps back out.
- return new TsScope(this, (n as ts.WithStatement).statement, 'with', parent, true);
- }
- case SK.EnumDeclaration:
- return new TsScope(this, n, 'tsEnum', parent, true);
- case SK.ModuleDeclaration:
- return new TsScope(this, n, 'tsModule', parent, true);
- case SK.InterfaceDeclaration:
- case SK.TypeAliasDeclaration:
- // Upstream only creates a 'type' scope when type parameters are
- // present (since they need to be scoped to this declaration).
- return (n as { typeParameters?: ts.NodeArray }).typeParameters
- ? new TsScope(this, n, 'type', parent, true)
- : undefined;
- case SK.MappedType:
- return new TsScope(this, n, 'mapped-type', parent, true);
- case SK.ConditionalType:
- return new TsScope(this, n, 'conditional-type', parent, true);
- case SK.FunctionType:
- case SK.ConstructorType:
- case SK.CallSignature:
- case SK.ConstructSignature:
- case SK.MethodSignature:
- return new TsScope(this, n, 'function-type', parent, true);
- default:
- return undefined;
- }
- }
-
- _getOrCreateVariable(symbol: ts.Symbol): TsVariable {
- let v = this._variableBySymbol.get(symbol);
- if (!v) {
- v = new TsVariable(this, symbol);
- this._variableBySymbol.set(symbol, v);
- }
- return v;
- }
-
- _collectBinding(binding: ts.BindingName | undefined, out: TsVariable[]) {
- if (!binding) return;
- const ts_ = ts;
- if (ts_.isIdentifier(binding)) {
- const sym = this.checker.getSymbolAtLocation(binding);
- if (sym) out.push(this._getOrCreateVariable(sym));
- return;
- }
- if (ts_.isObjectBindingPattern(binding) || ts_.isArrayBindingPattern(binding)) {
- for (const el of binding.elements) {
- if (ts_.isOmittedExpression(el)) continue;
- this._collectBinding(el.name, out);
- }
- }
- }
-
- // upstream: `@typescript-eslint/scope-manager/dist/ScopeManager.js`
- // `getDeclaredVariables(node)`. Returns every variable declared
- // directly in `node` — for `let [a, b] = ...` that's two vars
- // (`a`, `b`); for `function f(x, { y }) {}` it's three (`f`, `x`,
- // `y`). Critical for rules that walk declarations top-down
- // (prefer-const, no-unused-vars, naming-convention).
- getDeclaredVariables(node: TSESTree.Node): TsVariable[] {
- const tsNode = this.astMaps.esTreeNodeToTSNodeMap.get(node);
- if (!tsNode) return [];
- const out: TsVariable[] = [];
- const ts_ = ts;
- const collect = (decl: ts.Node | undefined) => {
- if (!decl) return;
- // VariableDeclaration / Parameter `.name` may be a binding pattern
- // (`let [a, b] = ...`, `function f({ x, y }) {}`) — symbol lives on
- // each leaf Identifier, not on the pattern container. Walk via
- // `_collectBinding` so destructured bindings are emitted.
- // `prefer-const` and friends call `getDeclaredVariables(letNode)`
- // to enumerate scoped bindings; without this, the rule sees an
- // empty array for any destructuring declaration.
- if (ts_.isVariableDeclaration(decl) || ts_.isParameter(decl)) {
- this._collectBinding(decl.name, out);
- return;
- }
- // CatchClause holds the param via `variableDeclaration` (which is
- // itself a VariableDeclaration with the param identifier or pattern
- // as `.name`). Without this, getDeclaredVariables returns [] for
- // catch clauses and rules like no-ex-assign / no-shadow can't
- // enumerate the catch-bound name. Repro:
- // `catch (e) { e = new Error() }` — no-ex-assign must report.
- if (ts_.isCatchClause(decl)) {
- if (decl.variableDeclaration) {
- this._collectBinding(decl.variableDeclaration.name, out);
- }
- return;
- }
- let sym: ts.Symbol | undefined = (decl as { symbol?: ts.Symbol }).symbol;
- if (!sym) {
- const nameNode = (decl as { name?: ts.Node }).name;
- if (nameNode) sym = this.checker.getSymbolAtLocation(nameNode);
- }
- if (sym) out.push(this._getOrCreateVariable(sym));
- };
- if (ts_.isVariableStatement(tsNode)) {
- for (const d of tsNode.declarationList.declarations) collect(d);
- }
- else if (ts_.isVariableDeclarationList(tsNode)) {
- for (const d of tsNode.declarations) collect(d);
- }
- else if (
- ts_.isVariableDeclaration(tsNode)
- || ts_.isParameter(tsNode)
- || ts_.isFunctionDeclaration(tsNode)
- || ts_.isFunctionExpression(tsNode)
- || ts_.isArrowFunction(tsNode)
- || ts_.isMethodDeclaration(tsNode)
- || ts_.isClassDeclaration(tsNode)
- || ts_.isClassExpression(tsNode)
- || ts_.isImportSpecifier(tsNode)
- || ts_.isImportClause(tsNode)
- || ts_.isNamespaceImport(tsNode)
- || ts_.isImportEqualsDeclaration(tsNode)
- || ts_.isEnumDeclaration(tsNode)
- || ts_.isModuleDeclaration(tsNode)
- || ts_.isTypeAliasDeclaration(tsNode)
- || ts_.isInterfaceDeclaration(tsNode)
- || ts_.isTypeParameterDeclaration(tsNode)
- || ts_.isCatchClause(tsNode)
- ) {
- collect(tsNode);
- }
- // For function-like nodes, also return their parameters.
- if (
- ts_.isFunctionDeclaration(tsNode)
- || ts_.isFunctionExpression(tsNode)
- || ts_.isArrowFunction(tsNode)
- || ts_.isMethodDeclaration(tsNode)
- ) {
- const fn = tsNode as ts.FunctionLikeDeclaration;
- if (fn.parameters) {
- for (const p of fn.parameters) {
- this._collectBinding(p.name, out);
- }
- }
- }
- return out;
- }
-
- acquire(node: TSESTree.Node, inner = false): TsScope | null {
- const tsNode = this.astMaps.esTreeNodeToTSNodeMap.get(node);
- if (!tsNode) return null;
- const arr = this.nodeToScope.get(tsNode);
- if (!arr || arr.length === 0) return null;
- if (arr.length === 1) return arr[0];
- // Multiple scopes share this node (e.g. Program → [global, module] or
- // FunctionExpression → [fn-expr-name, function]).
- const predicate = (s: TsScope) => !(s.type === 'function' && s.functionExpressionScope);
- if (inner) {
- for (let i = arr.length - 1; i >= 0; --i) {
- if (predicate(arr[i])) return arr[i];
- }
- return null;
- }
- return arr.find(predicate) ?? null;
- }
-
- get variables(): TsVariable[] {
- const out: TsVariable[] = [];
- for (const s of this.scopes) out.push(...s.variables);
- return out;
- }
-
- isES6() {
- return true;
- }
- isGlobalReturn() {
- return false;
- }
- isImpliedStrict() {
- return this.sourceType === 'module';
- }
- isModule() {
- return this.sourceType === 'module';
- }
- isStrictModeSupported() {
- return true;
- }
-
- // Fake globals registered by `addGlobals` whose through-ref reconciliation
- // is still pending. Drained when `_ensureRefIndex` actually runs (i.e.
- // when a rule first reads references). Reading `globalScope.variables`
- // directly already sees these — only the `through` -> `references`
- // re-resolve is deferred.
- _pendingFakeGlobals?: Map;
-
- // upstream: `eslint-scope/lib/scope-manager.js` `ScopeManager.addGlobals`,
- // invoked by `eslint/lib/languages/js/source-code/source-code.js`
- // `addDeclaredGlobals` for every name in `conf/globals.js`'s
- // `es${ecmaVersion}` set + user `globals` config. Synthesises a
- // Variable in `globalScope` per name and re-resolves matching
- // through-refs against it. Compat-eslint's entry point calls this
- // with the vendored `ESLINT_BUILTIN_GLOBALS` (es2026) so `no-undef`
- // doesn't fire on `undefined` / `Math` / `Array` / etc.
- //
- // ESLint's contract requires `scopeManager.variables` /
- // `globalScope.set` to immediately reflect an `addGlobals` call (see
- // `eslint-scope/test/add-globals.test.ts`'s "doesn't affect unrelated
- // references" case), so the per-file `TsVariable` is built eagerly.
- // What we DO save:
- // 1. Fake `ts.Symbol` placeholders are module-shared via
- // `_sharedFakeGlobalSymbol(name)` — every file's manager reuses
- // the same plain-object symbol per name. Across the TypeScript
- // repo's 710 files × ~250 declared names, that's ~177k throwaway
- // allocs avoided.
- // 2. The through-ref reconciliation is parked in
- // `_pendingFakeGlobals` and drained at the tail of
- // `_ensureRefIndex`. Rule sets that don't query references
- // (most lint configs without `no-undef` / `no-unused-vars`)
- // skip it entirely.
- addGlobals(names: string[]) {
- // Skip names that already exist as declared globals — upstream's
- // addGlobals is a no-op for already-known names (test name:
- // "doesn't affect already declared global variables").
- const existingNames = new Set();
- for (const v of this.globalScope.variables) {
- existingNames.add(v.name);
- }
- const pending = this._pendingFakeGlobals ??= new Map();
- for (const name of names) {
- if (existingNames.has(name)) continue;
- const fakeSym = _sharedFakeGlobalSymbol(name);
- const v = new TsVariable(this, fakeSym);
- this._variableBySymbol.set(fakeSym, v);
- this.globalScope._addLibVariable(v);
- pending.set(name, v);
- }
- // If the ref index is already built (rare — only when a rule queried
- // references before all `addGlobals` calls finished), reconcile now
- // since the deferred path won't fire again.
- if (this._refIndex) {
- this._reconcileFakeGlobals();
- }
- // Clear implicit-globals cache — added globals supersede the
- // auto-synthesized ones for any matching names.
- this._implicitGlobals = undefined;
- }
-
- // Drain `_pendingFakeGlobals` against the currently-built `_through`,
- // moving every ref whose identifier text matches a registered fake into
- // the per-symbol `_refIndex`. Called either from `addGlobals` (when the
- // ref index is already built) or at the tail of `_ensureRefIndex` (the
- // common path for the lint pipeline).
- _reconcileFakeGlobals() {
- const pending = this._pendingFakeGlobals;
- if (!pending || pending.size === 0) return;
- const through = this._through!;
- const refs = this._refIndex!;
- const remaining: TsReference[] = [];
- for (const ref of through) {
- // Read name from the ts.Identifier directly — `ref.identifier.name`
- // would trigger `tsToEstreeOrStub` → `materialize`, which walks
- // up the parent chain and may eagerly construct wrapper-class
- // ESTree nodes (ChainExpression, ExportNamedDeclaration). ts-ast-
- // scan's later traversal then sees those wrappers in the lazy
- // cache and dispatches enter/leave on them out of source order
- // — desyncing CPA's choice-context stack and crashing
- // `popChoiceContext` on null. Reading `tsIdentifier.text` is
- // pure and avoids the eager materialize.
- const name = ref.tsIdentifier.text;
- const added = pending.get(name);
- if (!added) {
- remaining.push(ref);
- continue;
- }
- // Re-point the ref's symbol so resolved/getReferencesFor work.
- (ref as { symbol: ts.Symbol }).symbol = added.symbol;
- let arr = refs.get(added.symbol);
- if (!arr) refs.set(added.symbol, arr = []);
- arr.push(ref);
- }
- this._through = remaining;
- this._pendingFakeGlobals = undefined;
- }
-
- // upstream equivalent: `@typescript-eslint/scope-manager/dist/referencer/Referencer.js`
- // running through every node and calling `currentScope().referenceValue(...)` /
- // `referenceType(...)`. Upstream is eager — the entire reference graph
- // is built during scope-tree construction. We split:
- // `_buildScopeTree` (constructor) — scopes + parented walk + collects
- // `_pendingIdentifiers`.
- // `_ensureRefIndex` (this method, lazy) — classifies each pending
- // Identifier into resolved references vs `_through` (escapes file).
- // Triggered on first `scope.through` / `scope.references` /
- // `var.references` / `getReferencesFor` access. Most rules read
- // nothing from this graph, paying nothing.
- _through?: TsReference[];
- _refIndex?: Map;
- _referencesByScope?: Map;
- _ensureRefIndex(): Map {
- if (this._refIndex) return this._refIndex;
- // Reference classification needs `_variableBySymbol` populated for
- // every scope so it can distinguish file-local symbols (resolved)
- // from cross-file globals (unresolved → goes to `through`). Walking
- // `s.variables` for each scope is what fills the map. Used to be
- // done eagerly in `_buildScopeTree`, but rule sets that never query
- // references (e.g. eqeqeq + no-var + no-bitwise) don't need this —
- // defer until the first ref query forces it. addGlobals reads
- // `globalScope.variables` explicitly, so the global scope still
- // gets populated at the right time.
- for (const s of this.scopes) {
- void s.variables;
- }
- const refs = new Map();
- const through: TsReference[] = [];
- const byScope = new Map();
- const recordScope = (ref: TsReference) => {
- const s = ref.from;
- let arr = byScope.get(s);
- if (!arr) byScope.set(s, arr = []);
- arr.push(ref);
- };
- const checker = this.checker;
- const SK = ts.SyntaxKind;
- const variableBySymbol = this._variableBySymbol;
- // Iterate identifiers gathered during the scope-tree walk — no second
- // AST traversal.
- const idents = this._pendingIdentifiers;
- for (let i = 0; i < idents.length; i++) {
- const node = idents[i];
- // Cheap filter first: many identifiers (property names, label
- // targets, decl names) are neither reference-eligible nor free —
- // skip them before triggering the (expensive) symbol resolver.
- // One switch over `parent.kind` produces both flags as a 2-bit
- // bitmap (bit 0 = freeRef, bit 1 = refUsage); 0 means skip.
- const flags = this._classifyIdentifier(node);
- if (flags === 0) continue;
- const refUsage = (flags & 0b10) !== 0;
- const freeRef = (flags & 0b01) !== 0;
-
- // `arguments` inside a non-arrow function resolves to the
- // function's synthetic arguments TsVariable. TS's checker gives
- // each function a separate `arguments` symbol; register the link
- // the first time we see it.
- if (node.text === 'arguments') {
- for (let cur: ts.Node | undefined = node.parent; cur; cur = cur.parent) {
- if (
- cur.kind === SK.FunctionDeclaration
- || cur.kind === SK.FunctionExpression
- || cur.kind === SK.MethodDeclaration
- || cur.kind === SK.Constructor
- || cur.kind === SK.GetAccessor
- || cur.kind === SK.SetAccessor
- ) {
- const arr = this.nodeToScope.get(cur);
- const fnScope = arr?.find(s => s.type === 'function');
- if (fnScope) {
- const argsVar = fnScope._getOrCreateArgumentsVar();
- const sym = checker.getSymbolAtLocation(node);
- if (sym && !variableBySymbol.has(sym)) {
- variableBySymbol.set(sym, argsVar);
- }
- }
- break;
- }
- }
- }
-
- // Symbol resolution. For binding identifiers (the `x` in `let x = …`,
- // destructured `{ x }`, `for (const x of …)`), the symbol is already
- // on the parent declaration node (set by the TS binder) — read it
- // directly instead of paying for a checker.getSymbolAtLocation walk.
- const parent = node.parent;
- let sym: ts.Symbol | undefined;
- const pk = parent?.kind;
- if (
- pk === SK.VariableDeclaration && (parent as ts.VariableDeclaration).name === node
- || pk === SK.BindingElement && (parent as ts.BindingElement).name === node
- ) {
- sym = (parent as { symbol?: ts.Symbol }).symbol;
- }
- if (!sym) {
- sym = checker.getSymbolAtLocation(node);
- }
- // `{ x }` shorthand: getSymbolAtLocation returns the property
- // symbol; we want the binding `x` resolves to.
- if (parent && pk === SK.ShorthandPropertyAssignment && (parent as ts.ShorthandPropertyAssignment).name === node) {
- const valSym = checker.getShorthandAssignmentValueSymbol(parent);
- if (valSym) sym = valSym;
- }
- // `export { x }` (no `from`): TS produces an alias Symbol whose
- // declaration is the ExportSpecifier itself — distinct from the
- // LOCAL symbol the import / let / fn declared. `getSymbolAtLocation`
- // returns the alias; the rule needs the local target so the ref
- // resolves into module scope. `getExportSpecifierLocalTargetSymbol`
- // follows the alias to the original local. Without this, every
- // `export { foo }` re-export reports `foo` as undefined.
- if (parent && pk === SK.ExportSpecifier && (parent as ts.ExportSpecifier).name === node) {
- const exportDecl = parent.parent.parent as ts.ExportDeclaration;
- if (!exportDecl.moduleSpecifier) {
- const localSym = checker.getExportSpecifierLocalTargetSymbol(parent as ts.ExportSpecifier);
- if (localSym) sym = localSym;
- }
- }
- if (sym && variableBySymbol.has(sym)) {
- // Resolved reference — add to per-symbol index (only when this
- // position counts as a reference, e.g. usage or init).
- if (refUsage) {
- // Key the bucket by the TsVariable's canonical symbol — for
- // most cases that's the same as `sym`, but synthetic
- // `arguments` is built before any reference is seen and
- // uses a symbol from `getSymbolsInScope`, while
- // `getSymbolAtLocation` on an actual `arguments`
- // identifier returns a function-scoped synthetic symbol
- // (the implicit `arguments` per non-arrow function). Both
- // alias to the same TsVariable via `variableBySymbol`,
- // but keying refs by `sym` would split them across two
- // buckets — `getReferencesFor(argsVar.symbol)` then
- // returns empty and prefer-rest-params reports nothing.
- const v = variableBySymbol.get(sym)!;
- const key = v.symbol;
- let arr = refs.get(key);
- if (!arr) refs.set(key, arr = []);
- {
- const ref = new TsReference(this, node, sym);
- arr.push(ref);
- recordScope(ref);
- }
- }
- }
- else if (sym) {
- // Lib-global decision is cached per symbol: check the cache
- // before re-walking the symbol's declarations through
- // `_isLibGlobalSymbol`. `null` in the cache means
- // "checked, not a lib global".
- let cached = this._libDecisionBySymbol.get(sym);
- if (cached === undefined) {
- cached = this._isLibGlobalSymbol(sym) ? 1 : 0;
- this._libDecisionBySymbol.set(sym, cached);
- }
- if (cached === 1) {
- const isValue = LIB_VALUE_GLOBALS.has(sym.name);
- const inTypePosition = !this._isValueReferencePosition(node);
- if (inTypePosition || isValue) {
- let v = this._libVariableBySymbol.get(sym);
- if (!v) {
- // `addGlobals` (TS_LIB_TYPE_GLOBALS /
- // ESLINT_BUILTIN_GLOBALS) may have already
- // registered a fake-symbol var for this name.
- // Reuse it instead of creating a fresh
- // TsVariable so globalScope.variables stays
- // stable: the `scope.through` getter snapshots
- // localVars BEFORE triggering the ref-index
- // build, so a fresh var added during the build
- // isn't in the snapshot and `isLocal(realSym)`
- // returns false, leaking the ref into
- // `through`. Reusing the fake var maps the real
- // ts.Symbol → fake TsVariable, which IS in the
- // snapshot. Without this, no-undef false-fires
- // on Record / Pick / Extract / etc. used in
- // type-arg positions across module imports.
- // Force `set` getter to populate the name-keyed
- // lookup map (lazily built — _set is undefined
- // until first access). addGlobals's
- // `_addLibVariable` only updates _set when it
- // already exists; rebuilding from variables is
- // a one-time O(N) cost amortized across the
- // entire ref scan.
- const existing = this.globalScope.set.get(sym.name);
- if (existing) {
- v = existing;
- }
- else {
- v = new TsVariable(this, sym);
- this.globalScope._addLibVariable(v);
- }
- this._libVariableBySymbol.set(sym, v);
- // Also register in `_variableBySymbol` so
- // `scope.through`'s alias-aware lookup finds the
- // var by ref.symbol. Without this, lib vars (Map,
- // Set, ...) don't show up as local in
- // globalScope and refs escape → no-undef false
- // positives.
- variableBySymbol.set(sym, v);
- }
- // Key the ref bucket by `v.symbol`, NOT `sym`. When the
- // reused var is a fake-symbol global (the existing-var
- // branch above), `v.symbol === fakeSym` while `sym ===
- // realSym`. The follow-up branch
- // (`variableBySymbol.has(sym)` → refUsage path) ALSO
- // keys by `v.symbol`. Mismatching keys here would
- // scatter refs across two buckets and `getReferencesFor
- // (v.symbol)` would silently drop the first hit. Same
- // reasoning as the synthetic `arguments` aliasing
- // case in the upper branch.
- const key = v.symbol;
- let arr = refs.get(key);
- if (!arr) refs.set(key, arr = []);
- {
- const ref = new TsReference(this, node, sym);
- arr.push(ref);
- recordScope(ref);
- }
- }
- else if (freeRef) {
- // Value ref to a type-only lib global (e.g. Set, Map,
- // Iterable in expression position) — upstream treats
- // as undefined. Match.
- {
- const ref = new TsReference(this, node, sym);
- through.push(ref);
- recordScope(ref);
- }
- }
- }
- else if (freeRef) {
- // Symbol present but unknown to lib globals. The compat
- // entry point pre-registers TS lib type names
- // (Record / Promise / Awaited / …) via `addGlobals`,
- // so any name that resolves cleanly with @typescript-
- // eslint scope-manager already lands in
- // `_variableBySymbol` after this point. Whatever falls
- // through here is a genuinely unresolved value- or
- // type-position reference — fire it (matches plain ESLint
- // no-undef on `NodeJS.ErrnoException` / `Visitor` / typo
- // names that aren't in the registered lib type set).
- {
- const ref = new TsReference(this, node, sym);
- through.push(ref);
- recordScope(ref);
- }
- }
- }
- else if (freeRef) {
- // Unresolved (no symbol). Through-only.
- const ref = new TsReference(this, node, undefined as any);
- through.push(ref);
- recordScope(ref);
- }
- }
- // Free the identifier list — won't be needed again.
- this._pendingIdentifiers = [];
- this._through = through;
- this._referencesByScope = byScope;
- this._refIndex = refs;
- // Drain any `addGlobals` calls that landed before the index was
- // built — `_through` now exists, so we can move matching refs
- // across in one pass instead of forcing the index per `addGlobals`.
- this._reconcileFakeGlobals();
- return this._refIndex;
- }
-
- getReferencesFor(symbol: ts.Symbol): TsReference[] {
- return this._ensureRefIndex().get(symbol) ?? [];
- }
-
- getThroughReferences(): TsReference[] {
- this._ensureRefIndex();
- return this._through!;
- }
-
- // Implicit globals: synthesized variables for unresolved write references
- // (e.g. `x = 1;` where `x` isn't declared) that ESLint's `no-undef`
- // alternatives might want to model. One Variable per unique name; each
- // gets a synthetic ImplicitGlobalVariable def + the first write's
- // identifier.
- _implicitGlobals?: TsVariable[];
- getImplicitGlobals(): TsVariable[] {
- if (this._implicitGlobals) return this._implicitGlobals;
- const through = this.globalScope.through;
- const out: TsVariable[] = [];
- const seen = new Set();
- for (const ref of through) {
- if (!ref.isWrite()) continue;
- const name = ref.identifier.name;
- if (seen.has(name)) continue;
- seen.add(name);
- const fakeSym = { name, declarations: [], flags: 0 } as unknown as ts.Symbol;
- const v = new TsVariable(this, fakeSym);
- // Override defs / identifiers via instance-level override so the
- // implicit global has a believable single ImplicitGlobalVariable
- // def pointing at the write reference's identifier.
- (v as TsVariable & { _defsOverride?: TsDefinition[] })._defsOverride = [
- new TsImplicitGlobalDefinition(this, v, ref.tsIdentifier),
- ];
- (v as TsVariable & { _identifiersOverride?: TSESTree.Identifier[] })._identifiersOverride = [ref.identifier];
- out.push(v);
- }
- return this._implicitGlobals = out;
- }
-
- // upstream equivalent: spread across `@typescript-eslint/scope-manager/dist/
- // referencer/{Referencer,TypeVisitor,ExportVisitor,...}.js`. Each
- // upstream visitor decides whether to emit `referenceValue`,
- // `referenceType`, or skip (e.g. TypeVisitor.TSImportType skips the
- // qualifier; PropertyAccessExpression visitor only descends into
- // `.object`, never `.property`).
- //
- // We collapse those visitor decisions into one branchy switch
- // keyed by `parent.kind`, returning a 2-bit bitmap:
- // bit 0 (FREE_REF=1): identifier is in a reference position (NOT a
- // declaration name, property access RHS, label, etc.)
- // bit 1 (REF_USAGE=2): identifier should produce an ESLint Reference —
- // either a usage (free position) or a declaration with init/iter
- // binding that counts as an init Reference.
- //
- // Adding a new case here requires reading the corresponding
- // upstream visitor to understand whether the identifier should be
- // classified as a value reference, type reference, or skipped.
- //
- // Hot-path layout:
- // 1. parent.kind not in `_CLASSIFY_HANDLED_KINDS` → return 0b11 (most
- // identifiers, e.g. inside expression positions).
- // 2. parent.kind in `_CLASSIFY_NAME_SLOT_KINDS` → fall-through "name
- // slot" body shared by ~22 declaration-shaped kinds.
- // 3. otherwise enter the switch for one of ~10 special-shape cases.
- _classifyIdentifier(id: ts.Identifier): number {
- const p = id.parent;
- if (!p) return 0b11;
- const k = p.kind;
- if (!_CLASSIFY_HANDLED_KINDS[k]) return 0b11;
- if (_CLASSIFY_NAME_SLOT_KINDS[k]) {
- return (p as { name?: ts.Node }).name === id ? 0 : 0b11;
- }
- const SK = ts.SyntaxKind;
- switch (k) {
- // Single-slot kinds — each consults its own slot field.
- case SK.PropertyAccessExpression:
- return (p as ts.PropertyAccessExpression).name === id ? 0 : 0b11;
- case SK.QualifiedName: {
- if ((p as ts.QualifiedName).right === id) return 0;
- // `import("mod").A.B.C` — every identifier in the qualifier
- // chain references an export of the imported module, NOT a
- // local. Walk up through QualifiedName ancestors; if we hit
- // an ImportType, skip the whole chain (matches upstream's
- // `TypeVisitor.TSImportType` skipping the qualifier).
- let cur: ts.Node | undefined = p;
- while (cur && cur.kind === SK.QualifiedName) cur = cur.parent;
- if (cur && cur.kind === SK.ImportType) return 0;
- return 0b11;
- }
- case SK.LabeledStatement:
- return (p as ts.LabeledStatement).label === id ? 0 : 0b11;
- case SK.BreakStatement:
- case SK.ContinueStatement:
- return (p as ts.BreakStatement | ts.ContinueStatement).label === id ? 0 : 0b11;
- case SK.MetaProperty:
- // `new.target` / `import.meta` — `target` / `meta` are syntactic
- // markers, not real references.
- return (p as ts.MetaProperty).name === id ? 0 : 0b11;
- case SK.ImportSpecifier: {
- // Import binding — the names are declarations, not references.
- const e = p as ts.ImportSpecifier;
- return (e.name === id || e.propertyName === id) ? 0 : 0b11;
- }
-
- // Declaration name slot but with init/iter rules — name slot is
- // not free, but may still produce an init Reference.
- case SK.VariableDeclaration: {
- const v = p as ts.VariableDeclaration;
- if (v.name !== id) return 0b11;
- // In the name slot: not free; refUsage if initializer or
- // for-of/in binding.
- if (v.initializer !== undefined) return 0b10;
- const list = v.parent;
- if (list && list.kind === SK.VariableDeclarationList) {
- const stmt = list.parent;
- if (stmt && (stmt.kind === SK.ForOfStatement || stmt.kind === SK.ForInStatement)) return 0b10;
- }
- return 0;
- }
- case SK.BindingElement: {
- const e = p as ts.BindingElement;
- if (e.name !== id && e.propertyName !== id) return 0b11;
- // In a binding name slot: not free. refUsage walks up to the
- // owning VariableDeclaration / Parameter to inherit init/iter
- // semantics.
- for (let cur: ts.Node | undefined = p; cur; cur = cur.parent) {
- if (cur.kind === SK.VariableDeclaration) {
- const v = cur as ts.VariableDeclaration;
- if (v.initializer !== undefined) return 0b10;
- const list = v.parent;
- if (list && list.kind === SK.VariableDeclarationList) {
- const stmt = list.parent;
- if (stmt && (stmt.kind === SK.ForOfStatement || stmt.kind === SK.ForInStatement)) return 0b10;
- }
- return 0;
- }
- if (cur.kind === SK.Parameter) {
- // `function f([a = 0] = [])` — outer Parameter init counts.
- return (cur as ts.ParameterDeclaration).initializer !== undefined ? 0b10 : 0;
- }
- }
- return 0;
- }
- case SK.Parameter: {
- const param = p as ts.ParameterDeclaration;
- if (param.name !== id) return 0b11;
- // In the name slot: not free; refUsage iff initializer.
- return param.initializer !== undefined ? 0b10 : 0;
- }
-
- case SK.ExportSpecifier: {
- // `export {x} from "mod";` — re-export, no local reference.
- const e = p as ts.ExportSpecifier;
- const decl = e.parent.parent;
- if (decl.moduleSpecifier) return 0;
- // `export {x}` — `x` references the local. `export {x as v}` —
- // `x` (propertyName) references the local; `v` (name) is the
- // public export name (not a local reference).
- if (e.propertyName) return e.name === id ? 0 : 0b11;
- return 0b11;
- }
-
- case SK.TypeReference:
- // `expr as const` — `const` is a syntactic marker. _isFreeReference
- // returned false; _isReferenceableUsage left it on the default
- // `return true` path → REF_USAGE only.
- if (
- id.text === 'const'
- && (p as ts.TypeReferenceNode).typeName === id
- && p.parent
- && (p.parent.kind === SK.AsExpression || p.parent.kind === SK.TypeAssertionExpression)
- && (p.parent as ts.AsExpression | ts.TypeAssertion).type === p
- ) return 0b10;
- return 0b11;
-
- case SK.ImportType:
- // `import("module").Foo` — `Foo` is the qualifier of an
- // EntityName referencing an export of the imported module,
- // NOT a local reference. Upstream's TypeVisitor explicitly
- // skips visiting the qualifier (see
- // `@typescript-eslint/scope-manager/.../TypeVisitor.ts`
- // `TSImportType`). Without this skip, the qualifier ends up
- // in `globalScope.through` → `no-undef` reports it.
- if ((p as ts.ImportTypeNode).qualifier === id) return 0;
- return 0b11;
-
- default:
- return 0b11;
- }
- }
-
- // Is this Identifier in a value position (not a type)? Used to decide
- // whether an unresolved free reference becomes a `no-undef`-eligible
- // through-reference.
- _isValueReferencePosition(id: ts.Identifier): boolean {
- const ts_ = ts;
- const SK = ts_.SyntaxKind;
- for (let cur: ts.Node | undefined = id.parent; cur; cur = cur.parent) {
- // `class Foo extends Base` — Base is a value reference (the
- // superclass must be a constructor function). The wrapping
- // ExpressionWithTypeArguments returns true from BOTH ts.isTypeNode
- // AND ts.isExpression, so the generic isTypeNode-first check
- // would short-circuit this position as "type only" and drop the
- // reference. `implements I` (class) and `extends I` (interface)
- // stay type-only and fall through to the generic logic.
- if (cur.kind === SK.ExpressionWithTypeArguments) {
- const hc = cur.parent;
- if (
- hc?.kind === SK.HeritageClause
- && (hc as ts.HeritageClause).token === SK.ExtendsKeyword
- && hc.parent
- && (hc.parent.kind === SK.ClassDeclaration || hc.parent.kind === SK.ClassExpression)
- ) {
- return true;
- }
- }
- if (ts_.isTypeNode(cur)) return false;
- if (ts_.isExpression(cur) || ts_.isStatement(cur)) return true;
- }
- return true;
- }
-
- // True if this symbol is declared exclusively in a TS default library
- // (`lib.es*.d.ts`). The caller decides whether the reference resolves —
- // type-position refs always do; value-position refs only do for the names
- // upstream marks as `isValueVariable: true` (LIB_VALUE_GLOBALS).
- //
- // `decl.getSourceFile()` walks `parent` to the SourceFile (5–20 hops), and
- // `program.isSourceFileDefaultLibrary` does its own path check. Lib symbols
- // commonly have multiple decls all landing in the same `lib.*.d.ts` file —
- // cache by SourceFile so repeat lookups within a symbol (and across symbols
- // within the same program) skip the rechecks.
- _isLibGlobalSymbol(sym: ts.Symbol): boolean {
- const decls = sym.declarations;
- if (!decls || decls.length === 0) return false;
- const sfCache = this._libSourceFileCache;
- for (const d of decls) {
- const sf = d.getSourceFile();
- let isLib = sfCache.get(sf);
- if (isLib === undefined) {
- isLib = this.program.isSourceFileDefaultLibrary(sf);
- sfCache.set(sf, isLib);
- }
- if (!isLib) return false;
- }
- return true;
- }
-
- tsToEstree(tsNode: ts.Node): T | undefined {
- return this.astMaps.tsNodeToESTreeNodeMap.get(tsNode) as T | undefined;
- }
-
- // Compat-eslint specific (no upstream equivalent — typescript-estree
- // converts the WHOLE program eagerly, so every ts.Node has its
- // ESTree counterpart from parse time). This is the bridge from
- // scope-manager's TS-keyed state to the lazy ESTree shim. Every
- // `def.node` / `def.parent` / `var.identifiers[]` / `ref.identifier`
- // getter reads through here. Returns undefined for kinds with no
- // ESTree counterpart (tokens, JSDoc, certain TS-internal containers
- // — see `hasNoEstreeCounterpart` in lazy-estree.ts), so callers can
- // distinguish "TS node exists but has no ESTree shape" from "TS node
- // is missing". Unknown future kinds still go through GenericTSNode
- // safety net (with `_parent` non-enumerable defense preventing the
- // VisitorBase recursion that previously stack-overflowed).
- tsToEstreeOrStub(tsNode: ts.Node | undefined): T | undefined {
- if (!tsNode) return undefined;
- const real = this.astMaps.tsNodeToESTreeNodeMap.get(tsNode) as T | undefined;
- if (real) return real;
- try {
- return materialize(tsNode, {
- ast: this.tsFile,
- maps: this.astMaps,
- }) as unknown as T;
- }
- catch (e) {
- if (e instanceof NoESTreeCounterpartError) return undefined;
- throw e;
- }
- }
-}
-
-// Public surface mirrors `@typescript-eslint/scope-manager/dist/scope/`'s
-// per-type Scope classes (BlockScope, FunctionScope, GlobalScope, …).
-// We collapse them into one class with a `type` discriminator.
-//
-// Rule-facing getters: `block`, `set`, `variables`, `references`,
-// `through`, `childScopes`, `upper`, `variableScope`, `isStrict`,
-// `functionExpressionScope`, `implicit`. Each has a corresponding
-// upstream method/getter on ScopeBase.js or one of its subclasses.
-export class TsScope {
- _variables?: TsVariable[];
- _childScopes: TsScope[] = [];
- _set?: Map;
- upper: TsScope | null;
-
- constructor(
- readonly manager: TsScopeManager,
- readonly tsNode: ts.Node,
- readonly type: ScopeType,
- upper: TsScope | null,
- register = false,
- ) {
- this.upper = upper;
- if (upper) upper._childScopes.push(this);
- if (register) manager._registerScope(tsNode, this);
- }
-
- // upstream: `eslint-scope/lib/scope.js` `Scope.block` — the AST
- // node that owns this scope (FunctionDeclaration / Program /
- // BlockStatement / etc.). Rules use `scope.block === node` to
- // match a listener firing to the scope it should enter
- // (no-redeclare's `checkForBlock`, no-shadow's stack walk).
- get block(): TSESTree.Node | undefined {
- const SK = ts.SyntaxKind;
- const k = this.tsNode.kind;
- // For class methods (Constructor / MethodDeclaration / accessors), TS
- // has a single node but ESTree splits into MethodDefinition + nested
- // FunctionExpression. ESLint's scope.block points at the
- // FunctionExpression — extract it from the MethodDefinition.value slot.
- if (
- k === SK.Constructor
- || k === SK.MethodDeclaration
- || k === SK.GetAccessor
- || k === SK.SetAccessor
- ) {
- const md = this.manager.tsToEstreeOrStub(this.tsNode) as
- | { value?: TSESTree.Node }
- | undefined;
- if (md?.value) return md.value;
- }
- // Switch scope's tsNode is the CaseBlock (boundary for `from`). ESLint
- // expects scope.block === SwitchStatement, so step out one level.
- // Same trick for with scope (tsNode is the body statement).
- if (k === SK.CaseBlock && this.tsNode.parent) {
- return this.manager.tsToEstreeOrStub(this.tsNode.parent);
- }
- if (this.type === 'with' && this.tsNode.parent) {
- return this.manager.tsToEstreeOrStub(this.tsNode.parent);
- }
- const result = this.manager.tsToEstreeOrStub(this.tsNode);
- // `export function f` / `export default function f` / `export class C`:
- // materialize wraps the inner declaration in
- // ExportNamedDeclaration / ExportDefaultDeclaration. ESLint listens
- // on the inner FunctionDeclaration / ClassDeclaration and tests
- // `scope.block === node` to decide whether to enter the scope —
- // so scope.block must point at the inner, not the wrapper.
- // Unwrap when this scope's tsNode is the inner declaration but
- // materialize yielded the wrapper.
- if (
- result && (
- (result as { type?: string }).type === 'ExportNamedDeclaration'
- || (result as { type?: string }).type === 'ExportDefaultDeclaration'
- )
- ) {
- const inner = (result as { declaration?: TSESTree.Node }).declaration;
- if (inner) return inner;
- }
- return result;
- }
-
- get variableScope(): TsScope {
- // Nearest enclosing scope that owns `var`-style declarations.
- // Mirrors upstream `VARIABLE_SCOPE_TYPES` (scope-manager
- // `ScopeBase.js`): function / global / module / tsModule /
- // class-field-initializer (and class-static-block, which we
- // don't tag yet). NOT `class` — classes are NOT variable
- // scopes (an upstream test asserts this).
- // Without `tsModule`, `markDeclarationChildAsUsed` in
- // no-unused-vars walks past the `declare global { var FOO }` /
- // `declare module 'X' { var BAR }` body looking for `FOO`/`BAR`
- // in the global scope's set, misses, and leaves them flagged
- // as unused (4 false fires across prisma's runtime/* declare-
- // global blocks).
- let s: TsScope | null = this;
- while (
- s
- && s.type !== 'function'
- && s.type !== 'global'
- && s.type !== 'module'
- && s.type !== 'tsModule'
- && s.type !== 'class-field-initializer'
- ) {
- s = s.upper;
- }
- return s ?? this.manager.globalScope;
- }
-
- get functionExpressionScope(): boolean {
- return this.type === 'function-expression-name';
- }
-
- get isStrict(): boolean {
- // Modules are implicitly strict.
- if (this.manager.isModule()) return true;
- // Class bodies are always strict.
- if (this.type === 'class') return true;
- const SK = ts.SyntaxKind;
- const hasUseStrictDirective = (statements: ReadonlyArray): boolean => {
- for (const stmt of statements) {
- if (
- stmt.kind === SK.ExpressionStatement
- && (stmt as ts.ExpressionStatement).expression.kind === SK.StringLiteral
- && ((stmt as ts.ExpressionStatement).expression as ts.StringLiteral).text === 'use strict'
- ) return true;
- break; // directive prologue only — first non-directive stops the search.
- }
- return false;
- };
- // SourceFile (script with `'use strict'` at top): propagates.
- if (this.type === 'global') {
- if (hasUseStrictDirective((this.tsNode as ts.SourceFile).statements)) return true;
- }
- // Function with 'use strict' directive in body.
- if (this.type === 'function') {
- const fn = this.tsNode as ts.FunctionLikeDeclaration;
- const body = (fn as { body?: ts.Node }).body;
- if (body && body.kind === SK.Block && hasUseStrictDirective((body as ts.Block).statements)) {
- return true;
- }
- }
- // Inherit from upper scope.
- return this.upper?.isStrict ?? false;
- }
-
- get variables(): TsVariable[] {
- if (!this._variables) {
- this._variables = this._collectVariables();
- }
- return this._variables;
- }
-
- // Inject a synthesized lib global (Object, Array, etc.) discovered during
- // reference-index walk. Only globalScope receives these.
- _addLibVariable(v: TsVariable) {
- void this.variables; // ensure populated
- if (!this._variables!.includes(v)) {
- this._variables!.push(v);
- this._set?.set(v.name, v);
- }
- }
-
- _collectVariables(): TsVariable[] {
- const ts_ = ts;
- const SK = ts_.SyntaxKind;
- const out: TsVariable[] = [];
- const seen = new Set();
- const seenNames = new Set();
- const push = (sym: ts.Symbol | undefined) => {
- if (!sym || seen.has(sym)) return;
- seen.add(sym);
- // Dedupe by name: TS sometimes produces multiple symbols for the
- // same lexical binding. Two cases:
- // 1. Same declaration seen as two symbols (export alias + local
- // symbol, parameter property + class member). These share at
- // least one declaration node → just register the symbol alias
- // for reference resolution, don't add extra defs.
- // 2. Genuine redeclarations with different symbols and different
- // declaration nodes: `function foo` + `var foo`, duplicate
- // `class X`, `enum C` + `interface C`. Merge so no-redeclare
- // sees all identifiers.
- if (seenNames.has(sym.name)) {
- const existing = out.find(v => v.name === sym.name);
- if (existing) {
- // Check if the new symbol has any declaration nodes NOT
- // already covered by the existing variable's symbol(s).
- const existingDecls = new Set();
- if (existing.symbol.declarations) {
- for (const d of existing.symbol.declarations) existingDecls.add(d);
- }
- if (existing._mergedSymbols) {
- for (const ms of existing._mergedSymbols) {
- if (ms.declarations) { for (const d of ms.declarations) existingDecls.add(d); }
- }
- }
- const newDecls = sym.declarations ?? [];
- const hasNewDecl = newDecls.some(d => !existingDecls.has(d));
- if (hasNewDecl) {
- // Genuine redeclaration — merge.
- (existing._mergedSymbols ??= []).push(sym);
- }
- // Always register for reference resolution.
- this.manager._variableBySymbol.set(sym, existing);
- }
- return;
- }
- seenNames.add(sym.name);
- out.push(this.manager._getOrCreateVariable(sym));
- };
- const symOf = (decl: ts.Node | undefined): ts.Symbol | undefined => {
- if (!decl) return undefined;
- // Prefer `checker.getSymbolAtLocation(name)` over `decl.symbol`
- // so declaration registration uses the SAME symbol identity
- // reference resolution returns. TS merges symbols across:
- // - `.d.ts` redeclarations (lib types declared in both
- // bundled `lib.*.d.ts` and user-loaded `src/lib/*.d.ts`),
- // - `declare module "X" { ... }` augmentations referencing
- // imports from the host file's outer module scope,
- // - sibling declaration-merging (interface + namespace,
- // function + namespace).
- // `decl.symbol` returns the local file/declaration symbol;
- // `getSymbolAtLocation(name)` returns the merged symbol that
- // `variableBySymbol.get(refSym)` will probe at use sites.
- // Falling back to `decl.symbol` covers the rare cases where
- // the name node is missing (e.g. anonymous default exports).
- const nameNode = (decl as { name?: ts.Node }).name;
- if (nameNode) {
- const sym = this.manager.checker.getSymbolAtLocation(nameNode);
- if (sym) return sym;
- }
- return (decl as { symbol?: ts.Symbol }).symbol;
- };
- // Walk the binding pattern of a declarator/parameter, pushing every
- // identifier's symbol into `out`.
- const pushBinding = (binding: ts.BindingName | undefined) => {
- if (!binding) return;
- if (ts_.isIdentifier(binding)) {
- push(this.manager.checker.getSymbolAtLocation(binding));
- return;
- }
- if (ts_.isObjectBindingPattern(binding) || ts_.isArrayBindingPattern(binding)) {
- for (const el of binding.elements) {
- if (ts_.isOmittedExpression(el)) continue;
- pushBinding(el.name);
- }
- }
- };
- // Walk children up to (but not into) any nested scope of these kinds.
- const walkUntilScope = (n: ts.Node, visit: (c: ts.Node) => void) => {
- ts_.forEachChild(n, c => {
- visit(c);
- // Skip into another scope (we don't recurse into function bodies etc.)
- if (this._isOwnScopeBoundary(c)) return;
- walkUntilScope(c, visit);
- });
- };
-
- // In JS files, TS's binder hoists `const x = require('…')` into
- // the enclosing `SourceFile.locals` / `FunctionLikeDeclaration.locals`
- // as a CommonJS Alias symbol (flags=0x200000) for module-shape
- // inference, even when the declaration is nested inside a
- // Block/IfStatement/etc. From an ESLint scope-analysis standpoint
- // that binding is purely block-scoped — the inner 'block' scope
- // already enumerates it, so adding it at the outer scope creates
- // a phantom upper-scope binding and `no-shadow` fires on the
- // inner declaration as if it shadowed itself.
- //
- // Filter: only skip if the symbol is a CommonJS Alias whose decl
- // sits inside a nested sub-scope. `var` (FunctionScopedVariable /
- // BlockScopedVariable) genuinely hoists to the function/global
- // scope under JS semantics, so those entries must stay — `for
- // (var a in xs)` at the file root would otherwise drop `a`'s
- // file-scope reference and break `no-undef` / reference
- // resolution. `function f()` in a block likewise hoists in
- // sloppy JS, but TS binder treats it as block-scoped and doesn't
- // put it in the outer locals so this code never sees it.
- const SymbolFlags_Alias = ts.SymbolFlags.Alias;
- const owner: ts.Node = this.tsNode;
- const directlyOwned = (sym: ts.Symbol): boolean => {
- if ((sym.flags & SymbolFlags_Alias) === 0) return true;
- const decls = sym.declarations;
- if (!decls || decls.length === 0) return true;
- outer: for (const decl of decls) {
- // Walk up from decl's parent. If we hit any sub-scope
- // boundary before reaching `owner`, this decl is nested.
- for (let cur: ts.Node | undefined = decl.parent; cur; cur = cur.parent) {
- if (cur === owner) return true;
- if (this._isOwnScopeBoundary(cur)) continue outer;
- }
- }
- return false;
- };
-
- switch (this.type) {
- case 'global': {
- // In module mode the globalScope is empty — moduleScope owns the
- // top-level decls. In script mode globalScope owns them. In
- // either case TS's binder pre-hoists `var` (including
- // `for (var x in …)`) into SourceFile.locals — pull those in too.
- if (this.manager.isModule()) break;
- const sf = this.tsNode as ts.SourceFile;
- for (const stmt of sf.statements) this._collectStatementBindings(stmt, push, pushBinding);
- const locals = (sf as { locals?: ts.SymbolTable }).locals;
- if (locals) {
- locals.forEach(sym => {
- // Skip TS's synthetic `default` export symbol.
- if (sym.name === 'default') return;
- // Skip TS-internal escaped names (double-underscore
- // prefix). These are NOT user-facing bindings — TS's
- // binder uses them for `__global` (declare global
- // augmentations), `__type` (anonymous type literals),
- // `__call` / `__index` / `__new` (signatures), etc.
- // `declare global { ... }` lifts a `__global` symbol
- // into `sf.locals`; without skipping it here, every
- // `declare global` block emits a fake
- // `'__global' is defined but never used`.
- if (sym.name.startsWith('__')) return;
- if (sym.flags & (ts_.SymbolFlags.ValueModule | ts_.SymbolFlags.NamespaceModule)) return;
- if (!directlyOwned(sym)) return;
- push(sym);
- });
- }
- break;
- }
- case 'module': {
- const sf = this.tsNode as ts.SourceFile;
- for (const stmt of sf.statements) this._collectStatementBindings(stmt, push, pushBinding);
- const locals = (sf as { locals?: ts.SymbolTable }).locals;
- if (locals) {
- locals.forEach(sym => {
- if (sym.name === 'default') return;
- // See `case 'global'`: TS-internal escaped names
- // (`__global`, `__type`, `__call`, …) are not
- // user-facing bindings. `declare global { ... }`
- // in a module-mode file (any file with import/
- // export) lifts `__global` into `sf.locals`; the
- // 'global' fix above doesn't apply here because
- // module-mode files take this branch instead.
- if (sym.name.startsWith('__')) return;
- if (sym.flags & (ts_.SymbolFlags.ValueModule | ts_.SymbolFlags.NamespaceModule)) return;
- if (!directlyOwned(sym)) return;
- push(sym);
- });
- }
- break;
- }
- case 'function': {
- const fn = this.tsNode as ts.FunctionLikeDeclaration;
- // Synthetic `arguments` first (matches upstream order).
- if (fn.kind !== SK.ArrowFunction) {
- const argsVar = this._getOrCreateArgumentsVar();
- if (!seen.has(argsVar.symbol)) {
- out.push(argsVar);
- seen.add(argsVar.symbol);
- }
- }
- // Parameters next.
- if (fn.parameters) {
- for (const p of fn.parameters) {
- pushBinding(p.name);
- // Parameter property (`constructor(public x: T)`): TS's
- // binder produces TWO symbols on the same declaration —
- // a Property symbol (flags=Property=4) returned by
- // `getSymbolAtLocation(p.name)` and a function-scoped
- // FunctionScopedVariable symbol (flags=1) returned by
- // `getSymbolAtLocation` on a USE of the binding inside
- // the constructor body. pushBinding registered the
- // Property symbol; references inside the body resolve
- // to the local symbol, miss `_variableBySymbol`, and
- // fall through to `through` → no-undef false-positives
- // every parameter-property binding. Alias the local
- // symbol to the same TsVariable so resolution lands.
- // Repro (real code): `class C { constructor(public
- // readonly program: ts.Program) { program.x } }`.
- const isParamProp = ts_.isParameter(p)
- && p.modifiers?.some(m =>
- m.kind === SK.PublicKeyword || m.kind === SK.PrivateKeyword
- || m.kind === SK.ProtectedKeyword || m.kind === SK.ReadonlyKeyword
- || m.kind === SK.OverrideKeyword
- );
- if (isParamProp && fn.body && ts_.isIdentifier(p.name)) {
- const propSym = this.manager.checker.getSymbolAtLocation(p.name);
- const localSym = this.manager.checker.getSymbolsInScope(
- fn.body,
- ts_.SymbolFlags.Variable,
- ).find(s => s.name === (p.name as ts.Identifier).text);
- if (propSym && localSym && propSym !== localSym) {
- const v = this.manager._variableBySymbol.get(propSym);
- if (v) this.manager._variableBySymbol.set(localSym, v);
- }
- }
- }
- }
- // Walk the body in source order to preserve declaration
- // ordering (matches upstream's referencer). Body statements
- // + var-hoisted decls collapse into the function scope.
- if (fn.body && ts_.isBlock(fn.body)) {
- for (const stmt of fn.body.statements) {
- this._collectStatementBindings(stmt, push, pushBinding);
- }
- }
- // Pick up any var-hoisted decls TS recorded in fn.locals that
- // the source-order walk missed (e.g. `var x` inside nested
- // blocks of the function body). Same Alias-hoist filter as
- // global/module: `const x = require('…')` nested in a sub-
- // block is JS-CJS-aliased into fn.locals, but lexically
- // belongs to its inner block scope.
- const fnLocals = (fn as { locals?: ts.SymbolTable }).locals;
- if (fnLocals) {
- fnLocals.forEach(sym => {
- if (!directlyOwned(sym)) return;
- push(sym);
- });
- }
- break;
- }
- case 'function-expression-name': {
- const fn = this.tsNode as ts.FunctionExpression;
- if (fn.name) push(this.manager.checker.getSymbolAtLocation(fn.name));
- break;
- }
- case 'class': {
- const cls = this.tsNode as ts.ClassLikeDeclaration;
- if (cls.name) push(this.manager.checker.getSymbolAtLocation(cls.name));
- if (cls.typeParameters) {
- for (const tp of cls.typeParameters) push(symOf(tp));
- }
- break;
- }
- case 'block': {
- // let/const/function/class declared directly in the block. var is
- // excluded — it hoists to the enclosing function/global scope.
- const block = this.tsNode as ts.Block;
- for (const stmt of block.statements) {
- if (ts_.isVariableStatement(stmt)) {
- const flags = stmt.declarationList.flags;
- if (flags & (ts_.NodeFlags.Let | ts_.NodeFlags.Const)) {
- for (const d of stmt.declarationList.declarations) pushBinding(d.name);
- }
- continue;
- }
- this._collectStatementBindings(stmt, push, pushBinding);
- }
- break;
- }
- case 'for': {
- const f = this.tsNode as ts.ForStatement | ts.ForInStatement | ts.ForOfStatement;
- const initializer = (f as ts.ForStatement).initializer
- ?? (f as ts.ForInStatement | ts.ForOfStatement).initializer;
- if (initializer && ts_.isVariableDeclarationList(initializer)) {
- for (const d of initializer.declarations) pushBinding(d.name);
- }
- break;
- }
- case 'catch': {
- const c = this.tsNode as ts.CatchClause;
- if (c.variableDeclaration) pushBinding(c.variableDeclaration.name);
- break;
- }
- case 'switch': {
- // `switch (…) { case X: let y = …; }` — let/const inside any
- // case clause are scoped to the switch (single block scope per
- // the spec). this.tsNode is the CaseBlock.
- const cb = this.tsNode as ts.CaseBlock;
- for (const clause of cb.clauses) {
- for (const stmt of clause.statements) {
- if (ts_.isVariableStatement(stmt)) {
- const flags = stmt.declarationList.flags;
- if (flags & (ts_.NodeFlags.Let | ts_.NodeFlags.Const)) {
- for (const d of stmt.declarationList.declarations) pushBinding(d.name);
- }
- continue;
- }
- this._collectStatementBindings(stmt, push, pushBinding);
- }
- }
- break;
- }
- case 'with':
- break;
- case 'tsEnum': {
- const e = this.tsNode as ts.EnumDeclaration;
- for (const m of e.members) push(symOf(m));
- break;
- }
- case 'tsModule': {
- const mod = this.tsNode as ts.ModuleDeclaration;
- const body = mod.body;
- if (body && ts_.isModuleBlock(body)) {
- for (const stmt of body.statements) this._collectStatementBindings(stmt, push, pushBinding);
- }
- break;
- }
- case 'type': {
- const tps = (this.tsNode as { typeParameters?: ts.NodeArray }).typeParameters;
- if (tps) {
- for (const tp of tps) {
- // Type parameters in `.d.ts` files can merge across
- // redeclarations (TS lib types declared in both
- // tsc's bundled `lib.*.d.ts` AND user-loaded
- // `src/lib/*.d.ts` — TypeScript's own repo lints
- // itself this way). Reference resolution at use-
- // sites goes through `checker.getSymbolAtLocation`
- // and returns the MERGED symbol. `tp.symbol` is the
- // LOCAL one, so binding the var under `tp.symbol`
- // makes `variableBySymbol.get(refSym)` miss and
- // the ref leaks to `through` — no-undef false
- // positive on the type parameter's own name.
- // Resolving via the name node makes declaration
- // and reference paths agree on the same symbol.
- const sym = this.manager.checker.getSymbolAtLocation(tp.name)
- ?? (tp as { symbol?: ts.Symbol }).symbol;
- push(sym);
- }
- }
- break;
- }
- case 'conditional-type': {
- // `T extends ... infer U ...` — `infer U` introduces `U`
- // into the conditional type's scope, accessible only from
- // the trueType (and any nested conditionals therein).
- // Upstream's TypeVisitor.TSInferType defines the type
- // parameter on the enclosing conditional scope. Walk the
- // extendsType and collect every InferTypeNode's name.
- const cond = this.tsNode as ts.ConditionalTypeNode;
- const collectInfer = (n: ts.Node) => {
- if (ts_.isInferTypeNode(n)) {
- push(symOf(n.typeParameter));
- }
- ts_.forEachChild(n, collectInfer);
- };
- if (cond.extendsType) collectInfer(cond.extendsType);
- break;
- }
- case 'mapped-type': {
- // Mapped types use a single `typeParameter` (not the plural array).
- const tp = (this.tsNode as ts.MappedTypeNode).typeParameter;
- if (tp) push(symOf(tp));
- break;
- }
- case 'function-type': {
- // Function type signatures (FunctionType, MethodSignature, etc.):
- // type params + parameter names are scoped here. Upstream's
- // no-unused-vars treats their parameter names as variables.
- const tps = (this.tsNode as { typeParameters?: ts.NodeArray }).typeParameters;
- if (tps) {
- for (const tp of tps) push(symOf(tp));
- }
- const params = (this.tsNode as { parameters?: ts.NodeArray }).parameters;
- if (params) {
- for (const p of params) pushBinding(p.name);
- }
- break;
- }
- }
- return out;
- }
-
- _isOwnScopeBoundary(n: ts.Node): boolean {
- const ts_ = ts;
- const SK = ts_.SyntaxKind;
- switch (n.kind) {
- case SK.FunctionDeclaration:
- case SK.FunctionExpression:
- case SK.ArrowFunction:
- case SK.MethodDeclaration:
- case SK.Constructor:
- case SK.GetAccessor:
- case SK.SetAccessor:
- case SK.ClassDeclaration:
- case SK.ClassExpression:
- case SK.ForStatement:
- case SK.ForInStatement:
- case SK.ForOfStatement:
- case SK.CatchClause:
- case SK.SwitchStatement:
- case SK.WithStatement:
- case SK.EnumDeclaration:
- case SK.ModuleDeclaration:
- case SK.InterfaceDeclaration:
- case SK.TypeAliasDeclaration:
- case SK.MappedType:
- case SK.ConditionalType:
- case SK.FunctionType:
- case SK.ConstructorType:
- case SK.CallSignature:
- case SK.ConstructSignature:
- case SK.MethodSignature:
- return true;
- case SK.Block: {
- // Function bodies are not their own block scope (they ARE the function
- // scope's body). Free-standing blocks are.
- const p = n.parent;
- if (
- p && (
- p.kind === SK.FunctionDeclaration
- || p.kind === SK.FunctionExpression
- || p.kind === SK.ArrowFunction
- || p.kind === SK.MethodDeclaration
- || p.kind === SK.Constructor
- || p.kind === SK.GetAccessor
- || p.kind === SK.SetAccessor
- )
- ) {
- return false;
- }
- return true;
- }
- }
- return false;
- }
-
- // Collect bindings from a statement that lives in this scope (i.e. block /
- // global / module / tsModule scope). Does not recurse into nested scopes.
- _collectStatementBindings(
- stmt: ts.Statement,
- push: (sym: ts.Symbol | undefined) => void,
- pushBinding: (b: ts.BindingName | undefined) => void,
- ) {
- const ts_ = ts;
- if (ts_.isVariableStatement(stmt)) {
- // Module/global/block scope: only let/const/var (var hoists to function,
- // but at top level it lives in the module/global scope).
- for (const d of stmt.declarationList.declarations) pushBinding(d.name);
- return;
- }
- if (ts_.isFunctionDeclaration(stmt) || ts_.isClassDeclaration(stmt)) {
- if (stmt.name) {
- const sym = this.manager.checker.getSymbolAtLocation(stmt.name);
- // `export default function f` makes the symbol's name 'default'.
- // Skip — the locals walk picks up the local `f` symbol instead.
- if (sym && sym.name !== 'default') push(sym);
- }
- return;
- }
- if (ts_.isImportDeclaration(stmt)) {
- const clause = stmt.importClause;
- if (!clause) return;
- if (clause.name) push(this.manager.checker.getSymbolAtLocation(clause.name));
- const bindings = clause.namedBindings;
- if (bindings) {
- if (ts_.isNamespaceImport(bindings)) {
- push(this.manager.checker.getSymbolAtLocation(bindings.name));
- }
- else {
- for (const el of bindings.elements) {
- push(this.manager.checker.getSymbolAtLocation(el.name));
- }
- }
- }
- return;
- }
- if (ts_.isImportEqualsDeclaration(stmt)) {
- // `import x = require('mod')` and `import x = OtherNS` — the name
- // binds in module/global scope.
- push(this.manager.checker.getSymbolAtLocation(stmt.name));
- return;
- }
- if (ts_.isEnumDeclaration(stmt) || ts_.isModuleDeclaration(stmt)) {
- // `declare global { ... }` is an augmentation of the ambient
- // global scope — its `name` is the synthetic `global` keyword
- // (TS represents the symbol with the escaped name `__global`).
- // scope-manager-upstream skips defineIdentifier on this branch
- // (`Referencer.TSModuleDeclaration` returns early when
- // `node.kind === 'global'`); without the parallel skip here,
- // every `declare global` block in the codebase emits a fake
- // `'__global' is defined but never used` diagnostic.
- if (
- ts_.isModuleDeclaration(stmt)
- && (stmt.flags & ts.NodeFlags.GlobalAugmentation)
- ) {
- return;
- }
- if (stmt.name && ts.isIdentifier(stmt.name)) {
- push(this.manager.checker.getSymbolAtLocation(stmt.name));
- }
- return;
- }
- if (ts_.isInterfaceDeclaration(stmt) || ts_.isTypeAliasDeclaration(stmt)) {
- if (stmt.name) push(this.manager.checker.getSymbolAtLocation(stmt.name));
- return;
- }
- }
-
- _getOrCreateArgumentsVar(): TsVariable {
- let v = this.manager._syntheticArguments.get(this);
- if (!v) {
- // Try to obtain TS's real `arguments` symbol so references to the
- // identifier resolve to this same TsVariable. Fall back to a fake
- // symbol for arrow functions and other edge cases (where the var
- // won't actually be referenced anyway).
- const fn = this.tsNode as ts.FunctionLikeDeclaration;
- let sym: ts.Symbol | undefined;
- const body = (fn as { body?: ts.Node }).body;
- if (body) {
- const found = this.manager.checker.getSymbolsInScope(
- body,
- ts.SymbolFlags.Variable,
- ).find(s => s.name === 'arguments');
- if (found) sym = found;
- }
- sym ??= { name: 'arguments', declarations: [], flags: 0 } as unknown as ts.Symbol;
- v = new TsVariable(this.manager, sym);
- this.manager._syntheticArguments.set(this, v);
- // Register so ref-index resolution finds it.
- this.manager._variableBySymbol.set(sym, v);
- }
- return v;
- }
-
- get set(): Map {
- if (!this._set) {
- this._set = new Map();
- for (const v of this.variables) this._set.set(v.name, v);
- }
- return this._set;
- }
-
- get childScopes(): TsScope[] {
- return this._childScopes;
- }
-
- get references(): TsReference[] {
- this.manager._ensureRefIndex();
- return this.manager._referencesByScope!.get(this) ?? [];
- }
-
- get implicit(): {
- variables: TsVariable[];
- left: TsReference[];
- leftToBeResolved: TsReference[];
- set: Map