Skip to content
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

feat(scope-manager): ignore ECMA version #5889

26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- v6
pull_request:
branches:
- '**'
Expand Down Expand Up @@ -249,3 +250,28 @@ jobs:
run: npx lerna publish --loglevel=verbose --canary --exact --force-publish --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish_canary_version_v6:
name: Publish the next major version code as a canary version
runs-on: ubuntu-latest
needs: [integration_tests, lint_with_build, lint_without_build, unit_tests]
if: github.ref == 'refs/heads/v${major}'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install
uses: ./.github/actions/prepare-install
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Build
uses: ./.github/actions/prepare-build

# Fetch all history for all tags and branches in this job because lerna needs it
- run: |
git fetch --prune --unshallow

- name: Publish all packages to npm
run: npx lerna publish premajor --loglevel=verbose --canary --exact --force-publish --yes --dist-tag rc-v${major}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
27 changes: 27 additions & 0 deletions docs/linting/typed-linting/MONOREPOS.md
Expand Up @@ -62,6 +62,33 @@ module.exports = {
};
```

### Wide globs in `parserOptions.project`

Using wide globs `**` in your `parserOptions.project` may degrade linting performance.
Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time.

```js title=".eslintrc.js"
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
// Remove this line
project: ['./tsconfig.eslint.json', './**/tsconfig.json'],
// Add this line
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
},
plugins: ['@typescript-eslint'],
root: true,
};
```

See [Glob pattern in parser's option "project" slows down linting](https://github.com/typescript-eslint/typescript-eslint/issues/2611) for more details.

### Important note regarding large (> 10) multi-package monorepos

We've had reports that for sufficiently large and/or interdependent projects, you may run into OOMs using this approach.
Expand Down
2 changes: 2 additions & 0 deletions packages/parser/README.md
Expand Up @@ -164,6 +164,8 @@ This option allows you to provide a path to your project's `tsconfig.json`. **Th

- If you use project references, TypeScript will not automatically use project references to resolve files. This means that you will have to add each referenced tsconfig to the `project` field either separately, or via a glob.

- Note that using wide globs `**` in your `parserOptions.project` may cause performance implications. Instead of globs that use `**` to recursively check all folders, prefer paths that use a single `*` at a time. For more info see [#2611](https://github.com/typescript-eslint/typescript-eslint/issues/2611).

- TypeScript will ignore files with duplicate filenames in the same folder (for example, `src/file.ts` and `src/file.js`). TypeScript purposely ignore all but one of the files, only keeping the one file with the highest priority extension (the extension priority order (from highest to lowest) is `.ts`, `.tsx`, `.d.ts`, `.js`, `.jsx`). For more info see #955.

- Note that if this setting is specified and `createDefaultProgram` is not, you must only lint files that are included in the projects as defined by the provided `tsconfig.json` files. If your existing configuration does not include all of the files you would like to lint, you can create a separate `tsconfig.eslint.json` as follows:
Expand Down
1 change: 0 additions & 1 deletion packages/parser/src/parser.ts
Expand Up @@ -105,7 +105,6 @@ function parseForESLint(
jsx: validateBoolean(options.ecmaFeatures.jsx),
});
const analyzeOptions: AnalyzeOptions = {
ecmaVersion: options.ecmaVersion === 'latest' ? 1e8 : options.ecmaVersion,
globalReturn: options.ecmaFeatures.globalReturn,
jsxPragma: options.jsxPragma,
jsxFragmentName: options.jsxFragmentName,
Expand Down
8 changes: 0 additions & 8 deletions packages/parser/tests/lib/parser.ts
Expand Up @@ -19,11 +19,6 @@ describe('parser', () => {
expect(() => parseForESLint(code, null)).not.toThrow();
});

it("parseForESLint() should work if options.ecmaVersion is `'latest'`", () => {
const code = 'const valid = true;';
expect(() => parseForESLint(code, { ecmaVersion: 'latest' })).not.toThrow();
});

it('parseAndGenerateServices() should be called with options', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
Expand All @@ -33,7 +28,6 @@ describe('parser', () => {
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaVersion: 2018,
ecmaFeatures: {
globalReturn: false,
jsx: false,
Expand Down Expand Up @@ -84,7 +78,6 @@ describe('parser', () => {
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaVersion: 2018,
ecmaFeatures: {
globalReturn: false,
jsx: false,
Expand All @@ -104,7 +97,6 @@ describe('parser', () => {
parseForESLint(code, config);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenLastCalledWith(expect.anything(), {
ecmaVersion: 2018,
globalReturn: false,
lib: ['dom.iterable'],
jsxPragma: 'Foo',
Expand Down
12 changes: 2 additions & 10 deletions packages/scope-manager/README.md
Expand Up @@ -36,13 +36,6 @@ interface AnalyzeOptions {
*/
childVisitorKeys?: Record<string, string[]> | null;

/**
* Which ECMAScript version is considered.
* Defaults to `2018`.
* `'latest'` is converted to 1e8 at parser.
*/
ecmaVersion?: EcmaVersion | 1e8;

/**
* Whether the whole script is executed under node.js environment.
* When enabled, the scope manager adds a function scope immediately following the global scope.
Expand All @@ -51,7 +44,7 @@ interface AnalyzeOptions {
globalReturn?: boolean;

/**
* Implied strict mode (if ecmaVersion >= 5).
* Implied strict mode.
* Defaults to `false`.
*/
impliedStrict?: boolean;
Expand All @@ -76,7 +69,7 @@ interface AnalyzeOptions {
* This automatically defines a type variable for any types provided by the configured TS libs.
* For more information, see https://www.typescriptlang.org/tsconfig#lib
*
* Defaults to the lib for the provided `ecmaVersion`.
* Defaults to ['esnext'].
*/
lib?: Lib[];

Expand Down Expand Up @@ -105,7 +98,6 @@ const ast = parse(code, {
range: true,
});
const scope = analyze(ast, {
ecmaVersion: 2020,
sourceType: 'module',
});
```
Expand Down
9 changes: 6 additions & 3 deletions packages/scope-manager/src/ScopeManager.ts
Expand Up @@ -28,9 +28,11 @@ interface ScopeManagerOptions {
globalReturn?: boolean;
sourceType?: 'module' | 'script';
impliedStrict?: boolean;
ecmaVersion?: number;
}

/**
* @see https://eslint.org/docs/latest/developer-guide/scope-manager-interface#scopemanager-interface
*/
class ScopeManager {
public currentScope: Scope | null;
public readonly declaredVariables: WeakMap<TSESTree.Node, Variable[]>;
Expand Down Expand Up @@ -77,12 +79,13 @@ class ScopeManager {
public isImpliedStrict(): boolean {
return this.#options.impliedStrict === true;
}

public isStrictModeSupported(): boolean {
return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 5;
return true;
}

public isES6(): boolean {
return this.#options.ecmaVersion != null && this.#options.ecmaVersion >= 6;
return true;
}

/**
Expand Down
35 changes: 4 additions & 31 deletions packages/scope-manager/src/analyze.ts
@@ -1,7 +1,6 @@
import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types';
import type { Lib, TSESTree } from '@typescript-eslint/types';
import { visitorKeys } from '@typescript-eslint/visitor-keys';

import { lib as TSLibraries } from './lib';
import type { ReferencerOptions } from './referencer';
import { Referencer } from './referencer';
import { ScopeManager } from './ScopeManager';
Expand All @@ -16,13 +15,6 @@ interface AnalyzeOptions {
*/
childVisitorKeys?: ReferencerOptions['childVisitorKeys'];

/**
* Which ECMAScript version is considered.
* Defaults to `2018`.
* `'latest'` is converted to 1e8 at parser.
*/
ecmaVersion?: EcmaVersion | 1e8;

/**
* Whether the whole script is executed under node.js environment.
* When enabled, the scope manager adds a function scope immediately following the global scope.
Expand All @@ -31,7 +23,7 @@ interface AnalyzeOptions {
globalReturn?: boolean;

/**
* Implied strict mode (if ecmaVersion >= 5).
* Implied strict mode.
* Defaults to `false`.
*/
impliedStrict?: boolean;
Expand All @@ -54,7 +46,7 @@ interface AnalyzeOptions {
/**
* The lib used by the project.
* This automatically defines a type variable for any types provided by the configured TS libs.
* Defaults to the lib for the provided `ecmaVersion`.
* Defaults to ['esnext'].
*
* https://www.typescriptlang.org/tsconfig#lib
*/
Expand All @@ -74,7 +66,6 @@ interface AnalyzeOptions {

const DEFAULT_OPTIONS: Required<AnalyzeOptions> = {
childVisitorKeys: visitorKeys,
ecmaVersion: 2018,
globalReturn: false,
impliedStrict: false,
jsxPragma: 'React',
Expand All @@ -84,34 +75,16 @@ const DEFAULT_OPTIONS: Required<AnalyzeOptions> = {
emitDecoratorMetadata: false,
};

/**
* Convert ecmaVersion to lib.
* `'latest'` is converted to 1e8 at parser.
*/
function mapEcmaVersion(version: EcmaVersion | 1e8 | undefined): Lib {
if (version == null || version === 3 || version === 5) {
return 'es5';
}

const year = version > 2000 ? version : 2015 + (version - 6);
const lib = `es${year}`;

return lib in TSLibraries ? (lib as Lib) : year > 2020 ? 'esnext' : 'es5';
}

/**
* Takes an AST and returns the analyzed scopes.
*/
function analyze(
tree: TSESTree.Node,
providedOptions?: AnalyzeOptions,
): ScopeManager {
const ecmaVersion =
providedOptions?.ecmaVersion ?? DEFAULT_OPTIONS.ecmaVersion;
const options: Required<AnalyzeOptions> = {
childVisitorKeys:
providedOptions?.childVisitorKeys ?? DEFAULT_OPTIONS.childVisitorKeys,
ecmaVersion,
globalReturn: providedOptions?.globalReturn ?? DEFAULT_OPTIONS.globalReturn,
impliedStrict:
providedOptions?.impliedStrict ?? DEFAULT_OPTIONS.impliedStrict,
Expand All @@ -122,7 +95,7 @@ function analyze(
jsxFragmentName:
providedOptions?.jsxFragmentName ?? DEFAULT_OPTIONS.jsxFragmentName,
sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType,
lib: providedOptions?.lib ?? [mapEcmaVersion(ecmaVersion)],
lib: providedOptions?.lib ?? ['esnext'],
emitDecoratorMetadata:
providedOptions?.emitDecoratorMetadata ??
DEFAULT_OPTIONS.emitDecoratorMetadata,
Expand Down
17 changes: 5 additions & 12 deletions packages/scope-manager/src/referencer/Referencer.ts
Expand Up @@ -371,9 +371,7 @@ class Referencer extends Visitor {
}

protected BlockStatement(node: TSESTree.BlockStatement): void {
if (this.scopeManager.isES6()) {
this.scopeManager.nestBlockScope(node);
}
this.scopeManager.nestBlockScope(node);

this.visitChildren(node);

Expand Down Expand Up @@ -487,7 +485,7 @@ class Referencer extends Visitor {

protected ImportDeclaration(node: TSESTree.ImportDeclaration): void {
assert(
this.scopeManager.isES6() && this.scopeManager.isModule(),
this.scopeManager.isModule(),
'ImportDeclaration should appear when the mode is ES6 and in the module context.',
);

Expand Down Expand Up @@ -579,14 +577,11 @@ class Referencer extends Visitor {
this.scopeManager.nestFunctionScope(node, false);
}

if (this.scopeManager.isES6() && this.scopeManager.isModule()) {
if (this.scopeManager.isModule()) {
this.scopeManager.nestModuleScope(node);
}

if (
this.scopeManager.isStrictModeSupported() &&
this.scopeManager.isImpliedStrict()
) {
if (this.scopeManager.isImpliedStrict()) {
this.currentScope().isStrict = true;
}

Expand All @@ -601,9 +596,7 @@ class Referencer extends Visitor {
protected SwitchStatement(node: TSESTree.SwitchStatement): void {
this.visit(node.discriminant);

if (this.scopeManager.isES6()) {
this.scopeManager.nestSwitchScope(node);
}
this.scopeManager.nestSwitchScope(node);

for (const switchCase of node.cases) {
this.visit(switchCase);
Expand Down
Expand Up @@ -12,7 +12,6 @@ describe('ScopeManager.prototype.getDeclaredVariables', () => {
expectedNamesList: string[][],
): void {
const scopeManager = analyze(ast, {
ecmaVersion: 6,
sourceType: 'module',
});

Expand Down