🌐 AI搜索 & 代理 主页
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/packages/Scope_Manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ interface AnalyzeOptions {
*/
sourceType?: 'script' | 'module';

/**
* Whether to resolve references to global `var`/function declarations when `sourceType` is `script`.
*
* - Defaults to `false` (matches ESLint 8/9 behavior): global `var`/function references stay in `through`.
* - Set to `true` to opt into ESLint 10 behavior: script-mode global `var`/function references are resolved before rules run.
*
* This is mainly relevant for ESLint 10 users; ESLint 8/9 users don’t need to change anything.
*/
resolveGlobalVarsInScript?: boolean;
// Injected globals are value-only by default; type references will not bind unless the variable is marked type-capable.

/**
* Emit design-type metadata for decorated declarations in source.
* Defaults to `false`.
Expand All @@ -89,6 +100,16 @@ const ast = parse(code, {
const scope = analyze(ast, {
sourceType: 'module',
});

// ESLint 10 script-mode global resolution (opt-in):
// const scopeScript = analyze(astScript, {
// sourceType: 'script',
// resolveGlobalVarsInScript: true, // resolve global var/function refs before rules run
// });
// Note: ESLint 8/9 users should leave `resolveGlobalVarsInScript` unset (default false).
// ESLint 10 users should set it to true to match ESLint 10's script-mode contract.

// Changelog note: addGlobals + resolveGlobalVarsInScript support ESLint 10. ESLint 8/9 users need no change; ESLint 10 users should opt in for script mode.
```

## References
Expand Down
98 changes: 98 additions & 0 deletions packages/scope-manager/src/ScopeManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SourceType, TSESTree } from '@typescript-eslint/types';

import type { Reference } from './referencer/Reference';
import type { Scope } from './scope';
import type { Variable } from './variable';

Expand Down Expand Up @@ -30,6 +31,11 @@ interface ScopeManagerOptions {
globalReturn?: boolean;
impliedStrict?: boolean;
sourceType?: SourceType;
/**
* When true, references to global var/function declarations are resolved in script mode.
* Defaults to false (ESLint 8/9 behavior): global `var` references remain in `through`.
*/
resolveGlobalVarsInScript?: boolean;
}

/**
Expand Down Expand Up @@ -84,6 +90,11 @@ export class ScopeManager {
return true;
}

public shouldResolveGlobalVarsInScript(): boolean {
// Default false (ESLint 9 behavior). Opt in to ESLint 10 script-mode resolution by setting true.
return this.#options.resolveGlobalVarsInScript === true;
}

public get variables(): Variable[] {
const variables = new Set<Variable>();
function recurse(scope: Scope): void {
Expand Down Expand Up @@ -259,6 +270,76 @@ export class ScopeManager {
return this.nestScope(new WithScope(this, this.currentScope, node));
}

/**
* Adds declared globals to the global scope.
*
* Used by ESLint to inject configured globals; required by ESLint 10+.
*
* We default to value-only globals (`isTypeVariable: false`, `isValueVariable: true`)
* so we don’t accidentally satisfy type-only references and hide missing ambient types.
* Unresolved references are rebound when names match.
* If a future use case needs type-capable injected globals, set `isTypeVariable: true`
* so type references can bind; the guard below will honor it.
*/
public addGlobals(names: string[]): void {
const globalScope = this.globalScope;
if (!globalScope || !Array.isArray(names)) {
return;
}

const unique = new Set<string>();
for (const name of names) {
if (typeof name === 'string' && name.length > 0) {
unique.add(name);
}
}
if (unique.size === 0) {
return;
}

for (const name of unique) {
if (!globalScope.set.has(name)) {
// mimic implicit global definition (no defs/identifiers) but ensure bookkeeping is consistent
globalScope.defineImplicitVariable(name, {
isTypeVariable: false,
isValueVariable: true,
});
}
}

const remainingThrough: typeof globalScope.through = [];
for (const ref of globalScope.through) {
const refName = ref.identifier.name;
const variable = unique.has(refName)
? globalScope.set.get(refName)
: null;
// Injected globals are value-only by default; bind value refs, and only bind type refs when the variable is marked type-capable.
const canBind =
variable &&
((ref.isValueReference && variable.isValueVariable) ||
(ref.isTypeReference && variable.isTypeVariable));
if (canBind) {
variable.references.push(ref);
ref.resolved = variable;
continue;
}
remainingThrough.push(ref);
}
globalScope.through.length = 0;
globalScope.through.push(...remainingThrough);

// Optional parity with eslint-scope: drop matching entries from implicit left-to-be-resolved.
// Access GlobalScope's private `implicit` field for eslint-scope compatibility; safe because
// implicit.leftToBeResolved is part of the eslint-scope contract. If eslint-scope changes this
// shape, we should revisit.
const implicit = getImplicit(globalScope);
if (implicit) {
implicit.leftToBeResolved = implicit.leftToBeResolved.filter(
ref => !unique.has(ref.identifier.name),
);
}
}

// Scope helpers

protected nestScope<T extends Scope>(scope: T): T;
Expand All @@ -271,3 +352,20 @@ export class ScopeManager {
return scope;
}
}

interface ImplicitState {
leftToBeResolved: Reference[];
}

function getImplicit(scope: GlobalScope | null): ImplicitState | undefined {
const candidate = scope as unknown as { implicit?: ImplicitState } | null;
// eslint-scope compat: implicit is a private field; we intentionally reach it to
// mirror eslint-scope's cleanup of implicit.leftToBeResolved on global injection.
if (!candidate?.implicit) {
return undefined;
}
if (!Array.isArray(candidate.implicit.leftToBeResolved)) {
return undefined;
}
return candidate.implicit;
}
12 changes: 12 additions & 0 deletions packages/scope-manager/src/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ export interface AnalyzeOptions {
*/
sourceType?: SourceType;

/**
* Whether to resolve references to global `var`/function declarations when `sourceType` is `script`.
* Defaults to `false` (ESLint 9 behavior) to preserve support for the current eslint range (^8.57.0 || ^9.0.0).
* Set to `true` to opt into ESLint 10 behavior where such references are resolved out of `through`.
*/
resolveGlobalVarsInScript?: boolean;

// TODO - remove this in v10
/**
* @deprecated This option never did what it was intended for and will be removed in a future major release.
Expand All @@ -74,6 +81,8 @@ const DEFAULT_OPTIONS: Required<AnalyzeOptions> = {
jsxFragmentName: null,
jsxPragma: 'React',
lib: ['es2018'],
// TODO(major): flip to true when ESLint 10 is within the supported range (see supported ESLint versions in project docs).
resolveGlobalVarsInScript: false,
sourceType: 'script',
};

Expand All @@ -99,6 +108,9 @@ export function analyze(
? DEFAULT_OPTIONS.jsxPragma
: providedOptions.jsxPragma,
lib: providedOptions?.lib ?? ['esnext'],
resolveGlobalVarsInScript:
providedOptions?.resolveGlobalVarsInScript ??
DEFAULT_OPTIONS.resolveGlobalVarsInScript,
sourceType: providedOptions?.sourceType ?? DEFAULT_OPTIONS.sourceType,
};

Expand Down
30 changes: 16 additions & 14 deletions packages/scope-manager/src/scope/ScopeBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,20 +330,22 @@ export abstract class ScopeBase<
return true;
}

// in script mode, only certain cases should be statically resolved
// Example:
// a `var` decl is ignored by the runtime if it clashes with a global name
// this means that we should not resolve the reference to the variable
const defs = variable.defs;
return (
defs.length > 0 &&
defs.every(def => {
if (def.type === DefinitionType.Variable && def.parent.kind === 'var') {
return false;
}
return true;
})
);
if (!scopeManager.shouldResolveGlobalVarsInScript()) {
// ESLint 8/9 behavior: in script mode, only certain cases should be statically resolved.
// Example: a `var` decl is ignored by the runtime if it clashes with a global name, so do not resolve it here.
// In effect: resolve only if no definition is a `var` declaration.
const defs = variable.defs;
return (
defs.length > 0 &&
defs.every(
def =>
def.type !== DefinitionType.Variable || def.parent.kind !== 'var',
)
);
}

// ESLint 10 behavior: resolve references to globals declared with var/function even in script mode.
return variable.defs.length > 0;
}

public close(scopeManager: ScopeManager): Scope | null {
Expand Down
Loading