-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat: support TypeScript syntax in no-unused-vars
#19812
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
Open
Tanujkanti4441
wants to merge
11
commits into
eslint:main
Choose a base branch
from
Tanujkanti4441:typescript-support-rule
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,700
−23
Open
Changes from 1 commit
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
828f5db
feat: support TS syntax in no-unused-vars
Tanujkanti4441 8c8e73d
add tests
Tanujkanti4441 42fc6da
add tests
Tanujkanti4441 084a407
add more ts tests
Tanujkanti4441 02cdd7f
add tests
Tanujkanti4441 c7140a0
fix CI error
Tanujkanti4441 9c1a1c9
add default case to resolve error
Tanujkanti4441 47a0c71
add docs examples
Tanujkanti4441 e4494fa
apply suggestions
Tanujkanti4441 602f618
apply some suggestions
Tanujkanti4441 4213540
format test code
Tanujkanti4441 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
feat: support TS syntax in no-unused-vars
- Loading branch information
commit 828f5dbc15f267a49a81bcfff0e8b7d3aaa9af63
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -39,6 +39,48 @@ const astUtils = require("./utils/ast-utils"); | |||||||||||||||||||
// Rule Definition | ||||||||||||||||||||
//------------------------------------------------------------------------------ | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Checks if the current file is a TypeScript definition file | ||||||||||||||||||||
* @param {string|undefined} filename The filename to check | ||||||||||||||||||||
* @returns {boolean} `true` if the file is a .d.ts file | ||||||||||||||||||||
* @private | ||||||||||||||||||||
*/ | ||||||||||||||||||||
function isDefinitionFile(filename) { | ||||||||||||||||||||
return filename?.endsWith(".d.ts") ?? false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Checks if a reference is a type-only usage (used in TypeScript type position) | ||||||||||||||||||||
* @param {Object} identifier The identifier to check | ||||||||||||||||||||
* @returns {boolean} `true` if the identifier is only used in a type position | ||||||||||||||||||||
* @private | ||||||||||||||||||||
*/ | ||||||||||||||||||||
function isTypeReference(identifier) { | ||||||||||||||||||||
const parent = identifier.parent; | ||||||||||||||||||||
|
||||||||||||||||||||
// TypeScript specific type contexts | ||||||||||||||||||||
if ( | ||||||||||||||||||||
// Type references: (x: Type) | ||||||||||||||||||||
parent.type === "TSTypeReference" || | ||||||||||||||||||||
// Type queries: (x: typeof Identifier) | ||||||||||||||||||||
parent.type === "TSTypeQuery" || | ||||||||||||||||||||
// Type parameters: <T> | ||||||||||||||||||||
parent.type === "TSTypeParameter" || | ||||||||||||||||||||
// Interface declarations | ||||||||||||||||||||
parent.type === "TSInterfaceDeclaration" || | ||||||||||||||||||||
// Type aliases | ||||||||||||||||||||
parent.type === "TSTypeAliasDeclaration" || | ||||||||||||||||||||
// Import type: import('module').Type | ||||||||||||||||||||
parent.type === "TSImportType" || | ||||||||||||||||||||
// Any other TS type node | ||||||||||||||||||||
(parent.type?.startsWith("TS") && parent.type.includes("Type")) | ||||||||||||||||||||
) { | ||||||||||||||||||||
return true; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
return false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** @type {import('../types').Rule.RuleModule} */ | ||||||||||||||||||||
module.exports = { | ||||||||||||||||||||
meta: { | ||||||||||||||||||||
|
@@ -104,6 +146,8 @@ module.exports = { | |||||||||||||||||||
usedIgnoredVar: | ||||||||||||||||||||
"'{{varName}}' is marked as ignored but is used{{additional}}.", | ||||||||||||||||||||
removeVar: "Remove unused variable '{{varName}}'.", | ||||||||||||||||||||
usedOnlyAsType: | ||||||||||||||||||||
"'{{varName}}' is {{action}} but only used as a type{{additional}}.", | ||||||||||||||||||||
}, | ||||||||||||||||||||
}, | ||||||||||||||||||||
Comment on lines
115
to
116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Could you add Lines 202 to 203 in 28cc7ab
|
||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -398,7 +442,7 @@ module.exports = { | |||||||||||||||||||
* @private | ||||||||||||||||||||
*/ | ||||||||||||||||||||
function isReadRef(ref) { | ||||||||||||||||||||
return ref.isRead(); | ||||||||||||||||||||
return ref.isRead() || isTypeReference(ref.identifier); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
|
@@ -734,10 +778,9 @@ module.exports = { | |||||||||||||||||||
function collectUnusedVariables(scope, unusedVars) { | ||||||||||||||||||||
const variables = scope.variables; | ||||||||||||||||||||
const childScopes = scope.childScopes; | ||||||||||||||||||||
let i, l; | ||||||||||||||||||||
|
||||||||||||||||||||
if (scope.type !== "global" || config.vars === "all") { | ||||||||||||||||||||
for (i = 0, l = variables.length; i < l; ++i) { | ||||||||||||||||||||
for (let i = 0, l = variables.length; i < l; ++i) { | ||||||||||||||||||||
const variable = variables[i]; | ||||||||||||||||||||
|
||||||||||||||||||||
// skip a variable of class itself name in the class scope | ||||||||||||||||||||
|
@@ -770,6 +813,26 @@ module.exports = { | |||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Skip TypeScript specific declarations that are automatically exported in definition files | ||||||||||||||||||||
if (isDefinitionFile(context.filename)) { | ||||||||||||||||||||
const parent = variable.defs[0]?.node?.parent; | ||||||||||||||||||||
if (parent) { | ||||||||||||||||||||
// Skip interface declarations in d.ts files | ||||||||||||||||||||
if ( | ||||||||||||||||||||
parent.type === "TSInterfaceDeclaration" || | ||||||||||||||||||||
parent.type === "TSTypeAliasDeclaration" || | ||||||||||||||||||||
parent.type === "TSModuleDeclaration" | ||||||||||||||||||||
) { | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Skip declared items in d.ts files | ||||||||||||||||||||
if (parent.declare) { | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// explicit global variables don't have definitions. | ||||||||||||||||||||
const def = variable.defs[0]; | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -819,6 +882,16 @@ module.exports = { | |||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Skip TypeScript specific items | ||||||||||||||||||||
if ( | ||||||||||||||||||||
def.type === "TSEnumName" || | ||||||||||||||||||||
def.type === "TSModuleName" || | ||||||||||||||||||||
def.node.type === "TSInterfaceDeclaration" || | ||||||||||||||||||||
def.node.type === "TSTypeAliasDeclaration" | ||||||||||||||||||||
) { | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// skip catch variables | ||||||||||||||||||||
if (type === "CatchClause") { | ||||||||||||||||||||
if (config.caughtErrors === "none") { | ||||||||||||||||||||
|
@@ -919,6 +992,41 @@ module.exports = { | |||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Check if variable is used only in type positions | ||||||||||||||||||||
const isOnlyUsedAsType = variable.references.some(ref => | ||||||||||||||||||||
// If it's not a type reference, it's a value reference | ||||||||||||||||||||
isTypeReference(ref.identifier), | ||||||||||||||||||||
); | ||||||||||||||||||||
|
||||||||||||||||||||
const isImportUsedOnlyAsType = | ||||||||||||||||||||
isOnlyUsedAsType && | ||||||||||||||||||||
variable.defs.some(d => d.type === "ImportBinding"); | ||||||||||||||||||||
|
||||||||||||||||||||
if (isImportUsedOnlyAsType) { | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Skip reporting variables that are only used as types | ||||||||||||||||||||
if (variable.references.length > 0 && isOnlyUsedAsType) { | ||||||||||||||||||||
// We can optionally report it as type-only usage | ||||||||||||||||||||
if ( | ||||||||||||||||||||
def && | ||||||||||||||||||||
!isExported(variable) && | ||||||||||||||||||||
!hasRestSpreadSibling(variable) | ||||||||||||||||||||
) { | ||||||||||||||||||||
context.report({ | ||||||||||||||||||||
node: variable.identifiers[0], | ||||||||||||||||||||
messageId: "usedOnlyAsType", | ||||||||||||||||||||
data: variable.references.some(ref => | ||||||||||||||||||||
ref.isWrite(), | ||||||||||||||||||||
) | ||||||||||||||||||||
? getAssignedMessageData(variable) | ||||||||||||||||||||
: getDefinedMessageData(variable), | ||||||||||||||||||||
}); | ||||||||||||||||||||
} | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if ( | ||||||||||||||||||||
!isUsedVariable(variable) && | ||||||||||||||||||||
!isExported(variable) && | ||||||||||||||||||||
|
@@ -929,7 +1037,7 @@ module.exports = { | |||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
for (i = 0, l = childScopes.length; i < l; ++i) { | ||||||||||||||||||||
for (let i = 0, l = childScopes.length; i < l; ++i) { | ||||||||||||||||||||
collectUnusedVariables(childScopes[i], unusedVars); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -1641,6 +1749,33 @@ module.exports = { | |||||||||||||||||||
//-------------------------------------------------------------------------- | ||||||||||||||||||||
|
||||||||||||||||||||
return { | ||||||||||||||||||||
// Add handlers for TypeScript-specific module declarations | ||||||||||||||||||||
TSModuleDeclaration(node) { | ||||||||||||||||||||
if (isDefinitionFile(context.filename) || node.declare) { | ||||||||||||||||||||
// Mark all identifiers in ambient modules as used | ||||||||||||||||||||
const moduleScope = sourceCode.getScope(node); | ||||||||||||||||||||
if (moduleScope) { | ||||||||||||||||||||
for (const variable of moduleScope.variables) { | ||||||||||||||||||||
variable.eslintUsed = true; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
}, | ||||||||||||||||||||
|
||||||||||||||||||||
// Make interfaces and type aliases considered "used" in d.ts files | ||||||||||||||||||||
"TSInterfaceDeclaration, TSTypeAliasDeclaration"(node) { | ||||||||||||||||||||
if (isDefinitionFile(context.filename) || node.declare) { | ||||||||||||||||||||
const id = node.id; | ||||||||||||||||||||
if (id && id.type === "Identifier") { | ||||||||||||||||||||
const scope = sourceCode.getScope(node); | ||||||||||||||||||||
const variable = scope.set.get(id.name); | ||||||||||||||||||||
if (variable) { | ||||||||||||||||||||
variable.eslintUsed = true; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
}, | ||||||||||||||||||||
|
||||||||||||||||||||
"Program:exit"(programNode) { | ||||||||||||||||||||
const unusedVars = collectUnusedVariables( | ||||||||||||||||||||
sourceCode.getScope(programNode), | ||||||||||||||||||||
|
@@ -1652,6 +1787,23 @@ module.exports = { | |||||||||||||||||||
|
||||||||||||||||||||
// Report the first declaration. | ||||||||||||||||||||
if (unusedVar.defs.length > 0) { | ||||||||||||||||||||
// Check if it's used only in type positions | ||||||||||||||||||||
const isOnlyUsedAsType = | ||||||||||||||||||||
unusedVar.references.length > 0 && | ||||||||||||||||||||
unusedVar.references.every(ref => | ||||||||||||||||||||
isTypeReference(ref.identifier), | ||||||||||||||||||||
); | ||||||||||||||||||||
|
||||||||||||||||||||
// For import specifiers used only as types, don't report them as unused | ||||||||||||||||||||
if ( | ||||||||||||||||||||
isOnlyUsedAsType && | ||||||||||||||||||||
unusedVar.defs.some( | ||||||||||||||||||||
def => def.type === "ImportBinding", | ||||||||||||||||||||
) | ||||||||||||||||||||
) { | ||||||||||||||||||||
continue; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// report last write reference, https://github.com/eslint/eslint/issues/14324 | ||||||||||||||||||||
const writeReferences = unusedVar.references.filter( | ||||||||||||||||||||
ref => | ||||||||||||||||||||
|
@@ -1666,11 +1818,17 @@ module.exports = { | |||||||||||||||||||
referenceToReport = writeReferences.at(-1); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
const node = referenceToReport | ||||||||||||||||||||
? referenceToReport.identifier | ||||||||||||||||||||
: unusedVar.identifiers[0]; | ||||||||||||||||||||
|
||||||||||||||||||||
const messageId = isOnlyUsedAsType | ||||||||||||||||||||
? "usedOnlyAsType" | ||||||||||||||||||||
: "unusedVar"; | ||||||||||||||||||||
|
||||||||||||||||||||
context.report({ | ||||||||||||||||||||
node: referenceToReport | ||||||||||||||||||||
? referenceToReport.identifier | ||||||||||||||||||||
: unusedVar.identifiers[0], | ||||||||||||||||||||
messageId: "unusedVar", | ||||||||||||||||||||
node, | ||||||||||||||||||||
messageId, | ||||||||||||||||||||
data: unusedVar.references.some(ref => | ||||||||||||||||||||
ref.isWrite(), | ||||||||||||||||||||
) | ||||||||||||||||||||
|
@@ -1709,4 +1867,4 @@ module.exports = { | |||||||||||||||||||
}, | ||||||||||||||||||||
}; | ||||||||||||||||||||
}, | ||||||||||||||||||||
}; | ||||||||||||||||||||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Can we add a logic to the
isDefinitionFile
helper function to also check for.d.mts
and.d.cts
files?Here is a reference for it:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#new-file-extensions