diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index 3ea0d996cfc6..0c7eb9946ab9 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -30,10 +30,10 @@ The following additional configuration options are available by specifying them ```ts interface ParserOptions { - allowAutomaticSingleRunInference?: boolean; cacheLifetime?: { glob?: number | 'Infinity'; }; + disallowAutomaticSingleRunInference?: boolean; ecmaFeatures?: { jsx?: boolean; globalReturn?: boolean; @@ -55,22 +55,23 @@ interface ParserOptions { } ``` -### `allowAutomaticSingleRunInference` +### `disallowAutomaticSingleRunInference` -> Default `process.env.TSESTREE_SINGLE_RUN` or `false`. +> Default `process.env.TSESTREE_SINGLE_RUN` or `true`. -Whether to use common heuristics to infer whether ESLint is being used as part of a single run (as opposed to `--fix` mode or in a persistent session such as an editor extension). +Whether to stop using common heuristics to infer whether ESLint is being used as part of a single run (as opposed to `--fix` mode or in a persistent session such as an editor extension). +In other words, typescript-eslint is faster by default, and this option disables an automatic performance optimization. When typescript-eslint handles TypeScript Program management behind the scenes for [linting with type information](../getting-started/Typed_Linting.mdx), this distinction is important for performance. There is significant overhead to managing TypeScript "Watch" Programs needed for the long-running use-case. Being able to assume the single run case allows typescript-eslint to faster immutable Programs instead. This setting's default value can be specified by setting a `TSESTREE_SINGLE_RUN` environment variable to `"false"` or `"true"`. -For example, `TSESTREE_SINGLE_RUN=true npx eslint .` will enable it. +For example, `TSESTREE_SINGLE_RUN=false npx eslint .` will disable it. -:::tip -We've seen `allowAutomaticSingleRunInference` improve linting speed in CI by up to 10-20%. -Our plan is to [enable `allowAutomaticSingleRunInference` by default in an upcoming major version](https://github.com/typescript-eslint/typescript-eslint/issues/8121). +:::note +We recommend leaving this option off if possible. +We've seen allowing automatic single run inference improve linting speed in CI by up to 10-20%. ::: ### `cacheLifetime` @@ -79,7 +80,7 @@ This option allows you to granularly control our internal cache expiry lengths. You can specify the number of seconds as an integer number, or the string 'Infinity' if you never want the cache to expire. -By default cache entries will be evicted after 30 seconds, or will persist indefinitely if the parser infers that it is a single run (see [`allowAutomaticSingleRunInference`](#allowAutomaticSingleRunInference)). +By default cache entries will be evicted after 30 seconds, or will persist indefinitely if the parser infers that it is a single run (see [`disallowAutomaticSingleRunInference`](#disallowAutomaticSingleRunInference)). ### `ecmaFeatures` diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index a23421b248aa..190dbb59e277 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -155,6 +155,44 @@ Parses the given string of code with the options provided and returns an ESTree- ```ts interface ParseAndGenerateServicesOptions extends ParseOptions { + /** + * Granular control of the expiry lifetime of our internal caches. + * You can specify the number of seconds as an integer number, or the string + * 'Infinity' if you never want the cache to expire. + * + * By default cache entries will be evicted after 30 seconds, or will persist + * indefinitely if `disallowAutomaticSingleRunInference = false` AND the parser + * infers that it is a single run. + */ + cacheLifetime?: { + /** + * Glob resolution for `parserOptions.project` values. + */ + glob?: number | 'Infinity'; + }; + + /** + * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, + * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback + * on a file in an IDE). + * + * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction + * is important because there is significant overhead to managing the so called Watch Programs + * needed for the long-running use-case. + * + * By default, we will use common heuristics to infer whether ESLint is being + * used as part of a single run. This option disables those heuristics, and + * therefore the performance optimizations gained by them. + * + * In other words, typescript-eslint is faster by default, and this option + * disables an automatic performance optimization. + * + * This setting's default value can be specified by setting a `TSESTREE_SINGLE_RUN` + * environment variable to `"false"` or `"true"`. + * Otherwise, the default value is `false`. + */ + disallowAutomaticSingleRunInference?: boolean; + /** * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. */ @@ -234,39 +272,6 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * All linted files must be part of the provided program(s). */ programs?: Program[]; - - /** - * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, - * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback - * on a file in an IDE). - * - * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction - * is important because there is significant overhead to managing the so called Watch Programs - * needed for the long-running use-case. - * - * When allowAutomaticSingleRunInference is enabled, we will use common heuristics to infer - * whether or not ESLint is being used as part of a single run. - * - * This setting's default value can be specified by setting a `TSESTREE_SINGLE_RUN` - * environment variable to `"false"` or `"true"`. - */ - allowAutomaticSingleRunInference?: boolean; - - /** - * Granular control of the expiry lifetime of our internal caches. - * You can specify the number of seconds as an integer number, or the string - * 'Infinity' if you never want the cache to expire. - * - * By default cache entries will be evicted after 30 seconds, or will persist - * indefinitely if `allowAutomaticSingleRunInference = true` AND the parser - * infers that it is a single run. - */ - cacheLifetime?: { - /** - * Glob resolution for `parserOptions.project` values. - */ - glob?: number | 'Infinity'; - }; } /** diff --git a/eslint.config.mjs b/eslint.config.mjs index bb2c0c491044..bde1e5edd589 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -76,12 +76,6 @@ export default tseslint.config( ...globals.node, }, parserOptions: { - allowAutomaticSingleRunInference: true, - cacheLifetime: { - // we pretty well never create/change tsconfig structure - so no need to ever evict the cache - // in the rare case that we do - just need to manually restart their IDE. - glob: 'Infinity', - }, project: [ 'tsconfig.json', 'packages/*/tsconfig.json', diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index 3c3842793ad0..dbec5c2fb907 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -391,6 +391,7 @@ describe('Validating rule docs', () => { { parser: '@typescript-eslint/parser', parserOptions: { + disallowAutomaticSingleRunInference: true, tsconfigRootDir: rootPath, project: './tsconfig.json', }, diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index f0ac7ca3dcac..8a408f4a9ece 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -214,13 +214,16 @@ export class RuleTester extends TestFramework { if (test.parser === TYPESCRIPT_ESLINT_PARSER) { throw new Error(DUPLICATE_PARSER_ERROR_MESSAGE); } - if (!test.filename) { - return { - ...test, - filename: getFilename(test.parserOptions), - }; - } - return test; + return { + ...test, + filename: test.filename || getFilename(test.parserOptions), + parserOptions: { + // Re-running simulates --fix mode, which implies an isolated program + // (i.e. parseAndGenerateServicesCalls[test.filename] > 1). + disallowAutomaticSingleRunInference: true, + ...test.parserOptions, + }, + }; }; const normalizedTests = { diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 8ddca54d3b34..b0be9a7a6e66 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -131,6 +131,7 @@ describe('TypeOrValueSpecifier', () => { ): void { const rootDir = path.join(__dirname, 'fixtures'); const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, project: './tsconfig.json', filePath: path.join(rootDir, 'file.ts'), tsconfigRootDir: rootDir, diff --git a/packages/type-utils/tests/isTypeReadonly.test.ts b/packages/type-utils/tests/isTypeReadonly.test.ts index c171417e5d32..903c92e5b003 100644 --- a/packages/type-utils/tests/isTypeReadonly.test.ts +++ b/packages/type-utils/tests/isTypeReadonly.test.ts @@ -16,6 +16,7 @@ describe('isTypeReadonly', () => { program: ts.Program; } { const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, project: './tsconfig.json', filePath: path.join(rootDir, 'file.ts'), tsconfigRootDir: rootDir, diff --git a/packages/type-utils/tests/isUnsafeAssignment.test.ts b/packages/type-utils/tests/isUnsafeAssignment.test.ts index 733682b5a559..69fc0e12c8b9 100644 --- a/packages/type-utils/tests/isUnsafeAssignment.test.ts +++ b/packages/type-utils/tests/isUnsafeAssignment.test.ts @@ -19,6 +19,7 @@ describe('isUnsafeAssignment', () => { checker: ts.TypeChecker; } { const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, project: './tsconfig.json', filePath: path.join(rootDir, 'file.ts'), tsconfigRootDir: rootDir, diff --git a/packages/typescript-estree/src/parseSettings/inferSingleRun.ts b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts index db64bc21435f..e753fddb2e90 100644 --- a/packages/typescript-estree/src/parseSettings/inferSingleRun.ts +++ b/packages/typescript-estree/src/parseSettings/inferSingleRun.ts @@ -1,4 +1,4 @@ -import { normalize } from 'path'; +import path from 'path'; import type { TSESTreeOptions } from '../parser-options'; @@ -33,8 +33,8 @@ export function inferSingleRun(options: TSESTreeOptions | undefined): boolean { return true; } - // Currently behind a flag while we gather real-world feedback - if (options.allowAutomaticSingleRunInference) { + // Ideally, we'd like to try to auto-detect CI or CLI usage that lets us infer a single CLI run. + if (!options.disallowAutomaticSingleRunInference) { const possibleEslintBinPaths = [ 'node_modules/.bin/eslint', // npm or yarn repo 'node_modules/eslint/bin/eslint.js', // pnpm repo @@ -43,8 +43,8 @@ export function inferSingleRun(options: TSESTreeOptions | undefined): boolean { // Default to single runs for CI processes. CI=true is set by most CI providers by default. process.env.CI === 'true' || // This will be true for invocations such as `npx eslint ...` and `./node_modules/.bin/eslint ...` - possibleEslintBinPaths.some(path => - process.argv[1].endsWith(normalize(path)), + possibleEslintBinPaths.some(binPath => + process.argv[1].endsWith(path.normalize(binPath)), ) ) { return true; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index b301b5ffbd37..d14924b13ae0 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -117,6 +117,44 @@ export interface ProjectServiceOptions { } interface ParseAndGenerateServicesOptions extends ParseOptions { + /** + * Granular control of the expiry lifetime of our internal caches. + * You can specify the number of seconds as an integer number, or the string + * 'Infinity' if you never want the cache to expire. + * + * By default cache entries will be evicted after 30 seconds, or will persist + * indefinitely if `disallowAutomaticSingleRunInference = false` AND the parser + * infers that it is a single run. + */ + cacheLifetime?: { + /** + * Glob resolution for `parserOptions.project` values. + */ + glob?: CacheDurationSeconds; + }; + + /** + * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, + * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback + * on a file in an IDE). + * + * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction + * is important because there is significant overhead to managing the so called Watch Programs + * needed for the long-running use-case. + * + * By default, we will use common heuristics to infer whether ESLint is being + * used as part of a single run. This option disables those heuristics, and + * therefore the performance optimizations gained by them. + * + * In other words, typescript-eslint is faster by default, and this option + * disables an automatic performance optimization. + * + * This setting's default value can be specified by setting a `TSESTREE_SINGLE_RUN` + * environment variable to `"false"` or `"true"`. + * Otherwise, the default value is `false`. + */ + disallowAutomaticSingleRunInference?: boolean; + /** * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. */ @@ -196,39 +234,6 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * All linted files must be part of the provided program(s). */ programs?: ts.Program[] | null; - - /** - * ESLint (and therefore typescript-eslint) is used in both "single run"/one-time contexts, - * such as an ESLint CLI invocation, and long-running sessions (such as continuous feedback - * on a file in an IDE). - * - * When typescript-eslint handles TypeScript Program management behind the scenes, this distinction - * is important because there is significant overhead to managing the so called Watch Programs - * needed for the long-running use-case. - * - * When allowAutomaticSingleRunInference is enabled, we will use common heuristics to infer - * whether or not ESLint is being used as part of a single run. - * - * This setting's default value can be specified by setting a `TSESTREE_SINGLE_RUN` - * environment variable to `"false"` or `"true"`. - */ - allowAutomaticSingleRunInference?: boolean; - - /** - * Granular control of the expiry lifetime of our internal caches. - * You can specify the number of seconds as an integer number, or the string - * 'Infinity' if you never want the cache to expire. - * - * By default cache entries will be evicted after 30 seconds, or will persist - * indefinitely if `allowAutomaticSingleRunInference = true` AND the parser - * infers that it is a single run. - */ - cacheLifetime?: { - /** - * Glob resolution for `parserOptions.project` values. - */ - glob?: CacheDurationSeconds; - }; } export type TSESTreeOptions = ParseAndGenerateServicesOptions; diff --git a/packages/typescript-estree/tests/lib/inferSingleRun.test.ts b/packages/typescript-estree/tests/lib/inferSingleRun.test.ts new file mode 100644 index 000000000000..769fb62df4a1 --- /dev/null +++ b/packages/typescript-estree/tests/lib/inferSingleRun.test.ts @@ -0,0 +1,78 @@ +import path from 'path'; + +import { inferSingleRun } from '../../src/parseSettings/inferSingleRun'; + +describe('inferSingleRun', () => { + beforeEach(() => { + process.argv = ['node', 'eslint']; + process.env.CI = undefined; + process.env.TSESTREE_SINGLE_RUN = undefined; + }); + + it('returns false when options is undefined', () => { + const actual = inferSingleRun(undefined); + + expect(actual).toBe(false); + }); + + it('returns false when options.project is null', () => { + const actual = inferSingleRun({ project: null }); + + expect(actual).toBe(false); + }); + + it('returns false when options.program is defined', () => { + const actual = inferSingleRun({ programs: [], project: true }); + + expect(actual).toBe(false); + }); + + it("returns false when TSESTREE_SINGLE_RUN is 'false'", () => { + process.env.TSESTREE_SINGLE_RUN = 'false'; + + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(false); + }); + + it("returns true when TSESTREE_SINGLE_RUN is 'true'", () => { + process.env.TSESTREE_SINGLE_RUN = 'true'; + + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(true); + }); + + it("returns true when CI is 'true'", () => { + process.env.CI = 'true'; + + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(true); + }); + + it('returns true when run by the ESLint CLI in npm/yarn', () => { + process.argv = ['node', path.normalize('node_modules/.bin/eslint')]; + + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(true); + }); + + it('returns true when run by the ESLint CLI in pnpm', () => { + process.argv = [ + 'node', + path.normalize('node_modules/eslint/bin/eslint.js'), + ]; + + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(true); + }); + + it('returns false when none of the known cases are true', () => { + const actual = inferSingleRun({ project: true }); + + expect(actual).toBe(false); + }); +}); diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index f973358d85a1..de6fc6936dc3 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -168,6 +168,7 @@ describe('parseAndGenerateServices', () => { const config: TSESTreeOptions = { EXPERIMENTAL_useProjectService: false, comment: true, + disallowAutomaticSingleRunInference: true, tokens: true, range: true, loc: true, @@ -346,6 +347,7 @@ describe('parseAndGenerateServices', () => { const code = 'var a = true'; const config: TSESTreeOptions = { comment: true, + disallowAutomaticSingleRunInference: true, tokens: true, range: true, loc: true, @@ -486,6 +488,7 @@ describe('parseAndGenerateServices', () => { const code = 'var a = true'; const config: TSESTreeOptions = { comment: true, + disallowAutomaticSingleRunInference: true, tokens: true, range: true, loc: true, @@ -530,6 +533,7 @@ describe('parseAndGenerateServices', () => { it("shouldn't turn on debugger if no options were provided", () => { parser.parseAndGenerateServices('const x = 1;', { debugLevel: [], + disallowAutomaticSingleRunInference: true, }); expect(debugEnable).not.toHaveBeenCalled(); }); @@ -537,6 +541,7 @@ describe('parseAndGenerateServices', () => { it('should turn on eslint debugger', () => { parser.parseAndGenerateServices('const x = 1;', { debugLevel: ['eslint'], + disallowAutomaticSingleRunInference: true, }); expect(debugEnable).toHaveBeenCalledTimes(1); expect(debugEnable).toHaveBeenCalledWith('eslint:*,-eslint:code-path'); @@ -545,6 +550,7 @@ describe('parseAndGenerateServices', () => { it('should turn on typescript-eslint debugger', () => { parser.parseAndGenerateServices('const x = 1;', { debugLevel: ['typescript-eslint'], + disallowAutomaticSingleRunInference: true, }); expect(debugEnable).toHaveBeenCalledTimes(1); expect(debugEnable).toHaveBeenCalledWith('typescript-eslint:*'); @@ -553,6 +559,7 @@ describe('parseAndGenerateServices', () => { it('should turn on both eslint and typescript-eslint debugger', () => { parser.parseAndGenerateServices('const x = 1;', { debugLevel: ['typescript-eslint', 'eslint'], + disallowAutomaticSingleRunInference: true, }); expect(debugEnable).toHaveBeenCalledTimes(1); expect(debugEnable).toHaveBeenCalledWith( @@ -565,6 +572,7 @@ describe('parseAndGenerateServices', () => { expect(() => parser.parseAndGenerateServices('const x = 1;', { debugLevel: ['typescript'], + disallowAutomaticSingleRunInference: true, filePath: './path-that-doesnt-exist.ts', project: ['./tsconfig-that-doesnt-exist.json'], }), @@ -590,6 +598,7 @@ describe('parseAndGenerateServices', () => { const code = 'var a = true'; const config: TSESTreeOptions = { comment: true, + disallowAutomaticSingleRunInference: true, tokens: true, range: true, loc: true, @@ -631,6 +640,7 @@ describe('parseAndGenerateServices', () => { cacheLifetime: { glob: lifetime, }, + disallowAutomaticSingleRunInference: true, filePath: join(FIXTURES_DIR, 'file.ts'), tsconfigRootDir: FIXTURES_DIR, project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], diff --git a/packages/typescript-estree/tests/lib/persistentParse.test.ts b/packages/typescript-estree/tests/lib/persistentParse.test.ts index dbfd2831dea5..0d66694a5b75 100644 --- a/packages/typescript-estree/tests/lib/persistentParse.test.ts +++ b/packages/typescript-estree/tests/lib/persistentParse.test.ts @@ -71,6 +71,7 @@ function parseFile( ignoreTsconfigRootDir?: boolean, ): void { parseAndGenerateServices(CONTENTS[filename], { + disallowAutomaticSingleRunInference: true, project: './tsconfig.json', tsconfigRootDir: ignoreTsconfigRootDir ? undefined : tmpDir, filePath: relative diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index eac23a5deeab..4709f313fcbc 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -23,6 +23,7 @@ const testFiles = glob.sync(`**/*.src.ts`, { function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { return { + disallowAutomaticSingleRunInference: true, loc: true, range: true, tokens: true,