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` parses exactly like the intentional `x = -1`, so the AST cannot tell them
18        // apart: only flag when the source fuses `=` to the unary. The gap between the LHS and
19        // `rhs.span` (which starts at the leading unary) holds only whitespace, comments and the
20        // `=` token, and no comment can end with `=`, so the gap ends with `=` exactly when the
21        // pair is fused. Solidity has no `~=` either, so `=~` is the same trap; unary `+` was
22        // removed in 0.5.0 and never produces a node.
23        if let ExprKind::Assign(lhs, None, rhs) = &expr.kind
24            && leads_with_fusable_unary(rhs)
25            && ctx.span_to_snippet(lhs.span.between(rhs.span)).is_some_and(|gap| gap.ends_with('='))
26        {
27            ctx.emit(&DANGEROUS_UNARY_OPERATOR, expr.span);
28        }
29    }
30}
31
32/// Whether the leftmost operand of `expr` is a `-` or `~` unary, following the left spine of
33/// binary and ternary expressions so `x =- a + 1` (`x = (-a) + 1`) is caught as well.
34fn leads_with_fusable_unary(expr: &Expr<'_>) -> bool {
35    match &expr.kind {
36        ExprKind::Unary(op, _) => matches!(op.kind, UnOpKind::Neg | UnOpKind::BitNot),
37        ExprKind::Binary(lhs, _, _) | ExprKind::Ternary(lhs, _, _) => leads_with_fusable_unary(lhs),
38        _ => false,
39    }
40}