perf(linter/eslint/no-unsafe-negation): narrow AST dispatch (#28351)

Visit logical negations and inspect their direct binary parent,
preserving operand position and explicit-parentheses behavior. Reduce
fixture callbacks from 20,604 to 9,106.


---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Cam McHenryandCopilot authored and GitHub committed 2026-10-28 22:28:44 -04:00
1 parent 84d974b9f7
commit 56c99c6fe7
3 files changed
+31 -22

No files matched your search

+1 -1
View File
@@ -1178,7 +1178,7 @@ impl RuleRunner for crate::rules::eslint::no_unsafe_finally::NoUnsafeFinally {
impl RuleRunner for crate::rules::eslint::no_unsafe_negation::NoUnsafeNegation {
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::BinaryExpression]));
Some(&AstTypesBitset::from_types(&[AstType::UnaryExpression]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
@@ -1,4 +1,4 @@
use oxc_ast::{AstKind, ast::Expression};
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
@@ -82,35 +82,39 @@ impl Rule for NoUnsafeNegation {
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::BinaryExpression(expr) = node.kind() else {
let AstKind::UnaryExpression(left) = node.kind() else {
return;
};
if left.operator != UnaryOperator::LogicalNot {
return;
}
let AstKind::BinaryExpression(expr) = ctx.nodes().parent_kind(node.id()) else {
return;
};
if expr.left.span() != left.span {
return;
}
let should_check = expr.operator.is_relational()
|| (self.enforce_for_ordering_relations && expr.operator.is_compare());
if should_check {
let Expression::UnaryExpression(left) = &expr.left else {
return;
};
if left.operator == UnaryOperator::LogicalNot {
// Diagnostic points at the unexpected negation
let diagnostic =
no_unsafe_negation_diagnostic(expr.operator.as_str(), expr.left.span());
// Diagnostic points at the unexpected negation
let diagnostic =
no_unsafe_negation_diagnostic(expr.operator.as_str(), expr.left.span());
let fix_producer = |fixer: RuleFixer<'_, 'a>| {
// modify `!a instance of B` to `!(a instanceof B)`
let modified_code = {
let left = ctx.source_range(left.argument.span());
let operator = expr.operator.as_str();
let right = ctx.source_range(expr.right.span());
let fix_producer = |fixer: RuleFixer<'_, 'a>| {
// modify `!a instance of B` to `!(a instanceof B)`
let modified_code = {
let left = ctx.source_range(left.argument.span());
let operator = expr.operator.as_str();
let right = ctx.source_range(expr.right.span());
format!("!({left} {operator} {right})")
};
fixer.replace(expr.span, modified_code)
format!("!({left} {operator} {right})")
};
fixer.replace(expr.span, modified_code)
};
ctx.diagnostic_with_fix(diagnostic, fix_producer);
}
ctx.diagnostic_with_fix(diagnostic, fix_producer);
}
}
}
@@ -124,6 +128,9 @@ fn test() {
("a in b === false", None),
("!(a in b)", None),
("(!a) in b", None),
("a in !b", None),
("(!a as boolean) in b", None),
("a < !b", Some(serde_json::json!([{ "enforceForOrderingRelations": true }]))),
("a instanceof b", None),
("a instanceof b === false", None),
("!(a instanceof b)", None),
@@ -157,6 +164,8 @@ fn test() {
let fix = vec![
("!a in b", "!(a in b)", None),
("!a in !b", "!(a in !b)", None),
("!!a in b", "!(!a in b)", None),
("(!a in b)", "(!(a in b))", None),
("!(a) in b", "!((a) in b)", None),
("!a instanceof b", "!(a instanceof b)", None),
@@ -131,7 +131,7 @@ eslint/no-unneeded-ternary | 4722
eslint/no-unreachable | 7
eslint/no-unreachable-loop | 1607
eslint/no-unsafe-finally | 15259
eslint/no-unsafe-negation | 20604
eslint/no-unsafe-negation | 9106
eslint/no-unsafe-optional-chaining | 792
eslint/no-unused-expressions | 24802
eslint/no-unused-labels | 7