diff --git a/crates/oxc_linter/src/rules/unicorn/no_negation_in_equality_check.rs b/crates/oxc_linter/src/rules/unicorn/no_negation_in_equality_check.rs index 22f1f724a0763..cf402c7ea69e2 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_negation_in_equality_check.rs +++ b/crates/oxc_linter/src/rules/unicorn/no_negation_in_equality_check.rs @@ -1,10 +1,10 @@ use oxc_ast::{AstKind, ast::Expression}; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{GetSpan, Span}; use oxc_syntax::operator::{BinaryOperator, UnaryOperator}; -use crate::{AstNode, context::LintContext, rule::Rule}; +use crate::{AstNode, ast_util, context::LintContext, rule::Rule}; fn no_negation_in_equality_check_diagnostic( span: Span, @@ -50,7 +50,7 @@ declare_oxc_lint!( NoNegationInEqualityCheck, unicorn, pedantic, - pending + suggestion, ); impl Rule for NoNegationInEqualityCheck { @@ -78,11 +78,45 @@ impl Rule for NoNegationInEqualityCheck { return; }; - ctx.diagnostic(no_negation_in_equality_check_diagnostic( - left_unary_expr.span, - binary_expr.operator, - suggested_operator, - )); + ctx.diagnostic_with_suggestion( + no_negation_in_equality_check_diagnostic( + left_unary_expr.span, + binary_expr.operator, + suggested_operator, + ), + |fixer| { + let source_text = ctx.source_text(); + let unary_start = left_unary_expr.span.start as usize; + + let argument_text = ctx.source_range(left_unary_expr.argument.span()); + let right_text = ctx.source_range(binary_expr.right.span()); + let first_char = argument_text.chars().next(); + + let is_asi_hazard = ast_util::could_be_asi_hazard(node, ctx) + && matches!(first_char, Some('(' | '[')); + + let char_before = if unary_start > 0 { + source_text[..unary_start].chars().next_back() + } else { + None + }; + let needs_space_before = char_before + .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$'); + + let fixed_expr = + format!("{argument_text} {} {right_text}", suggested_operator.as_str()); + + if is_asi_hazard { + // Add semicolon before to prevent ASI issues + fixer.replace(binary_expr.span, format!(";{fixed_expr}")) + } else if needs_space_before { + // Add space before (e.g., `return!foo` -> `return foo`) + fixer.replace(binary_expr.span, format!(" {fixed_expr}")) + } else { + fixer.replace(binary_expr.span, fixed_expr) + } + }, + ); } } } @@ -136,6 +170,82 @@ fn test() { ", ]; + let fix = vec![ + ("!foo === bar", "foo !== bar"), + ("!foo !== bar", "foo === bar"), + ("!foo == bar", "foo != bar"), + ("!foo != bar", "foo == bar"), + ( + " + function x() { + return!foo === bar; + } + ", + " + function x() { + return foo !== bar; + } + ", + ), + ( + " + function x() { + return! + foo === bar; + throw! + foo === bar; + } + ", + " + function x() { + return foo !== bar; + throw foo !== bar; + } + ", + ), + ( + " + foo + !(a) === b + ", + " + foo + ;(a) !== b + ", + ), + ( + " + foo + ![a, b].join('') === c + ", + " + foo + ;[a, b].join('') !== c + ", + ), + ( + " + foo + ! [a, b].join('') === c + ", + " + foo + ;[a, b].join('') !== c + ", + ), + ( + " + foo + !/* comment */[a, b].join('') === c + ", + " + foo + ;[a, b].join('') !== c + ", + ), + ]; + Tester::new(NoNegationInEqualityCheck::NAME, NoNegationInEqualityCheck::PLUGIN, pass, fail) + .expect_fix(fix) .test_and_snapshot(); }