rslint_core/groups/errors/
use_isnan.rs

1use crate::rule_prelude::*;
2use ast::*;
3use SyntaxKind::*;
4
5declare_lint! {
6    /**
7    Disallow incorrect comparisons against `NaN`.
8
9    `NaN` is a special `Number` value used to represent "not a number" results in calculations.
10    This value is specified in the IEEE Standard for Binary Floating-Point-Arithmetic.
11
12    In JavaScript, `NaN` is unique, it is not equal to anything, including itself! therefore
13    any comparisons to it will either always yield `true` or `false`. Therefore you should
14    use `isNaN(/* num */)` instead to test if a value is `NaN`. This rule is aimed at removing this footgun.
15
16    ## Invalid Code Examples
17
18    ```js
19    if (foo == NaN) {
20        // unreachable
21    }
22
23    if (NaN != NaN) {
24        // always runs
25    }
26    ```
27
28    ## Correct Code Examples
29
30    ```js
31    if (isNaN(foo)) {
32        /* */
33    }
34
35    if (!isNaN(foo)) {
36        /* */
37    }
38    ```
39    */
40    #[serde(default)]
41    UseIsnan,
42    errors,
43    tags(Recommended),
44    "use-isnan",
45    /// Switch statements use `===` internally to match an expression, therefore `switch (NaN)` and `case NaN` will never match.
46    /// This rule disables uses like that which are always incorrect (true by default)
47    pub enforce_for_switch_case: bool,
48    /// Index functions like `indexOf` and `lastIndexOf` use `===` internally, therefore matching them against `NaN` will always
49    /// yield `-1`. This option disallows using `indexOf(NaN)` and `lastIndexOf(NaN)` (false by default)
50    pub enforce_for_index_of: bool
51}
52
53impl Default for UseIsnan {
54    fn default() -> Self {
55        Self {
56            enforce_for_switch_case: true,
57            enforce_for_index_of: false,
58        }
59    }
60}
61
62#[typetag::serde]
63impl CstRule for UseIsnan {
64    fn check_node(&self, node: &SyntaxNode, ctx: &mut RuleCtx) -> Option<()> {
65        match node.kind() {
66            BIN_EXPR => {
67                let expr = node.to::<BinExpr>();
68                if !expr.comparison() {
69                    return None;
70                }
71
72                let opposite = if expr.lhs().filter(|e| e.text() == "NaN").is_some() {
73                    expr.rhs()?
74                } else if expr.rhs().filter(|e| e.text() == "NaN").is_some() {
75                    expr.lhs()?
76                } else {
77                    return None;
78                };
79                let op = expr.op().unwrap();
80
81                let always_text = if matches!(op, op!(!=) | op!(!==)) {
82                    "true"
83                } else {
84                    "false"
85                };
86
87                let mut err = if opposite.text().len() <= 20 {
88                    ctx.err(
89                        self.name(),
90                        format!(
91                            "comparing `{}` to `NaN` using `{}` will always return {}",
92                            opposite.text(),
93                            expr.op_token().unwrap().text(),
94                            always_text
95                        ),
96                    )
97                } else {
98                    ctx.err(
99                        self.name(),
100                        format!(
101                            "comparisons to `NaN` with `{}` will always return {}",
102                            expr.op_token().unwrap().text(),
103                            always_text
104                        ),
105                    )
106                }
107                .primary(expr.range(), "")
108                .footer_note("`NaN` is not equal to anything including itself");
109
110                // telling the user to use isNaN for `<`, `>`, etc is a bit misleading so we won't do it if that is the case
111                if op == op!(==) || op == op!(===) {
112                    err = err.suggestion(
113                        expr.range(),
114                        "use `isNaN` instead",
115                        format!("isNaN({})", opposite),
116                        Applicability::Always,
117                    );
118                } else if op == op!(!=) || op == op!(!==) {
119                    err = err.suggestion(
120                        expr.range(),
121                        "use `isNaN` instead",
122                        format!("!isNaN({})", opposite),
123                        Applicability::Always,
124                    );
125                }
126
127                ctx.add_err(err);
128            }
129            SWITCH_STMT if self.enforce_for_switch_case => {
130                // TODO: a suggestion for this
131                let stmt = node.to::<SwitchStmt>();
132                let expr = stmt.test()?.condition()?;
133                if expr.text() == "NaN" {
134                    let err = ctx
135                        .err(
136                            self.name(),
137                            "a switch statement with a test of `NaN` will never match",
138                        )
139                        .primary(expr.range(), "")
140                        .footer_note("`NaN` is not equal to anything including itself");
141
142                    ctx.add_err(err);
143                }
144            }
145            CASE_CLAUSE if self.enforce_for_switch_case => {
146                // TODO: suggestion for this
147                let case = node.to::<CaseClause>();
148                let expr = case.test()?;
149                if expr.text() == "NaN" {
150                    let err = ctx
151                        .err(self.name(), "a case with a test of `NaN` will never match")
152                        .primary(expr.range(), "")
153                        .footer_note("`NaN` is not equal to anything including itself");
154
155                    ctx.add_err(err);
156                }
157            }
158            CALL_EXPR if self.enforce_for_index_of => {
159                // TODO: suggestion for this
160                let expr = node.to::<CallExpr>();
161                let callee = expr.callee()?;
162                let node = callee.syntax();
163                // rustfmt puts the last call's args each on a new line for some reason which is very ugly
164                #[rustfmt::skip]
165                let is_index_call =
166                    node.structural_lossy_token_eq(&["Array", ".", "prototype", ".", "indexOf"])
167                        || node.structural_lossy_token_eq(&["Array", ".", "prototype", ".", "lastIndexOf"])
168                        || node.structural_lossy_token_eq(&["String", ".", "prototype", ".", "indexOf"])
169                        || node.structural_lossy_token_eq(&["String", ".", "prototype", ".", "lastIndexOf"]);
170
171                let second_arg_is_nan = expr
172                    .arguments()
173                    .map(|a| a.args().nth(1).filter(|x| x.text() == "NaN"))
174                    .flatten()
175                    .is_some();
176
177                if (is_indexof_static_prop(&callee)
178                    && expr.arguments()?.args().next()?.text() == "NaN"
179                    && !is_index_call)
180                    || (is_index_call && second_arg_is_nan)
181                {
182                    let err = ctx
183                        .err(
184                            self.name(),
185                            "an index check with `NaN` will always return `-1`",
186                        )
187                        .primary(expr.range(), "")
188                        .footer_help("index checks use `===` internally, which will never match because `NaN` is not equal to anything");
189
190                    ctx.add_err(err);
191                }
192            }
193            _ => {}
194        }
195        None
196    }
197}
198
199const INDEX_OF_NAMES: [&str; 2] = ["lastIndexOf", "indexOf"];
200
201fn is_indexof_static_prop(expr: &Expr) -> bool {
202    match expr {
203        Expr::BracketExpr(brack_expr) => brack_expr
204            .syntax()
205            .try_to::<Literal>()
206            .and_then(|l| l.inner_string_text())
207            .filter(|text| INDEX_OF_NAMES.contains(&text.to_string().as_str()))
208            .is_some(),
209        Expr::DotExpr(dotexpr) => dotexpr
210            .prop()
211            .filter(|prop| INDEX_OF_NAMES.contains(&prop.to_string().as_str()))
212            .is_some(),
213        _ => false,
214    }
215}
216
217rule_tests! {
218    UseIsnan::default(),
219    err: {
220        "123 == NaN;",
221        "123 === NaN;",
222        "NaN === \"abc\";",
223        "NaN == \"abc\";",
224        "123 != NaN;",
225        "123 !== NaN;",
226        "NaN !== \"abc\";",
227        "NaN != \"abc\";",
228        "NaN < \"abc\";",
229        "\"abc\" < NaN;",
230        "NaN > \"abc\";",
231        "\"abc\" > NaN;",
232        "NaN <= \"abc\";",
233        "\"abc\" <= NaN;",
234        "NaN >= \"abc\";",
235        "\"abc\" >= NaN;"
236    },
237    ok: {
238        "var x = NaN;",
239        "isNaN(NaN) === true;",
240        "isNaN(123) !== true;",
241        "Number.isNaN(NaN) === true;",
242        "Number.isNaN(123) !== true;",
243        "foo(NaN + 1);",
244        "foo(1 + NaN);",
245        "foo(NaN - 1)",
246        "foo(1 - NaN)",
247        "foo(NaN * 2)",
248        "foo(2 * NaN)",
249        "foo(NaN / 2)",
250        "foo(2 / NaN)",
251        "var x; if (x = NaN) { }",
252        "foo.indexOf(NaN)",
253        "foo.lastIndexOf(NaN)",
254    }
255}
256
257rule_tests! {
258    indexof_ok,
259    indexof_err,
260    UseIsnan {
261        enforce_for_index_of: true,
262        enforce_for_switch_case: false
263    },
264    err: {
265        "Array.prototype.indexOf(foo, NaN)",
266        "Array.prototype.lastIndexOf(foo, NaN)",
267        "String.prototype.indexOf(foo, NaN)",
268        "String.prototype.lastIndexOf(foo, NaN)",
269    },
270    ok: {
271        "Array.prototype.indexOf(NaN)",
272        "Array.prototype.lastIndexOf(NaN)",
273        "String.prototype.indexOf(NaN)",
274        "String.prototype.lastIndexOf(NaN)",
275    }
276}