1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
use crate::rule_prelude::*;
use ast::{BinExpr, BinOp, Expr, UnaryOp};
use rslint_parser::{TextRange, TextSize};
use SyntaxKind::*;

declare_lint! {
    /**
    Deny the use of `!` on the left hand side of an `instanceof` or `in` expression where it is ambiguous.

    JavaScript precedence is higher for logical not than it is for in or instanceof. Oftentimes you see
    expressions such as `!foo instanceof bar`, which most of the times produces unexpected behavior.
    precedence will group the expressions like `(!foo) instanceof bar`. Most of the times the developer expects
    the expression to check if `foo` is not an instance of `bar` however.

    ## Incorrect Code Examples

    ```js
    if (!foo instanceof String) {

    }
    ```

    ```js
    if (!bar in {}) {

    }
    ```
    */
    #[derive(Default)]
    NoUnsafeNegation,
    errors,
    "no-unsafe-negation"
}

#[typetag::serde]
impl CstRule for NoUnsafeNegation {
    fn check_node(&self, node: &SyntaxNode, ctx: &mut RuleCtx) -> Option<()> {
        if node.kind() == BIN_EXPR
            && matches!(node.to::<BinExpr>().op()?, BinOp::Instanceof | BinOp::In)
        {
            let expr = node.to::<BinExpr>();

            if let Expr::UnaryExpr(unary) = expr.lhs()? {
                if unary.op()? == UnaryOp::LogicalNot {
                    let unary_node = unary.expr()?.syntax().clone();
                    let no_op_text = &node.trimmed_text().to_string()[1..];
                    let mut eq_expr = format!("(!{}", no_op_text);
                    eq_expr.insert(usize::from(unary_node.trimmed_text().len()) + 2, ')');
                    let rest_range = TextRange::new(
                        node.trimmed_range().start() + TextSize::from(1),
                        node.trimmed_range().end(),
                    );

                    let err = ctx
                        .err(
                            self.name(),
                            "Unsafe negation of a value in a binary expression",
                        )
                        .primary(
                            unary.op_token().unwrap(),
                            format!(
                                "precedence makes this expression equivalent to `{}`",
                                eq_expr
                            ),
                        )
                        .secondary(rest_range, "`!` is not negating this expression")
                        .suggestion_with_labels(
                            expr.range(),
                            "wrap the instanceof check in parentheses",
                            format!("!({})", no_op_text),
                            Applicability::MaybeIncorrect,
                            vec![1..2, no_op_text.len() + 2..no_op_text.len() + 3],
                        );

                    ctx.fix().wrap(node.add_start(1), Wrapping::Parens);
                    ctx.add_err(err);
                }
            }
        }
        None
    }
}

rule_tests! {
    NoUnsafeNegation::default(),
    err: {
        "!foo in bar",
        "![5] instanceof !4",
        /// ignore
        "!!!!!instanceof !!foo instanceof !!bar"
    },
    ok: {
        /// If this is intended behavior, you can wrap the expression
        "(!foo) instanceof bar",
        "key in bar",
        "bar instanceof bar",
        /// ignore
        "1 in [1, 1, 1, ((!1) in [1111111111, 111])]"
    }
}