Skip to content

Conversation

@fasttime
Copy link
Member

@fasttime fasttime commented Nov 20, 2025

Prerequisites checklist

What is the purpose of this pull request?

Add built-in types to ESLint Scope

What changes did you make? (Give an overview)

  • Copied and updated types and tests from DefinitelyTyped
  • Enabled type checking for source files in packages/eslint-scope/lib to verify they match the type declarations
  • Added npm scripts to test types
  • Added "Are the types wrong?" CI job
  • Linting *.ts and *.cts files
  • Minor fixes in the logic:
    • Reference#isStatic() now always returns a boolean even when resolved is not set
    • ScopeManager#isImpliedStrict() now always returns a boolean even when the impliedStrict option is not set

Related Issues

fixes #708

Is there anything you'd like reviewers to focus on?

@github-project-automation github-project-automation bot moved this to Needs Triage in Triage Nov 20, 2025
@nzakas nzakas moved this from Needs Triage to Implementing in Triage Nov 21, 2025
Comment on lines +1 to +4
/**
* @fileoverview This file contains the types for ESLint Scope.
* It was initially extracted from the DefinitelyTyped repository.
*/
Copy link
Member Author

Choose a reason for hiding this comment

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

Adapting the DefinitelyTyped types should be fine in terms of licensing, although I think it doesn't quite align with this project's guidelines:

- You will only Submit Contributions where You have authored 100% of the content.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds built-in TypeScript type definitions to the ESLint Scope package, eliminating the need for separate @types/eslint-scope packages. The types were adapted from DefinitelyTyped with enhancements including nullable global scope, additional scope subclasses, deprecated member tags, and improved type safety through type checking of JavaScript source files.

Key changes:

  • Added TypeScript declaration files (index.d.ts, index.d.cts) with comprehensive type definitions
  • Enabled type checking for JavaScript source files using JSDoc annotations
  • Added type testing infrastructure with npm scripts and CI workflow

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/eslint-scope/tsconfig.json TypeScript configuration for type-checking JavaScript source files
packages/eslint-scope/tests/types/tsconfig.json TypeScript configuration for type test files
packages/eslint-scope/tests/types/types.test.ts Comprehensive type tests covering all exported APIs
packages/eslint-scope/tests/types/cjs-import.test.cts CommonJS import type test
packages/eslint-scope/package.json Added type exports, dependencies, and scripts
packages/eslint-scope/lib/*.js Added JSDoc type annotations to source files
packages/eslint-scope/lib/index.d.ts ESM type declaration file (re-exports from .cts)
packages/eslint-scope/lib/index.d.cts Main CommonJS type declaration file
eslint.config.js Added TypeScript file linting configuration
package.json Added TypeScript parser and expect-type plugin
.github/workflows/ci.yml Added "Are the types wrong?" CI job

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@fasttime fasttime marked this pull request as ready for review November 25, 2025 11:06
/**
* The identifier node of the reference.
*/
identifier: ESTree.Identifier;
Copy link
Member

Choose a reason for hiding this comment

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

This can also be JSXIdentifier.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's true, but we need to update the base interface Scope.Reference in ESLint first, because we can't broaden the type of the identifier member in the implementing class.

export class Reference implements eslint.Scope.Reference {

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2025

Hi everyone, it looks like we lost track of this pull request. Please review and see what the next steps are. This pull request will auto-close in 7 days without an update.

@github-actions github-actions bot added the Stale label Dec 7, 2025
@lumirlumir lumirlumir added accepted and removed Stale labels Dec 8, 2025
Copy link
Member

@lumirlumir lumirlumir left a comment

Choose a reason for hiding this comment

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

This looks like a good start!

I've left some comments throughout, especially regarding documentation updates.

/**
* Represents a variable in a scope.
*/
export class Variable<TReference extends Reference = Reference> {
Copy link
Member

Choose a reason for hiding this comment

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

Question: there are some static properties like the following:

Should these types also be applied to the Variable class?

Variable.CatchClause = "CatchClause";
Variable.Parameter = "Parameter";
Variable.FunctionName = "FunctionName";
Variable.ClassName = "ClassName";
Variable.Variable = "Variable";
Variable.ImportBinding = "ImportBinding";
Variable.ImplicitGlobalVariable = "ImplicitGlobalVariable";

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. These are the possible values for the type property of aDefinition. These constants aren't documented, but neither are the constructor signatures for Scope, Variable, Definition, etc, which all have their type definitions. I'd say it's fine to add types in this case, but I don't feel strongly.

Copy link
Member

@lumirlumir lumirlumir Dec 11, 2025

Choose a reason for hiding this comment

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

I also don't feel strongly about it either, but I think it's helpful to have one, since the typing below is just string rather than a more specific type:

image

Variable.ImportBinding,

Variable.FunctionName,

Variable.CatchClause,

But, I'm also open to other suggestions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Could you clarify your suggestion with an example?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for not being clearer.

I intended to ask that the following (or similar) properties be added to the Variable class type:

/**
 * Represents a variable in a scope.
 */
export class Variable<TReference extends Reference = Reference> {
    // Existing implementation...

+   static readonly CatchClause: "CatchClause";
+   static readonly Parameter: "Parameter";
+   static readonly FunctionName: "FunctionName";
+   static readonly ClassName: "ClassName";
+   static readonly Variable: "Variable";
+   static readonly ImportBinding: "ImportBinding";
+   static readonly ImplicitGlobalVariable: "ImplicitGlobalVariable";
}

Similarly, in the Reference class:

export class Reference {
    // Existing implementation...

+   static readonly READ: 1;
+   static readonly WRITE: 2;
+   static readonly RW: 3;
}

As a result,

Before: TypeScript shows a warning:

Image

After: TypeScript doesn't show a warning:

Image

/**
* Represents a reference to a variable.
*/
export class Reference {
Copy link
Member

Choose a reason for hiding this comment

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

Question: there are also some static properties like the following:

Should these types also be applied to the Reference class?

/**
* @constant Reference.READ
* @private
*/
Reference.READ = READ;
/**
* @constant Reference.WRITE
* @private
*/
Reference.WRITE = WRITE;
/**
* @constant Reference.RW
* @private
*/
Reference.RW = RW;

Copy link
Member Author

Choose a reason for hiding this comment

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

As with the previous question, these values are undocumented, so I don't feel strongly whether they should be included in the types or not.

@fasttime fasttime requested a review from lumirlumir December 9, 2025 20:14
@fasttime
Copy link
Member Author

fasttime commented Dec 9, 2025

Okay, I think I've addressed all the feedback now.

/**
* Represents a variable in a scope.
*/
export class Variable<TReference extends Reference = Reference> {
Copy link
Member

@lumirlumir lumirlumir Dec 11, 2025

Choose a reason for hiding this comment

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

I also don't feel strongly about it either, but I think it's helpful to have one, since the typing below is just string rather than a more specific type:

image

Variable.ImportBinding,

Variable.FunctionName,

Variable.CatchClause,

But, I'm also open to other suggestions.

@fasttime fasttime requested a review from lumirlumir December 11, 2025 18:24
Copy link
Member

@lumirlumir lumirlumir left a comment

Choose a reason for hiding this comment

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

Thanks for all the hard work on the reviews!

I think I've gone through almost every file so far. I've left some more comments throughout.

]
},
"devDependencies": {
"@typescript-eslint/parser": "^8.47.0",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"@typescript-eslint/parser": "^8.47.0",
"@typescript-eslint/parser": "^8.49.0",

Non-blocking, but a newer version just has been released :)

],
"dependencies": {
"@types/esrecurse": "^4.3.1",
"@types/estree": "^1.0.8",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"@types/estree": "^1.0.8",
"@types/estree": "^1.0.8",
"eslint-visitor-keys": "^5.0.0"

I'm not 100% sure, but for the same reason mentioned in #709 (comment), I think eslint-visitor-keys may also need to be listed in dependencies instead of dev-dependencies.


Assuming we've installed the new version of eslint-scope that includes type support for this change.

There are now four sub-dependencies: @types/esrecurse, @types/estree, esrecurse, and estraverse:

@types/esrecurse: https://npmgraph.js.org/?q=%40types%2Fesrecurse

Image

@types/estree: https://npmgraph.js.org/?q=%40types%2Festree

Image

esrecurse: https://npmgraph.js.org/?q=esrecurse

Image

estraverse: https://npmgraph.js.org/?q=estraverse

Image

However, none of the packages above include eslint-visitor-keys under the hood, even though we're importing types from it:

Image

So, I think this explains why the previous @types/eslint-scope also included eslint-visitor-keys as a sub-dependency:

https://npmgraph.js.org/?q=%40types%2Feslint-scope#select=%40types%2Festree%401.0.8

Image

Comment on lines +643 to +657
/**
* The expression being written, if applicable.
*/
writeExpr: ESTree.Expression | null;

/**
* Whether this is a partial reference.
* @deprecated
*/
partial: boolean;

/**
* Whether this is an initialization reference.
*/
init: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* The expression being written, if applicable.
*/
writeExpr: ESTree.Expression | null;
/**
* Whether this is a partial reference.
* @deprecated
*/
partial: boolean;
/**
* Whether this is an initialization reference.
*/
init: boolean;
/**
* The expression being written, if applicable.
*/
writeExpr?: ESTree.Expression | null | undefined;
/**
* Whether this is a partial reference.
* @deprecated
*/
partial?: boolean | undefined;
/**
* Whether this is an initialization reference.
*/
init?: boolean | undefined;

Would it make sense to mark writeExpr, partial, and init as optional properties?

In the actual implementation, these properties may or may not be set depending on the result of the isWrite() method, so I think they can sometimes be omitted. What do you think?

if (this.isWrite()) {
/**
* If reference is writeable, this is the tree being written to it.
* @member {espreeNode} Reference#writeExpr
*/
this.writeExpr = writeExpr;
/**
* Whether the Reference might refer to a partial value of writeExpr.
* @member {boolean} Reference#partial
*/
this.partial = partial;
/**
* Whether the Reference is to write of initialization.
* @member {boolean} Reference#init
*/
this.init = init;
}

/**
* Represents a variable in a scope.
*/
export class Variable<TReference extends Reference = Reference> {
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for not being clearer.

I intended to ask that the following (or similar) properties be added to the Variable class type:

/**
 * Represents a variable in a scope.
 */
export class Variable<TReference extends Reference = Reference> {
    // Existing implementation...

+   static readonly CatchClause: "CatchClause";
+   static readonly Parameter: "Parameter";
+   static readonly FunctionName: "FunctionName";
+   static readonly ClassName: "ClassName";
+   static readonly Variable: "Variable";
+   static readonly ImportBinding: "ImportBinding";
+   static readonly ImplicitGlobalVariable: "ImplicitGlobalVariable";
}

Similarly, in the Reference class:

export class Reference {
    // Existing implementation...

+   static readonly READ: 1;
+   static readonly WRITE: 2;
+   static readonly RW: 3;
}

As a result,

Before: TypeScript shows a warning:

Image

After: TypeScript doesn't show a warning:

Image

Copy link
Member

Choose a reason for hiding this comment

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

Currently, we also export version and the Referencer type from the main entry point, index.js:

Image

export {
/** @name module:escope.version */
eslintScopeVersion as version,

/** @name module:escope.Referencer */
Referencer,

However, it seems the two types version and Referencer are not defined in index.d.cts. Would it make sense to include them in the index.d.cts file?

/**
* Manages the scope hierarchy of an AST.
*/
export class ScopeManager {
Copy link
Member

Choose a reason for hiding this comment

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

Just out of curiosity, it seems like there are two no-op methods, attach and detach, in ScopeManager:

attach() { } // eslint-disable-line class-methods-use-this -- Desired as instance method
detach() { } // eslint-disable-line class-methods-use-this -- Desired as instance method

I checked the archived eslint-scope repository and these methods appear to have been present since the project’s early days:

https://github.com/eslint/eslint-scope/blame/9b58d9d6943c6b74185bed2a27dad210fd9c02f0/lib/scope-manager.js

So, I am wonder, would it make sense to add these methods to the ScopeManager class type? They appear to be public methods, since they don’t use the __ prefix.

Comment on lines +12 to +16
Definition,
Reference,
ScopeManager,
Scope,
Variable
Copy link
Member

@lumirlumir lumirlumir Dec 14, 2025

Choose a reason for hiding this comment

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

Suggested change
Definition,
Reference,
ScopeManager,
Scope,
Variable
Definition,
PatternVisitor,
Reference,
Referencer,
ScopeManager,
Scope,
Variable,
analyze,
version,

Looking at the built packages/eslint-scope/dist/eslint-scope.cjs file, it appears there are some additional named exports:

Image

The Referencer and version types aren't defined as noted earlier (Ref: #709 (comment)), so I think it would be helpful to include them as well if we add their type definitions.

FYI: Currently, the type definitions for Referencer and version are missing:

Image

Comment on lines +306 to +312
isArgumentsMaterialized(): boolean;

/**
* Returns whether this scope has materialized `this` reference.
* @deprecated
*/
isThisMaterialized(): boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
isArgumentsMaterialized(): boolean;
/**
* Returns whether this scope has materialized `this` reference.
* @deprecated
*/
isThisMaterialized(): boolean;
isArgumentsMaterialized(): true;
/**
* Returns whether this scope has materialized `this` reference.
* @deprecated
*/
isThisMaterialized(): true;

It looks like these two methods always return true because they've been deprecated, so I think using true instead of boolean may be more appropriate here.

/**
* returns this scope has materialized arguments
* @function Scope#isArgumentsMaterialized
* @returns {boolean} arguemnts materialized
*/
isArgumentsMaterialized() { // eslint-disable-line class-methods-use-this -- Desired as instance method
return true;
}

/**
* returns this scope has materialized `this` reference
* @function Scope#isThisMaterialized
* @returns {boolean} this materialized
*/
isThisMaterialized() { // eslint-disable-line class-methods-use-this -- Desired as instance method
return true;
}

Also, since only FunctionScope returns a boolean instead of true, it might be nice to override these two methods only in FunctionScope. What do you think?

isArgumentsMaterialized() {
// TODO(Constellation)
// We can more aggressive on this condition like this.
//
// function t() {
// // arguments of t is always hidden.
// function arguments() {
// }
// }
if (this.block.type === Syntax.ArrowFunctionExpression) {
return false;
}
if (!this.isStatic()) {
return true;
}
const variable = this.set.get("arguments");
assert(variable, "Always have arguments variable.");
return variable.tainted || variable.references.length !== 0;
}
isThisMaterialized() {
if (!this.isStatic()) {
return true;
}
return this.thisFound;
}

For example:

export class FunctionScope extends Scope {
    /**
     * Creates a new FunctionScope instance.
     * @param scopeManager The scope manager this scope belongs to.
     * @param upperScope The parent scope.
     * @param block The AST node that created this scope.
     * @param isMethodDefinition Whether this scope is for a method definition.
     */
    constructor(
        scopeManager: ScopeManager,
        upperScope: Scope,
        block: ESTree.Node,
        isMethodDefinition: boolean,
    );

    type: "function";

    functionExpressionScope: false;

+   isArgumentsMaterialized(): boolean;

+   isThisMaterialized(): boolean;
}

scope.variables;
// $ExpectType Reference[]
scope.references;
// $ExpectType Scope<Variable<Reference>, Reference>[]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// $ExpectType Scope<Variable<Reference>, Reference>[]
// $ExpectType Scope<Variable<Reference>, Reference>[]

Minor typo!

scope.block;
// $ExpectType boolean
scope.functionExpressionScope;
// $ExpectType Map<string, Variable<Reference>>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// $ExpectType Map<string, Variable<Reference>>
// $ExpectType Map<string, Variable<Reference>>

Same here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Implementing

Development

Successfully merging this pull request may close these issues.

Change Request: update eslint-scope types

4 participants