rslint_core/groups/errors/
use_isnan.rs1use crate::rule_prelude::*;
2use ast::*;
3use SyntaxKind::*;
4
5declare_lint! {
6 #[serde(default)]
41 UseIsnan,
42 errors,
43 tags(Recommended),
44 "use-isnan",
45 pub enforce_for_switch_case: bool,
48 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 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 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 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 let expr = node.to::<CallExpr>();
161 let callee = expr.callee()?;
162 let node = callee.syntax();
163 #[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}