Skip to content

feat(scope-manager): only populate implicit globals whose names appear in the file #10561

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
157 changes: 143 additions & 14 deletions packages/scope-manager/src/referencer/Referencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import type { GlobalScope, Scope } from '../scope';
import type { ScopeManager } from '../ScopeManager';
import type { ImplicitLibVariableOptions, Variable } from '../variable';
import type { ReferenceImplicitGlobal } from './Reference';
import type { VisitorOptions } from './Visitor';

Expand All @@ -19,6 +20,8 @@
VariableDefinition,
} from '../definition';
import { lib as TSLibraries } from '../lib';
import { TYPE, VALUE, TYPE_VALUE } from '../lib/base-config';
import { ImplicitLibVariable } from '../variable';
import { ClassVisitor } from './ClassVisitor';
import { ExportVisitor } from './ExportVisitor';
import { ImportVisitor } from './ImportVisitor';
Expand All @@ -33,6 +36,15 @@
lib: Lib[];
}

type ImplicitVariableMap = Map<string, ImplicitLibVariableOptions>;

// The `TSLibraries` object should have this type instead, but that's outside the scope of this prototype
const entriesByLib = new Map<Lib, [string, ImplicitLibVariableOptions][]>();
// This object caches by the serialized JSON representation of the file's libs array
const variablesFromSerializedLibs = new Map<string, ImplicitVariableMap>();
// This object caches by reference from the file's libs array
const variablesFromRawLibs = new WeakMap<Lib[], ImplicitVariableMap>();

// Referencing variables and creating bindings.
class Referencer extends Visitor {
#hasReferencedJsxFactory = false;
Expand All @@ -51,23 +63,130 @@
}

private populateGlobalsFromLib(globalScope: GlobalScope): void {
for (const lib of this.#lib) {
const variables = TSLibraries[lib];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
/* istanbul ignore if */ if (!variables) {
throw new Error(`Invalid value for lib provided: ${lib}`);
let implicitVariablesFromLibs: ImplicitVariableMap | undefined =
variablesFromRawLibs.get(this.#lib);
if (!implicitVariablesFromLibs) {
// Try to find out if this is a different object but same content we've seen before.
const libs: Lib[] = [...this.#lib].sort();
const serialized: string = JSON.stringify(libs);
implicitVariablesFromLibs = variablesFromSerializedLibs.get(serialized);
if (!implicitVariablesFromLibs) {
// No match, need to create it
implicitVariablesFromLibs = new Map();
for (const lib of libs) {
let entriesForLib:
| [string, ImplicitLibVariableOptions][]
| undefined = entriesByLib.get(lib);
if (!entriesForLib) {
const variables = TSLibraries[lib];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
/* istanbul ignore if */ if (!variables) {
throw new Error(`Invalid value for lib provided: ${lib}`);
}
entriesForLib = Object.entries(variables);
entriesByLib.set(lib, entriesForLib);
}

for (const [name, variable] of entriesForLib) {
const existing = implicitVariablesFromLibs.get(name);
// Since a variable can be a type, a value, or both, and it isn't
// guaranteed to be the same between libs, merges
if (existing) {
if (existing === TYPE_VALUE || existing === variable) {
continue;

Check warning on line 96 in packages/scope-manager/src/referencer/Referencer.ts

View check run for this annotation

Codecov / codecov/patch

packages/scope-manager/src/referencer/Referencer.ts#L96

Added line #L96 was not covered by tests
} else if (
(existing === VALUE && variable === TYPE) ||
(existing === TYPE && variable === VALUE)
) {
implicitVariablesFromLibs.set(name, TYPE_VALUE);
continue;

Check warning on line 102 in packages/scope-manager/src/referencer/Referencer.ts

View check run for this annotation

Codecov / codecov/patch

packages/scope-manager/src/referencer/Referencer.ts#L101-L102

Added lines #L101 - L102 were not covered by tests
}
}
// variable is either net-new, or is upgrading to TYPE_VALUE
implicitVariablesFromLibs.set(name, variable);
}
}
variablesFromSerializedLibs.set(serialized, implicitVariablesFromLibs);
}
variablesFromRawLibs.set(this.#lib, implicitVariablesFromLibs);
}

// Collect all names that are defined or referenced.
const allScopes = new Set<Scope>([globalScope]);
const allNames = new Set<string>();

for (const scope of allScopes) {
for (const childScope of scope.childScopes) {
allScopes.add(childScope);
}
for (const [name, variable] of Object.entries(variables)) {
globalScope.defineImplicitVariable(name, variable);

for (const reference of scope.references) {
allNames.add(reference.identifier.name);
}

for (const variable of scope.variables) {
allNames.add(variable.name);
}
}

// for const assertions (`{} as const` / `<const>{}`)
globalScope.defineImplicitVariable('const', {
eslintImplicitGlobalSetting: 'readonly',
isTypeVariable: true,
isValueVariable: false,
});
// Rules only care about implicit globals if they are referenced or overlap with a local declaration,
// so add only those implicit globals.
const replacements = new Map<Variable, Variable>();
const arraysToProcess = new Set<Variable[]>([globalScope.variables]);
const { declaredVariables } = this.scopeManager;
for (const name of allNames) {
const libVariable: ImplicitLibVariableOptions | undefined =
implicitVariablesFromLibs.get(name);
if (libVariable) {
const existingVariable = globalScope.set.get(name);
if (existingVariable) {
// This should be cleaned up
const newVariable = new ImplicitLibVariable(
globalScope,
name,
libVariable,
);
replacements.set(existingVariable, newVariable);
globalScope.set.set(name, newVariable);

// Copy over information about the conflicting declaration
for (const def of existingVariable.defs) {
newVariable.defs.push(def);
// These arrays were created during defineVariable invocation
const parentDeclarations =
def.parent && declaredVariables.get(def.parent);
if (parentDeclarations) {
arraysToProcess.add(parentDeclarations);
}

const nodeDeclarations = declaredVariables.get(def.node);
if (nodeDeclarations) {
arraysToProcess.add(nodeDeclarations);
}
}

// This mapping is unidirectional, so just copy
for (const identifier of existingVariable.identifiers) {
newVariable.identifiers.push(identifier);
}

// References should not have been bound yet, so no action needed
} else {
globalScope.defineImplicitVariable(name, libVariable);
}
}
}

if (replacements.size > 0) {
for (const array of arraysToProcess) {
for (let i = array.length - 1; i >= 0; i--) {
const replacement = replacements.get(array[i]);
if (replacement) {
array[i] = replacement;
}
}
}
}
}
public close(node: TSESTree.Node): void {
while (this.currentScope(true) && node === this.currentScope().block) {
Expand Down Expand Up @@ -569,7 +688,14 @@

protected Program(node: TSESTree.Program): void {
const globalScope = this.scopeManager.nestGlobalScope(node);
this.populateGlobalsFromLib(globalScope);

// Keep this first since it reduces the amount of snapshot changes significantly
// for const assertions (`{} as const` / `<const>{}`)
globalScope.defineImplicitVariable('const', {
eslintImplicitGlobalSetting: 'readonly',
isTypeVariable: true,
isValueVariable: false,
});

if (this.scopeManager.isGlobalReturn()) {
// Force strictness of GlobalScope to false when using node.js scope.
Expand All @@ -586,6 +712,9 @@
}

this.visitChildren(node);
// Need to call this after all references and variables are defined,
// but before we bind the global scope's references.
this.populateGlobalsFromLib(globalScope);
this.close(node);
}

Expand Down
105 changes: 100 additions & 5 deletions packages/scope-manager/tests/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,110 @@ describe('implicit lib definitions', () => {
expect(variables).toHaveLength(1);
});

it('should define implicit variables', () => {
const { scopeManager } = parseAndAnalyze('', {
it('should define an implicit variable if there is a value reference', () => {
const { scopeManager } = parseAndAnalyze('new ArrayBuffer();', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
const arrayBufferVariables = variables.filter(
v => v.name === 'ArrayBuffer',
);
expect(arrayBufferVariables).toHaveLength(1);
expect(arrayBufferVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

it('should define an implicit variable if there is a type reference', () => {
const { scopeManager } = parseAndAnalyze('type T = ArrayBuffer;', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
const arrayBufferVariables = variables.filter(
v => v.name === 'ArrayBuffer',
);
expect(arrayBufferVariables).toHaveLength(1);
expect(arrayBufferVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

it('should define an implicit variable if there is a nested value reference', () => {
const { scopeManager } = parseAndAnalyze(
'var f = () => new ArrayBuffer();',
{
lib: ['es2015'],
},
);

const variables = scopeManager.variables;
const arrayBufferVariables = variables.filter(
v => v.name === 'ArrayBuffer',
);
expect(arrayBufferVariables).toHaveLength(1);
expect(arrayBufferVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

it('should define an implicit variable if there is a nested type reference', () => {
const { scopeManager } = parseAndAnalyze(
'var f = <T extends ArrayBuffer>(): T => undefined as T;',
{
lib: ['es2015'],
},
);

const variables = scopeManager.variables;
const arrayBufferVariables = variables.filter(
v => v.name === 'ArrayBuffer',
);
expect(arrayBufferVariables).toHaveLength(1);
expect(arrayBufferVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

it('should define an implicit variable if there is a value collision', () => {
const { scopeManager } = parseAndAnalyze('var Symbol = 1;', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
const symbolVariables = variables.filter(v => v.name === 'Symbol');
expect(symbolVariables).toHaveLength(1);
expect(symbolVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

it('should define an implicit variable if there is a type collision', () => {
const { scopeManager } = parseAndAnalyze('type Symbol = 1;', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
expect(variables.length).toBeGreaterThan(1);
const symbolVariables = variables.filter(v => v.name === 'Symbol');
expect(symbolVariables).toHaveLength(1);
expect(symbolVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});

const variable = variables[0];
expect(variable).toBeInstanceOf(ImplicitLibVariable);
it('should define an implicit variable if there is a nested value collision', () => {
const { scopeManager } = parseAndAnalyze('var f = (Symbol) => Symbol;', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
const symbolVariables = variables.filter(v => v.name === 'Symbol');
expect(symbolVariables).toHaveLength(2);
expect(symbolVariables.some(v => v instanceof ImplicitLibVariable)).toBe(
true,
);
expect(symbolVariables.some(v => !(v instanceof ImplicitLibVariable))).toBe(
true,
);
});

it('should define an implicit variable if there is a nested type collision', () => {
const { scopeManager } = parseAndAnalyze('var f = (a: Symbol) => a;', {
lib: ['es2015'],
});

const variables = scopeManager.variables;
const symbolVariables = variables.filter(v => v.name === 'Symbol');
expect(symbolVariables).toHaveLength(1);
expect(symbolVariables[0]).toBeInstanceOf(ImplicitLibVariable);
});
});
Loading