Skip to content

Commit

Permalink
Revert "feat(scope-manager): ignore ECMA version" (#5888)
Browse files Browse the repository at this point in the history
Revert "feat(scope-manager): ignore ECMA version (#5881)"

This reverts commit 3b8d449.
  • Loading branch information
JoshuaKGoldberg committed Oct 26, 2022
1 parent 3b8d449 commit 2ee81df
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/parser/src/parser.ts
Expand Up @@ -105,6 +105,7 @@ 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: 8 additions & 0 deletions packages/parser/tests/lib/parser.ts
Expand Up @@ -19,6 +19,11 @@ 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 @@ -28,6 +33,7 @@ describe('parser', () => {
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaVersion: 2018,
ecmaFeatures: {
globalReturn: false,
jsx: false,
Expand Down Expand Up @@ -78,6 +84,7 @@ describe('parser', () => {
range: false,
tokens: false,
sourceType: 'module' as const,
ecmaVersion: 2018,
ecmaFeatures: {
globalReturn: false,
jsx: false,
Expand All @@ -97,6 +104,7 @@ 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: 10 additions & 2 deletions packages/scope-manager/README.md
Expand Up @@ -36,6 +36,13 @@ 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 @@ -44,7 +51,7 @@ interface AnalyzeOptions {
globalReturn?: boolean;

/**
* Implied strict mode.
* Implied strict mode (if ecmaVersion >= 5).
* Defaults to `false`.
*/
impliedStrict?: boolean;
Expand All @@ -69,7 +76,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 ['esnext'].
* Defaults to the lib for the provided `ecmaVersion`.
*/
lib?: Lib[];

Expand Down Expand Up @@ -98,6 +105,7 @@ const ast = parse(code, {
range: true,
});
const scope = analyze(ast, {
ecmaVersion: 2020,
sourceType: 'module',
});
```
Expand Down
9 changes: 3 additions & 6 deletions packages/scope-manager/src/ScopeManager.ts
Expand Up @@ -28,11 +28,9 @@ 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 @@ -79,13 +77,12 @@ class ScopeManager {
public isImpliedStrict(): boolean {
return this.#options.impliedStrict === true;
}

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

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

/**
Expand Down
35 changes: 31 additions & 4 deletions packages/scope-manager/src/analyze.ts
@@ -1,6 +1,7 @@
import type { Lib, TSESTree } from '@typescript-eslint/types';
import type { EcmaVersion, 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 @@ -15,6 +16,13 @@ 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 @@ -23,7 +31,7 @@ interface AnalyzeOptions {
globalReturn?: boolean;

/**
* Implied strict mode.
* Implied strict mode (if ecmaVersion >= 5).
* Defaults to `false`.
*/
impliedStrict?: boolean;
Expand All @@ -46,7 +54,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 ['esnext'].
* Defaults to the lib for the provided `ecmaVersion`.
*
* https://www.typescriptlang.org/tsconfig#lib
*/
Expand All @@ -66,6 +74,7 @@ interface AnalyzeOptions {

const DEFAULT_OPTIONS: Required<AnalyzeOptions> = {
childVisitorKeys: visitorKeys,
ecmaVersion: 2018,
globalReturn: false,
impliedStrict: false,
jsxPragma: 'React',
Expand All @@ -75,16 +84,34 @@ 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 @@ -95,7 +122,7 @@ function analyze(
jsxFragmentName:
providedOptions?.jsxFragmentName ?? DEFAULT_OPTIONS.jsxFragmentName,
sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType,
lib: providedOptions?.lib ?? ['esnext'],
lib: providedOptions?.lib ?? [mapEcmaVersion(ecmaVersion)],
emitDecoratorMetadata:
providedOptions?.emitDecoratorMetadata ??
DEFAULT_OPTIONS.emitDecoratorMetadata,
Expand Down
17 changes: 12 additions & 5 deletions packages/scope-manager/src/referencer/Referencer.ts
Expand Up @@ -371,7 +371,9 @@ class Referencer extends Visitor {
}

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

this.visitChildren(node);

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

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

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

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

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

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

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

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

Expand Down
30 changes: 29 additions & 1 deletion 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', () => {
it('ensures all user scopes are strict if ecmaVersion >= 5', () => {
const { scopeManager } = parseAndAnalyze(
`
function foo() {
Expand All @@ -18,6 +18,7 @@ describe('impliedStrict option', () => {
}
`,
{
ecmaVersion: 5,
impliedStrict: true,
},
);
Expand All @@ -41,12 +42,38 @@ 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 @@ -73,6 +100,7 @@ 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: 51 additions & 0 deletions packages/scope-manager/tests/eslint-scope/map-ecma-version.test.ts
@@ -0,0 +1,51 @@
import type { EcmaVersion, Lib, TSESTree } from '@typescript-eslint/types';

import { analyze } from '../../src/analyze';
import { Referencer } from '../../src/referencer';

jest.mock('../../src/referencer');
jest.mock('../../src/ScopeManager');

describe('ecma version mapping', () => {
it("should map to 'esnext' when unsuported and new", () => {
expectMapping(2042, 'esnext');
expectMapping(42, 'esnext');
});

it("should map to 'es5' when unsuported and old", () => {
expectMapping(2002, 'es5');
expectMapping(2, 'es5');
});

it("should map to 'es{year}' when supported and >= 6", () => {
expectMapping(2015, 'es2015');
expectMapping(6, 'es2015');
expectMapping(2020, 'es2020');
expectMapping(11, 'es2020');
});

it("should map to 'es5' when 5 or 3", () => {
expectMapping(5, 'es5');
expectMapping(3, 'es5');
});

it("should map to 'es2018' when undefined", () => {
expectMapping(undefined, 'es2018');
});

it("should map to 'esnext' when 'latest'", () => {
// `'latest'` is converted to 1e8 at parser.
expectMapping(1e8, 'esnext');
});
});

const fakeNode = {} as unknown as TSESTree.Node;

function expectMapping(ecmaVersion: number | undefined, lib: Lib): void {
(Referencer as jest.Mock).mockClear();
analyze(fakeNode, { ecmaVersion: ecmaVersion as EcmaVersion });
expect(Referencer).toHaveBeenCalledWith(
expect.objectContaining({ lib: [lib] }),
expect.any(Object),
);
}
1 change: 1 addition & 0 deletions packages/scope-manager/tests/fixtures.test.ts
Expand Up @@ -42,6 +42,7 @@ 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: 4 additions & 0 deletions packages/website/src/components/linter/WebLinter.ts
Expand Up @@ -111,6 +111,10 @@ 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

0 comments on commit 2ee81df

Please sign in to comment.