-
-
Notifications
You must be signed in to change notification settings - Fork 221
feat: add types to ESLint Scope #709
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
base: main
Are you sure you want to change the base?
Conversation
| /** | ||
| * @fileoverview This file contains the types for ESLint Scope. | ||
| * It was initially extracted from the DefinitelyTyped repository. | ||
| */ |
There was a problem hiding this comment.
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. |
There was a problem hiding this 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.
| /** | ||
| * The identifier node of the reference. | ||
| */ | ||
| identifier: ESTree.Identifier; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
js/packages/eslint-scope/lib/index.d.cts
Line 568 in c6d38b2
| export class Reference implements eslint.Scope.Reference { |
|
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. |
lumirlumir
left a comment
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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?
js/packages/eslint-scope/lib/variable.js
Lines 77 to 83 in 26eb6e2
| Variable.CatchClause = "CatchClause"; | |
| Variable.Parameter = "Parameter"; | |
| Variable.FunctionName = "FunctionName"; | |
| Variable.ClassName = "ClassName"; | |
| Variable.Variable = "Variable"; | |
| Variable.ImportBinding = "ImportBinding"; | |
| Variable.ImplicitGlobalVariable = "ImplicitGlobalVariable"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
| Variable.ImportBinding, |
js/packages/eslint-scope/lib/referencer.js
Line 190 in 50f9908
| Variable.FunctionName, |
js/packages/eslint-scope/lib/referencer.js
Line 397 in 50f9908
| Variable.CatchClause, |
But, I'm also open to other suggestions.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
After: TypeScript doesn't show a warning:
| /** | ||
| * Represents a reference to a variable. | ||
| */ | ||
| export class Reference { |
There was a problem hiding this comment.
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?
js/packages/eslint-scope/lib/reference.js
Lines 146 to 162 in 26eb6e2
| /** | |
| * @constant Reference.READ | |
| * @private | |
| */ | |
| Reference.READ = READ; | |
| /** | |
| * @constant Reference.WRITE | |
| * @private | |
| */ | |
| Reference.WRITE = WRITE; | |
| /** | |
| * @constant Reference.RW | |
| * @private | |
| */ | |
| Reference.RW = RW; |
There was a problem hiding this comment.
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.
|
Okay, I think I've addressed all the feedback now. |
| /** | ||
| * Represents a variable in a scope. | ||
| */ | ||
| export class Variable<TReference extends Reference = Reference> { |
There was a problem hiding this comment.
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:
| Variable.ImportBinding, |
js/packages/eslint-scope/lib/referencer.js
Line 190 in 50f9908
| Variable.FunctionName, |
js/packages/eslint-scope/lib/referencer.js
Line 397 in 50f9908
| Variable.CatchClause, |
But, I'm also open to other suggestions.
Co-authored-by: 루밀LuMir <[email protected]>
Co-authored-by: 루밀LuMir <[email protected]>
01bd9aa to
9a1c587
Compare
lumirlumir
left a comment
There was a problem hiding this 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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| "@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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| "@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
@types/estree: https://npmgraph.js.org/?q=%40types%2Festree
estraverse: https://npmgraph.js.org/?q=estraverse
However, none of the packages above include eslint-visitor-keys under the hood, even though we're importing types from it:
So, I think this explains why the previous @types/eslint-scope also included eslint-visitor-keys as a sub-dependency:
| /** | ||
| * 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /** | |
| * 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?
js/packages/eslint-scope/lib/reference.js
Lines 68 to 87 in 50f9908
| 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> { |
There was a problem hiding this comment.
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:
After: TypeScript doesn't show a warning:
There was a problem hiding this comment.
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:
js/packages/eslint-scope/lib/index.js
Lines 141 to 144 in 50f9908
| export { | |
| /** @name module:escope.version */ | |
| eslintScopeVersion as version, |
js/packages/eslint-scope/lib/index.js
Lines 155 to 156 in 50f9908
| /** @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 { |
There was a problem hiding this comment.
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:
js/packages/eslint-scope/lib/scope-manager.js
Lines 195 to 197 in 50f9908
| 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:
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.
| Definition, | ||
| Reference, | ||
| ScopeManager, | ||
| Scope, | ||
| Variable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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:
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:
| isArgumentsMaterialized(): boolean; | ||
|
|
||
| /** | ||
| * Returns whether this scope has materialized `this` reference. | ||
| * @deprecated | ||
| */ | ||
| isThisMaterialized(): boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
js/packages/eslint-scope/lib/scope.js
Lines 446 to 453 in 50f9908
| /** | |
| * 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; | |
| } |
js/packages/eslint-scope/lib/scope.js
Lines 455 to 462 in 50f9908
| /** | |
| * 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?
js/packages/eslint-scope/lib/scope.js
Lines 682 to 711 in 50f9908
| 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>[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // $ExpectType Scope<Variable<Reference>, Reference>[] | |
| // $ExpectType Scope<Variable<Reference>, Reference>[] |
Minor typo!
| scope.block; | ||
| // $ExpectType boolean | ||
| scope.functionExpressionScope; | ||
| // $ExpectType Map<string, Variable<Reference>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // $ExpectType Map<string, Variable<Reference>> | |
| // $ExpectType Map<string, Variable<Reference>> |
Same here.
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)
addGlobals()toScopeManagerglobalScopeinScopeManageras nullableimplicitfromScopetoGlobalScopeand addedimplicit.variablesScopesubclasses@deprecatedtag to deprecated members per https://github.com/eslint/eslint/blob/v10.0.0-alpha.0/docs/src/extend/scope-manager-interface.mdfunctionExpressionScopetotrueorfalsedepending on whether theScopeisFunctionExpressionNameScoperesolve()inScope(the definition in@types/eslint-scopeis very different, possibly unrelated)PatternVisitorpackages/eslint-scope/libto verify they match the type declarationsReference#isStatic()now always returns a boolean even whenresolvedis not setScopeManager#isImpliedStrict()now always returns a boolean even when theimpliedStrictoption is not setRelated Issues
fixes #708
Is there anything you'd like reviewers to focus on?