Skip to content

Commit

Permalink
fix: nuv options tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Jul 23, 2024
1 parent 6883506 commit be32177
Show file tree
Hide file tree
Showing 10 changed files with 2,156 additions and 318 deletions.
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ extend-exclude = [
"**/*.snap",
"**/*/CHANGELOG.md",
"crates/oxc_linter/fixtures",
"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",
Expand Down
85 changes: 70 additions & 15 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
#[allow(clippy::wildcard_imports)]
use oxc_ast::{ast::*, AstKind};
use oxc_semantic::Semantic;
use oxc_syntax::operator::UnaryOperator;

use crate::rules::eslint::no_unused_vars::binding_pattern::{BindingContext, HasAnyUsedBinding};

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

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

for parent in self.iter_parents() {
Expand All @@ -20,6 +24,10 @@ impl<'s, 'a> Symbol<'s, 'a> {
AstKind::CallExpression(_) => {
return true;
}
// !function() {}; is an IIFE
AstKind::UnaryExpression(expr) => {
return expr.operator == UnaryOperator::LogicalNot;
}
_ => {
return false;
}
Expand Down Expand Up @@ -56,8 +64,49 @@ impl<'s, 'a> Symbol<'s, 'a> {

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
.get(0)
.is_some_and(|s| matches!(s, Statement::ReturnStatement(_)))
}
_ => return false,
},
_ => return false,
}
}

false
}
}

impl NoUnusedVars {
pub(super) fn is_allowed_class<'a>(&self, class: &Class<'a>) -> bool {
if self.ignore_class_with_static_init_block
&& class.body.body.iter().any(|el| matches!(el, ClassElement::StaticBlock(_)))
{
return true;
}

false
}

pub(super) fn is_allowed_function<'a>(&self, symbol: &Symbol<'_, 'a>) -> bool {
symbol.is_callback_or_iife() || symbol.is_function_assigned_to_same_name_variable()
}

/// Returns `true` if this unused variable declaration should be allowed
/// (i.e. not reported)
pub(super) fn is_allowed_variable_declaration<'a>(
Expand All @@ -69,10 +118,17 @@ impl NoUnusedVars {
return true;
}

// allow unused iterators, since they're required for valid syntax
if symbol.is_declared_in_for_of_loop() {
return true;
}

// check if res is an array/object unpacking pattern that should be ignored
matches!(decl.id.check_unused_binding_pattern(self, symbol), Some(res) if res.is_ignore())
}

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

// Parameters are always checked. Must be done after above checks,
// because in those cases a parameter is required
if self.args.is_none() {
// 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() {
if param.pattern.kind.is_destructuring_pattern() {
// allow unpacked parameters that are ignored via destructuredArrayIgnorePattern
let maybe_unused_binding_pattern =
param.pattern.check_unused_binding_pattern(self, symbol);
return maybe_unused_binding_pattern.map_or(false, |res| res.is_ignore());
}
return false;
}

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

params.items.iter().skip(position + 1).any(|p| {
let Some(id) = p.pattern.get_binding_identifier() else {
return false;
};
let Some(symbol_id) = id.symbol_id.get() else {
return false;
};
let symbol = Symbol::new(semantic, symbol_id);
symbol.has_usages()
})
let ctx = BindingContext { options: self, semantic };
params.items.iter().skip(position + 1).any(|p| p.pattern.has_any_used_binding(ctx))
}
}
201 changes: 201 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/binding_pattern.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_semantic::{Semantic, SymbolId};
use oxc_span::{GetSpan, Span};
use std::ops::BitOr;

Expand Down Expand Up @@ -45,6 +46,10 @@ pub(super) trait CheckBinding<'a> {
) -> Option<UnusedBindingResult>;
}

// =============================================================================
// ================================= BINDINGS ==================================
// =============================================================================

impl<'a> CheckBinding<'a> for BindingPattern<'a> {
fn check_unused_binding_pattern(
&self,
Expand Down Expand Up @@ -132,6 +137,113 @@ impl<'a> CheckBinding<'a> for BindingRestElement<'a> {
}

impl<'a> CheckBinding<'a> for ArrayPattern<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
for el in &self.elements {
let Some(el) = el.as_ref() else {
continue;
};
// const [a, _b, c] = arr; console.log(a, b)
// here, res will contain data for _b, and we want to check if it
// can be ignored (if it matches destructuredArrayIgnorePattern)
if let Some(res) = el.check_unused_binding_pattern(options, symbol) {
// const [{ _a }] = arr shouldn't get ignored since _a is inside
// an object destructure
if el.kind.is_destructuring_pattern() {
return Some(res);
}
let is_ignorable = options
.destructured_array_ignore_pattern
.as_ref()
.is_some_and(|pattern| pattern.is_match(symbol.name()));
return Some(res | is_ignorable);
}
}
None
}
}

// =============================================================================
// ============================== RE-ASSIGNMENTS ===============================
// =============================================================================

impl<'a> CheckBinding<'a> for AssignmentExpression<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
self.left.check_unused_binding_pattern(options, symbol)
}
}

impl<'a> CheckBinding<'a> for AssignmentTarget<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
match self {
AssignmentTarget::AssignmentTargetIdentifier(id) => {
id.check_unused_binding_pattern(options, symbol)
}
AssignmentTarget::ArrayAssignmentTarget(arr) => {
arr.check_unused_binding_pattern(options, symbol)
}
AssignmentTarget::ObjectAssignmentTarget(obj) => {
obj.check_unused_binding_pattern(options, symbol)
}
_ => None,
}
}
}

impl<'a> CheckBinding<'a> for ObjectAssignmentTarget<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
if options.ignore_rest_siblings && self.rest.is_some() {
return Some(UnusedBindingResult::from(self.span()).ignore());
}
for el in &self.properties {
if let Some(res) = el.check_unused_binding_pattern(options, symbol) {
// has a rest sibling and the rule is configured to
// ignore variables that have them
let is_ignorable = options.ignore_rest_siblings && self.rest.is_some();
return Some(res | is_ignorable);
}
}
return self
.rest
.as_ref()
.and_then(|rest| rest.target.check_unused_binding_pattern(options, symbol));
}
}

impl<'a> CheckBinding<'a> for AssignmentTargetMaybeDefault<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
match self {
Self::AssignmentTargetWithDefault(target) => {
target.binding.check_unused_binding_pattern(options, symbol)
}
target @ match_assignment_target!(Self) => {
let target = target.as_assignment_target().expect("match_assignment_target matched a node that couldn't be converted into an AssignmentTarget");
target.check_unused_binding_pattern(options, symbol)
}
}
}
}

impl<'a> CheckBinding<'a> for ArrayAssignmentTarget<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
Expand Down Expand Up @@ -159,3 +271,92 @@ impl<'a> CheckBinding<'a> for ArrayPattern<'a> {
None
}
}
impl<'a> CheckBinding<'a> for AssignmentTargetProperty<'a> {
fn check_unused_binding_pattern(
&self,
options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
// self.binding.check_unused_binding_pattern(options, symbol)
match self {
Self::AssignmentTargetPropertyIdentifier(id) => {
id.binding.check_unused_binding_pattern(options, symbol)
}
Self::AssignmentTargetPropertyProperty(prop) => {
prop.binding.check_unused_binding_pattern(options, symbol)
}
}
}
}

impl<'a> CheckBinding<'a> for IdentifierReference<'a> {
fn check_unused_binding_pattern(
&self,
_options: &NoUnusedVarsOptions,
symbol: &Symbol<'_, 'a>,
) -> Option<UnusedBindingResult> {
(symbol == self).then(|| UnusedBindingResult::from(self.span()))
}
}

#[derive(Clone, Copy)]
pub(super) struct BindingContext<'s, 'a> {
pub options: &'s NoUnusedVarsOptions,
pub semantic: &'s Semantic<'a>,
// pub symbol: &'s Symbol<'s, '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> {
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))
}
}
Loading

0 comments on commit be32177

Please sign in to comment.