-
-
Notifications
You must be signed in to change notification settings - Fork 482
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): add eslint/no-unused-vars (⭐ attempt 3.2) (#4445)
> Re-creation of #4427 due to rebasing issues. Original attempt: #642 ----- Third time's the charm? Each time I attempt this rule, I find a bunch of bugs in `Semantic`, and I expect this attempt to be no different. Expect sidecar issues+PRs stemming from this PR here. ## Not Supported These are cases supported in the original eslint rule, but that I'm intentionally deciding not to support - export comments in scripts ```js /* exported a */ var a; ``` - global comments ```js /* global a */ var a; ``` ## Behavior Changes These are intentional deviations from the original rule's behavior: - logical re-assignments are not considered usages ```js // passes in eslint/no-unused-vars, fails in this implementation let a = 0; a ||= 1; let b = 0; b &&= 2; let c = undefined; c ??= [] ``` ## Known Limitations - Lint rules do not have babel or tsconfig information, meaning we can't determine if `React` imports are being used or not. The relevant tsconfig settings here are `jsx`, `jsxPragma`, and `jsxFragmentName`. To accommodate this, all imports to symbols named `React` or `h` are ignored in JSX files. - References to symbols used in JSDoc `{@link}` tags are not created, so symbols that are only used in doc comments will be reported as unused. See: #4443 - `.vue` files are skipped completely, since variables can be used in templates in ways we cannot detect > note: `.d.ts` files are skipped as well. ## Todo - [x] Skip unused TS enum members on used enums - [x] Skip unused parameters followed by used variables in object/array spreads - [x] Re-assignments to array/object spreads do not respect `destructuredArrayIgnorePattern` (related to: #4435) - [x] #4493 - [x] References inside a nested scope are not considered usages (#4447) - [x] Port over typescript-eslint test cases _(wip, they've been copied and I'm slowly enabling them)_ - [x] Handle constructor properties ```ts class Foo { constructor(public a) {} // `a` should be allowed } ``` - [x] Read references in sequence expressions (that are not in the last position) should not count as a usage ```js let a = 0; let b = (a++, 0); console.log(b) ``` > Honestly, is anyone even writing code like this? - [x] function overload signatures should not be reported - [x] Named functions returned from other functions get incorrectly reported as unused (found by @camc314) ```js function foo() { return function bar() { } } Foo()() ``` - [x] false positive for TS modules within ambient modules ```ts declare global { // incorrectly marked as unused namespace jest { } } ``` ## Blockers - #4436 - #4437 - #4446 - #4447 - #4494 - #4495 ## Non-Blocking Issues - #4443 - #4475 (prevents checks on exported symbols from namespaces)
- Loading branch information
Showing
40 changed files
with
8,353 additions
and
10 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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
290 changes: 290 additions & 0 deletions
290
crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs
This file contains 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 |
---|---|---|
@@ -0,0 +1,290 @@ | ||
//! This module checks if an unused variable is allowed. Note that this does not | ||
//! consider variables ignored by name pattern, but by where they are declared. | ||
#[allow(clippy::wildcard_imports)] | ||
use oxc_ast::{ast::*, AstKind}; | ||
use oxc_semantic::{AstNode, AstNodeId, Semantic}; | ||
use oxc_span::GetSpan; | ||
|
||
use crate::rules::eslint::no_unused_vars::binding_pattern::{BindingContext, HasAnyUsedBinding}; | ||
|
||
use super::{options::ArgsOption, NoUnusedVars, Symbol}; | ||
|
||
impl<'s, 'a> Symbol<'s, 'a> { | ||
/// Returns `true` if this function is use. | ||
/// | ||
/// Checks for these cases | ||
/// 1. passed as a callback to another [`CallExpression`] or [`NewExpression`] | ||
/// 2. invoked as an IIFE | ||
/// 3. Returned from another function | ||
/// 4. Used as an attribute in a JSX element | ||
#[inline] | ||
pub fn is_function_or_class_declaration_used(&self) -> bool { | ||
#[cfg(debug_assertions)] | ||
{ | ||
let kind = self.declaration().kind(); | ||
assert!(kind.is_function_like() || matches!(kind, AstKind::Class(_))); | ||
} | ||
|
||
for parent in self.iter_parents() { | ||
match parent.kind() { | ||
AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) => { | ||
continue; | ||
} | ||
// Returned from another function. Definitely won't be the same | ||
// function because we're walking up from its declaration | ||
AstKind::ReturnStatement(_) | ||
// <Component onClick={function onClick(e) { }} /> | ||
| AstKind::JSXExpressionContainer(_) | ||
// Function declaration is passed as an argument to another function. | ||
| AstKind::CallExpression(_) | AstKind::Argument(_) | ||
// e.g. `const x = { foo: function foo() {} }` | ||
| AstKind::ObjectProperty(_) | ||
// e.g. var foo = function bar() { } | ||
// we don't want to check for violations on `bar`, just `foo` | ||
| AstKind::VariableDeclarator(_) | ||
=> { | ||
return true; | ||
} | ||
// !function() {}; is an IIFE | ||
AstKind::UnaryExpression(expr) => return expr.operator.is_not(), | ||
// function is used as a value for an assignment | ||
// e.g. Array.prototype.sort ||= function sort(a, b) { } | ||
AstKind::AssignmentExpression(assignment) if assignment.right.span().contains_inclusive(self.span()) => { | ||
return self != &assignment.left; | ||
} | ||
_ => { | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
false | ||
} | ||
|
||
fn is_declared_in_for_of_loop(&self) -> bool { | ||
for parent in self.iter_parents() { | ||
match parent.kind() { | ||
AstKind::ParenthesizedExpression(_) | ||
| AstKind::VariableDeclaration(_) | ||
| AstKind::BindingIdentifier(_) | ||
| AstKind::SimpleAssignmentTarget(_) | ||
| AstKind::AssignmentTarget(_) => continue, | ||
AstKind::ForInStatement(ForInStatement { body, .. }) | ||
| AstKind::ForOfStatement(ForOfStatement { body, .. }) => match body { | ||
Statement::ReturnStatement(_) => return true, | ||
Statement::BlockStatement(b) => { | ||
return b | ||
.body | ||
.first() | ||
.is_some_and(|s| matches!(s, Statement::ReturnStatement(_))) | ||
} | ||
_ => return false, | ||
}, | ||
_ => return false, | ||
} | ||
} | ||
|
||
false | ||
} | ||
|
||
pub fn is_in_declared_module(&self) -> bool { | ||
let scopes = self.scopes(); | ||
let nodes = self.nodes(); | ||
scopes.ancestors(self.scope_id()) | ||
.map(|scope_id| scopes.get_node_id(scope_id)) | ||
.map(|node_id| nodes.get_node(node_id)) | ||
.any(|node| matches!(node.kind(), AstKind::TSModuleDeclaration(namespace) if is_ambient_namespace(namespace))) | ||
} | ||
} | ||
|
||
#[inline] | ||
fn is_ambient_namespace(namespace: &TSModuleDeclaration) -> bool { | ||
namespace.declare || namespace.kind.is_global() | ||
} | ||
|
||
impl NoUnusedVars { | ||
#[allow(clippy::unused_self)] | ||
pub(super) fn is_allowed_class_or_function(&self, symbol: &Symbol<'_, '_>) -> bool { | ||
symbol.is_function_or_class_declaration_used() | ||
// || symbol.is_function_or_class_assigned_to_same_name_variable() | ||
} | ||
|
||
#[allow(clippy::unused_self)] | ||
pub(super) fn is_allowed_ts_namespace<'a>( | ||
&self, | ||
symbol: &Symbol<'_, 'a>, | ||
namespace: &TSModuleDeclaration<'a>, | ||
) -> bool { | ||
if is_ambient_namespace(namespace) { | ||
return true; | ||
} | ||
symbol.is_in_declared_module() | ||
} | ||
|
||
/// Returns `true` if this unused variable declaration should be allowed | ||
/// (i.e. not reported) | ||
pub(super) fn is_allowed_variable_declaration<'a>( | ||
&self, | ||
symbol: &Symbol<'_, 'a>, | ||
decl: &VariableDeclarator<'a>, | ||
) -> bool { | ||
if decl.kind.is_var() && self.vars.is_local() && symbol.is_root() { | ||
return true; | ||
} | ||
|
||
// allow unused iterators, since they're required for valid syntax | ||
if symbol.is_declared_in_for_of_loop() { | ||
return true; | ||
} | ||
|
||
false | ||
} | ||
|
||
#[allow(clippy::unused_self)] | ||
pub(super) fn is_allowed_type_parameter( | ||
&self, | ||
symbol: &Symbol<'_, '_>, | ||
declaration_id: AstNodeId, | ||
) -> bool { | ||
matches!(symbol.nodes().parent_kind(declaration_id), Some(AstKind::TSMappedType(_))) | ||
} | ||
|
||
/// Returns `true` if this unused parameter should be allowed (i.e. not | ||
/// reported) | ||
pub(super) fn is_allowed_argument<'a>( | ||
&self, | ||
semantic: &Semantic<'a>, | ||
symbol: &Symbol<'_, 'a>, | ||
param: &FormalParameter<'a>, | ||
) -> bool { | ||
// early short-circuit when no argument checking should be performed | ||
if self.args.is_none() { | ||
return true; | ||
} | ||
|
||
// find FormalParameters. Should be the next parent of param, but this | ||
// is safer. | ||
let Some((params, params_id)) = symbol.iter_parents().find_map(|p| { | ||
if let AstKind::FormalParameters(params) = p.kind() { | ||
Some((params, p.id())) | ||
} else { | ||
None | ||
} | ||
}) else { | ||
debug_assert!(false, "FormalParameter should always have a parent FormalParameters"); | ||
return false; | ||
}; | ||
|
||
if Self::is_allowed_param_because_of_method(semantic, param, params_id) { | ||
return true; | ||
} | ||
|
||
// Parameters are always checked. Must be done after above checks, | ||
// because in those cases a parameter is required. However, even if | ||
// `args` is `all`, it may be ignored using `ignoreRestSiblings` or `destructuredArrayIgnorePattern`. | ||
if self.args.is_all() { | ||
return false; | ||
} | ||
|
||
debug_assert_eq!(self.args, ArgsOption::AfterUsed); | ||
|
||
// from eslint rule documentation: | ||
// after-used - unused positional arguments that occur before the last | ||
// used argument will not be checked, but all named arguments and all | ||
// positional arguments after the last used argument will be checked. | ||
|
||
// unused non-positional arguments are never allowed | ||
if param.pattern.kind.is_destructuring_pattern() { | ||
return false; | ||
} | ||
|
||
// find the index of the parameter in the parameters list. We want to | ||
// check all parameters after this one for usages. | ||
let position = | ||
params.items.iter().enumerate().find(|(_, p)| p.span == param.span).map(|(i, _)| i); | ||
debug_assert!( | ||
position.is_some(), | ||
"could not find FormalParameter in a FormalParameters node that is its parent." | ||
); | ||
let Some(position) = position else { | ||
return false; | ||
}; | ||
|
||
// This is the last parameter, so need to check for usages on following parameters | ||
if position == params.items.len() - 1 { | ||
return false; | ||
} | ||
|
||
let ctx = BindingContext { options: self, semantic }; | ||
params | ||
.items | ||
.iter() | ||
.skip(position + 1) | ||
// has_modifier() to handle: | ||
// constructor(unused: number, public property: string) {} | ||
// no need to check if param is in a constructor, because if it's | ||
// not that's a parse error. | ||
.any(|p| p.has_modifier() || p.pattern.has_any_used_binding(ctx)) | ||
} | ||
|
||
/// `params_id` is the [`AstNodeId`] to a [`AstKind::FormalParameters`] node. | ||
/// | ||
/// The following allowed conditions are handled: | ||
/// 1. setter parameters - removing them causes a syntax error. | ||
/// 2. TS constructor property definitions - they declare class members. | ||
fn is_allowed_param_because_of_method<'a>( | ||
semantic: &Semantic<'a>, | ||
param: &FormalParameter<'a>, | ||
params_id: AstNodeId, | ||
) -> bool { | ||
let mut parents_iter = semantic.nodes().iter_parents(params_id).skip(1).map(AstNode::kind); | ||
|
||
// in function declarations, the parent immediately before the | ||
// FormalParameters is a TSDeclareBlock | ||
let Some(parent) = parents_iter.next() else { | ||
return false; | ||
}; | ||
if matches!(parent, AstKind::Function(f) if f.r#type == FunctionType::TSDeclareFunction) { | ||
return true; | ||
} | ||
|
||
// for non-overloads, the next parent will be the function | ||
let Some(maybe_method_or_fn) = parents_iter.next() else { | ||
return false; | ||
}; | ||
|
||
match maybe_method_or_fn { | ||
// arguments inside setters are allowed. Without them, the program | ||
// has invalid syntax | ||
AstKind::MethodDefinition(MethodDefinition { | ||
kind: MethodDefinitionKind::Set, .. | ||
}) | ||
| AstKind::ObjectProperty(ObjectProperty { kind: PropertyKind::Set, .. }) => true, | ||
|
||
// Allow unused parameters in function overloads | ||
AstKind::Function(f) | ||
if f.body.is_none() || f.r#type == FunctionType::TSDeclareFunction => | ||
{ | ||
true | ||
} | ||
// Allow unused parameters in method overloads and overrides | ||
AstKind::MethodDefinition(method) | ||
if method.value.r#type == FunctionType::TSEmptyBodyFunctionExpression | ||
|| method.r#override => | ||
{ | ||
true | ||
} | ||
// constructor property definitions are allowed because they declare | ||
// class members | ||
// e.g. `class Foo { constructor(public a) {} }` | ||
AstKind::MethodDefinition(method) if method.kind.is_constructor() => { | ||
param.has_modifier() | ||
} | ||
// parameters in abstract methods will never be directly used b/c | ||
// abstract methods have no bodies. However, since this establishes | ||
// an API contract and gets used by subclasses, it is allowed. | ||
AstKind::MethodDefinition(method) if method.r#type.is_abstract() => true, | ||
_ => false, | ||
} | ||
} | ||
} |
Oops, something went wrong.