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 #5881

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

/**
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)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. I wonder if maybe I was too over-zealous with my recommendation to cull higher?

Having this auto-resolved is a really nice property. For the most part it shouldn't cause any issues for people (it just includes a few more global variables for people using rules like no-undef or no-use-before-define), but there might be edge cases?

Hmm. WDYT? I'm torn on if it's worth the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of those are rules we recommend folks use - and folks should generally always be in >=ES2021 or so these days. I say let's kill them for now, and use the RC period to see if anybody notices! 🔪

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I will mark this as a breaking change now though.

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
30 changes: 1 addition & 29 deletions packages/scope-manager/tests/eslint-scope/implied-strict.test.ts
Expand Up @@ -8,7 +8,7 @@ import {
} from '../util';

describe('impliedStrict option', () => {
it('ensures all user scopes are strict if ecmaVersion >= 5', () => {
it('ensures all user scopes are strict', () => {
const { scopeManager } = parseAndAnalyze(
`
function foo() {
Expand All @@ -18,7 +18,6 @@ describe('impliedStrict option', () => {
}
`,
{
ecmaVersion: 5,
impliedStrict: true,
},
);
Expand All @@ -42,38 +41,12 @@ describe('impliedStrict option', () => {
expect(scope.isStrict).toBeTruthy();
});

it('ensures impliedStrict option is only effective when ecmaVersion option >= 5', () => {
const { scopeManager } = parseAndAnalyze(
`
function foo() {}
`,
{
ecmaVersion: 3,
impliedStrict: true,
},
);

expect(scopeManager.scopes).toHaveLength(2);

let scope = scopeManager.scopes[0];

expectToBeGlobalScope(scope);
expect(scope.block.type).toBe(AST_NODE_TYPES.Program);
expect(scope.isStrict).toBeFalsy();

scope = scopeManager.scopes[1];
expectToBeFunctionScope(scope);
expect(scope.block.type).toBe(AST_NODE_TYPES.FunctionDeclaration);
expect(scope.isStrict).toBeFalsy();
});

it('omits a nodejs global scope when ensuring all user scopes are strict', () => {
const { scopeManager } = parseAndAnalyze(
`
function foo() {}
`,
{
ecmaVersion: 5,
globalReturn: true,
impliedStrict: true,
},
Expand All @@ -100,7 +73,6 @@ describe('impliedStrict option', () => {

it('omits a module global scope when ensuring all user scopes are strict', () => {
const { scopeManager } = parseAndAnalyze('function foo() {}', {
ecmaVersion: 6,
impliedStrict: true,
sourceType: 'module',
});
Expand Down
51 changes: 0 additions & 51 deletions packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/scope-manager/tests/fixtures.test.ts
Expand Up @@ -42,7 +42,6 @@ const ALLOWED_OPTIONS: Map<string, ALLOWED_VALUE> = new Map<
keyof AnalyzeOptions,
ALLOWED_VALUE
>([
['ecmaVersion', ['number']],
['globalReturn', ['boolean']],
['impliedStrict', ['boolean']],
['jsxPragma', ['string']],
Expand Down
4 changes: 0 additions & 4 deletions packages/website/src/components/linter/WebLinter.ts
Expand Up @@ -111,10 +111,6 @@ export class WebLinter {
);

const scopeManager = this.lintUtils.analyze(ast, {
ecmaVersion:
eslintOptions.ecmaVersion === 'latest'
? 1e8
: eslintOptions.ecmaVersion,
globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false,
sourceType: eslintOptions.sourceType ?? 'script',
});
Expand Down