Skip to content

Commit

Permalink
wip: get most passing cases working
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Jul 23, 2024
1 parent 6883506 commit e5f30a9
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 77 deletions.
108 changes: 94 additions & 14 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
//! 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_ast::{ast::*, syntax_directed_operations::BoundNames, 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,39 @@ 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(_) | AstKind::ForOfStatement(_) => return true,
_ => 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 +108,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 @@ -115,9 +161,35 @@ impl NoUnusedVars {
return true;
}

// match self.args {
// ArgsOption::All => {

// // Parameters are always checked. Must be done after above checks,
// // because in those cases a parameter is required. However, if
// // this is a destructuring pattern, it may be ignored using
// // `ignoreRestSiblings` or `destructuredArrayIgnorePattern`.
// 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());
// }
// },
// ArgsOption::AfterUsed => {

// },
// ArgsOption::None => unreachable!()
// }
// 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 +203,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 +226,23 @@ impl NoUnusedVars {
return false;
}

let ctx = BindingContext { options: self, semantic };
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()
p.pattern.has_any_used_binding(ctx)
// let mut is_used = false;

// p.pattern.bound_names(&mut |id| {
// if is_used {
// return;
// }
// let Some(symbol_id) = id.symbol_id.get() else {
// return;
// };
// let symbol = Symbol::new(semantic, symbol_id);
// is_used = symbol.has_usages(self);
// });

// is_used
})
}
}
203 changes: 203 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 @@ -96,6 +101,9 @@ impl<'a> CheckBinding<'a> for ObjectPattern<'a> {
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
Expand Down Expand Up @@ -159,3 +167,198 @@ impl<'a> CheckBinding<'a> for ArrayPattern<'a> {
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,
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)
let res = el.check_unused_binding_pattern(options, symbol).map(|res| {
let is_ignorable = options
.destructured_array_ignore_pattern
.as_ref()
.is_some_and(|pattern| pattern.is_match(symbol.name()));
res | is_ignorable
});

if res.is_some() {
return res;
}
}
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 e5f30a9

Please sign in to comment.