Skip to content

Commit be32177

Browse files
committed
fix: nuv options tests
1 parent 6883506 commit be32177

File tree

10 files changed

+2156
-318
lines changed

10 files changed

+2156
-318
lines changed

.typos.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ extend-exclude = [
77
"**/*.snap",
88
"**/*/CHANGELOG.md",
99
"crates/oxc_linter/fixtures",
10+
"crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs",
11+
"crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs",
1012
"crates/oxc_linter/src/rules/jsx_a11y/aria_props.rs",
1113
"crates/oxc_linter/src/rules/jsx_a11y/img_redundant_alt.rs",
1214
"crates/oxc_linter/src/rules/react/no_unknown_property.rs",

crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
#[allow(clippy::wildcard_imports)]
44
use oxc_ast::{ast::*, AstKind};
55
use oxc_semantic::Semantic;
6+
use oxc_syntax::operator::UnaryOperator;
7+
8+
use crate::rules::eslint::no_unused_vars::binding_pattern::{BindingContext, HasAnyUsedBinding};
69

710
use super::binding_pattern::CheckBinding;
811
use super::{options::ArgsOption, NoUnusedVars, Symbol};
912

1013
impl<'s, 'a> Symbol<'s, 'a> {
11-
/// Returns `true` if this function is a callback passed to another function
12-
pub fn is_function_callback(&self) -> bool {
14+
/// Returns `true` if this function is a callback passed to another
15+
/// function. Includes IIFEs.
16+
pub fn is_callback_or_iife(&self) -> bool {
1317
debug_assert!(self.declaration().kind().is_function_like());
1418

1519
for parent in self.iter_parents() {
@@ -20,6 +24,10 @@ impl<'s, 'a> Symbol<'s, 'a> {
2024
AstKind::CallExpression(_) => {
2125
return true;
2226
}
27+
// !function() {}; is an IIFE
28+
AstKind::UnaryExpression(expr) => {
29+
return expr.operator == UnaryOperator::LogicalNot;
30+
}
2331
_ => {
2432
return false;
2533
}
@@ -56,8 +64,49 @@ impl<'s, 'a> Symbol<'s, 'a> {
5664

5765
false
5866
}
67+
68+
fn is_declared_in_for_of_loop(&self) -> bool {
69+
for parent in self.iter_parents() {
70+
match parent.kind() {
71+
AstKind::ParenthesizedExpression(_)
72+
| AstKind::VariableDeclaration(_)
73+
| AstKind::BindingIdentifier(_)
74+
| AstKind::SimpleAssignmentTarget(_)
75+
| AstKind::AssignmentTarget(_) => continue,
76+
AstKind::ForInStatement(ForInStatement { body, .. })
77+
| AstKind::ForOfStatement(ForOfStatement { body, .. }) => match body {
78+
Statement::ReturnStatement(_) => return true,
79+
Statement::BlockStatement(b) => {
80+
return b
81+
.body
82+
.get(0)
83+
.is_some_and(|s| matches!(s, Statement::ReturnStatement(_)))
84+
}
85+
_ => return false,
86+
},
87+
_ => return false,
88+
}
89+
}
90+
91+
false
92+
}
5993
}
94+
6095
impl NoUnusedVars {
96+
pub(super) fn is_allowed_class<'a>(&self, class: &Class<'a>) -> bool {
97+
if self.ignore_class_with_static_init_block
98+
&& class.body.body.iter().any(|el| matches!(el, ClassElement::StaticBlock(_)))
99+
{
100+
return true;
101+
}
102+
103+
false
104+
}
105+
106+
pub(super) fn is_allowed_function<'a>(&self, symbol: &Symbol<'_, 'a>) -> bool {
107+
symbol.is_callback_or_iife() || symbol.is_function_assigned_to_same_name_variable()
108+
}
109+
61110
/// Returns `true` if this unused variable declaration should be allowed
62111
/// (i.e. not reported)
63112
pub(super) fn is_allowed_variable_declaration<'a>(
@@ -69,10 +118,17 @@ impl NoUnusedVars {
69118
return true;
70119
}
71120

121+
// allow unused iterators, since they're required for valid syntax
122+
if symbol.is_declared_in_for_of_loop() {
123+
return true;
124+
}
125+
72126
// check if res is an array/object unpacking pattern that should be ignored
73127
matches!(decl.id.check_unused_binding_pattern(self, symbol), Some(res) if res.is_ignore())
74128
}
75129

130+
/// Returns `true` if this unused parameter should be allowed (i.e. not
131+
/// reported)
76132
pub(super) fn is_allowed_argument<'a>(
77133
&self,
78134
semantic: &Semantic<'a>,
@@ -116,8 +172,15 @@ impl NoUnusedVars {
116172
}
117173

118174
// Parameters are always checked. Must be done after above checks,
119-
// because in those cases a parameter is required
120-
if self.args.is_none() {
175+
// because in those cases a parameter is required. However, even if
176+
// `args` is `all`, it may be ignored using `ignoreRestSiblings` or `destructuredArrayIgnorePattern`.
177+
if self.args.is_all() {
178+
if param.pattern.kind.is_destructuring_pattern() {
179+
// allow unpacked parameters that are ignored via destructuredArrayIgnorePattern
180+
let maybe_unused_binding_pattern =
181+
param.pattern.check_unused_binding_pattern(self, symbol);
182+
return maybe_unused_binding_pattern.map_or(false, |res| res.is_ignore());
183+
}
121184
return false;
122185
}
123186

@@ -131,7 +194,7 @@ impl NoUnusedVars {
131194
// check if this is a positional argument - unused non-positional
132195
// arguments are never allowed
133196
if param.pattern.kind.is_destructuring_pattern() {
134-
// allow unpacked parameters that are ignored
197+
// allow unpacked parameters that are ignored via destructuredArrayIgnorePattern
135198
let maybe_unused_binding_pattern =
136199
param.pattern.check_unused_binding_pattern(self, symbol);
137200
return maybe_unused_binding_pattern.map_or(false, |res| res.is_ignore());
@@ -154,15 +217,7 @@ impl NoUnusedVars {
154217
return false;
155218
}
156219

157-
params.items.iter().skip(position + 1).any(|p| {
158-
let Some(id) = p.pattern.get_binding_identifier() else {
159-
return false;
160-
};
161-
let Some(symbol_id) = id.symbol_id.get() else {
162-
return false;
163-
};
164-
let symbol = Symbol::new(semantic, symbol_id);
165-
symbol.has_usages()
166-
})
220+
let ctx = BindingContext { options: self, semantic };
221+
params.items.iter().skip(position + 1).any(|p| p.pattern.has_any_used_binding(ctx))
167222
}
168223
}

crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#[allow(clippy::wildcard_imports)]
22
use oxc_ast::ast::*;
3+
use oxc_semantic::{Semantic, SymbolId};
34
use oxc_span::{GetSpan, Span};
45
use std::ops::BitOr;
56

@@ -45,6 +46,10 @@ pub(super) trait CheckBinding<'a> {
4546
) -> Option<UnusedBindingResult>;
4647
}
4748

49+
// =============================================================================
50+
// ================================= BINDINGS ==================================
51+
// =============================================================================
52+
4853
impl<'a> CheckBinding<'a> for BindingPattern<'a> {
4954
fn check_unused_binding_pattern(
5055
&self,
@@ -132,6 +137,113 @@ impl<'a> CheckBinding<'a> for BindingRestElement<'a> {
132137
}
133138

134139
impl<'a> CheckBinding<'a> for ArrayPattern<'a> {
140+
fn check_unused_binding_pattern(
141+
&self,
142+
options: &NoUnusedVarsOptions,
143+
symbol: &Symbol<'_, 'a>,
144+
) -> Option<UnusedBindingResult> {
145+
for el in &self.elements {
146+
let Some(el) = el.as_ref() else {
147+
continue;
148+
};
149+
// const [a, _b, c] = arr; console.log(a, b)
150+
// here, res will contain data for _b, and we want to check if it
151+
// can be ignored (if it matches destructuredArrayIgnorePattern)
152+
if let Some(res) = el.check_unused_binding_pattern(options, symbol) {
153+
// const [{ _a }] = arr shouldn't get ignored since _a is inside
154+
// an object destructure
155+
if el.kind.is_destructuring_pattern() {
156+
return Some(res);
157+
}
158+
let is_ignorable = options
159+
.destructured_array_ignore_pattern
160+
.as_ref()
161+
.is_some_and(|pattern| pattern.is_match(symbol.name()));
162+
return Some(res | is_ignorable);
163+
}
164+
}
165+
None
166+
}
167+
}
168+
169+
// =============================================================================
170+
// ============================== RE-ASSIGNMENTS ===============================
171+
// =============================================================================
172+
173+
impl<'a> CheckBinding<'a> for AssignmentExpression<'a> {
174+
fn check_unused_binding_pattern(
175+
&self,
176+
options: &NoUnusedVarsOptions,
177+
symbol: &Symbol<'_, 'a>,
178+
) -> Option<UnusedBindingResult> {
179+
self.left.check_unused_binding_pattern(options, symbol)
180+
}
181+
}
182+
183+
impl<'a> CheckBinding<'a> for AssignmentTarget<'a> {
184+
fn check_unused_binding_pattern(
185+
&self,
186+
options: &NoUnusedVarsOptions,
187+
symbol: &Symbol<'_, 'a>,
188+
) -> Option<UnusedBindingResult> {
189+
match self {
190+
AssignmentTarget::AssignmentTargetIdentifier(id) => {
191+
id.check_unused_binding_pattern(options, symbol)
192+
}
193+
AssignmentTarget::ArrayAssignmentTarget(arr) => {
194+
arr.check_unused_binding_pattern(options, symbol)
195+
}
196+
AssignmentTarget::ObjectAssignmentTarget(obj) => {
197+
obj.check_unused_binding_pattern(options, symbol)
198+
}
199+
_ => None,
200+
}
201+
}
202+
}
203+
204+
impl<'a> CheckBinding<'a> for ObjectAssignmentTarget<'a> {
205+
fn check_unused_binding_pattern(
206+
&self,
207+
options: &NoUnusedVarsOptions,
208+
symbol: &Symbol<'_, 'a>,
209+
) -> Option<UnusedBindingResult> {
210+
if options.ignore_rest_siblings && self.rest.is_some() {
211+
return Some(UnusedBindingResult::from(self.span()).ignore());
212+
}
213+
for el in &self.properties {
214+
if let Some(res) = el.check_unused_binding_pattern(options, symbol) {
215+
// has a rest sibling and the rule is configured to
216+
// ignore variables that have them
217+
let is_ignorable = options.ignore_rest_siblings && self.rest.is_some();
218+
return Some(res | is_ignorable);
219+
}
220+
}
221+
return self
222+
.rest
223+
.as_ref()
224+
.and_then(|rest| rest.target.check_unused_binding_pattern(options, symbol));
225+
}
226+
}
227+
228+
impl<'a> CheckBinding<'a> for AssignmentTargetMaybeDefault<'a> {
229+
fn check_unused_binding_pattern(
230+
&self,
231+
options: &NoUnusedVarsOptions,
232+
symbol: &Symbol<'_, 'a>,
233+
) -> Option<UnusedBindingResult> {
234+
match self {
235+
Self::AssignmentTargetWithDefault(target) => {
236+
target.binding.check_unused_binding_pattern(options, symbol)
237+
}
238+
target @ match_assignment_target!(Self) => {
239+
let target = target.as_assignment_target().expect("match_assignment_target matched a node that couldn't be converted into an AssignmentTarget");
240+
target.check_unused_binding_pattern(options, symbol)
241+
}
242+
}
243+
}
244+
}
245+
246+
impl<'a> CheckBinding<'a> for ArrayAssignmentTarget<'a> {
135247
fn check_unused_binding_pattern(
136248
&self,
137249
options: &NoUnusedVarsOptions,
@@ -159,3 +271,92 @@ impl<'a> CheckBinding<'a> for ArrayPattern<'a> {
159271
None
160272
}
161273
}
274+
impl<'a> CheckBinding<'a> for AssignmentTargetProperty<'a> {
275+
fn check_unused_binding_pattern(
276+
&self,
277+
options: &NoUnusedVarsOptions,
278+
symbol: &Symbol<'_, 'a>,
279+
) -> Option<UnusedBindingResult> {
280+
// self.binding.check_unused_binding_pattern(options, symbol)
281+
match self {
282+
Self::AssignmentTargetPropertyIdentifier(id) => {
283+
id.binding.check_unused_binding_pattern(options, symbol)
284+
}
285+
Self::AssignmentTargetPropertyProperty(prop) => {
286+
prop.binding.check_unused_binding_pattern(options, symbol)
287+
}
288+
}
289+
}
290+
}
291+
292+
impl<'a> CheckBinding<'a> for IdentifierReference<'a> {
293+
fn check_unused_binding_pattern(
294+
&self,
295+
_options: &NoUnusedVarsOptions,
296+
symbol: &Symbol<'_, 'a>,
297+
) -> Option<UnusedBindingResult> {
298+
(symbol == self).then(|| UnusedBindingResult::from(self.span()))
299+
}
300+
}
301+
302+
#[derive(Clone, Copy)]
303+
pub(super) struct BindingContext<'s, 'a> {
304+
pub options: &'s NoUnusedVarsOptions,
305+
pub semantic: &'s Semantic<'a>,
306+
// pub symbol: &'s Symbol<'s, 'a>,
307+
}
308+
impl<'s, 'a> BindingContext<'s, 'a> {
309+
#[inline]
310+
pub fn symbol(&self, symbol_id: SymbolId) -> Symbol<'s, 'a> {
311+
Symbol::new(self.semantic, symbol_id)
312+
}
313+
#[inline]
314+
pub fn has_usages(&self, symbol_id: SymbolId) -> bool {
315+
self.symbol(symbol_id).has_usages(self.options)
316+
}
317+
}
318+
319+
pub(super) trait HasAnyUsedBinding<'a> {
320+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool;
321+
}
322+
323+
impl<'a> HasAnyUsedBinding<'a> for BindingPattern<'a> {
324+
#[inline]
325+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool {
326+
self.kind.has_any_used_binding(ctx)
327+
}
328+
}
329+
impl<'a> HasAnyUsedBinding<'a> for BindingPatternKind<'a> {
330+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool {
331+
match self {
332+
Self::BindingIdentifier(id) => id.has_any_used_binding(ctx),
333+
Self::AssignmentPattern(id) => id.left.has_any_used_binding(ctx),
334+
Self::ObjectPattern(id) => id.has_any_used_binding(ctx),
335+
Self::ArrayPattern(id) => id.has_any_used_binding(ctx),
336+
}
337+
}
338+
}
339+
340+
impl<'a> HasAnyUsedBinding<'a> for BindingIdentifier<'a> {
341+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool {
342+
self.symbol_id.get().is_some_and(|symbol_id| ctx.has_usages(symbol_id))
343+
}
344+
}
345+
impl<'a> HasAnyUsedBinding<'a> for ObjectPattern<'a> {
346+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool {
347+
if ctx.options.ignore_rest_siblings && self.rest.is_some() {
348+
return true;
349+
}
350+
self.properties.iter().any(|p| p.value.has_any_used_binding(ctx))
351+
|| self.rest.as_ref().map_or(false, |rest| rest.argument.has_any_used_binding(ctx))
352+
}
353+
}
354+
impl<'a> HasAnyUsedBinding<'a> for ArrayPattern<'a> {
355+
fn has_any_used_binding(&self, ctx: BindingContext<'_, 'a>) -> bool {
356+
self.elements.iter().flatten().any(|el| {
357+
// if the destructured element is ignored, it is considered used
358+
el.get_identifier().is_some_and(|name| ctx.options.is_ignored_array_destructured(&name))
359+
|| el.has_any_used_binding(ctx)
360+
}) || self.rest.as_ref().map_or(false, |rest| rest.argument.has_any_used_binding(ctx))
361+
}
362+
}

0 commit comments

Comments
 (0)