diff --git a/.typos.toml b/.typos.toml index 28aa370e20bff..e427c79d81d52 100644 --- a/.typos.toml +++ b/.typos.toml @@ -7,6 +7,9 @@ extend-exclude = [ "**/*.snap", "**/*/CHANGELOG.md", "crates/oxc_linter/fixtures", + "crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs", + "crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs", + "crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs", "crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs", "crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs", "crates/oxc_linter/src/rules/react/no_unknown_property.rs", diff --git a/apps/oxlint/src/lint/mod.rs b/apps/oxlint/src/lint/mod.rs index dd78c771e3e02..e19d40bbbac73 100644 --- a/apps/oxlint/src/lint/mod.rs +++ b/apps/oxlint/src/lint/mod.rs @@ -427,7 +427,7 @@ mod test { ]; let result = test(args); assert_eq!(result.number_of_files, 1); - assert_eq!(result.number_of_warnings, 2); + assert_eq!(result.number_of_warnings, 3); assert_eq!(result.number_of_errors, 0); } @@ -441,7 +441,7 @@ mod test { ]; let result = test(args); assert_eq!(result.number_of_files, 1); - assert_eq!(result.number_of_warnings, 1); + assert_eq!(result.number_of_warnings, 2); assert_eq!(result.number_of_errors, 0); } @@ -477,7 +477,7 @@ mod test { let args = &["fixtures/svelte/debugger.svelte"]; let result = test(args); assert_eq!(result.number_of_files, 1); - assert_eq!(result.number_of_warnings, 1); + assert_eq!(result.number_of_warnings, 2); assert_eq!(result.number_of_errors, 0); } diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index 8dd17b38b88e7..16ea8c4e93306 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -1203,6 +1203,11 @@ impl<'a> Hash for Class<'a> { } impl<'a> ClassElement<'a> { + /// Returns `true` if this is a [`ClassElement::StaticBlock`]. + pub fn is_static_block(&self) -> bool { + matches!(self, Self::StaticBlock(_)) + } + /// Returns `true` if this [`ClassElement`] is a static block or has a /// static modifier. pub fn r#static(&self) -> bool { diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 270723ae31125..358594e8b6d99 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -106,6 +106,7 @@ mod eslint { pub mod no_unsafe_optional_chaining; pub mod no_unused_labels; pub mod no_unused_private_class_members; + pub mod no_unused_vars; pub mod no_useless_catch; pub mod no_useless_concat; pub mod no_useless_constructor; @@ -526,6 +527,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_unsafe_negation, eslint::no_unsafe_optional_chaining, eslint::no_unused_labels, + eslint::no_unused_vars, eslint::no_unused_private_class_members, eslint::no_useless_catch, eslint::no_useless_escape, diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs new file mode 100644 index 0000000000000..5c2fcee36f24d --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -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(_) + // + | 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, + } + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs new file mode 100644 index 0000000000000..b1cf3cccfff7e --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs @@ -0,0 +1,67 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::ast::*; +use oxc_semantic::{Semantic, SymbolId}; + +use super::{symbol::Symbol, NoUnusedVars}; + +#[derive(Clone, Copy)] +pub(super) struct BindingContext<'s, 'a> { + pub options: &'s NoUnusedVars, + pub semantic: &'s Semantic<'a>, +} +impl<'s, 'a> BindingContext<'s, 'a> { + #[inline] + pub fn symbol(&self, symbol_id: SymbolId) -> Symbol<'s, 'a> { + Symbol::new(self.semantic, symbol_id) + } + #[inline] + pub fn has_usages(&self, symbol_id: SymbolId) -> bool { + self.symbol(symbol_id).has_usages(self.options) + } +} + +pub(super) trait HasAnyUsedBinding<'a> { + /// Returns `true` if this node contains a binding that is used or ignored. + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool; +} + +impl<'a> HasAnyUsedBinding<'a> for BindingPattern<'a> { + #[inline] + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { + self.kind.has_any_used_binding(ctx) + } +} +impl<'a> HasAnyUsedBinding<'a> for BindingPatternKind<'a> { + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { + match self { + Self::BindingIdentifier(id) => id.has_any_used_binding(ctx), + Self::AssignmentPattern(id) => id.left.has_any_used_binding(ctx), + Self::ObjectPattern(id) => id.has_any_used_binding(ctx), + Self::ArrayPattern(id) => id.has_any_used_binding(ctx), + } + } +} + +impl<'a> HasAnyUsedBinding<'a> for BindingIdentifier<'a> { + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { + self.symbol_id.get().is_some_and(|symbol_id| ctx.has_usages(symbol_id)) + } +} +impl<'a> HasAnyUsedBinding<'a> for ObjectPattern<'a> { + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { + if ctx.options.ignore_rest_siblings && self.rest.is_some() { + return true; + } + self.properties.iter().any(|p| p.value.has_any_used_binding(ctx)) + || self.rest.as_ref().map_or(false, |rest| rest.argument.has_any_used_binding(ctx)) + } +} +impl<'a> HasAnyUsedBinding<'a> for ArrayPattern<'a> { + fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool { + self.elements.iter().flatten().any(|el| { + // if the destructured element is ignored, it is considered used + el.get_identifier().is_some_and(|name| ctx.options.is_ignored_array_destructured(&name)) + || el.has_any_used_binding(ctx) + }) || self.rest.as_ref().map_or(false, |rest| rest.argument.has_any_used_binding(ctx)) + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/diagnostic.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/diagnostic.rs new file mode 100644 index 0000000000000..0a1c0e5b94bfa --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/diagnostic.rs @@ -0,0 +1,82 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_semantic::SymbolFlags; +use oxc_span::Span; + +use super::Symbol; + +fn pronoun_for_symbol(symbol_flags: SymbolFlags) -> &'static str { + if symbol_flags.is_function() { + "Function" + } else if symbol_flags.is_class() { + "Class" + } else if symbol_flags.is_interface() { + "Interface" + } else if symbol_flags.is_type_alias() { + "Type alias" + } else if symbol_flags.is_enum() { + "Enum" + } else if symbol_flags.is_enum_member() { + "Enum member" + } else if symbol_flags.is_type_import() { + "Type" + } else if symbol_flags.is_import() { + "Identifier" + } else { + "Variable" + } +} + +pub fn used_ignored(symbol: &Symbol<'_, '_>) -> OxcDiagnostic { + let pronoun = pronoun_for_symbol(symbol.flags()); + let name = symbol.name(); + + OxcDiagnostic::warn(format!("{pronoun} '{name}' is marked as ignored but is used.")) + .with_label(symbol.span().label(format!("'{name}' is declared here"))) + .with_help(format!("Consider renaming this {}.", pronoun.to_lowercase())) +} +/// Variable 'x' is declared but never used. +pub fn declared(symbol: &Symbol<'_, '_>) -> OxcDiagnostic { + let (verb, help) = if symbol.flags().is_catch_variable() { + ("caught", "Consider handling this error.") + } else { + ("declared", "Consider removing this declaration.") + }; + let pronoun = pronoun_for_symbol(symbol.flags()); + let name = symbol.name(); + + OxcDiagnostic::error(format!("{pronoun} '{name}' is {verb} but never used.")) + .with_label(symbol.span().label(format!("'{name}' is declared here"))) + .with_help(help) +} + +/// Variable 'x' is assigned a value but never used. +pub fn assign(symbol: &Symbol<'_, '_>, assign_span: Span) -> OxcDiagnostic { + let pronoun = pronoun_for_symbol(symbol.flags()); + let name = symbol.name(); + + OxcDiagnostic::error(format!("{pronoun} '{name}' is assigned a value but never used.")) + .with_labels([ + symbol.span().label(format!("'{name}' is declared here")), + assign_span.label("it was last assigned here"), + ]) + .with_help("Did you mean to use this variable?") +} + +/// Parameter 'x' is declared but never used. +pub fn param(symbol: &Symbol<'_, '_>) -> OxcDiagnostic { + let name = symbol.name(); + + OxcDiagnostic::error(format!("Parameter '{name}' is declared but never used.")) + .with_label(symbol.span().label(format!("'{name}' is declared here"))) + .with_help("Consider removing this parameter.") +} + +/// Identifier 'x' imported but never used. +pub fn imported(symbol: &Symbol<'_, '_>) -> OxcDiagnostic { + let pronoun = pronoun_for_symbol(symbol.flags()); + let name = symbol.name(); + + OxcDiagnostic::error(format!("{pronoun} '{name}' is imported but never used.")) + .with_label(symbol.span().label(format!("'{name}' is imported here"))) + .with_help("Consider removing this import.") +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs new file mode 100644 index 0000000000000..12623015bd5ba --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/ignored.rs @@ -0,0 +1,379 @@ +use super::{NoUnusedVars, Symbol}; +use oxc_ast::{ + ast::{ + ArrayAssignmentTarget, AssignmentTarget, AssignmentTargetMaybeDefault, + AssignmentTargetProperty, BindingPattern, BindingPatternKind, ClassElement, + ObjectAssignmentTarget, + }, + AstKind, +}; +use regex::Regex; + +#[derive(Debug, Default, Clone, Copy)] +pub(super) enum FoundStatus { + /// The target identifier was not found + #[default] + NotFound, + /// The target identifier was found and it meets ignore criteria + Ignored, + /// The target identifier was found and does not meet ignore criteria + NotIgnored, +} + +impl FoundStatus { + #[inline] + pub const fn is_found(self) -> bool { + matches!(self, Self::Ignored | Self::NotIgnored) + } + + #[inline] + pub const fn is_ignored(self) -> bool { + matches!(self, Self::Ignored) + } + + #[inline] + pub const fn found(is_found: bool) -> Self { + if is_found { + Self::NotIgnored + } else { + Self::NotFound + } + } + + /// Mark a target as ignored if it's found. + /// + /// `false` does not make already ignored values not-ignored. + #[inline] + pub fn ignore(self, is_ignored: bool) -> Self { + match self { + Self::NotIgnored if is_ignored => Self::Ignored, + _ => self, + } + } +} + +impl NoUnusedVars { + /// Check if a symbol should be ignored based on how it's declared. + /// + /// Does not handle ignore checks for re-assignments to array/object destructures. + pub(super) fn is_ignored(&self, symbol: &Symbol<'_, '_>) -> bool { + let declared_binding = symbol.name(); + match symbol.declaration().kind() { + AstKind::BindingRestElement(_) + | AstKind::Function(_) + | AstKind::ImportDefaultSpecifier(_) + | AstKind::ImportNamespaceSpecifier(_) + | AstKind::ImportSpecifier(_) + | AstKind::ModuleDeclaration(_) + | AstKind::TSEnumDeclaration(_) + | AstKind::TSEnumMember(_) + | AstKind::TSImportEqualsDeclaration(_) + | AstKind::TSInterfaceDeclaration(_) + | AstKind::TSModuleDeclaration(_) + | AstKind::TSTypeAliasDeclaration(_) + | AstKind::TSTypeParameter(_) => self.is_ignored_var(declared_binding), + AstKind::Class(class) => { + if self.ignore_class_with_static_init_block + && class.body.body.iter().any(ClassElement::is_static_block) + { + return true; + } + self.is_ignored_var(declared_binding) + } + AstKind::CatchParameter(catch) => { + self.is_ignored_catch_err(declared_binding) + || self.is_ignored_binding_pattern(symbol, &catch.pattern) + } + AstKind::VariableDeclarator(decl) => { + self.is_ignored_var(declared_binding) + || self.is_ignored_binding_pattern(symbol, &decl.id) + } + AstKind::FormalParameter(param) => { + self.is_ignored_arg(declared_binding) + || self.is_ignored_binding_pattern(symbol, ¶m.pattern) + } + s => { + // panic when running test cases so we can find unsupported node kinds + debug_assert!( + false, + "is_ignored_decl did not know how to handle node of kind {}", + s.debug_name() + ); + false + } + } + } + + pub(super) fn is_ignored_binding_pattern<'a>( + &self, + symbol: &Symbol<'_, 'a>, + binding: &BindingPattern<'a>, + ) -> bool { + self.should_search_destructures() + && self.search_binding_pattern(symbol, binding).is_ignored() + } + + pub(super) fn is_ignored_assignment_target<'a>( + &self, + symbol: &Symbol<'_, 'a>, + assignment: &AssignmentTarget<'a>, + ) -> bool { + self.should_search_destructures() + && self.search_assignment_target(symbol, assignment).is_ignored() + } + + /// Do we need to search binding patterns to tell if a symbol is ignored, or + /// can we just rely on [`SymbolFlags`] + the symbol's name? + /// + /// [`SymbolFlags`]: oxc_semantic::SymbolFlags + #[inline] + pub fn should_search_destructures(&self) -> bool { + self.ignore_rest_siblings || self.destructured_array_ignore_pattern.is_some() + } + + /// This method does the `ignoreRestNeighbors` and + /// `arrayDestructureIgnorePattern` ignore checks for variable declarations. + /// Not needed on function/class/interface/etc declarations because those + /// will never be destructures. + #[must_use] + fn search_binding_pattern<'a>( + &self, + target: &Symbol<'_, 'a>, + binding: &BindingPattern<'a>, + ) -> FoundStatus { + match &binding.kind { + // if found, not ignored. Ignoring only happens in destructuring patterns. + BindingPatternKind::BindingIdentifier(id) => FoundStatus::found(target == id.as_ref()), + BindingPatternKind::AssignmentPattern(id) => { + self.search_binding_pattern(target, &id.left) + } + BindingPatternKind::ObjectPattern(obj) => { + for prop in &obj.properties { + // check if the prop is a binding identifier (with or + // without an assignment) since ignore_rest_siblings does + // not apply to spreads that have spreads + // + // const { x: { y, z }, ...rest } = obj + // ^ not ignored by ignore_rest_siblings + let status = self.search_binding_pattern(target, &prop.value); + match prop.value.get_binding_identifier() { + // property is the target we're looking for and is on + // this destructure's "level" - ignore it if the config + // says to ignore rest neighbors and this destructure + // has a ...rest. + Some(_) if status.is_found() => { + return status + .ignore(self.is_ignored_spread_neighbor(obj.rest.is_some())); + } + // property is an array/object destructure containing + // the target, our search is done. However, since it's + // not on this destructure's "level", we don't mess + // with the ignored status. + None if status.is_found() => { + return status; + } + // target not found, keep looking + Some(_) | None => { + continue; + } + } + } + + // not found in properties, check binding pattern + obj.rest.as_ref().map_or(FoundStatus::NotFound, |rest| { + self.search_binding_pattern(target, &rest.argument) + }) + } + BindingPatternKind::ArrayPattern(arr) => { + for el in arr.elements.iter().flatten() { + let status = self.search_binding_pattern(target, el); + match el.get_binding_identifier() { + // el is a simple pattern for the symbol we're looking + // for. Check if it is ignored. + Some(id) if target == id => { + return status.ignore(self.is_ignored_array_destructured(&id.name)); + } + // el is a destructuring pattern containing the target + // symbol; our search is done, propegate it upwards + None if status.is_found() => { + debug_assert!(el.kind.is_destructuring_pattern()); + return status; + } + // el is a simple pattern for a different symbol, or is + // a destructuring pattern that doesn't contain the target. Keep looking + Some(_) | None => { + continue; + } + } + } + + FoundStatus::NotFound + } + } + } + + /// Follows the same logic as [`NoUnusedVars::search_binding_pattern`], but + /// for assignments instead of declarations + fn search_assignment_target<'a>( + &self, + target: &Symbol<'_, 'a>, + assignment: &AssignmentTarget<'a>, + ) -> FoundStatus { + match assignment { + AssignmentTarget::AssignmentTargetIdentifier(id) => { + FoundStatus::found(target == id.as_ref()) + } + AssignmentTarget::ObjectAssignmentTarget(obj) => { + self.search_obj_assignment_target(target, obj.as_ref()) + } + AssignmentTarget::ArrayAssignmentTarget(arr) => { + self.search_array_assignment_target(target, arr.as_ref()) + } + // other assignments are going to be member expressions, identifier + // references, or one of those two wrapped in a ts annotation-like + // expression. Those will never have destructures and can be safely ignored. + _ => FoundStatus::NotFound, + } + } + + pub(super) fn search_obj_assignment_target<'a>( + &self, + target: &Symbol<'_, 'a>, + obj: &ObjectAssignmentTarget<'a>, + ) -> FoundStatus { + for prop in &obj.properties { + // I'm confused about what's going on here tbh, and I wrote + // this function... but I don't really know what a + // `AssignmentTargetProperty::AssignmentTargetPropertyProperty` + // is, I just know that the name is confusing. + let status = match prop { + AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(id) => { + FoundStatus::found(target == &id.binding) + } + // recurse down nested destructured assignments + AssignmentTargetProperty::AssignmentTargetPropertyProperty(prop) => { + self.search_assignment_maybe_default(target, &prop.binding) + } + }; + + // found, mark `status` as ignored if this destructure + // contains a ...rest and the user asks for it + if status.is_found() { + let ignore_because_rest = self.is_ignored_spread_neighbor(obj.rest.is_some()); + return status.ignore(ignore_because_rest); + } + } + + // symbol not found in properties, try searching through ...rest + obj.rest.as_ref().map_or(FoundStatus::NotFound, |rest| { + self.search_assignment_target(target, &rest.target) + }) + } + + pub(super) fn search_array_assignment_target<'a>( + &self, + target: &Symbol<'_, 'a>, + arr: &ArrayAssignmentTarget<'a>, + ) -> FoundStatus { + // check each element in the array spread assignment + for el in arr.elements.iter().flatten() { + let status = self.search_assignment_maybe_default(target, el); + + // if we found the target symbol and it's not nested in some + // other destructure, mark it as ignored if it matches the + // configured array destructure ignore pattern. + if status.is_found() { + return status.ignore( + el.is_simple_assignment_target() + && self.is_ignored_array_destructured(target.name()), + ); + } + // continue with search + } + + FoundStatus::NotFound + } + + fn search_assignment_maybe_default<'a>( + &self, + target: &Symbol<'_, 'a>, + assignment: &AssignmentTargetMaybeDefault<'a>, + ) -> FoundStatus { + assignment.as_assignment_target().map_or(FoundStatus::NotFound, |assignment| { + self.search_assignment_target(target, assignment) + }) + } + + #[inline] + fn is_ignored_spread_neighbor(&self, has_rest: bool) -> bool { + self.ignore_rest_siblings && has_rest + } + + // ========================================================================= + // ========================= NAME/KIND-ONLY CHECKS ========================= + // ========================================================================= + + #[inline] + pub(super) fn is_ignored_var(&self, name: &str) -> bool { + Self::is_none_or_match(self.vars_ignore_pattern.as_ref(), name) + } + + #[inline] + pub(super) fn is_ignored_arg(&self, name: &str) -> bool { + Self::is_none_or_match(self.args_ignore_pattern.as_ref(), name) + } + + #[inline] + pub(super) fn is_ignored_array_destructured(&self, name: &str) -> bool { + Self::is_none_or_match(self.destructured_array_ignore_pattern.as_ref(), name) + } + + #[inline] + pub(super) fn is_ignored_catch_err(&self, name: &str) -> bool { + *!self.caught_errors + || Self::is_none_or_match(self.caught_errors_ignore_pattern.as_ref(), name) + } + + #[inline] + fn is_none_or_match(re: Option<&Regex>, haystack: &str) -> bool { + re.map_or(false, |pat| pat.is_match(haystack)) + } +} + +#[cfg(test)] +mod test { + use crate::rule::Rule as _; + + use super::super::NoUnusedVars; + use oxc_span::Atom; + + #[test] + fn test_ignored() { + let rule = NoUnusedVars::from_configuration(serde_json::json!([ + { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "[iI]gnored", + "caughtErrorsIgnorePattern": "err.*", + "caughtErrors": "all", + "destructuredArrayIgnorePattern": "^_", + } + ])); + + assert!(rule.is_ignored_var("_x")); + assert!(rule.is_ignored_var(&Atom::from("_x"))); + assert!(!rule.is_ignored_var("notIgnored")); + + assert!(rule.is_ignored_arg("ignored")); + assert!(rule.is_ignored_arg("alsoIgnored")); + assert!(rule.is_ignored_arg(&Atom::from("ignored"))); + assert!(rule.is_ignored_arg(&Atom::from("alsoIgnored"))); + + assert!(rule.is_ignored_catch_err("err")); + assert!(rule.is_ignored_catch_err("error")); + assert!(!rule.is_ignored_catch_err("e")); + + assert!(rule.is_ignored_array_destructured("_x")); + assert!(rule.is_ignored_array_destructured(&Atom::from("_x"))); + assert!(!rule.is_ignored_array_destructured("notIgnored")); + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs new file mode 100644 index 0000000000000..5c236926a3d1c --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs @@ -0,0 +1,319 @@ +mod allowed; +mod binding_pattern; +mod diagnostic; +mod ignored; +mod options; +mod symbol; +#[cfg(test)] +mod tests; +mod usage; + +use std::ops::Deref; + +use oxc_ast::AstKind; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::{ScopeFlags, SymbolFlags, SymbolId}; +use oxc_span::GetSpan; + +use crate::{context::LintContext, rule::Rule}; +use options::NoUnusedVarsOptions; + +use symbol::Symbol; + +#[derive(Debug, Default, Clone)] +pub struct NoUnusedVars(Box); + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows variable declarations or imports that are not used in code. + /// + /// ### Why is this bad? + /// + /// Variables that are declared and not used anywhere in the code are most + /// likely an error due to incomplete refactoring. Such variables take up + /// space in the code and can lead to confusion by readers. + /// + /// A variable `foo` is considered to be used if any of the following are + /// true: + /// + /// * It is called (`foo()`) or constructed (`new foo()`) + /// * It is read (`var bar = foo`) + /// * It is passed into a function as an argument (`doSomething(foo)`) + /// * It is read inside of a function that is passed to another function + /// (`doSomething(function() { foo(); })`) + /// + /// A variable is _not_ considered to be used if it is only ever declared + /// (`var foo = 5`) or assigned to (`foo = 7`). + /// + /// #### Exported + /// + /// In environments outside of CommonJS or ECMAScript modules, you may use + /// `var` to create a global variable that may be used by other scripts. You + /// can use the `/* exported variableName */` comment block to indicate that + /// this variable is being exported and therefore should not be considered + /// unused. + /// + /// Note that `/* exported */` has no effect for any of the following: + /// * when the environment is `node` or `commonjs` + /// * when `parserOptions.sourceType` is `module` + /// * when `ecmaFeatures.globalReturn` is `true` + /// + /// The line comment `//exported variableName` will not work as `exported` + /// is not line-specific. + /// + /// ### Example + /// + /// Examples of **incorrect** code for this rule: + /// + /// ```javascript + /// /*eslint no-unused-vars: "error"*/ + /// /*global some_unused_var*/ + /// + /// // It checks variables you have defined as global + /// some_unused_var = 42; + /// + /// var x; + /// + /// // Write-only variables are not considered as used. + /// var y = 10; + /// y = 5; + /// + /// // A read for a modification of itself is not considered as used. + /// var z = 0; + /// z = z + 1; + /// + /// // By default, unused arguments cause warnings. + /// (function(foo) { + /// return 5; + /// })(); + /// + /// // Unused recursive functions also cause warnings. + /// function fact(n) { + /// if (n < 2) return 1; + /// return n * fact(n - 1); + /// } + /// + /// // When a function definition destructures an array, unused entries from + /// // the array also cause warnings. + /// function getY([x, y]) { + /// return y; + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// /*eslint no-unused-vars: "error"*/ + /// + /// var x = 10; + /// alert(x); + /// + /// // foo is considered used here + /// myFunc(function foo() { + /// // ... + /// }.bind(this)); + /// + /// (function(foo) { + /// return foo; + /// })(); + /// + /// var myFunc; + /// myFunc = setTimeout(function() { + /// // myFunc is considered used + /// myFunc(); + /// }, 50); + /// + /// // Only the second argument from the destructured array is used. + /// function getY([, y]) { + /// return y; + /// } + /// ``` + /// + /// Examples of **correct** code for `/* exported variableName */` operation: + /// ```javascript + /// /* exported global_var */ + /// + /// var global_var = 42; + /// ``` + NoUnusedVars, + correctness +); + +impl Deref for NoUnusedVars { + type Target = NoUnusedVarsOptions; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Rule for NoUnusedVars { + fn from_configuration(value: serde_json::Value) -> Self { + Self(Box::new(NoUnusedVarsOptions::from(value))) + } + + fn run_on_symbol(&self, symbol_id: SymbolId, ctx: &LintContext<'_>) { + let symbol = Symbol::new(ctx.semantic().as_ref(), symbol_id); + if Self::should_skip_symbol(&symbol) { + return; + } + + self.run_on_symbol_internal(&symbol, ctx); + } + + fn should_run(&self, ctx: &LintContext) -> bool { + // ignore .d.ts and vue files. + // 1. declarations have side effects (they get merged together) + // 2. vue scripts declare variables that get used in the template, which + // we can't detect + !ctx.source_type().is_typescript_definition() + && !ctx.file_path().extension().is_some_and(|ext| ext == "vue") + } +} + +impl NoUnusedVars { + fn run_on_symbol_internal<'a>(&self, symbol: &Symbol<'_, 'a>, ctx: &LintContext<'a>) { + let is_ignored = self.is_ignored(symbol); + + if is_ignored && !self.report_used_ignore_pattern { + return; + } + + // Order matters. We want to call cheap/high "yield" functions first. + let is_exported = symbol.is_exported(); + let is_used = is_exported || symbol.has_usages(self); + + match (is_used, is_ignored) { + (true, true) => { + if self.report_used_ignore_pattern { + ctx.diagnostic(diagnostic::used_ignored(symbol)); + } + return; + }, + // not used but ignored, no violation + (false, true) + // used and not ignored, no violation + | (true, false) => { + return + }, + // needs acceptance check and/or reporting + (false, false) => {} + } + + let declaration = symbol.declaration(); + match declaration.kind() { + // NOTE: match_module_declaration(AstKind) does not work here + AstKind::ImportDeclaration(_) + | AstKind::ImportSpecifier(_) + | AstKind::ImportExpression(_) + | AstKind::ImportDefaultSpecifier(_) + | AstKind::ImportNamespaceSpecifier(_) => { + if !is_ignored { + ctx.diagnostic(diagnostic::imported(symbol)); + } + } + AstKind::VariableDeclarator(decl) => { + if self.is_allowed_variable_declaration(symbol, decl) { + return; + }; + let report = + if let Some(last_write) = symbol.references().rev().find(|r| r.is_write()) { + // ahg + let span = ctx.nodes().get_node(last_write.node_id()).kind().span(); + diagnostic::assign(symbol, span) + } else { + diagnostic::declared(symbol) + }; + ctx.diagnostic(report); + } + AstKind::FormalParameter(param) => { + if self.is_allowed_argument(ctx.semantic().as_ref(), symbol, param) { + return; + } + ctx.diagnostic(diagnostic::param(symbol)); + } + AstKind::Class(_) | AstKind::Function(_) => { + if self.is_allowed_class_or_function(symbol) { + return; + } + ctx.diagnostic(diagnostic::declared(symbol)); + } + AstKind::TSModuleDeclaration(namespace) => { + if self.is_allowed_ts_namespace(symbol, namespace) { + return; + } + ctx.diagnostic(diagnostic::declared(symbol)); + } + AstKind::TSInterfaceDeclaration(_) => { + if symbol.is_in_declared_module() { + return; + } + ctx.diagnostic(diagnostic::declared(symbol)); + } + AstKind::TSTypeParameter(_) => { + if self.is_allowed_type_parameter(symbol, declaration.id()) { + return; + } + ctx.diagnostic(diagnostic::declared(symbol)); + } + _ => ctx.diagnostic(diagnostic::declared(symbol)), + }; + } + + fn should_skip_symbol(symbol: &Symbol<'_, '_>) -> bool { + const AMBIENT_NAMESPACE_FLAGS: SymbolFlags = + SymbolFlags::NameSpaceModule.union(SymbolFlags::Ambient); + let flags = symbol.flags(); + + // 1. ignore enum members. Only enums get checked + // 2. ignore all ambient TS declarations, e.g. `declare class Foo {}` + if flags.intersects(SymbolFlags::EnumMember.union(SymbolFlags::Ambient)) + // ambient namespaces + || flags == AMBIENT_NAMESPACE_FLAGS + || (symbol.is_in_ts() && symbol.is_in_declare_global()) + { + return true; + } + + // In some cases (e.g. "jsx": "react" in tsconfig.json), React imports + // get used in generated code. We don't have a way to detect + // "jsxPragmas" or whether TSX files are using "jsx": "react-jsx", so we + // just allow all cases. + if symbol.flags().contains(SymbolFlags::Import) + && symbol.is_in_jsx() + && symbol.is_possibly_jsx_factory() + { + return true; + } + + false + } +} + +impl Symbol<'_, '_> { + #[inline] + fn is_possibly_jsx_factory(&self) -> bool { + let name = self.name(); + name == "React" || name == "h" + } + + fn is_in_declare_global(&self) -> bool { + self.scopes() + .ancestors(self.scope_id()) + .filter(|scope_id| { + let flags = self.scopes().get_flags(*scope_id); + flags.contains(ScopeFlags::TsModuleBlock) + }) + .any(|ambient_module_scope_id| { + let AstKind::TSModuleDeclaration(module) = self + .nodes() + .get_node(self.scopes().get_node_id(ambient_module_scope_id)) + .kind() + else { + return false; + }; + + module.kind.is_global() + }) + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs new file mode 100644 index 0000000000000..8f666efc4a48e --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs @@ -0,0 +1,549 @@ +use std::{borrow::Cow, ops::Deref}; + +use oxc_diagnostics::OxcDiagnostic; +use regex::Regex; +use serde_json::Value; + +/// See [ESLint - no-unused-vars config schema](https://github.com/eslint/eslint/blob/53b1ff047948e36682fade502c949f4e371e53cd/lib/rules/no-unused-vars.js#L61) +#[derive(Debug, Default, Clone)] +#[must_use] +#[non_exhaustive] +pub struct NoUnusedVarsOptions { + /// Controls how usage of a variable in the global scope is checked. + /// + /// This option has two settings: + /// 1. `all` checks all variables for usage, including those in the global + /// scope. This is the default setting. + /// 2. `local` checks only that locally-declared variables are used but will + /// allow global variables to be unused. + pub vars: VarsOption, + + /// Specifies exceptions to this rule for unused variables. Variables whose + /// names match this pattern will be ignored. + /// + /// ## Example + /// + /// Examples of **correct** code for this option when the pattern is `^_`: + /// ```javascript + /// var _a = 10; + /// var b = 10; + /// console.log(b); + /// ``` + pub vars_ignore_pattern: Option, + + /// Controls how unused arguments are checked. + /// + /// This option has three settings: + /// 1. `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. + /// 2. `all` - All named arguments must be used. + /// 3. `none` - Do not check arguments. + pub args: ArgsOption, + + /// Specifies exceptions to this rule for unused arguments. Arguments whose + /// names match this pattern will be ignored. + /// + /// ## Example + /// + /// Examples of **correct** code for this option when the pattern is `^_`: + /// + /// ```javascript + /// function foo(_a, b) { + /// console.log(b); + /// } + /// foo(1, 2); + /// ``` + pub args_ignore_pattern: Option, + + /// Using a Rest property it is possible to "omit" properties from an + /// object, but by default the sibling properties are marked as "unused". + /// With this option enabled the rest property's siblings are ignored. + /// + /// ## Example + /// Examples of **correct** code when this option is set to `true`: + /// ```js + /// // 'foo' and 'bar' were ignored because they have a rest property sibling. + /// var { foo, ...coords } = data; + /// + /// var bar; + /// ({ bar, ...coords } = data); + /// ``` + pub ignore_rest_siblings: bool, + + /// Used for `catch` block validation. + /// It has two settings: + /// * `none` - do not check error objects. This is the default setting + /// * `all` - all named arguments must be used` + /// + #[doc(hidden)] + /// `none` corresponds to `false`, while `all` corresponds to `true`. + pub caught_errors: CaughtErrors, + + /// Specifies exceptions to this rule for errors caught within a `catch` block. + /// Variables declared within a `catch` block whose names match this pattern + /// will be ignored. + /// + /// ## Example + /// + /// Examples of **correct** code when the pattern is `^ignore`: + /// + /// ```javascript + /// try { + /// // ... + /// } catch (ignoreErr) { + /// console.error("Error caught in catch block"); + /// } + /// ``` + pub caught_errors_ignore_pattern: Option, + + /// This option specifies exceptions within destructuring patterns that will + /// not be checked for usage. Variables declared within array destructuring + /// whose names match this pattern will be ignored. + /// + /// ## Example + /// + /// Examples of **correct** code for this option, when the pattern is `^_`: + /// ```javascript + /// const [a, _b, c] = ["a", "b", "c"]; + /// console.log(a + c); + /// + /// const { x: [_a, foo] } = bar; + /// console.log(foo); + /// + /// let _m, n; + /// foo.forEach(item => { + /// [_m, n] = item; + /// console.log(n); + /// }); + /// ``` + pub destructured_array_ignore_pattern: Option, + + /// The `ignoreClassWithStaticInitBlock` option is a boolean (default: + /// `false`). Static initialization blocks allow you to initialize static + /// variables and execute code during the evaluation of a class definition, + /// meaning the static block code is executed without creating a new + /// instance of the class. When set to true, this option ignores classes + /// containing static initialization blocks. + /// + /// ## Example + /// + /// Examples of **incorrect** code for the `{ "ignoreClassWithStaticInitBlock": true }` option + /// + /// ```javascript + /// /*eslint no-unused-vars: ["error", { "ignoreClassWithStaticInitBlock": true }]*/ + /// + /// class Foo { + /// static myProperty = "some string"; + /// static mymethod() { + /// return "some string"; + /// } + /// } + /// + /// class Bar { + /// static { + /// let baz; // unused variable + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for the `{ "ignoreClassWithStaticInitBlock": true }` option + /// + /// ```javascript + /// /*eslint no-unused-vars: ["error", { "ignoreClassWithStaticInitBlock": true }]*/ + /// + /// class Foo { + /// static { + /// let bar = "some string"; + /// + /// console.log(bar); + /// } + /// } + /// ``` + pub ignore_class_with_static_init_block: bool, + + /// The `reportUsedIgnorePattern` option is a boolean (default: `false`). + /// Using this option will report variables that match any of the valid + /// ignore pattern options (`varsIgnorePattern`, `argsIgnorePattern`, + /// `caughtErrorsIgnorePattern`, or `destructuredArrayIgnorePattern`) if + /// they have been used. + /// + /// ## Example + /// + /// Examples of **incorrect** code for the `{ "reportUsedIgnorePattern": true }` option: + /// + /// ```javascript + /// /*eslint no-unused-vars: ["error", { "reportUsedIgnorePattern": true, "varsIgnorePattern": "[iI]gnored" }]*/ + /// + /// var firstVarIgnored = 1; + /// var secondVar = 2; + /// console.log(firstVarIgnored, secondVar); + /// ``` + /// + /// Examples of **correct** code for the `{ "reportUsedIgnorePattern": true }` option: + /// + /// ```javascript + /// /*eslint no-unused-vars: ["error", { "reportUsedIgnorePattern": true, "varsIgnorePattern": "[iI]gnored" }]*/ + /// + /// var firstVar = 1; + /// var secondVar = 2; + /// console.log(firstVar, secondVar); + /// ``` + pub report_used_ignore_pattern: bool, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum VarsOption { + /// All variables are checked for usage, including those in the global scope. + #[default] + All, + /// Checks only that locally-declared variables are used but will allow + /// global variables to be unused. + Local, +} +impl VarsOption { + pub const fn is_local(&self) -> bool { + matches!(self, Self::Local) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum ArgsOption { + /// 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. + #[default] + AfterUsed, + /// All named arguments must be used + All, + /// Do not check arguments + None, +} +impl ArgsOption { + #[inline] + pub const fn is_after_used(&self) -> bool { + matches!(self, Self::AfterUsed) + } + #[inline] + pub const fn is_all(&self) -> bool { + matches!(self, Self::All) + } + #[inline] + pub const fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct CaughtErrors(bool); + +impl Default for CaughtErrors { + fn default() -> Self { + Self::all() + } +} + +impl CaughtErrors { + pub const fn all() -> Self { + Self(true) + } + pub const fn none() -> Self { + Self(false) + } +} + +impl Deref for CaughtErrors { + type Target = bool; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::Not for CaughtErrors { + type Output = Self; + + #[inline] + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +fn invalid_option_mismatch_error(option_name: &str, expected: E, actual: A) -> OxcDiagnostic +where + E: IntoIterator, + A: AsRef, +{ + let expected = expected.into_iter(); + let initial_capacity = expected.size_hint().0 * 8; + let expected = + expected.fold(String::with_capacity(initial_capacity), |acc, s| acc + " or " + s); + let actual = actual.as_ref(); + + invalid_option_error(option_name, format!("Expected {expected}, got {actual}")) +} + +fn invalid_option_error>>( + option_name: &str, + message: M, +) -> OxcDiagnostic { + let message = message.into(); + OxcDiagnostic::error(format!("Invalid '{option_name}' option for no-unused-vars: {message}")) +} + +impl TryFrom<&String> for VarsOption { + type Error = OxcDiagnostic; + + fn try_from(value: &String) -> Result { + match value.as_str() { + "all" => Ok(Self::All), + "local" => Ok(Self::Local), + v => Err(invalid_option_mismatch_error("vars", ["all", "local"], v)), + } + } +} + +impl TryFrom<&Value> for VarsOption { + type Error = OxcDiagnostic; + + fn try_from(value: &Value) -> Result { + match value { + Value::String(s) => Self::try_from(s), + _ => Err(invalid_option_error("vars", format!("Expected a string, got {value}"))), + } + } +} + +impl TryFrom<&Value> for ArgsOption { + type Error = OxcDiagnostic; + + fn try_from(value: &Value) -> Result { + match value { + Value::String(s) => match s.as_str() { + "after-used" => Ok(Self::AfterUsed), + "all" => Ok(Self::All), + "none" => Ok(Self::None), + s => Err(invalid_option_mismatch_error("args", ["after-used", "all", "none"], s)), + }, + v => Err(invalid_option_error("args", format!("Expected a string, got {v}"))), + } + } +} + +impl TryFrom<&String> for CaughtErrors { + type Error = OxcDiagnostic; + + fn try_from(value: &String) -> Result { + match value.as_str() { + "all" => Ok(Self(true)), + "none" => Ok(Self(false)), + v => Err(invalid_option_mismatch_error("caughtErrors", ["all", "none"], v)), + } + } +} + +impl From for CaughtErrors { + fn from(value: bool) -> Self { + Self(value) + } +} +impl TryFrom<&Value> for CaughtErrors { + type Error = OxcDiagnostic; + + fn try_from(value: &Value) -> Result { + match value { + Value::String(s) => Self::try_from(s), + Value::Bool(b) => Ok(Self(*b)), + v => Err(invalid_option_error("caughtErrors", format!("Expected a string, got {v}"))), + } + } +} + +/// Parses a potential pattern into a [`Regex`] that accepts unicode characters. +fn parse_unicode_rule(value: Option<&Value>, name: &str) -> Option { + value + .and_then(Value::as_str) + .map(|pattern| regex::RegexBuilder::new(pattern).unicode(true).build()) + .transpose() + .map_err(|err| panic!("Invalid '{name}' option for no-unused-vars: {err}")) + .unwrap() +} +impl From for NoUnusedVarsOptions { + fn from(value: Value) -> Self { + let Some(config) = value.get(0) else { return Self::default() }; + match config { + Value::String(vars) => { + let vars: VarsOption = vars + .try_into() + .unwrap(); + Self { vars, ..Default::default() } + } + Value::Object(config) => { + let vars = config + .get("vars") + .map(|vars| { + let vars: VarsOption = vars + .try_into() + .unwrap(); + vars + }) + .unwrap_or_default(); + + let vars_ignore_pattern: Option = + parse_unicode_rule(config.get("varsIgnorePattern"), "varsIgnorePattern"); + + let args: ArgsOption = config + .get("args") + .map(|args| { + let args: ArgsOption = args + .try_into() + .unwrap(); + args + }) + .unwrap_or_default(); + + let args_ignore_pattern: Option = + parse_unicode_rule(config.get("argsIgnorePattern"), "argsIgnorePattern"); + + let caught_errors: CaughtErrors = config + .get("caughtErrors") + .map(|caught_errors| { + let caught_errors: CaughtErrors = caught_errors + .try_into() + .unwrap(); + caught_errors + }) + .unwrap_or_default(); + + let caught_errors_ignore_pattern = parse_unicode_rule( + config.get("caughtErrorsIgnorePattern"), + "caughtErrorsIgnorePattern", + ); + + let destructured_array_ignore_pattern: Option = parse_unicode_rule( + config.get("destructuredArrayIgnorePattern"), + "destructuredArrayIgnorePattern", + ); + + let ignore_rest_siblings: bool = config + .get("ignoreRestSiblings") + .map_or(Some(false), Value::as_bool) + .unwrap_or(false); + + let ignore_class_with_static_init_block: bool = config + .get("ignoreClassWithStaticInitBlock") + .map_or(Some(false), Value::as_bool) + .unwrap_or(false); + + let report_used_ignore_pattern: bool = config + .get("reportUsedIgnorePattern") + .map_or(Some(false), Value::as_bool) + .unwrap_or(false); + + Self { + vars, + vars_ignore_pattern, + args, + args_ignore_pattern, + ignore_rest_siblings, + caught_errors, + caught_errors_ignore_pattern, + destructured_array_ignore_pattern, + ignore_class_with_static_init_block, + report_used_ignore_pattern + } + } + Value::Null => Self::default(), + _ => panic!( + "Invalid 'vars' option for no-unused-vars: Expected a string or an object, got {config}" + ), + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_options_default() { + let rule = NoUnusedVarsOptions::default(); + assert_eq!(rule.vars, VarsOption::All); + assert!(rule.vars_ignore_pattern.is_none()); + assert_eq!(rule.args, ArgsOption::AfterUsed); + assert!(rule.args_ignore_pattern.is_none()); + assert_eq!(rule.caught_errors, CaughtErrors::all()); + assert!(rule.caught_errors_ignore_pattern.is_none()); + assert!(rule.destructured_array_ignore_pattern.is_none()); + assert!(!rule.ignore_rest_siblings); + assert!(!rule.ignore_class_with_static_init_block); + assert!(!rule.report_used_ignore_pattern); + } + + #[test] + fn test_options_from_string() { + let rule: NoUnusedVarsOptions = json!(["all"]).into(); + assert_eq!(rule.vars, VarsOption::All); + + let rule: NoUnusedVarsOptions = json!(["local"]).into(); + assert_eq!(rule.vars, VarsOption::Local); + } + + #[test] + fn test_options_from_object() { + let rule: NoUnusedVarsOptions = json!([ + { + "vars": "local", + "varsIgnorePattern": "^_", + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "ignoreRestSiblings": true, + "reportUsedIgnorePattern": true + } + ]) + .into(); + + assert_eq!(rule.vars, VarsOption::Local); + assert_eq!(rule.vars_ignore_pattern.unwrap().as_str(), "^_"); + assert_eq!(rule.args, ArgsOption::All); + assert_eq!(rule.args_ignore_pattern.unwrap().as_str(), "^_"); + assert_eq!(rule.caught_errors, CaughtErrors::all()); + assert_eq!(rule.caught_errors_ignore_pattern.unwrap().as_str(), "^_"); + assert_eq!(rule.destructured_array_ignore_pattern.unwrap().as_str(), "^_"); + assert!(rule.ignore_rest_siblings); + assert!(!rule.ignore_class_with_static_init_block); + assert!(rule.report_used_ignore_pattern); + } + + #[test] + fn test_options_from_null() { + let opts = NoUnusedVarsOptions::from(json!(null)); + let default = NoUnusedVarsOptions::default(); + assert_eq!(opts.vars, default.vars); + assert!(opts.vars_ignore_pattern.is_none()); + assert!(default.vars_ignore_pattern.is_none()); + + assert_eq!(opts.args, default.args); + assert!(opts.args_ignore_pattern.is_none()); + assert!(default.args_ignore_pattern.is_none()); + + assert_eq!(opts.caught_errors, default.caught_errors); + assert!(opts.caught_errors_ignore_pattern.is_none()); + assert!(default.caught_errors_ignore_pattern.is_none()); + + assert_eq!(opts.ignore_rest_siblings, default.ignore_rest_siblings); + } + + #[test] + fn test_parse_unicode_regex() { + let pat = json!("^_"); + parse_unicode_rule(Some(&pat), "varsIgnorePattern") + .expect("json strings should get parsed into a regex"); + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs new file mode 100644 index 0000000000000..b61b93fe04ccb --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/symbol.rs @@ -0,0 +1,221 @@ +use std::fmt; + +use oxc_ast::{ + ast::{AssignmentTarget, BindingIdentifier, BindingPattern, IdentifierReference}, + AstKind, +}; +use oxc_semantic::{ + AstNode, AstNodeId, AstNodes, Reference, ScopeId, ScopeTree, Semantic, SymbolFlags, SymbolId, + SymbolTable, +}; +use oxc_span::Span; + +#[derive(Clone)] +pub(super) struct Symbol<'s, 'a> { + semantic: &'s Semantic<'a>, + id: SymbolId, + flags: SymbolFlags, +} + +impl PartialEq for Symbol<'_, '_> { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +// constructor and simple getters +impl<'s, 'a> Symbol<'s, 'a> { + pub fn new(semantic: &'s Semantic<'a>, symbol_id: SymbolId) -> Self { + let flags = semantic.symbols().get_flag(symbol_id); + Self { semantic, id: symbol_id, flags } + } + + #[inline] + pub fn id(&self) -> SymbolId { + self.id + } + + #[inline] + pub fn name(&self) -> &str { + self.symbols().get_name(self.id) + } + + /// [`Span`] for the node declaring the [`Symbol`]. + #[inline] + pub fn span(&self) -> Span { + self.symbols().get_span(self.id) + } + + #[inline] + pub const fn flags(&self) -> SymbolFlags { + self.flags + } + + #[inline] + pub fn scope_id(&self) -> ScopeId { + self.symbols().get_scope_id(self.id) + } + + #[inline] + pub fn declaration(&self) -> &AstNode<'a> { + self.nodes().get_node(self.declaration_id()) + } + + #[inline] + pub fn references(&self) -> impl DoubleEndedIterator + '_ { + self.symbols().get_resolved_references(self.id) + } + + /// Is this [`Symbol`] declared in the root scope? + pub fn is_root(&self) -> bool { + self.symbols().get_scope_id(self.id) == self.scopes().root_scope_id() + } + + #[inline] + fn declaration_id(&self) -> AstNodeId { + self.symbols().get_declaration(self.id) + } + + #[inline] + pub fn nodes(&self) -> &AstNodes<'a> { + self.semantic.nodes() + } + + #[inline] + pub fn scopes(&self) -> &ScopeTree { + self.semantic.scopes() + } + + #[inline] + pub fn symbols(&self) -> &SymbolTable { + self.semantic.symbols() + } + + pub fn iter_parents(&self) -> impl Iterator> + '_ { + self.nodes().iter_parents(self.declaration_id()).skip(1) + } + + pub fn iter_relevant_parents( + &self, + node_id: AstNodeId, + ) -> impl Iterator> + Clone + '_ { + self.nodes().iter_parents(node_id).skip(1).filter(|n| Self::is_relevant_kind(n.kind())) + } + + pub fn iter_relevant_parent_and_grandparent_kinds( + &self, + node_id: AstNodeId, + ) -> impl Iterator, /* grandparent */ AstKind<'a>)> + Clone + '_ + { + let parents_iter = self + .nodes() + .iter_parents(node_id) + .map(AstNode::kind) + // no skip + .filter(|kind| Self::is_relevant_kind(*kind)); + + let grandparents_iter = parents_iter.clone().skip(1); + + parents_iter.zip(grandparents_iter) + } + + #[inline] + const fn is_relevant_kind(kind: AstKind<'a>) -> bool { + !matches!(kind, AstKind::ParenthesizedExpression(_)) + } +} + +impl<'s, 'a> Symbol<'s, 'a> { + /// Is this [`Symbol`] exported? + /// + /// NOTE: does not support CJS right now. + pub fn is_exported(&self) -> bool { + let is_in_exportable_scope = self.is_root() || self.is_in_ts_namespace(); + (is_in_exportable_scope + && (self.flags.contains(SymbolFlags::Export) + || self.semantic.module_record().exported_bindings.contains_key(self.name()))) + || self.in_export_node() + } + + #[inline] + fn is_in_ts_namespace(&self) -> bool { + self.scopes().get_flags(self.scope_id()).is_ts_module_block() + } + + /// We need to do this due to limitations of [`Semantic`]. + fn in_export_node(&self) -> bool { + for parent in self.nodes().iter_parents(self.declaration_id()).skip(1) { + match parent.kind() { + AstKind::ModuleDeclaration(module) => { + return module.is_export(); + } + AstKind::VariableDeclaration(_) => { + continue; + } + _ => { + return false; + } + } + } + false + } + + #[inline] + pub fn is_in_jsx(&self) -> bool { + self.semantic.source_type().is_jsx() + } + + #[inline] + pub fn is_in_ts(&self) -> bool { + self.semantic.source_type().is_typescript() + } + + #[inline] + pub fn get_snippet(&self, span: Span) -> &'a str { + span.source_text(self.semantic.source_text()) + } +} + +impl<'a> PartialEq> for Symbol<'_, 'a> { + fn eq(&self, other: &IdentifierReference<'a>) -> bool { + // cheap: no resolved reference means its a global reference + let Some(reference_id) = other.reference_id.get() else { + return false; + }; + let reference = self.symbols().get_reference(reference_id); + reference.symbol_id().is_some_and(|symbol_id| self.id == symbol_id) + } +} + +impl<'a> PartialEq> for Symbol<'_, 'a> { + fn eq(&self, id: &BindingIdentifier<'a>) -> bool { + id.symbol_id.get().is_some_and(|id| self.id == id) + } +} + +impl<'a> PartialEq> for Symbol<'_, 'a> { + fn eq(&self, id: &BindingPattern<'a>) -> bool { + id.get_binding_identifier().is_some_and(|id| self == id) + } +} + +impl<'a> PartialEq> for Symbol<'_, 'a> { + fn eq(&self, target: &AssignmentTarget<'a>) -> bool { + match target { + AssignmentTarget::AssignmentTargetIdentifier(id) => self == id.as_ref(), + _ => false, + } + } +} + +impl fmt::Debug for Symbol<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Symbol") + .field("id", &self.id) + .field("name", &self.name()) + .field("flags", &self.flags) + .field("declaration_node", &self.declaration().kind().debug_name()) + .field("references", &self.references().collect::>()) + .finish() + } +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs new file mode 100644 index 0000000000000..34c4856e68268 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs @@ -0,0 +1,827 @@ +//! Test cases from vanilla eslint + +use crate::{tester::Tester, RuleMeta as _}; + +use super::NoUnusedVars; + +/// These are tests from ESLint that are not passing. If you make a change that +/// causes this test to fail, that's a good thing! +/// +/// 1. Delete the offending test case from this function +/// 2. Find where it is commented out in [`test`] and un-comment it. Cases that are in `pass` in +/// [`fixme`] will be in [`fail`] in [`test`] +/// 3. Add it to your PR :) +#[test] +fn fixme() { + let pass = vec![ + // ESLint says this one should pass, but I disagree. foox could be + // safely removed here. + ("function foo(cb) { cb = function(a) { return cb(1 + a); }(); } foo();", None), + ("function foo(cb) { cb = (0, function(a) { cb(1 + a); }); } foo();", None), + ( + "let x = []; + x = x.concat(x);", + None, + ), // { "ecmaVersion": 2015 }, + ]; + let fail = vec![]; + Tester::new(NoUnusedVars::NAME, pass, fail).test(); +} +#[test] +fn test() { + let pass = vec![ + ("var foo = 5; + + label: while (true) { + console.log(foo); + break label; + }", None), +("var foo = 5; + + while (true) { + console.log(foo); + break; + }", None), +("for (let prop in box) { + box[prop] = parseInt(box[prop]); + }", None), // { "ecmaVersion": 6 }, +("var box = {a: 2}; + for (var prop in box) { + box[prop] = parseInt(box[prop]); + }", None), +("f({ set foo(a) { return; } });", None), +("a; var a;", Some(serde_json::json!(["all"]))), +("var a=10; alert(a);", Some(serde_json::json!(["all"]))), +("var a=10; (function() { alert(a); })();", Some(serde_json::json!(["all"]))), +("var a=10; (function() { setTimeout(function() { alert(a); }, 0); })();", Some(serde_json::json!(["all"]))), +("var a=10; d[a] = 0;", Some(serde_json::json!(["all"]))), +("(function() { var a=10; return a; })();", Some(serde_json::json!(["all"]))), +("(function g() {})()", Some(serde_json::json!(["all"]))), +("function f(a) {alert(a);}; f();", Some(serde_json::json!(["all"]))), +("var c = 0; function f(a){ var b = a; return b; }; f(c);", Some(serde_json::json!(["all"]))), +("function a(x, y){ return y; }; a();", Some(serde_json::json!(["all"]))), +("var arr1 = [1, 2]; var arr2 = [3, 4]; for (var i in arr1) { arr1[i] = 5; } for (var i in arr2) { arr2[i] = 10; }", Some(serde_json::json!(["all"]))), +("var a=10;", Some(serde_json::json!(["local"]))), +(r#"var min = "min"; Math[min];"#, Some(serde_json::json!(["all"]))), +("Foo.bar = function(baz) { return baz; };", Some(serde_json::json!(["all"]))), +("myFunc(function foo() {}.bind(this))", None), +("myFunc(function foo(){}.toString())", None), +("function foo(first, second) { + doStuff(function() { + console.log(second);});}; foo()", None), +("(function() { var doSomething = function doSomething() {}; doSomething() }())", None), +// ("/*global a */ a;", None), +("var a=10; (function() { alert(a); })();", Some(serde_json::json!([{ "vars": "all" }]))), +("function g(bar, baz) { return baz; }; g();", Some(serde_json::json!([{ "vars": "all" }]))), +("function g(bar, baz) { return baz; }; g();", Some(serde_json::json!([{ "vars": "all", "args": "after-used" }]))), +("function g(bar, baz) { return bar; }; g();", Some(serde_json::json!([{ "vars": "all", "args": "none" }]))), +("function g(bar, baz) { return 2; }; g();", Some(serde_json::json!([{ "vars": "all", "args": "none" }]))), +("function g(bar, baz) { return bar + baz; }; g();", Some(serde_json::json!([{ "vars": "local", "args": "all" }]))), +("var g = function(bar, baz) { return 2; }; g();", Some(serde_json::json!([{ "vars": "all", "args": "none" }]))), +("(function z() { z(); })();", None), +(" ", None), // { "globals": { "a": true } }, +(r#"var who = "Paul"; + module.exports = `Hello ${who}!`;"#, None), // { "ecmaVersion": 6 }, +("export var foo = 123;", None), // { "ecmaVersion": 6, "sourceType": "module" }, +("export function foo () {}", None), // { "ecmaVersion": 6, "sourceType": "module" }, +("let toUpper = (partial) => partial.toUpperCase; export {toUpper}", None), // { "ecmaVersion": 6, "sourceType": "module" }, +("export class foo {}", None), // { "ecmaVersion": 6, "sourceType": "module" }, +("class Foo{}; var x = new Foo(); x.foo()", None), // { "ecmaVersion": 6 }, +(r#"const foo = "hello!";function bar(foobar = foo) { foobar.replace(/!$/, " world!");} + bar();"#, None), // { "ecmaVersion": 6 }, +("function Foo(){}; var x = new Foo(); x.foo()", None), +("function foo() {var foo = 1; return foo}; foo();", None), +("function foo(foo) {return foo}; foo(1);", None), +("function foo() {function foo() {return 1;}; return foo()}; foo();", None), +("function foo() {var foo = 1; return foo}; foo();", None), // { "ecmaVersion": 6 }, +("function foo(foo) {return foo}; foo(1);", None), // { "ecmaVersion": 6 }, +("function foo() {function foo() {return 1;}; return foo()}; foo();", None), // { "ecmaVersion": 6 }, +("const x = 1; const [y = x] = []; foo(y);", None), // { "ecmaVersion": 6 }, +("const x = 1; const {y = x} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("const x = 1; const {z: [y = x]} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("const x = []; const {z: [y] = x} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("const x = 1; let y; [y = x] = []; foo(y);", None), // { "ecmaVersion": 6 }, +("const x = 1; let y; ({z: [y = x]} = {}); foo(y);", None), // { "ecmaVersion": 6 }, +("const x = []; let y; ({z: [y] = x} = {}); foo(y);", None), // { "ecmaVersion": 6 }, +("const x = 1; function foo(y = x) { bar(y); } foo();", None), // { "ecmaVersion": 6 }, +("const x = 1; function foo({y = x} = {}) { bar(y); } foo();", None), // { "ecmaVersion": 6 }, +("const x = 1; function foo(y = function(z = x) { bar(z); }) { y(); } foo();", None), // { "ecmaVersion": 6 }, +("const x = 1; function foo(y = function() { bar(x); }) { y(); } foo();", None), // { "ecmaVersion": 6 }, +("var x = 1; var [y = x] = []; foo(y);", None), // { "ecmaVersion": 6 }, +("var x = 1; var {y = x} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("var x = 1; var {z: [y = x]} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("var x = []; var {z: [y] = x} = {}; foo(y);", None), // { "ecmaVersion": 6 }, +("var x = 1, y; [y = x] = []; foo(y);", None), // { "ecmaVersion": 6 }, +("var x = 1, y; ({z: [y = x]} = {}); foo(y);", None), // { "ecmaVersion": 6 }, +("var x = [], y; ({z: [y] = x} = {}); foo(y);", None), // { "ecmaVersion": 6 }, +("var x = 1; function foo(y = x) { bar(y); } foo();", None), // { "ecmaVersion": 6 }, +("var x = 1; function foo({y = x} = {}) { bar(y); } foo();", None), // { "ecmaVersion": 6 }, +("var x = 1; function foo(y = function(z = x) { bar(z); }) { y(); } foo();", None), // { "ecmaVersion": 6 }, +("var x = 1; function foo(y = function() { bar(x); }) { y(); } foo();", None), // { "ecmaVersion": 6 }, +// ("/*exported toaster*/ var toaster = 'great'", None), +// ("/*exported toaster, poster*/ var toaster = 1; poster = 0;", None), +// ("/*exported x*/ var { x } = y", None), // { "ecmaVersion": 6 }, +// ("/*exported x, y*/ var { x, y } = z", None), // { "ecmaVersion": 6 }, +// ("/*eslint custom/use-every-a:1*/ var a;", None), +// ("/*eslint custom/use-every-a:1*/ !function(a) { return 1; }", None), +// ("/*eslint custom/use-every-a:1*/ !function() { var a; return 1 }", None), +("var _a;", Some(serde_json::json!([{ "vars": "all", "varsIgnorePattern": "^_" }]))), +("var a; function foo() { var _b; } foo();", Some(serde_json::json!([{ "vars": "local", "varsIgnorePattern": "^_" }]))), +("function foo(_a) { } foo();", Some(serde_json::json!([{ "args": "all", "argsIgnorePattern": "^_" }]))), +("function foo(a, _b) { return a; } foo();", Some(serde_json::json!([{ "args": "after-used", "argsIgnorePattern": "^_" }]))), +("var [ firstItemIgnored, secondItem ] = items; + console.log(secondItem);", Some(serde_json::json!([{ "vars": "all", "varsIgnorePattern": "[iI]gnored" }]))), // { "ecmaVersion": 6 }, +("const [ a, _b, c ] = items; + console.log(a+c);", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +("const [ [a, _b, c] ] = items; + console.log(a+c);", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +("const { x: [_a, foo] } = bar; + console.log(foo);", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +("function baz([_b, foo]) { foo; }; + baz()", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +("function baz({x: [_b, foo]}) {foo}; + baz()", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +("function baz([{x: [_b, foo]}]) {foo}; + baz()", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +(" + let _a, b; + foo.forEach(item => { + [_a, b] = item; + doSomething(b); + }); + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 6 }, +(" + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; + + // doesn't report _a + let _a, b; + [_a, b] = foo; + _a = 1; + b; + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }]))), // { "ecmaVersion": 2018 }, +(" + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; + + // doesn't report _a + let _a, b; + _a = 1; + ({_a, ...b } = foo); + b; + ", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "ignoreRestSiblings": true }]))), // { "ecmaVersion": 2018 }, +("try {} catch ([firstError]) {}", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "Error$" }]))), // { "ecmaVersion": 2015 }, +("(function(obj) { var name; for ( name in obj ) return; })({});", None), +("(function(obj) { var name; for ( name in obj ) { return; } })({});", None), +("(function(obj) { for ( var name in obj ) { return true } })({})", None), +("(function(obj) { for ( var name in obj ) return true })({})", None), +("(function(obj) { let name; for ( name in obj ) return; })({});", None), // { "ecmaVersion": 6 }, +("(function(obj) { let name; for ( name in obj ) { return; } })({});", None), // { "ecmaVersion": 6 }, +("(function(obj) { for ( let name in obj ) { return true } })({})", None), // { "ecmaVersion": 6 }, +("(function(obj) { for ( let name in obj ) return true })({})", None), // { "ecmaVersion": 6 }, +("(function(obj) { for ( const name in obj ) { return true } })({})", None), // { "ecmaVersion": 6 }, +("(function(obj) { for ( const name in obj ) return true })({})", None), // { "ecmaVersion": 6 }, +("(function(iter) { let name; for ( name of iter ) return; })({});", None), // { "ecmaVersion": 6 }, +("(function(iter) { let name; for ( name of iter ) { return; } })({});", None), // { "ecmaVersion": 6 }, +("(function(iter) { for ( let name of iter ) { return true } })({})", None), // { "ecmaVersion": 6 }, +("(function(iter) { for ( let name of iter ) return true })({})", None), // { "ecmaVersion": 6 }, +("(function(iter) { for ( const name of iter ) { return true } })({})", None), // { "ecmaVersion": 6 }, +("(function(iter) { for ( const name of iter ) return true })({})", None), // { "ecmaVersion": 6 }, +("let x = 0; foo = (0, x++);", None), // { "ecmaVersion": 6 }, +("let x = 0; foo = (0, x += 1);", None), // { "ecmaVersion": 6 }, +("let x = 0; foo = (0, x = x + 1);", None), // { "ecmaVersion": 6 }, +("try{}catch(err){}", Some(serde_json::json!([{ "caughtErrors": "none" }]))), +("try{}catch(err){console.error(err);}", Some(serde_json::json!([{ "caughtErrors": "all" }]))), +("try{}catch(ignoreErr){}", Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "^ignore" }]))), +("try{}catch(ignoreErr){}", Some(serde_json::json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^ignore" }]))), +("try {} catch ({ message, stack }) {}", Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "message|stack" }]))), // { "ecmaVersion": 2015 }, +("try {} catch ({ errors: [firstError] }) {}", Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "Error$" }]))), // { "ecmaVersion": 2015 }, +("try{}catch(err){}", Some(serde_json::json!([{ "caughtErrors": "none", "vars": "all", "args": "all" }]))), +("const data = { type: 'coords', x: 1, y: 2 }; + const { type, ...coords } = data; + console.log(coords);", Some(serde_json::json!([{ "ignoreRestSiblings": true }]))), // { "ecmaVersion": 2018 }, +("try {} catch ({ foo, ...bar }) { console.log(bar); }", Some(serde_json::json!([{ "ignoreRestSiblings": true }]))), // { "ecmaVersion": 2018 }, +("var a = 0, b; b = a = a + 1; foo(b);", None), +("var a = 0, b; b = a += a + 1; foo(b);", None), +("var a = 0, b; b = a++; foo(b);", None), +("function foo(a) { var b = a = a + 1; bar(b) } foo();", None), +("function foo(a) { var b = a += a + 1; bar(b) } foo();", None), +("function foo(a) { var b = a++; bar(b) } foo();", None), +(r#"var unregisterFooWatcher; + // ... + unregisterFooWatcher = $scope.$watch( "foo", function() { + // ...some code.. + unregisterFooWatcher(); + }); + "#, None), +("var ref; + ref = setInterval( + function(){ + clearInterval(ref); + }, 10); + ", None), +("var _timer; + function f() { + _timer = setTimeout(function () {}, _timer ? 100 : 0); + } + f(); + ", None), +("function foo(cb) { cb = function() { function something(a) { cb(1 + a); } register(something); }(); } foo();", None), +("function* foo(cb) { cb = yield function(a) { cb(1 + a); }; } foo();", None), // { "ecmaVersion": 6 }, +("function foo(cb) { cb = tag`hello${function(a) { cb(1 + a); }}`; } foo();", None), // { "ecmaVersion": 6 }, +("function foo(cb) { var b; cb = b = function(a) { cb(1 + a); }; b(); } foo();", None), +("function someFunction() { + var a = 0, i; + for (i = 0; i < 2; i++) { + a = myFunction(a); + } + } + someFunction(); + ", None), +("(function(a, b, {c, d}) { d })", Some(serde_json::json!([{ "argsIgnorePattern": "c" }]))), // { "ecmaVersion": 6 }, +("(function(a, b, {c, d}) { c })", Some(serde_json::json!([{ "argsIgnorePattern": "d" }]))), // { "ecmaVersion": 6 }, +("(function(a, b, c) { c })", Some(serde_json::json!([{ "argsIgnorePattern": "c" }]))), +("(function(a, b, {c, d}) { c })", Some(serde_json::json!([{ "argsIgnorePattern": "[cd]" }]))), // { "ecmaVersion": 6 }, +("(class { set foo(UNUSED) {} })", None), // { "ecmaVersion": 6 }, +("class Foo { set bar(UNUSED) {} } console.log(Foo)", None), // { "ecmaVersion": 6 }, +("(({a, ...rest}) => rest)", Some(serde_json::json!([{ "args": "all", "ignoreRestSiblings": true }]))), // { "ecmaVersion": 2018 }, +("let foo, rest; + ({ foo, ...rest } = something); + console.log(rest);", Some(serde_json::json!([{ "ignoreRestSiblings": true }]))), // { "ecmaVersion": 2020 }, +// ("/*eslint custom/use-every-a:1*/ !function(b, a) { return 1 }", None), +("var a = function () { a(); }; a();", None), +("var a = function(){ return function () { a(); } }; a();", None), +("const a = () => { a(); }; a();", None), // { "ecmaVersion": 2015 }, +("const a = () => () => { a(); }; a();", None), // { "ecmaVersion": 2015 }, +(r#"export * as ns from "source""#, None), // { "ecmaVersion": 2020, "sourceType": "module" }, +("import.meta", None), // { "ecmaVersion": 2020, "sourceType": "module" }, +// NOTE (@DonIsaac) ESLint thinks this counts as being used, I disagree +// ("var a; a ||= 1;", None), // { "ecmaVersion": 2021 }, +// ("var a; a &&= 1;", None), // { "ecmaVersion": 2021 }, +// ("var a; a ??= 1;", None), // { "ecmaVersion": 2021 }, +("class Foo { static {} }", Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true }]))), // { "ecmaVersion": 2022 }, +("class Foo { static {} }", Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true, "varsIgnorePattern": "^_" }]))), // { "ecmaVersion": 2022 }, +("class Foo { static {} }", Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": false, "varsIgnorePattern": "^Foo" }]))), // { "ecmaVersion": 2022 }, +("const a = 5; const _c = a + 5;", Some(serde_json::json!([{ "args": "all", "varsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]))), // { "ecmaVersion": 6 }, +("(function foo(a, _b) { return a + 5 })(5)", Some(serde_json::json!([{ "args": "all", "argsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]))), +("const [ a, _b, c ] = items; + console.log(a+c);", Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "reportUsedIgnorePattern": true }]))), // { "ecmaVersion": 6 } + ]; + + let fail = vec![ + ("function foox() { return foox(); }", None), + // ("(function() { function foox() { if (true) { return foox(); } } }())", None), + ("var a=10", None), + ("function f() { var a = 1; return function(){ f(a *= 2); }; }", None), + ("function f() { var a = 1; return function(){ f(++a); }; }", None), + // ("/*global a */", None), + ( + "function foo(first, second) { + doStuff(function() { + console.log(second);});};", + None, + ), + ("var a=10;", Some(serde_json::json!(["all"]))), + ("var a=10; a=20;", Some(serde_json::json!(["all"]))), + ("var a=10; (function() { var a = 1; alert(a); })();", Some(serde_json::json!(["all"]))), + ("var a=10, b=0, c=null; alert(a+b)", Some(serde_json::json!(["all"]))), + ( + "var a=10, b=0, c=null; setTimeout(function() { var b=2; alert(a+b+c); }, 0);", + Some(serde_json::json!(["all"])), + ), + ( + "var a=10, b=0, c=null; setTimeout(function() { var b=2; var c=2; alert(a+b+c); }, 0);", + Some(serde_json::json!(["all"])), + ), + ("function f(){var a=[];return a.map(function(){});}", Some(serde_json::json!(["all"]))), + ("function f(){var a=[];return a.map(function g(){});}", Some(serde_json::json!(["all"]))), + ( + "function foo() {function foo(x) { + return x; }; return function() {return foo; }; }", + None, + ), + ( + "function f(){var x;function a(){x=42;}function b(){alert(x);}}", + Some(serde_json::json!(["all"])), + ), + ("function f(a) {}; f();", Some(serde_json::json!(["all"]))), + ("function a(x, y, z){ return y; }; a();", Some(serde_json::json!(["all"]))), + ("var min = Math.min", Some(serde_json::json!(["all"]))), + ("var min = {min: 1}", Some(serde_json::json!(["all"]))), + ("Foo.bar = function(baz) { return 1; };", Some(serde_json::json!(["all"]))), + ("var min = {min: 1}", Some(serde_json::json!([{ "vars": "all" }]))), + ( + "function gg(baz, bar) { return baz; }; gg();", + Some(serde_json::json!([{ "vars": "all" }])), + ), + ( + "(function(foo, baz, bar) { return baz; })();", + Some(serde_json::json!([{ "vars": "all", "args": "after-used" }])), + ), + ( + "(function(foo, baz, bar) { return baz; })();", + Some(serde_json::json!([{ "vars": "all", "args": "all" }])), + ), + ( + "(function z(foo) { var bar = 33; })();", + Some(serde_json::json!([{ "vars": "all", "args": "all" }])), + ), + ("(function z(foo) { z(); })();", Some(serde_json::json!([{}]))), + ( + "function f() { var a = 1; return function(){ f(a = 2); }; }", + Some(serde_json::json!([{}])), + ), + (r#"import x from "y";"#, None), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "export function fn2({ x, y }) { + console.log(x); + };", + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ( + "export function fn2( x, y ) { + console.log(x); + };", + None, + ), // { "ecmaVersion": 6, "sourceType": "module" }, + ("/*exported max*/ var max = 1, min = {min: 1}", None), + ("/*exported x*/ var { x, y } = z", None), // { "ecmaVersion": 6 }, + ("var _a; var b;", Some(serde_json::json!([{ "vars": "all", "varsIgnorePattern": "^_" }]))), + ( + "var a; function foo() { var _b; var c_; } foo();", + Some(serde_json::json!([{ "vars": "local", "varsIgnorePattern": "^_" }])), + ), + ( + "function foo(a, _b) { } foo();", + Some(serde_json::json!([{ "args": "all", "argsIgnorePattern": "^_" }])), + ), + ( + "function foo(a, _b, c) { return a; } foo();", + Some(serde_json::json!([{ "args": "after-used", "argsIgnorePattern": "^_" }])), + ), + ( + "function foo(_a) { } foo();", + Some(serde_json::json!([{ "args": "all", "argsIgnorePattern": "[iI]gnored" }])), + ), + ( + "var [ firstItemIgnored, secondItem ] = items;", + Some(serde_json::json!([{ "vars": "all", "varsIgnorePattern": "[iI]gnored" }])), + ), // { "ecmaVersion": 6 }, + ( + " + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const newArray = [a, c]; + ", + Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), + ), // { "ecmaVersion": 2020 }, + ( + " + const array = ['a', 'b', 'c', 'd', 'e']; + const [a, _b, c] = array; + ", + Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), + ), // { "ecmaVersion": 2020 }, + ( + " + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const fooArray = ['foo']; + const barArray = ['bar']; + const ignoreArray = ['ignore']; + ", + Some( + serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "varsIgnorePattern": "ignore" }]), + ), + ), // { "ecmaVersion": 2020 }, + ( + " + const array = [obj]; + const [{_a, foo}] = array; + console.log(foo); + ", + Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), + ), // { "ecmaVersion": 2020 }, + ( + " + function foo([{_a, bar}]) { + bar; + } + foo(); + ", + Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), + ), // { "ecmaVersion": 2020 }, + ( + " + let _a, b; + + foo.forEach(item => { + [a, b] = item; + }); + ", + Some(serde_json::json!([{ "destructuredArrayIgnorePattern": "^_" }])), + ), // { "ecmaVersion": 2020 }, + ("(function(obj) { var name; for ( name in obj ) { i(); return; } })({});", None), + ("(function(obj) { var name; for ( name in obj ) { } })({});", None), + ("(function(obj) { for ( var name in obj ) { } })({});", None), + ("(function(iter) { var name; for ( name of iter ) { i(); return; } })({});", None), // { "ecmaVersion": 6 }, + ("(function(iter) { var name; for ( name of iter ) { } })({});", None), // { "ecmaVersion": 6 }, + ("(function(iter) { for ( var name of iter ) { } })({});", None), // { "ecmaVersion": 6 }, + // ( + // " + // /* global foobar, foo, bar */ + // foobar;", + // None, + // ), + // ( + // " + // /* global foobar, + // foo, + // bar + // */ + // foobar;", + // None, + // ), + ( + "const data = { type: 'coords', x: 1, y: 2 }; + const { type, ...coords } = data; + console.log(coords);", + None, + ), // { "ecmaVersion": 2018 }, + ( + "const data = { type: 'coords', x: 2, y: 2 }; + const { type, ...coords } = data; + console.log(type)", + Some(serde_json::json!([{ "ignoreRestSiblings": true }])), + ), // { "ecmaVersion": 2018 }, + ( + "let type, coords; + ({ type, ...coords } = data); + console.log(type)", + Some(serde_json::json!([{ "ignoreRestSiblings": true }])), + ), // { "ecmaVersion": 2018 }, + ( + "const data = { type: 'coords', x: 3, y: 2 }; + const { type, ...coords } = data; + console.log(type)", + None, + ), // { "ecmaVersion": 2018 }, + ( + "const data = { vars: ['x','y'], x: 1, y: 2 }; + const { vars: [x], ...coords } = data; + console.log(coords)", + None, + ), // { "ecmaVersion": 2018 }, + ( + "const data = { defaults: { x: 0 }, x: 1, y: 2 }; + const { defaults: { x }, ...coords } = data; + console.log(coords)", + None, + ), // { "ecmaVersion": 2018 }, + ( + "(({a, ...rest}) => {})", + Some(serde_json::json!([{ "args": "all", "ignoreRestSiblings": true }])), + ), // { "ecmaVersion": 2018 }, + // ( + // "/* global a$fooz,$foo */ + // a$fooz;", + // None, + // ), + // ( + // "/* globals a$fooz, $ */ + // a$fooz;", + // None, + // ), + // ("/*globals $foo*/", None), + // ("/* global global*/", None), + // ("/*global foo:true*/", None), + // ( + // "/*global ๅค‰ๆ•ฐ, ๆ•ฐ*/ + // ๅค‰ๆ•ฐ;", + // None, + // ), + // ( + // "/*global ๐ ฎท๐ฉธฝ, ๐ ฎท*/ + // \\u{20BB7}\\u{29E3D};", + // None, + // ), // { "ecmaVersion": 6 }, + ("export default function(a) {}", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export default function(a, b) { console.log(a); }", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export default (function(a) {});", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export default (function(a, b) { console.log(a); });", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export default (a) => {};", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("export default (a, b) => { console.log(a); };", None), // { "ecmaVersion": 6, "sourceType": "module" }, + ("try{}catch(err){};", None), + ("try{}catch(err){};", Some(serde_json::json!([{ "caughtErrors": "all" }]))), + ( + "try{}catch(err){};", + Some( + serde_json::json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^ignore" }]), + ), + ), + ( + "try{}catch(err){};", + Some(serde_json::json!([{ "caughtErrors": "all", "varsIgnorePattern": "^err" }])), + ), + ( + "try{}catch(err){};", + Some(serde_json::json!([{ "caughtErrors": "all", "varsIgnorePattern": "^." }])), + ), + ( + "try{}catch(ignoreErr){}try{}catch(err){};", + Some( + serde_json::json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^ignore" }]), + ), + ), + ( + "try{}catch(error){}try{}catch(err){};", + Some( + serde_json::json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^ignore" }]), + ), + ), + ( + "try{}catch(err){};", + Some(serde_json::json!([{ "vars": "all", "args": "all", "caughtErrors": "all" }])), + ), + ( + "try{}catch(err){};", + Some( + serde_json::json!([ { "vars": "all", "args": "all", "caughtErrors": "all", "argsIgnorePattern": "^er" } ]), + ), + ), + ("var a = 0; a = a + 1;", None), + ("var a = 0; a = a + a;", None), + ("var a = 0; a += a + 1;", None), + ("var a = 0; a++;", None), + ("function foo(a) { a = a + 1 } foo();", None), + ("function foo(a) { a += a + 1 } foo();", None), + ("function foo(a) { a++ } foo();", None), + ("var a = 3; a = a * 5 + 6;", None), + ("var a = 2, b = 4; a = a * 2 + b;", None), + // https://github.com/oxc-project/oxc/issues/4436 + ("function foo(cb) { cb = function(a) { cb(1 + a); }; bar(not_cb); } foo();", None), + ("function foo(cb) { cb = (function(a) { cb(1 + a); }, cb); } foo();", None), + // ("function foo(cb) { cb = (0, function(a) { cb(1 + a); }); } foo();", None), + ( + "while (a) { + function foo(b) { + b = b + 1; + } + foo() + }", + None, + ), + ("(function(a, b, c) {})", Some(serde_json::json!([{ "argsIgnorePattern": "c" }]))), + ("(function(a, b, {c, d}) {})", Some(serde_json::json!([{ "argsIgnorePattern": "[cd]" }]))), // { "ecmaVersion": 6 }, + ("(function(a, b, {c, d}) {})", Some(serde_json::json!([{ "argsIgnorePattern": "c" }]))), // { "ecmaVersion": 6 }, + ("(function(a, b, {c, d}) {})", Some(serde_json::json!([{ "argsIgnorePattern": "d" }]))), // { "ecmaVersion": 6 }, + // ( + // "/*global + // foo*/", + // None, + // ), + ("(function ({ a }, b ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ("(function ({ a }, { b, c } ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ( + "let x = 0; + x++, x = 0;", + None, + ), // { "ecmaVersion": 2015 }, + ( + "let x = 0; + x++, x = 0; + x=3;", + None, + ), // { "ecmaVersion": 2015 }, + ("let x = 0; x++, 0;", None), // { "ecmaVersion": 2015 }, + ("let x = 0; 0, x++;", None), // { "ecmaVersion": 2015 }, + ("let x = 0; 0, (1, x++);", None), // { "ecmaVersion": 2015 }, + ("let x = 0; foo = (x++, 0);", None), // { "ecmaVersion": 2015 }, + ("let x = 0; foo = ((0, x++), 0);", None), // { "ecmaVersion": 2015 }, + ("let x = 0; x += 1, 0;", None), // { "ecmaVersion": 2015 }, + ("let x = 0; 0, x += 1;", None), // { "ecmaVersion": 2015 }, + ("let x = 0; 0, (1, x += 1);", None), // { "ecmaVersion": 2015 }, + ("let x = 0; foo = (x += 1, 0);", None), // { "ecmaVersion": 2015 }, + ("let x = 0; foo = ((0, x += 1), 0);", None), // { "ecmaVersion": 2015 }, + ( + "let z = 0; + z = z + 1, z = 2; + ", + None, + ), // { "ecmaVersion": 2020 }, + ( + "let z = 0; + z = z+1, z = 2; + z = 3;", + None, + ), // { "ecmaVersion": 2020 }, + ( + "let z = 0; + z = z+1, z = 2; + z = z+3; + ", + None, + ), // { "ecmaVersion": 2020 }, + ("let x = 0; 0, x = x+1;", None), // { "ecmaVersion": 2020 }, + ("let x = 0; x = x+1, 0;", None), // { "ecmaVersion": 2020 }, + // https://github.com/oxc-project/oxc/issues/4437 + // ("let x = 0; foo = ((0, x = x + 1), 0);", None), // { "ecmaVersion": 2020 }, + // ("let x = 0; foo = (x = x+1, 0);", None), // { "ecmaVersion": 2020 }, + ("let x = 0; 0, (1, x=x+1);", None), // { "ecmaVersion": 2020 }, + ("(function ({ a, b }, { c } ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ("(function ([ a ], b ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ("(function ([ a ], [ b, c ] ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ("(function ([ a, b ], [ c ] ) { return b; })();", None), // { "ecmaVersion": 2015 }, + ( + "(function(_a) {})();", + Some(serde_json::json!([{ "args": "all", "varsIgnorePattern": "^_" }])), + ), + ( + "(function(_a) {})();", + Some(serde_json::json!([{ "args": "all", "caughtErrorsIgnorePattern": "^_" }])), + ), + ("var a = function() { a(); };", None), + ("var a = function(){ return function() { a(); } };", None), + ("const a = () => () => { a(); };", None), // { "ecmaVersion": 2015 }, + ( + "let myArray = [1,2,3,4].filter((x) => x == 0); + myArray = myArray.filter((x) => x == 1);", + None, + ), // { "ecmaVersion": 2015 }, + ("const a = 1; a += 1;", None), // { "ecmaVersion": 2015 }, + ("const a = () => { a(); };", None), // { "ecmaVersion": 2015 }, + // TODO + // ( + // "let x = []; + // x = x.concat(x);", + // None, + // ), // { "ecmaVersion": 2015 }, + ( + "let a = 'a'; + a = 10; + function foo(){ + a = 11; + a = () => { + a = 13 + } + }", + None, + ), // { "ecmaVersion": 2020 }, + ( + "let foo; + init(); + foo = foo + 2; + function init() { + foo = 1; + }", + None, + ), // { "ecmaVersion": 2020 }, + ( + "function foo(n) { + if (n < 2) return 1; + return n * foo(n - 1); + }", + None, + ), // { "ecmaVersion": 2020 }, + ( + "let c = 'c' + c = 10 + function foo1() { + c = 11 + c = () => { + c = 13 + } + } + + c = foo1", + None, + ), // { "ecmaVersion": 2020 }, + ( + "class Foo { static {} }", + Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": false }])), + ), // { "ecmaVersion": 2022 }, + ("class Foo { static {} }", None), // { "ecmaVersion": 2022 }, + ( + "class Foo { static { var bar; } }", + Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true }])), + ), // { "ecmaVersion": 2022 }, + ("class Foo {}", Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true }]))), // { "ecmaVersion": 2022 }, + ( + "class Foo { static bar; }", + Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true }])), + ), // { "ecmaVersion": 2022 }, + ( + "class Foo { static bar() {} }", + Some(serde_json::json!([{ "ignoreClassWithStaticInitBlock": true }])), + ), // { "ecmaVersion": 2022 }, + ( + "const _a = 5;const _b = _a + 5", + Some( + serde_json::json!([{ "args": "all", "varsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 6 }, + ( + "const _a = 42; foo(() => _a);", + Some( + serde_json::json!([{ "args": "all", "varsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 6 }, + ( + "(function foo(_a) { return _a + 5 })(5)", + Some( + serde_json::json!([{ "args": "all", "argsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), + // TODO + ( + "const [ a, _b ] = items; + console.log(a+_b);", + Some( + serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 6 }, + // ( + // "let _x; + // [_x] = arr; + // foo(_x);", + // Some( + // serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "reportUsedIgnorePattern": true, "varsIgnorePattern": "[iI]gnored" }]), + // ), + // ), // { "ecmaVersion": 6 }, + ( + "const [ignored] = arr; + foo(ignored);", + Some( + serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "reportUsedIgnorePattern": true, "varsIgnorePattern": "[iI]gnored" }]), + ), + ), // { "ecmaVersion": 6 }, + ( + "try{}catch(_err){console.error(_err)}", + Some( + serde_json::json!([{ "caughtErrors": "all", "caughtErrorsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), + ( + "try {} catch ({ message }) { console.error(message); }", + Some( + serde_json::json!([{ "caughtErrorsIgnorePattern": "message", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 2015 }, + ( + "try {} catch ([_a, _b]) { doSomething(_a, _b); }", + Some( + serde_json::json!([{ "caughtErrorsIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 6 }, + ( + "try {} catch ([_a, _b]) { doSomething(_a, _b); }", + Some( + serde_json::json!([{ "destructuredArrayIgnorePattern": "^_", "reportUsedIgnorePattern": true }]), + ), + ), // { "ecmaVersion": 6 }, + ( + " + try { + } catch (_) { + _ = 'foo' + } + ", + Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "foo" }])), + ), + ( + " + try { + } catch (_) { + _ = 'foo' + } + ", + Some( + serde_json::json!([{ "caughtErrorsIgnorePattern": "ignored", "varsIgnorePattern": "_" }]), + ), + ), + ( + "try {} catch ({ message, errors: [firstError] }) {}", + Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "foo" }])), + ), // { "ecmaVersion": 2015 }, + ( + "try {} catch ({ stack: $ }) { $ = 'Something broke: ' + $; }", + Some(serde_json::json!([{ "caughtErrorsIgnorePattern": "\\w" }])), + ), // { "ecmaVersion": 2015 }, + ( + " + _ => { _ = _ + 1 }; + ", + Some( + serde_json::json!([{ "argsIgnorePattern": "ignored", "varsIgnorePattern": "_" }]), + ), + ), // { "ecmaVersion": 2015 } + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail).with_snapshot_suffix("eslint").test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/mod.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/mod.rs new file mode 100644 index 0000000000000..56e51fd9da359 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/mod.rs @@ -0,0 +1,5 @@ +mod eslint; +mod oxc; +mod typescript_eslint; + +use super::NoUnusedVars; diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs new file mode 100644 index 0000000000000..13f831ad63555 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -0,0 +1,660 @@ +//! Test cases created by oxc maintainers + +use super::NoUnusedVars; +use crate::{tester::Tester, RuleMeta as _}; +use serde_json::json; + +#[test] +fn test_vars_simple() { + let pass = vec![ + ("let a = 1; console.log(a)", None), + ("let a = 1; new Foo(a)", None), + ("let a = 1; let b = a + 1; console.log(b)", None), + ("let a = 1; if (true) { console.log(a) }", None), + ("let _a = 1", Some(json!([{ "varsIgnorePattern": "^_" }]))), + ]; + let fail = vec![ + ("let a = 1", None), + ("let a = 1; a = 2", None), + ( + "let _a = 1; console.log(_a)", + Some(json!([{ "varsIgnorePattern": "^_", "reportUsedIgnorePattern": true }])), + ), + ("let _a = 1", Some(json!([{ "argsIgnorePattern": "^_" }]))), + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-simple") + .test_and_snapshot(); +} + +#[test] +fn test_vars_self_use() { + let pass = vec![ + " + function foo() { + let bar = 0; + return bar++; + } + foo(); + ", + ]; + let fail = vec![ + " + function foo() { + return foo + } + ", + " + const foo = () => { + return foo + } + ", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-self-use") + .test_and_snapshot(); +} + +#[test] +fn test_vars_discarded_reads() { + let pass = vec![ + // https://github.com/oxc-project/oxc/pull/4445#issuecomment-2254122889 + " + (() => { + const t = import.meta.url, + s = {}; + return '' !== t && (s.resourcesUrl = new URL('.', t).href), e(s); + })(); + ", + "var a; b !== '' && (x = a, f(c))", + " + class Test { + async updateContextGroup(t, i, s = !0) { + s ? await this.leave(i) : await this.join(t, i), false; + } + } + + new Test(); + ", + ]; + + let fail = vec![ + " + function foo(a) { return (a, 0); } + foo(1); + ", + " + const I = (e) => (l) => { + e.push(l), n || false; + }; + ", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-discarded-read") + .test_and_snapshot(); +} + +#[test] +fn test_vars_reassignment() { + let pass = vec![ + "let i = 0; someFunction(i++);", + " + const thunk = () => 3; + let result = undefined; + result &&= thunk(); + console.log(result); + ", + r" + const thunk = () => 3; + { + let a = thunk(); + console.log(a); + } + ", + "let a = 0; let b = a++; f(b);", + "let a = 0, b = 1; let c = b = a = 1; f(c+b);", + // implicit returns + " + let i = 0; + const func = () => 'value: ' + i++; + func(); + ", + // parenthesis are transparent + "let a = 0; let b = ((a++)); f(b);", + // type casting is transparent + "let a = 0; let b = a as any; f(b);", + "let a = 0; let b = a as unknown as string as unknown as number; f(b);", + "let a = 0; let b = a++ as string | number; f(b);", + // pathological sequence assignments + "let a = 0; let b = (0, a++); f(b);", + "let a = 0; let b = (0, (a++)); f(b);", + "let a = 0; let b = (0, (a++) as string | number); f(b);", + "let a = 0; let b = (0, (0, a++)); f(b);", + "let a = 0; let b = (0, (((0, a++)))); f(b);", + "let a = 0; let b = (0, a) + 1; f(b);", + // reassignment in conditions + " + function foo() { + if (i++ === 0) { + return 'zero'; + } else { + return 'not zero'; + } + var i = 0; + } + foo(); + ", + " + let i = 10; + while (i-- > 0) { + console.log('countdown'); + }; + ", + " + let i = 10; + do { + console.log('countdown'); + } while(i-- > 0); + ", + "let i = 0; i > 0 ? 'positive' : 'negative';", + "let i = 0; i > 0 && console.log('positive');", + ]; + + let fail = vec![ + "let a = 1; a ||= 2;", + "let a = 0; a = a + 1;", + // type casting is transparent + "let a = 0; a = a++ as any;", + "let a = 0; a = a as unknown as string as unknown as number;", + // pathological sequence assignments + "let a = 0; a = ++a;", + "let a = 0; a = (0, ++a);", + "let a = 0; a = (a++, 0);", + "let a = 0; let b = (a++, 0); f(b);", + "let a = 0; let b = (0, (a++, 0)); f(b);", + "let a = 0; let b = ((0, a++), 0); f(b);", + "let a = 0; let b = (a, 0) + 1; f(b);", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-reassignment") + .test_and_snapshot(); +} + +#[test] +fn test_vars_destructure_ignored() { + let pass = vec![ + // ("const { a, ...rest } = obj; console.log(rest)", Some(json![{ "ignoreRestSiblings": true }])) + ]; + let fail = vec![ + ("const { a, ...rest } = obj", Some(json![{ "ignoreRestSiblings": true }])), + ("const [a, ...rest] = arr", Some(json![{ "ignoreRestSiblings": true }])), + ( + "const { a: { b }, ...rest } = obj; console.log(a)", + Some(json![{ "ignoreRestSiblings": true }]), + ), + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-destructure-ignored") + .test_and_snapshot(); +} + +#[test] +fn test_vars_catch() { + let pass = vec![ + // lb + ("try {} catch (e) { throw e }", None), + ("try {} catch (e) { }", Some(json!([{ "caughtErrors": "none" }]))), + ("try {} catch { }", None), + ]; + let fail = vec![ + // lb + ("try {} catch (e) { }", Some(json!([{ "caughtErrors": "all" }]))), + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .with_snapshot_suffix("oxc-vars-catch") + .test_and_snapshot(); +} + +#[test] +fn test_functions() { + let pass = vec![ + "function foo() {}\nfoo()", + "const a = () => {}; a();", + "var foo = function foo() {}\n foo();", + "var foo = function bar() {}\n foo();", + "var foo; foo = function bar() {}; foo();", + " + const obj = { + foo: function foo () {} + } + f(obj) + ", + " + function foo() {} + function bar() { foo() } + bar() + ", + " + function foo() {} + if (true) { + foo() + } + ", + " + function main() { + function foo() {} + if (true) { foo() } + } + main() + ", + " + function foo() { + return function bar() {} + } + foo()() + ", + " + import debounce from 'debounce'; + + const debouncedFoo = debounce(function foo() { + console.log('do a thing'); + }, 100); + + debouncedFoo(); + ", + // FIXME + " + const createIdFactory = ((): (() => string) => { + let count = 0; + return () => `${count++}` + })(); + + const getId = createIdFactory(); + console.log(getId()); + ", + // calls on optional chains should be valid + " + let foo = () => {}; + foo?.(); + ", + " + function foo(a: number): number; + function foo(a: number | string): number { + return Number(a) + } + foo(); + ", + "export const Component = () =>