Skip to content

feat(typescript-estree): rename automaticSingleRunInference to disallowAutomaticSingleRunInference #8922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
19 changes: 10 additions & 9 deletions docs/packages/Parser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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`
Expand All @@ -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`

Expand Down
71 changes: 38 additions & 33 deletions docs/packages/TypeScript_ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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';
};
}

/**
Expand Down
6 changes: 0 additions & 6 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/tests/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ describe('Validating rule docs', () => {
{
parser: '@typescript-eslint/parser',
parserOptions: {
disallowAutomaticSingleRunInference: true,
tsconfigRootDir: rootPath,
project: './tsconfig.json',
},
Expand Down
17 changes: 10 additions & 7 deletions packages/rule-tester/src/RuleTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions packages/type-utils/tests/TypeOrValueSpecifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/type-utils/tests/isTypeReadonly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/type-utils/tests/isUnsafeAssignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions packages/typescript-estree/src/parseSettings/inferSingleRun.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { normalize } from 'path';
import path from 'path';

import type { TSESTreeOptions } from '../parser-options';

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
71 changes: 38 additions & 33 deletions packages/typescript-estree/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions packages/typescript-estree/tests/lib/inferSingleRun.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading