Skip to main content

forge_lint/sol/med/
dangerous_unary_operator.rs

1use super::DangerousUnaryOperator;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::ast::{Expr, ExprKind, UnOpKind};
7
8declare_forge_lint!(
9    DANGEROUS_UNARY_OPERATOR,
10    Severity::Med,
11    "dangerous-unary-operator",
12    "unary operator fused to `=`: `x =- 1` parses as `x = -1`, not `x -= 1`"
13);
14
15impl<'ast> EarlyLintPass<'ast> for DangerousUnaryOperator {
16    fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
17        // `x =- 1` lexes as `x` `=` `-` `1` and parses as a plain assignment of `-1`, identical to
18        // the intentional `x = -1`, yet it reads like the compound `x -= 1` it was probably meant
19        // to be. Solidity has no `~=` operator either, so `x =~ y` is the same trap.
20        // The fused unary can also lead a larger RHS: `x =- a + 1` parses as `x = (-a) + 1`, whose
21        // RHS is a `Binary` (or `Ternary`) with the `-a` unary as its leftmost operand. Follow the
22        // left spine to that leading unary so those forms are caught too, not only `x =- a`.
23        // Because the parsed node matches the legitimate spaced form, only flag when the source
24        // fuses `=` to the leading unary (`=-` / `=~`), never `= -`. The gap between the LHS and
25        // `rhs.span` (which starts at that unary) holds only whitespace, comments and the `=`
26        // token, and no comment can end with `=` (block comments end with `*/`, line comments
27        // with a newline), so the gap ends with `=` exactly when the pair is fused. `=+` never
28        // reaches here: unary `+` was removed in Solidity 0.5.0, and solar drops it during
29        // parsing without producing a node.
30        if let ExprKind::Assign(lhs, None, rhs) = &expr.kind
31            && leads_with_fusable_unary(rhs)
32            && ctx.span_to_snippet(lhs.span.between(rhs.span)).is_some_and(|gap| gap.ends_with('='))
33        {
34            ctx.emit(&DANGEROUS_UNARY_OPERATOR, expr.span);
35        }
36    }
37}
38
39/// Whether the leftmost operand of `expr` is a `-` or `~` unary, following the left spine of
40/// binary and ternary expressions. `rhs.span` begins at that leading unary, so a fused `=-` / `=~`
41/// is caught whether the unary is the whole RHS (`x =- a`) or only leads it (`x =- a + 1`).
42fn leads_with_fusable_unary(expr: &Expr<'_>) -> bool {
43    match &expr.kind {
44        ExprKind::Unary(op, _) => matches!(op.kind, UnOpKind::Neg | UnOpKind::BitNot),
45        ExprKind::Binary(lhs, _, _) => leads_with_fusable_unary(lhs),
46        ExprKind::Ternary(cond, _, _) => leads_with_fusable_unary(cond),
47        _ => false,
48    }
49}