fix(linter/const-comparisons): detect equality contradictions (#21065)

Related to #21064
This commit is contained in:
camc314 committed 2026-04-05 16:45:28 +00:00
1 parent 2bb2be2fe4
commit 60a57cd424
2 files changed
+179 -12

No files matched your search

@@ -61,17 +61,35 @@ fn identical_expressions_logical_operator(left_span: Span, right_span: Span) ->
])
}
fn identical_expressions_logical_operator_negated(
fn equivalent_expressions_logical_operator(left_span: Span, right_span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Both sides of the logical operator are equivalent")
.with_help("This logical expression will always evaluate to the same value as either side.")
.with_labels([
left_span.label("If this expression evaluates to true"),
right_span.label("This equivalent expression will always evaluate to true"),
])
}
fn complementary_expressions_logical_operator(
always_truthy: bool,
left_span: Span,
right_span: Span,
) -> OxcDiagnostic {
let (left_label, right_label) = if always_truthy {
("If this expression evaluates to false", "This expression must evaluate to true")
} else {
("If this expression evaluates to true", "This expression cannot also evaluate to true")
};
OxcDiagnostic::warn("Unexpected constant comparison")
.with_help(format!("This logical expression will always evaluate to {always_truthy}"))
.with_labels([
left_span.label("If this expression evaluates to true"),
right_span.label("This expression will never evaluate to true"),
])
.with_labels([left_span.label(left_label), right_span.label(right_label)])
}
#[derive(Clone, Copy)]
enum EqualityRelation {
Equivalent,
Inverse,
}
/// <https://rust-lang.github.io/rust-clippy/master/index.html#/impossible>
@@ -246,6 +264,30 @@ impl ConstComparisons {
logical_expr.left.span(),
logical_expr.right.span(),
));
return;
}
if let Some(relation) = equality_relation(
logical_expr.left.get_inner_expression(),
logical_expr.right.get_inner_expression(),
ctx,
) {
match relation {
EqualityRelation::Equivalent => {
ctx.diagnostic(equivalent_expressions_logical_operator(
logical_expr.left.span(),
logical_expr.right.span(),
));
}
EqualityRelation::Inverse => {
ctx.diagnostic(complementary_expressions_logical_operator(
matches!(logical_expr.operator, LogicalOperator::Or),
logical_expr.left.span(),
logical_expr.right.span(),
));
}
}
return;
}
// if either are `!foo`, check whether it looks like `foo && !foo` or `foo || !foo`
@@ -256,7 +298,7 @@ impl ConstComparisons {
if negated_expr.operator == UnaryOperator::LogicalNot
&& is_same_expression(&negated_expr.argument, other_expr, ctx)
{
ctx.diagnostic(identical_expressions_logical_operator_negated(
ctx.diagnostic(complementary_expressions_logical_operator(
matches!(logical_expr.operator, LogicalOperator::Or),
logical_expr.left.span(),
logical_expr.right.span(),
@@ -361,6 +403,51 @@ fn comparison_to_const<'a, 'b>(
None
}
fn equality_relation(
left: &Expression,
right: &Expression,
ctx: &LintContext,
) -> Option<EqualityRelation> {
if let Expression::BinaryExpression(left_bin_expr) = left
&& let Expression::BinaryExpression(right_bin_expr) = right
&& left_bin_expr.operator.is_equality()
&& right_bin_expr.operator.is_equality()
{
let same_order = is_same_expression(
left_bin_expr.left.get_inner_expression(),
right_bin_expr.left.get_inner_expression(),
ctx,
) && is_same_expression(
left_bin_expr.right.get_inner_expression(),
right_bin_expr.right.get_inner_expression(),
ctx,
);
let swapped_order = is_same_expression(
left_bin_expr.left.get_inner_expression(),
right_bin_expr.right.get_inner_expression(),
ctx,
) && is_same_expression(
left_bin_expr.right.get_inner_expression(),
right_bin_expr.left.get_inner_expression(),
ctx,
);
if !same_order && !swapped_order {
return None;
}
if left_bin_expr.operator == right_bin_expr.operator {
return Some(EqualityRelation::Equivalent);
} else if left_bin_expr.operator.equality_inverse_operator()
== Some(right_bin_expr.operator)
{
return Some(EqualityRelation::Inverse);
}
}
None
}
fn all_and_comparison_to_const<'a, 'b>(
expr: &'b Expression<'a>,
) -> Box<dyn Iterator<Item = (CmpOp, &'b Expression<'a>, &'b NumericLiteral<'a>, Span)> + 'b> {
@@ -590,6 +677,9 @@ fn test() {
"a.b.c >= a.b.c",
"a == b && a == b",
"a == b || a == b",
"a == b && b == a",
"a == (b) && (b) == a",
"a != b || b != a",
"!foo && !foo",
"!foo || !foo",
"class Foo { #a; #b; constructor() { this.#a = 1; }; test() { return this.#a > this.#a } }",
@@ -597,6 +687,11 @@ fn test() {
"foo && !foo",
"!foo || foo",
"foo || !foo",
"a == b && a != b",
"a != b && b == a",
"a !== a && a === a",
"a === b || a !== b",
"a !== a || a === a",
"let numOrUndefined: number | undefined = 6; if (numOrUndefined || 0 > 0) {}",
"let numOrUndefined: number | undefined = 6; if (numOrUndefined ?? 0 > 0) {}",
];
@@ -289,6 +289,33 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: This logical expression will always evaluate to the same value as the expression itself.
⚠ oxc(const-comparisons): Both sides of the logical operator are equivalent
╭─[const_comparisons.tsx:1:1]
1 │ a == b && b == a
· ───┬── ───┬──
· │ ╰── This equivalent expression will always evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to the same value as either side.
⚠ oxc(const-comparisons): Both sides of the logical operator are equivalent
╭─[const_comparisons.tsx:1:1]
1 │ a == (b) && (b) == a
· ────┬─── ────┬───
· │ ╰── This equivalent expression will always evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to the same value as either side.
⚠ oxc(const-comparisons): Both sides of the logical operator are equivalent
╭─[const_comparisons.tsx:1:1]
1 │ a != b || b != a
· ───┬── ───┬──
· │ ╰── This equivalent expression will always evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to the same value as either side.
⚠ oxc(const-comparisons): Both sides of the logical operator are the same
╭─[const_comparisons.tsx:1:1]
1 │ !foo && !foo
@@ -318,7 +345,7 @@ source: crates/oxc_linter/src/tester.rs
╭─[const_comparisons.tsx:1:1]
1 │ !foo && foo
· ──┬─ ─┬─
· │ ╰── This expression will never evaluate to true
· │ ╰── This expression cannot also evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to false
@@ -327,7 +354,7 @@ source: crates/oxc_linter/src/tester.rs
╭─[const_comparisons.tsx:1:1]
1 │ foo && !foo
· ─┬─ ──┬─
· │ ╰── This expression will never evaluate to true
· │ ╰── This expression cannot also evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to false
@@ -336,8 +363,8 @@ source: crates/oxc_linter/src/tester.rs
╭─[const_comparisons.tsx:1:1]
1 │ !foo || foo
· ──┬─ ─┬─
· │ ╰── This expression will never evaluate to true
· ╰── If this expression evaluates to true
· │ ╰── This expression must evaluate to true
· ╰── If this expression evaluates to false
╰────
help: This logical expression will always evaluate to true
@@ -345,8 +372,53 @@ source: crates/oxc_linter/src/tester.rs
╭─[const_comparisons.tsx:1:1]
1 │ foo || !foo
· ─┬─ ──┬─
· │ ╰── This expression will never evaluate to true
· ╰── If this expression evaluates to true
· │ ╰── This expression must evaluate to true
· ╰── If this expression evaluates to false
╰────
help: This logical expression will always evaluate to true
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]
1 │ a == b && a != b
· ───┬── ───┬──
· │ ╰── This expression cannot also evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to false
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]
1 │ a != b && b == a
· ───┬── ───┬──
· │ ╰── This expression cannot also evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to false
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]
1 │ a !== a && a === a
· ───┬─── ───┬───
· │ ╰── This expression cannot also evaluate to true
· ╰── If this expression evaluates to true
╰────
help: This logical expression will always evaluate to false
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]
1 │ a === b || a !== b
· ───┬─── ───┬───
· │ ╰── This expression must evaluate to true
· ╰── If this expression evaluates to false
╰────
help: This logical expression will always evaluate to true
⚠ oxc(const-comparisons): Unexpected constant comparison
╭─[const_comparisons.tsx:1:1]
1 │ a !== a || a === a
· ───┬─── ───┬───
· │ ╰── This expression must evaluate to true
· ╰── If this expression evaluates to false
╰────
help: This logical expression will always evaluate to true