feat(linter): Add rule eslint(no_regex_spaces) (#1129)

This commit is contained in:
Iván Ovejero 2023-11-15 07:04:48 +01:00 committed by GitHub
parent ba0e4d1a6a
commit 5ade8950e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 449 additions and 0 deletions

View file

@ -68,6 +68,7 @@ mod eslint {
pub mod no_obj_calls;
pub mod no_prototype_builtins;
pub mod no_redeclare;
pub mod no_regex_spaces;
pub mod no_return_await;
pub mod no_self_assign;
pub mod no_self_compare;
@ -245,6 +246,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::no_obj_calls,
eslint::no_prototype_builtins,
eslint::no_redeclare,
eslint::no_regex_spaces,
eslint::no_return_await,
eslint::no_self_assign,
eslint::no_self_compare,

View file

@ -0,0 +1,267 @@
use oxc_allocator::Vec;
use oxc_ast::{
ast::{Argument, CallExpression, Expression, NewExpression, RegExpLiteral},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{context::LintContext, rule::Rule, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint(no-regex-spaces): Spaces are hard to count.")]
#[diagnostic(severity(warning), help("Use a quantifier, e.g. {{2}}"))]
struct NoRegexSpacesDiagnostic(#[label] pub Span);
#[derive(Debug, Default, Clone)]
pub struct NoRegexSpaces;
declare_oxc_lint!(
/// ### What it does
/// Disallow 2+ consecutive spaces in regular expressions.
///
/// ### Why is this bad?
///
/// In a regular expression, it is hard to tell how many spaces are
/// intended to be matched. It is better to use only one space and
/// then specify how many spaces are expected using a quantifier.
///
/// ```javascript
/// var re = /foo {3}bar/;
/// ```
///
/// ### Example
/// ```javascript
/// var re = /foo bar/;
/// ```
NoRegexSpaces,
restriction,
);
impl Rule for NoRegexSpaces {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::RegExpLiteral(lit) => {
if let Some(span) = Self::find_literal_to_report(lit) {
ctx.diagnostic(NoRegexSpacesDiagnostic(span)); // /a b/
}
}
AstKind::CallExpression(expr) if Self::is_regexp_call_expression(expr) => {
if let Some(span) = Self::find_expr_to_report(&expr.arguments) {
ctx.diagnostic(NoRegexSpacesDiagnostic(span)); // RegExp('a b')
}
}
AstKind::NewExpression(expr) if Self::is_regexp_new_expression(expr) => {
if let Some(span) = Self::find_expr_to_report(&expr.arguments) {
ctx.diagnostic(NoRegexSpacesDiagnostic(span)); // new RegExp('a b')
}
}
_ => {}
}
}
}
impl NoRegexSpaces {
fn find_literal_to_report(literal: &RegExpLiteral) -> Option<Span> {
if Self::has_exempted_char_class(&literal.regex.pattern) {
return None;
}
if let Some((idx_start, idx_end)) =
Self::find_consecutive_spaces_indices(&literal.regex.pattern)
{
let start = literal.span.start + u32::try_from(idx_start).unwrap() + 1;
let end = literal.span.start + u32::try_from(idx_end).unwrap() + 2;
return Some(Span { start, end });
}
None
}
fn find_expr_to_report(args: &Vec<'_, Argument<'_>>) -> Option<Span> {
if let Some(Argument::Expression(expr)) = args.get(1) {
if !expr.is_string_literal() {
return None; // skip on indeterminate flag, e.g. RegExp('a b', flags)
}
}
if let Some(Argument::Expression(Expression::StringLiteral(pattern))) = args.get(0) {
if Self::has_exempted_char_class(&pattern.value) {
return None; // skip spaces inside char class, e.g. RegExp('[ ]')
}
if let Some((idx_start, idx_end)) =
Self::find_consecutive_spaces_indices(&pattern.value)
{
let start = pattern.span.start + u32::try_from(idx_start).unwrap() + 1;
let end = pattern.span.start + u32::try_from(idx_end).unwrap() + 2;
return Some(Span { start, end });
}
}
None
}
fn find_consecutive_spaces_indices(input: &str) -> Option<(usize, usize)> {
let mut consecutive_spaces = 0;
let mut start: Option<usize> = None;
let mut inside_square_brackets: usize = 0;
for (cur_idx, char) in input.char_indices() {
if char == '[' {
inside_square_brackets += 1;
} else if char == ']' {
inside_square_brackets = inside_square_brackets.saturating_sub(1);
}
if char != ' ' || inside_square_brackets > 0 {
// ignore spaces inside char class, including nested ones
consecutive_spaces = 0;
start = None;
continue;
}
if start.is_none() {
start = Some(cur_idx);
}
consecutive_spaces += 1;
if consecutive_spaces < 2 {
continue;
}
// 2 or more consecutive spaces
if let Some(next_char) = input.chars().nth(cur_idx + 1) {
if next_char == ' ' {
continue; // keep collecting spaces
}
if "+*{?".contains(next_char) && consecutive_spaces == 2 {
continue; // ignore 2 consecutive spaces before quantifier
}
return Some((start.unwrap(), cur_idx));
}
// end of string
return Some((start.unwrap(), cur_idx));
}
None
}
fn is_regexp_new_expression(expr: &NewExpression<'_>) -> bool {
expr.callee.is_specific_id("RegExp") && expr.arguments.len() > 0
}
fn is_regexp_call_expression(expr: &CallExpression<'_>) -> bool {
expr.callee.is_specific_id("RegExp") && expr.arguments.len() > 0
}
/// Whether the input has a character class but no consecutive spaces
/// outside the character class.
fn has_exempted_char_class(input: &str) -> bool {
let mut inside_class = false;
for (i, c) in input.chars().enumerate() {
match c {
'[' => inside_class = true,
']' => inside_class = false,
' ' if input.chars().nth(i + 1) == Some(' ') && !inside_class => {
return false;
}
_ => {}
}
}
true
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
"var foo = /foo/;",
"var foo = RegExp('foo')",
"var foo = / /;",
"var foo = RegExp(' ')",
"var foo = / a b c d /;",
"var foo = /bar {3}baz/g;",
"var foo = RegExp('bar {3}baz', 'g')",
"var foo = new RegExp('bar {3}baz')",
"var foo = /bar baz/;",
"var foo = RegExp('bar baz');",
"var foo = new RegExp('bar baz');",
"var foo = / +/;",
"var foo = / ?/;",
"var foo = / */;",
"var foo = / {2}/;",
"var foo = / {2}/v;",
r"var foo = /bar \\ baz/;",
r"var foo = /bar\\ \\ baz/;",
r"var foo = /bar \\u0020 baz/;",
r"var foo = /bar \\u0020\\u0020baz/;",
r"var foo = new RegExp('bar \\ baz')",
r"var foo = new RegExp('bar\\ \\ baz')",
r"var foo = new RegExp('bar \\\\ baz')",
r"var foo = new RegExp('bar\\u0020\\u0020baz')",
r"var foo = new RegExp('bar \\u0020 baz')",
"new RegExp(' ', flags)",
"var foo = /[ ]/;",
"var foo = /[ ]/;",
"var foo = / [ ] /;",
"var foo = / [ ] [ ] /;",
"var foo = new RegExp('[ ]');",
"var foo = new RegExp('[ ]');",
"var foo = new RegExp(' [ ] ');",
"var foo = RegExp(' [ ] [ ] ');",
r"var foo = new RegExp(' \[ ');",
r"var foo = new RegExp(' \[ \] ');",
r"var foo = /[\\q{ }]/v;",
"var foo = new RegExp('[ ');",
"new RegExp('[[abc] ]', flags + 'v')",
];
let fail = vec![
"var foo = /bar baz/;",
"var foo = /bar baz/;",
"var foo = / a b c d /;",
"var foo = RegExp(' a b c d ');",
"var foo = RegExp('bar baz');",
"var foo = new RegExp('bar baz');",
"var foo = /bar {3}baz/;",
"var foo = /bar ?baz/;",
"var foo = new RegExp('bar *baz')",
"var foo = RegExp('bar +baz')",
"var foo = new RegExp('bar ');",
r"var foo = /bar\\ baz/;",
"var foo = /(?: )/;",
"var foo = RegExp('^foo(?= )');",
r"var foo = /\\ /",
r"var foo = / \\ /",
"var foo = / foo /;",
r"var foo = new RegExp('\\d ')",
r"var foo = RegExp('\\u0041 ')",
"var foo = /[ ] /;",
"var foo = / [ ] /;",
"var foo = RegExp(' [ ]');",
"var foo = /[[ ] ] /v;",
"var foo = new RegExp('[ ] ');",
"var foo = new RegExp('[[ ] ] ', 'v');",
];
Tester::new_without_config(NoRegexSpaces::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,180 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_regex_spaces
---
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /bar baz/;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /bar baz/;
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = / a b c d /;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp(' a b c d ');
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp('bar baz');
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('bar baz');
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /bar {3}baz/;
· ───
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /bar ?baz/;
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('bar *baz')
· ───
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp('bar +baz')
· ───
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('bar ');
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /bar\\ baz/;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /(?: )/;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp('^foo(?= )');
· ───
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /\\ /
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = / \\ /
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = / foo /;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('\\d ')
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp('\\u0041 ')
· ───
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /[ ] /;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = / [ ] /;
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = RegExp(' [ ]');
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = /[[ ] ] /v;
· ────
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('[ ] ');
· ──
╰────
help: Use a quantifier, e.g. {2}
⚠ eslint(no-regex-spaces): Spaces are hard to count.
╭─[no_regex_spaces.tsx:1:1]
1 │ var foo = new RegExp('[[ ] ] ', 'v');
· ────
╰────
help: Use a quantifier, e.g. {2}