diff --git a/src/rules.rs b/src/rules.rs index ff86954a..032aa8c8 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -19,6 +19,7 @@ pub mod camelcase; pub mod constructor_super; pub mod default_param_last; pub mod eqeqeq; +pub mod erasing_op; pub mod explicit_function_return_type; pub mod explicit_module_boundary_types; pub mod for_direction; @@ -263,6 +264,7 @@ fn get_all_rules_raw() -> Vec> { Box::new(constructor_super::ConstructorSuper), Box::new(default_param_last::DefaultParamLast), Box::new(eqeqeq::Eqeqeq), + Box::new(erasing_op::ErasingOp), Box::new(explicit_function_return_type::ExplicitFunctionReturnType), Box::new(explicit_module_boundary_types::ExplicitModuleBoundaryTypes), Box::new(for_direction::ForDirection), diff --git a/src/rules/erasing_op.rs b/src/rules/erasing_op.rs new file mode 100644 index 00000000..d9a0030d --- /dev/null +++ b/src/rules/erasing_op.rs @@ -0,0 +1,165 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::{Context, LintRule}; +use crate::handler::{Handler, Traverse}; +use crate::tags::{self, Tags}; +use crate::Program; +use deno_ast::swc::ast::BinaryOp; +use deno_ast::swc::ast::Expr; +use deno_ast::swc::ast::Lit; +use deno_ast::swc::ast::ParenExpr; +use deno_ast::view::BinExpr; +use deno_ast::SourceRanged; +use derive_more::Display; + +#[derive(Debug)] +pub struct ErasingOp; + +const CODE: &str = "erasing-op"; + +#[derive(Display)] +enum ErasingOpMessage { + #[display(fmt = "This operation will always evaluate to zero")] + Unexpected, +} + +#[derive(Display)] +enum ErasingOpHint { + #[display( + fmt = "This is most likely a mistake; remove the operation or assign zero directly" + )] + Remove, +} + +impl LintRule for ErasingOp { + fn tags(&self) -> Tags { + &[tags::RECOMMENDED] + } + + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view<'view>( + &self, + context: &mut Context, + program: Program<'_>, + ) { + ErasingOpHandler.traverse(program, context); + } +} + +struct ErasingOpHandler; + +impl Handler for ErasingOpHandler { + fn bin_expr(&mut self, bin_expr: &BinExpr, context: &mut Context) { + let inner = bin_expr.inner; + + let erases = match inner.op { + // `x * 0`, `0 * x`, `x & 0`, `0 & x` all evaluate to zero. + BinaryOp::Mul | BinaryOp::BitAnd => { + is_zero(&inner.left) || is_zero(&inner.right) + } + // `0 / x` is zero, but `0 / 0` is `NaN`, so don't flag a zero divisor. + BinaryOp::Div => is_zero(&inner.left) && !is_zero(&inner.right), + _ => false, + }; + + if erases { + context.add_diagnostic_with_hint( + bin_expr.range(), + CODE, + ErasingOpMessage::Unexpected, + ErasingOpHint::Remove, + ); + } + } +} + +fn is_zero(expr: &Expr) -> bool { + match expr { + Expr::Paren(ParenExpr { expr, .. }) => is_zero(expr), + Expr::Lit(Lit::Num(num)) => num.value == 0.0, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Some tests are derived from + // https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/rules/oxc/erasing_op.rs + // which is in turn based on the Clippy `erasing_op` lint. MIT Licensed. + + #[test] + fn erasing_op_valid() { + assert_lint_ok! { + ErasingOp, + "x * 1;", + "1 * x;", + "5 & x;", + "x / 1;", + "1 / x;", + // `0 / 0` is `NaN`, not zero. + "0 / 0;", + // `x / 0` is `Infinity` / `NaN`, not zero. + "x / 0;", + // Operators that are not erasing. + "x + 0;", + "x - 0;", + "x | 0;", + "0 - x;", + }; + } + + #[test] + fn erasing_op_invalid() { + assert_lint_err! { + ErasingOp, + "x * 0;": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ], + "0 * x;": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ], + "x & 0;": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ], + "0 & x;": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ], + "0 / x;": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ], + // Parenthesized zero is still zero. + "x * (0);": [ + { + col: 0, + message: ErasingOpMessage::Unexpected, + hint: ErasingOpHint::Remove, + } + ] + }; + } +}