Skip to content

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
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Next Next commit
feat: support TS syntax in no-unused-vars
  • Loading branch information
Tanujkanti4441 committed Jun 2, 2025
commit 828f5dbc15f267a49a81bcfff0e8b7d3aaa9af63
176 changes: 167 additions & 9 deletions lib/rules/no-unused-vars.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

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

Image

}

/**
* 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: {
Expand Down Expand Up @@ -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
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
},
},
},
dialects: ["typescript", "javascript"],
language: "javascript",
},

Could you add dialects and language fields to the meta property? As far as I know, the newly updated rules that support TypeScript use these fields.

eslint/lib/rules/no-var.js

Lines 202 to 203 in 28cc7ab

dialects: ["typescript", "javascript"],
language: "javascript",


Expand Down Expand Up @@ -398,7 +442,7 @@ module.exports = {
* @private
*/
function isReadRef(ref) {
return ref.isRead();
return ref.isRead() || isTypeReference(ref.identifier);
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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) &&
Expand All @@ -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);
}

Expand Down Expand Up @@ -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),
Expand All @@ -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 =>
Expand All @@ -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(),
)
Expand Down Expand Up @@ -1709,4 +1867,4 @@ module.exports = {
},
};
},
};
};
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy