forked from oxc-project/oxc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter) deepscan: bad char at comparison (oxc-project#1750)
- Loading branch information
Showing
3 changed files
with
182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
crates/oxc_linter/src/rules/deepscan/bad_char_at_comparison.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
use oxc_ast::{ast::Expression, AstKind}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::Error, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::{GetSpan, Span}; | ||
use oxc_syntax::operator::BinaryOperator; | ||
|
||
use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error("deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method")] | ||
#[diagnostic(severity(warning), help("`String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false."))] | ||
struct BadCharAtComparisonDiagnostic( | ||
#[label("`charAt` called here")] pub Span, | ||
#[label("And compared with a string of length {2} here")] pub Span, | ||
usize, | ||
); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct BadCharAtComparison; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// This rule warns when the return value of the `charAt` method is used to compare a string of length greater than 1. | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// The `charAt` method returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// // Bad: The return value of the `charAt` method is compared with a string of length greater than 1. | ||
/// a.charAt(4) === 'a2'; | ||
/// a.charAt(4) === '/n'; | ||
/// | ||
/// // Good: The return value of the `charAt` method is compared with a string of length 1. | ||
/// a.charAt(4) === 'a' | ||
/// a.charAt(4) === '\n'; | ||
/// ``` | ||
BadCharAtComparison, | ||
correctness | ||
); | ||
|
||
impl Rule for BadCharAtComparison { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
let AstKind::CallExpression(call_expr) = node.kind() else { return }; | ||
|
||
if !is_method_call(call_expr, None, Some(&["charAt"]), Some(1), Some(1)) { | ||
return; | ||
} | ||
|
||
let Some(parent) = ctx.nodes().parent_node(node.id()) else { | ||
return; | ||
}; | ||
|
||
let AstKind::BinaryExpression(binary_expr) = parent.kind() else { return }; | ||
if !matches!( | ||
binary_expr.operator, | ||
BinaryOperator::Equality | ||
| BinaryOperator::Inequality | ||
| BinaryOperator::StrictEquality | ||
| BinaryOperator::StrictInequality | ||
) { | ||
return; | ||
}; | ||
|
||
let comparison_with = if binary_expr.left.span() == call_expr.span { | ||
&binary_expr.right | ||
} else { | ||
&binary_expr.left | ||
}; | ||
|
||
if let Expression::StringLiteral(string_lit) = comparison_with { | ||
if !is_string_valid(string_lit.value.as_str()) { | ||
ctx.diagnostic(BadCharAtComparisonDiagnostic( | ||
call_expr.span, | ||
string_lit.span, | ||
string_lit.value.len(), | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn is_string_valid(str: &str) -> bool { | ||
if str.len() < 2 || str.chars().count() == 1 { | ||
return true; | ||
} | ||
|
||
false | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
r"a.charAt(4) === 'a'", | ||
"a.charAt(4) === '\\n'", | ||
"a.charAt(4) === '\t'", | ||
r"a.charAt(4) === 'a'", | ||
r"a.charAt(4) === '\ufeff'", | ||
r"a.charAt(4) !== '\ufeff'", | ||
"chatAt(4) === 'a2'", | ||
"new chatAt(4) === 'a'", | ||
]; | ||
|
||
let fail = vec![ | ||
r"a.charAt(4) === 'aa'", | ||
"a.charAt(4) === '/n'", | ||
"a.charAt(3) === '/t'", | ||
r"a.charAt(4) === 'ac'", | ||
r"a.charAt(822) !== 'foo'", | ||
r"a.charAt(4) === '\\ukeff'", | ||
]; | ||
|
||
Tester::new_without_config(BadCharAtComparison::NAME, pass, fail).test_and_snapshot(); | ||
} |
59 changes: 59 additions & 0 deletions
59
crates/oxc_linter/src/snapshots/bad_char_at_comparison.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: bad_char_at_comparison | ||
--- | ||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(4) === 'aa' | ||
· ─────┬───── ──┬─ | ||
· │ ╰── And compared with a string of length 2 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(4) === '/n' | ||
· ─────┬───── ──┬─ | ||
· │ ╰── And compared with a string of length 2 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(3) === '/t' | ||
· ─────┬───── ──┬─ | ||
· │ ╰── And compared with a string of length 2 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(4) === 'ac' | ||
· ─────┬───── ──┬─ | ||
· │ ╰── And compared with a string of length 2 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(822) !== 'foo' | ||
· ──────┬────── ──┬── | ||
· │ ╰── And compared with a string of length 3 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
⚠ deepscan(bad-char-at-comparison): Invalid comparison with `charAt` method | ||
╭─[bad_char_at_comparison.tsx:1:1] | ||
1 │ a.charAt(4) === '\\ukeff' | ||
· ─────┬───── ────┬──── | ||
· │ ╰── And compared with a string of length 6 here | ||
· ╰── `charAt` called here | ||
╰──── | ||
help: `String.prototype.charAt` returns a string of length 1. If the return value is compared with a string of length greater than 1, the comparison will always be false. | ||
|
||
|