diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index d338dd6b1c3..9ae3141167a 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -81,6 +81,7 @@ define_categories! { "lint/nursery/noConfusingArrow": "https://docs.rome.tools/lint/rules/noConfusingArrow", "lint/nursery/noConsoleLog": "https://docs.rome.tools/lint/rules/noConsoleLog", "lint/nursery/noConstantCondition": "https://docs.rome.tools/lint/rules/noConstantCondition", + "lint/nursery/noControlCharactersInRegex": "https://docs.rome.tools/lint/rules/noControlCharactersInRegex", "lint/nursery/noDuplicateJsonKeys": "https://docs.rome.tools/lint/rules/noDuplicateJsonKeys", "lint/nursery/noDuplicateJsxProps": "https://docs.rome.tools/lint/rules/noDuplicateJsxProps", "lint/nursery/noExcessiveComplexity": "https://docs.rome.tools/lint/rules/noExcessiveComplexity", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 942f41becbc..2583f8e5dbd 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -3,6 +3,7 @@ use rome_analyze::declare_group; pub(crate) mod no_confusing_arrow; +pub(crate) mod no_control_characters_in_regex; pub(crate) mod no_duplicate_jsx_props; pub(crate) mod no_excessive_complexity; pub(crate) mod no_fallthrough_switch_clause; @@ -24,6 +25,7 @@ declare_group! { name : "nursery" , rules : [ self :: no_confusing_arrow :: NoConfusingArrow , + self :: no_control_characters_in_regex :: NoControlCharactersInRegex , self :: no_duplicate_jsx_props :: NoDuplicateJsxProps , self :: no_excessive_complexity :: NoExcessiveComplexity , self :: no_fallthrough_switch_clause :: NoFallthroughSwitchClause , diff --git a/crates/rome_js_analyze/src/analyzers/nursery/no_control_characters_in_regex.rs b/crates/rome_js_analyze/src/analyzers/nursery/no_control_characters_in_regex.rs new file mode 100644 index 00000000000..6c1c3888355 --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/no_control_characters_in_regex.rs @@ -0,0 +1,274 @@ +use crate::utils::escape_string; +use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_js_syntax::{ + AnyJsExpression, JsCallArguments, JsCallExpression, JsNewExpression, JsRegexLiteralExpression, + JsStringLiteralExpression, +}; +use rome_rowan::{declare_node_union, AstNode, AstSeparatedList}; +use std::{iter::Peekable, str::Chars}; + +declare_rule! { + /// Prevents from having control characters and some escape sequences that match control characters in regular expressions. + /// + /// Control characters are hidden special characters that are numbered from 0 to 31 in the ASCII system. + /// They're not commonly used in JavaScript text. So, if you see them in a pattern (called a regular expression), it's probably a mistake. + /// + /// The following elements of regular expression patterns are considered possible errors in typing and are therefore disallowed by this rule: + /// + /// - Hexadecimal character escapes from `\x00` to `\x1F` + /// - Unicode character escapes from `\u0000` to `\u001F` + /// - Unicode code point escapes from `\u{0}` to `\u{1F}` + /// - Unescaped raw characters from U+0000 to U+001F + /// + /// Control escapes such as `\t` and `\n` are allowed by this rule. + /// + /// Source: https://eslint.org/docs/latest/rules/no-control-regex + /// + /// ## Examples + /// + /// ### Invalid + /// ```js,expect_diagnostic + /// var pattern1 = /\x00/; + /// ``` + /// ```js,expect_diagnostic + /// var pattern2 = /\x0C/; + /// ``` + /// ```js,expect_diagnostic + /// var pattern3 = /\x1F/; + /// ``` + /// ```js,expect_diagnostic + /// var pattern4 = /\u000C/; + /// ``` + /// ```js,expect_diagnostic + /// var pattern5 = /\u{C}/u; + /// ``` + /// ```js,expect_diagnostic + /// var pattern7 = new RegExp("\x0C"); + /// ``` + /// ```js,expect_diagnostic + /// var pattern7 = new RegExp("\\x0C"); + /// ``` + /// + /// ### Valid + /// ```js + /// var pattern1 = /\x20/; + /// var pattern2 = /\u0020/; + /// var pattern3 = /\u{20}/u; + /// var pattern4 = /\t/; + /// var pattern5 = /\n/; + /// var pattern6 = new RegExp("\x20"); + /// ``` + /// + pub(crate) NoControlCharactersInRegex { + version: "next", + name: "noControlCharactersInRegex", + recommended: true, + } +} + +declare_node_union! { + pub(crate) RegexExpressionLike = JsNewExpression | JsCallExpression | JsRegexLiteralExpression +} + +fn decode_hex_character_to_code_point(iter: &mut Peekable) -> Option<(String, i64)> { + let first = iter.next()?; + let second = iter.next()?; + let digits = format!("{first}{second}"); + let code_point = i64::from_str_radix(&digits, 16).ok()?; + Some((digits, code_point)) +} + +fn decode_unicode_escape_to_code_point(iter: &mut Peekable) -> Option<(String, i64)> { + let mut digits = String::new(); + // Loop 4 times as unicode escape sequence has exactly 4 hexadecimal digits + for _ in 0..4 { + if let Some(&c) = iter.peek() { + match c { + '0'..='9' | 'a'..='f' | 'A'..='F' => digits.push(iter.next()?), + _ => continue, + } + } + } + let code_point = i64::from_str_radix(digits.as_str(), 16).ok()?; + Some((digits, code_point)) +} + +fn decode_escaped_code_point_to_code_point(iter: &mut Peekable) -> Option<(String, i64)> { + let mut digits = String::new(); + if iter.peek() == Some(&'{') { + iter.next(); + while let Some(&c) = iter.peek() { + if c == '}' { + iter.next(); + let code_point = i64::from_str_radix(&digits, 16).ok()?; + return Some((format!("{{{}}}", digits), code_point)); + } else { + digits.push(iter.next()?); + } + } + } + None +} + +fn add_control_character_to_vec( + prefix: &str, + iter: &mut Peekable, + control_characters: &mut Vec, + decode: fn(&mut Peekable) -> Option<(String, i64)>, +) { + if let Some((s, code_point)) = decode(iter) { + // ASCII control characters are represented by code points from 0 to 31 + if (0..=31).contains(&code_point) { + control_characters.push(format!("{prefix}{s}")); + } + } +} + +/// Collecting control characters for regex. The following characters in regular expression patterns are considered as control characters: +/// - Hexadecimal character escapes from `\x00` to `\x1F`. +/// - Unicode character escapes from `\u0000` to `\u001F`. +/// - Unicode code point escapes range from `\u{0}` to `\u{1F}`. +/// - The Unicode flag must be set as true in order for these Unicode code point escapes to work: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode. +/// - Unescaped raw characters from U+0000 to U+001F. +fn collect_control_characters(pattern: String, flags: Option) -> Option> { + let mut control_characters: Vec = Vec::new(); + let is_unicode_flag_set = flags.unwrap_or_default().contains('u'); + let mut iter = pattern.chars().peekable(); + + while let Some(c) = iter.next() { + match c { + '\\' => match iter.next() { + Some('x') => add_control_character_to_vec( + "\\x", + &mut iter, + &mut control_characters, + decode_hex_character_to_code_point, + ), + Some('u') if is_unicode_flag_set => add_control_character_to_vec( + "\\u", + &mut iter, + &mut control_characters, + decode_escaped_code_point_to_code_point, + ), + Some('u') => add_control_character_to_vec( + "\\u", + &mut iter, + &mut control_characters, + decode_unicode_escape_to_code_point, + ), + Some('\\') => continue, + _ => break, + }, + _ => continue, + } + } + + if !control_characters.is_empty() { + Some(control_characters) + } else { + None + } +} + +fn collect_control_characters_from_expression( + callee: &AnyJsExpression, + js_call_arguments: &JsCallArguments, +) -> Option> { + let js_identifier = match callee { + AnyJsExpression::JsIdentifierExpression(js_identifier) => js_identifier, + _ => return None, + }; + + if js_identifier.name().ok()?.has_name("RegExp") { + let mut args = js_call_arguments.args().iter(); + let raw_pattern = args + .next() + .and_then(|arg| arg.ok()) + .and_then(|arg| JsStringLiteralExpression::cast_ref(arg.syntax())) + .and_then(|js_string_literal| js_string_literal.inner_string_text().ok())? + .to_string(); + + let pattern = escape_string(&raw_pattern).unwrap_or(raw_pattern); + + let regexp_flags = args + .next() + .and_then(|arg| arg.ok()) + .and_then(|arg| JsStringLiteralExpression::cast_ref(arg.syntax())) + .map(|js_string_literal| js_string_literal.text()); + + return collect_control_characters(pattern, regexp_flags); + } + None +} + +impl Rule for NoControlCharactersInRegex { + type Query = Ast; + type State = Vec; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + match node { + RegexExpressionLike::JsNewExpression(js_new_expression) => { + collect_control_characters_from_expression( + &js_new_expression.callee().ok()?, + &js_new_expression.arguments()?, + ) + } + RegexExpressionLike::JsCallExpression(js_call_expression) => { + collect_control_characters_from_expression( + &js_call_expression.callee().ok()?, + &js_call_expression.arguments().ok()?, + ) + } + RegexExpressionLike::JsRegexLiteralExpression(js_regex_literal_expression) => { + collect_control_characters( + js_regex_literal_expression.pattern().ok()?, + js_regex_literal_expression.flags().ok(), + ) + } + } + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "Unexpected control character(s) in regular expression: "{state.join(", ")}"" + }, + ).note( + markup! { + "Control characters are unusual and potentially incorrect inputs, so they are disallowed." + } + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_collect_control_characters() { + assert_eq!( + collect_control_characters(String::from("\\x00\\x0F\\u0010\\u001F"), None), + Some(vec![ + String::from("\\x00"), + String::from("\\x0F"), + String::from("\\u0010"), + String::from("\\u001F") + ]) + ); + assert_eq!( + collect_control_characters(String::from("\\u{0}\\u{1F}"), Some(String::from("u"))), + Some(vec![String::from("\\u{0}"), String::from("\\u{1F}")]) + ); + assert_eq!( + collect_control_characters(String::from("\\x20\\u0020\\u{20}\\t\\n"), None), + None + ); + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js new file mode 100644 index 00000000000..29baa0f0b41 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js @@ -0,0 +1,25 @@ +var regex = RegExp("\\x1f"); +var regex = RegExp("\\u{1111}*\\x1F", "u"); +var regex = new RegExp("\\x1f\\x1e"); +var regex = new RegExp("\\x1fFOO\\x00"); +var regex = new RegExp("FOO\\x1fFOO\\x1f"); +var regex = new RegExp("\\x1f"); +var regex = new RegExp("\\u001F", flags); +var regex = new RegExp("\\u{1111}*\\x1F", "u"); +var regex = new RegExp("\\u{1F}", "u"); +var regex = new RegExp("\\u{1F}", "gui"); +var regex = new RegExp("\\x0C"); +var regex = new RegExp("\x0C"); +var regex = /\x00/; +var regex = /\x0C/; +var regex = /\x1F/; +var regex = /\u000C/; +var regex = /\u{C}/u; +var regex = /\\\x1f\\x1e/; +var regex = /\\\x1fFOO\\x00/; +var regex = /FOO\\\x1fFOO\\x1f/; +var regex = /(?\\x1f)/; +var regex = /(?<\u{1d49c}>.)\x1f/; +var regex = /\u{1111}*\x1F/u; +var regex = /\u{1F}/u; +var regex = /\u{1F}/gui; diff --git a/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js.snap new file mode 100644 index 00000000000..86c43c412ac --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/invalid.js.snap @@ -0,0 +1,440 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +var regex = RegExp("\\x1f"); +var regex = RegExp("\\u{1111}*\\x1F", "u"); +var regex = new RegExp("\\x1f\\x1e"); +var regex = new RegExp("\\x1fFOO\\x00"); +var regex = new RegExp("FOO\\x1fFOO\\x1f"); +var regex = new RegExp("\\x1f"); +var regex = new RegExp("\\u001F", flags); +var regex = new RegExp("\\u{1111}*\\x1F", "u"); +var regex = new RegExp("\\u{1F}", "u"); +var regex = new RegExp("\\u{1F}", "gui"); +var regex = new RegExp("\\x0C"); +var regex = new RegExp("\x0C"); +var regex = /\x00/; +var regex = /\x0C/; +var regex = /\x1F/; +var regex = /\u000C/; +var regex = /\u{C}/u; +var regex = /\\\x1f\\x1e/; +var regex = /\\\x1fFOO\\x00/; +var regex = /FOO\\\x1fFOO\\x1f/; +var regex = /(?\\x1f)/; +var regex = /(?<\u{1d49c}>.)\x1f/; +var regex = /\u{1111}*\x1F/u; +var regex = /\u{1F}/u; +var regex = /\u{1F}/gui; + +``` + +# Diagnostics +``` +invalid.js:1:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + > 1 │ var regex = RegExp("\\x1f"); + │ ^^^^^^^^^^^^^^^ + 2 │ var regex = RegExp("\\u{1111}*\\x1F", "u"); + 3 │ var regex = new RegExp("\\x1f\\x1e"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:2:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1F + + 1 │ var regex = RegExp("\\x1f"); + > 2 │ var regex = RegExp("\\u{1111}*\\x1F", "u"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 3 │ var regex = new RegExp("\\x1f\\x1e"); + 4 │ var regex = new RegExp("\\x1fFOO\\x00"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:3:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f, \x1e + + 1 │ var regex = RegExp("\\x1f"); + 2 │ var regex = RegExp("\\u{1111}*\\x1F", "u"); + > 3 │ var regex = new RegExp("\\x1f\\x1e"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ var regex = new RegExp("\\x1fFOO\\x00"); + 5 │ var regex = new RegExp("FOO\\x1fFOO\\x1f"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:4:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f, \x00 + + 2 │ var regex = RegExp("\\u{1111}*\\x1F", "u"); + 3 │ var regex = new RegExp("\\x1f\\x1e"); + > 4 │ var regex = new RegExp("\\x1fFOO\\x00"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ var regex = new RegExp("FOO\\x1fFOO\\x1f"); + 6 │ var regex = new RegExp("\\x1f"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:5:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f, \x1f + + 3 │ var regex = new RegExp("\\x1f\\x1e"); + 4 │ var regex = new RegExp("\\x1fFOO\\x00"); + > 5 │ var regex = new RegExp("FOO\\x1fFOO\\x1f"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ var regex = new RegExp("\\x1f"); + 7 │ var regex = new RegExp("\\u001F", flags); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:6:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + 4 │ var regex = new RegExp("\\x1fFOO\\x00"); + 5 │ var regex = new RegExp("FOO\\x1fFOO\\x1f"); + > 6 │ var regex = new RegExp("\\x1f"); + │ ^^^^^^^^^^^^^^^^^^^ + 7 │ var regex = new RegExp("\\u001F", flags); + 8 │ var regex = new RegExp("\\u{1111}*\\x1F", "u"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:7:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u001F + + 5 │ var regex = new RegExp("FOO\\x1fFOO\\x1f"); + 6 │ var regex = new RegExp("\\x1f"); + > 7 │ var regex = new RegExp("\\u001F", flags); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ var regex = new RegExp("\\u{1111}*\\x1F", "u"); + 9 │ var regex = new RegExp("\\u{1F}", "u"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:8:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1F + + 6 │ var regex = new RegExp("\\x1f"); + 7 │ var regex = new RegExp("\\u001F", flags); + > 8 │ var regex = new RegExp("\\u{1111}*\\x1F", "u"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 9 │ var regex = new RegExp("\\u{1F}", "u"); + 10 │ var regex = new RegExp("\\u{1F}", "gui"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:9:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u{1F} + + 7 │ var regex = new RegExp("\\u001F", flags); + 8 │ var regex = new RegExp("\\u{1111}*\\x1F", "u"); + > 9 │ var regex = new RegExp("\\u{1F}", "u"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ var regex = new RegExp("\\u{1F}", "gui"); + 11 │ var regex = new RegExp("\\x0C"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:10:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u{1F} + + 8 │ var regex = new RegExp("\\u{1111}*\\x1F", "u"); + 9 │ var regex = new RegExp("\\u{1F}", "u"); + > 10 │ var regex = new RegExp("\\u{1F}", "gui"); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ var regex = new RegExp("\\x0C"); + 12 │ var regex = new RegExp("\x0C"); + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:11:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x0C + + 9 │ var regex = new RegExp("\\u{1F}", "u"); + 10 │ var regex = new RegExp("\\u{1F}", "gui"); + > 11 │ var regex = new RegExp("\\x0C"); + │ ^^^^^^^^^^^^^^^^^^^ + 12 │ var regex = new RegExp("\x0C"); + 13 │ var regex = /\x00/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:12:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x0C + + 10 │ var regex = new RegExp("\\u{1F}", "gui"); + 11 │ var regex = new RegExp("\\x0C"); + > 12 │ var regex = new RegExp("\x0C"); + │ ^^^^^^^^^^^^^^^^^^ + 13 │ var regex = /\x00/; + 14 │ var regex = /\x0C/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:13:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x00 + + 11 │ var regex = new RegExp("\\x0C"); + 12 │ var regex = new RegExp("\x0C"); + > 13 │ var regex = /\x00/; + │ ^^^^^^ + 14 │ var regex = /\x0C/; + 15 │ var regex = /\x1F/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:14:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x0C + + 12 │ var regex = new RegExp("\x0C"); + 13 │ var regex = /\x00/; + > 14 │ var regex = /\x0C/; + │ ^^^^^^ + 15 │ var regex = /\x1F/; + 16 │ var regex = /\u000C/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:15:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1F + + 13 │ var regex = /\x00/; + 14 │ var regex = /\x0C/; + > 15 │ var regex = /\x1F/; + │ ^^^^^^ + 16 │ var regex = /\u000C/; + 17 │ var regex = /\u{C}/u; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:16:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u000C + + 14 │ var regex = /\x0C/; + 15 │ var regex = /\x1F/; + > 16 │ var regex = /\u000C/; + │ ^^^^^^^^ + 17 │ var regex = /\u{C}/u; + 18 │ var regex = /\\\x1f\\x1e/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:17:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u{C} + + 15 │ var regex = /\x1F/; + 16 │ var regex = /\u000C/; + > 17 │ var regex = /\u{C}/u; + │ ^^^^^^^^ + 18 │ var regex = /\\\x1f\\x1e/; + 19 │ var regex = /\\\x1fFOO\\x00/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:18:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + 16 │ var regex = /\u000C/; + 17 │ var regex = /\u{C}/u; + > 18 │ var regex = /\\\x1f\\x1e/; + │ ^^^^^^^^^^^^^ + 19 │ var regex = /\\\x1fFOO\\x00/; + 20 │ var regex = /FOO\\\x1fFOO\\x1f/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:19:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + 17 │ var regex = /\u{C}/u; + 18 │ var regex = /\\\x1f\\x1e/; + > 19 │ var regex = /\\\x1fFOO\\x00/; + │ ^^^^^^^^^^^^^^^^ + 20 │ var regex = /FOO\\\x1fFOO\\x1f/; + 21 │ var regex = /(?\\x1f)/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:20:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + 18 │ var regex = /\\\x1f\\x1e/; + 19 │ var regex = /\\\x1fFOO\\x00/; + > 20 │ var regex = /FOO\\\x1fFOO\\x1f/; + │ ^^^^^^^^^^^^^^^^^^^ + 21 │ var regex = /(?\\x1f)/; + 22 │ var regex = /(?<\u{1d49c}>.)\x1f/; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:22:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1f + + 20 │ var regex = /FOO\\\x1fFOO\\x1f/; + 21 │ var regex = /(?\\x1f)/; + > 22 │ var regex = /(?<\u{1d49c}>.)\x1f/; + │ ^^^^^^^^^^^^^^^^^^^^^ + 23 │ var regex = /\u{1111}*\x1F/u; + 24 │ var regex = /\u{1F}/u; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:23:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \x1F + + 21 │ var regex = /(?\\x1f)/; + 22 │ var regex = /(?<\u{1d49c}>.)\x1f/; + > 23 │ var regex = /\u{1111}*\x1F/u; + │ ^^^^^^^^^^^^^^^^ + 24 │ var regex = /\u{1F}/u; + 25 │ var regex = /\u{1F}/gui; + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:24:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u{1F} + + 22 │ var regex = /(?<\u{1d49c}>.)\x1f/; + 23 │ var regex = /\u{1111}*\x1F/u; + > 24 │ var regex = /\u{1F}/u; + │ ^^^^^^^^^ + 25 │ var regex = /\u{1F}/gui; + 26 │ + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + +``` +invalid.js:25:13 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected control character(s) in regular expression: \u{1F} + + 23 │ var regex = /\u{1111}*\x1F/u; + 24 │ var regex = /\u{1F}/u; + > 25 │ var regex = /\u{1F}/gui; + │ ^^^^^^^^^^^ + 26 │ + + i Control characters are unusual and potentially incorrect inputs, so they are disallowed. + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js new file mode 100644 index 00000000000..aa92680d823 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js @@ -0,0 +1,20 @@ +var regex = RegExp("x1f"); +var regex = RegExp("["); +var regex = new RegExp("x1f"); +var regex = new RegExp("["); +var regex = new RegExp("\\u{20}", "u"); +var regex = new RegExp("\\u{1F}"); +var regex = new RegExp("\\u{1F}", "g"); +var regex = new RegExp("\\u{1F}", uflags); +var regex = new RegExp("\t"); +var regex = new RegExp("\n"); +var regex = /\t/; +var regex = /\n/; +var regex = /x1f/; +var regex = /\\x1f/; +var regex = /\u{20}/u; +var regex = /\u{1F}/; +var regex = /\u{1F}/g; +var regex = /\t/; +var regex = /\n/; +new (function foo() {})("\\x1f"); diff --git a/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js.snap new file mode 100644 index 00000000000..42c49c05d9a --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noControlCharactersInRegex/valid.js.snap @@ -0,0 +1,30 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +var regex = RegExp("x1f"); +var regex = RegExp("["); +var regex = new RegExp("x1f"); +var regex = new RegExp("["); +var regex = new RegExp("\\u{20}", "u"); +var regex = new RegExp("\\u{1F}"); +var regex = new RegExp("\\u{1F}", "g"); +var regex = new RegExp("\\u{1F}", uflags); +var regex = new RegExp("\t"); +var regex = new RegExp("\n"); +var regex = /\t/; +var regex = /\n/; +var regex = /x1f/; +var regex = /\\x1f/; +var regex = /\u{20}/u; +var regex = /\u{1F}/; +var regex = /\u{1F}/g; +var regex = /\t/; +var regex = /\n/; +new (function foo() {})("\\x1f"); + +``` + + diff --git a/crates/rome_js_syntax/src/expr_ext.rs b/crates/rome_js_syntax/src/expr_ext.rs index 4c9ef708931..5755c005f26 100644 --- a/crates/rome_js_syntax/src/expr_ext.rs +++ b/crates/rome_js_syntax/src/expr_ext.rs @@ -618,10 +618,24 @@ impl JsRegexLiteralExpression { let text_trimmed = token.text_trimmed(); // SAFETY: a valid regex literal must have a end slash - let end_slash_pos = text_trimmed.rfind('/').unwrap(); + let end_slash_pos = text_trimmed + .rfind('/') + .expect("regex literal must have an end slash"); Ok(String::from(&text_trimmed[1..end_slash_pos])) } + + pub fn flags(&self) -> SyntaxResult { + let token = self.value_token()?; + let text_trimmed = token.text_trimmed(); + + // SAFETY: a valid regex literal must have a end slash + let end_slash_pos = text_trimmed + .rfind('/') + .expect("regex literal must have an end slash"); + + Ok(String::from(&text_trimmed[end_slash_pos..])) + } } impl AnyJsExpression { diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index c3fd2cd503b..b2f784cc696 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -1821,6 +1821,15 @@ pub struct Nursery { #[bpaf(long("no-constant-condition"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub no_constant_condition: Option, + #[doc = "Prevents from having control characters and some escape sequences that match control characters in regular expressions."] + #[bpaf( + long("no-control-characters-in-regex"), + argument("on|off|warn"), + optional, + hide + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_control_characters_in_regex: Option, #[doc = "Disallow two keys with the same name inside a JSON object."] #[bpaf( long("no-duplicate-json-keys"), @@ -1983,13 +1992,14 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 32] = [ + pub(crate) const GROUP_RULES: [&'static str; 33] = [ "noAccumulatingSpread", "noAriaUnsupportedElements", "noBannedTypes", "noConfusingArrow", "noConsoleLog", "noConstantCondition", + "noControlCharactersInRegex", "noDuplicateJsonKeys", "noDuplicateJsxProps", "noExcessiveComplexity", @@ -2017,10 +2027,11 @@ impl Nursery { "useNamingConvention", "useSimpleNumberKeys", ]; - const RECOMMENDED_RULES: [&'static str; 18] = [ + const RECOMMENDED_RULES: [&'static str; 19] = [ "noAriaUnsupportedElements", "noBannedTypes", "noConstantCondition", + "noControlCharactersInRegex", "noDuplicateJsonKeys", "noDuplicateJsxProps", "noGlobalIsFinite", @@ -2037,27 +2048,28 @@ impl Nursery { "useLiteralEnumMembers", "useLiteralKeys", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 18] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 19] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 32] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 33] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2090,6 +2102,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { matches!(self.recommended, Some(true)) } @@ -2130,136 +2143,141 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_control_characters_in_regex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_jsx_props.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_excessive_complexity.as_ref() { + if let Some(rule) = self.no_duplicate_jsx_props.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_fallthrough_switch_clause.as_ref() { + if let Some(rule) = self.no_excessive_complexity.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_for_each.as_ref() { + if let Some(rule) = self.no_fallthrough_switch_clause.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_global_is_finite.as_ref() { + if let Some(rule) = self.no_for_each.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_global_is_nan.as_ref() { + if let Some(rule) = self.no_global_is_finite.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_noninteractive_tabindex.as_ref() { + if let Some(rule) = self.no_global_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_nonoctal_decimal_escape.as_ref() { + if let Some(rule) = self.no_noninteractive_tabindex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_redundant_roles.as_ref() { + if let Some(rule) = self.no_nonoctal_decimal_escape.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_self_assign.as_ref() { + if let Some(rule) = self.no_redundant_roles.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_static_only_class.as_ref() { + if let Some(rule) = self.no_self_assign.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_useless_empty_export.as_ref() { + if let Some(rule) = self.no_static_only_class.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_empty_export.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_heading_content.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_heading_content.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2294,136 +2312,141 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_control_characters_in_regex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_jsx_props.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_excessive_complexity.as_ref() { + if let Some(rule) = self.no_duplicate_jsx_props.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_fallthrough_switch_clause.as_ref() { + if let Some(rule) = self.no_excessive_complexity.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_for_each.as_ref() { + if let Some(rule) = self.no_fallthrough_switch_clause.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_global_is_finite.as_ref() { + if let Some(rule) = self.no_for_each.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_global_is_nan.as_ref() { + if let Some(rule) = self.no_global_is_finite.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_noninteractive_tabindex.as_ref() { + if let Some(rule) = self.no_global_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_nonoctal_decimal_escape.as_ref() { + if let Some(rule) = self.no_noninteractive_tabindex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_redundant_roles.as_ref() { + if let Some(rule) = self.no_nonoctal_decimal_escape.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_self_assign.as_ref() { + if let Some(rule) = self.no_redundant_roles.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_static_only_class.as_ref() { + if let Some(rule) = self.no_self_assign.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_useless_empty_export.as_ref() { + if let Some(rule) = self.no_static_only_class.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_empty_export.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.use_aria_prop_types.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_prop_types.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_camel_case.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_camel_case.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_heading_content.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_heading_content.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_is_nan.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_nan.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_literal_keys.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_simple_number_keys.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } + if let Some(rule) = self.use_simple_number_keys.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2432,10 +2455,10 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 18] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 19] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 32] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 33] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2462,6 +2485,7 @@ impl Nursery { "noConfusingArrow" => self.no_confusing_arrow.as_ref(), "noConsoleLog" => self.no_console_log.as_ref(), "noConstantCondition" => self.no_constant_condition.as_ref(), + "noControlCharactersInRegex" => self.no_control_characters_in_regex.as_ref(), "noDuplicateJsonKeys" => self.no_duplicate_json_keys.as_ref(), "noDuplicateJsxProps" => self.no_duplicate_jsx_props.as_ref(), "noExcessiveComplexity" => self.no_excessive_complexity.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index f00bbc93979..0f8ec418a59 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -1650,6 +1650,7 @@ impl VisitNode for Nursery { "noConfusingArrow", "noConsoleLog", "noConstantCondition", + "noControlCharactersInRegex", "noDuplicateJsonKeys", "noDuplicateJsxProps", "noExcessiveComplexity", @@ -1833,6 +1834,29 @@ impl VisitNode for Nursery { )); } }, + "noControlCharactersInRegex" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.no_control_characters_in_regex = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "noControlCharactersInRegex", + diagnostics, + )?; + self.no_control_characters_in_regex = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "noDuplicateJsonKeys" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index b1c8812b128..f1bd163bb4a 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -796,6 +796,13 @@ { "type": "null" } ] }, + "noControlCharactersInRegex": { + "description": "Prevents from having control characters and some escape sequences that match control characters in regular expressions.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noDuplicateJsonKeys": { "description": "Disallow two keys with the same name inside a JSON object.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index ed7480e2662..3897a73ba99 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -529,6 +529,10 @@ export interface Nursery { * Disallow constant expressions in conditions */ noConstantCondition?: RuleConfiguration; + /** + * Prevents from having control characters and some escape sequences that match control characters in regular expressions. + */ + noControlCharactersInRegex?: RuleConfiguration; /** * Disallow two keys with the same name inside a JSON object. */ @@ -1138,6 +1142,7 @@ export type Category = | "lint/nursery/noConfusingArrow" | "lint/nursery/noConsoleLog" | "lint/nursery/noConstantCondition" + | "lint/nursery/noControlCharactersInRegex" | "lint/nursery/noDuplicateJsonKeys" | "lint/nursery/noDuplicateJsxProps" | "lint/nursery/noExcessiveComplexity" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index b1c8812b128..f1bd163bb4a 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -796,6 +796,13 @@ { "type": "null" } ] }, + "noControlCharactersInRegex": { + "description": "Prevents from having control characters and some escape sequences that match control characters in regular expressions.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noDuplicateJsonKeys": { "description": "Disallow two keys with the same name inside a JSON object.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 8859b1cc2ef..0e1f859bf5d 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Rome's linter has a total of 151 rules

\ No newline at end of file +

Rome's linter has a total of 152 rules

\ No newline at end of file diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index b6f62d6746f..30b6d406694 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -933,6 +933,12 @@ Disallow the use of console.log Disallow constant expressions in conditions

+

+ noControlCharactersInRegex +

+Prevents from having control characters and some escape sequences that match control characters in regular expressions. +
+

noDuplicateJsonKeys

diff --git a/website/src/pages/lint/rules/noControlCharactersInRegex.md b/website/src/pages/lint/rules/noControlCharactersInRegex.md new file mode 100644 index 00000000000..69004bf0dbd --- /dev/null +++ b/website/src/pages/lint/rules/noControlCharactersInRegex.md @@ -0,0 +1,154 @@ +--- +title: Lint Rule noControlCharactersInRegex +parent: lint/rules/index +--- + +# noControlCharactersInRegex (since vnext) + +Prevents from having control characters and some escape sequences that match control characters in regular expressions. + +Control characters are hidden special characters that are numbered from 0 to 31 in the ASCII system. +They're not commonly used in JavaScript text. So, if you see them in a pattern (called a regular expression), it's probably a mistake. + +The following elements of regular expression patterns are considered possible errors in typing and are therefore disallowed by this rule: + +- Hexadecimal character escapes from `\x00` to `\x1F` +- Unicode character escapes from `\u0000` to `\u001F` +- Unicode code point escapes from `\u{0}` to `\u{1F}` +- Unescaped raw characters from U+0000 to U+001F + +Control escapes such as `\t` and `\n` are allowed by this rule. + +Source: https://eslint.org/docs/latest/rules/no-control-regex + +## Examples + +### Invalid + +```jsx + var pattern1 = /\x00/; +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \x00
+  
+  > 1 │  var pattern1 = /\x00/;
+                   ^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern2 = /\x0C/; +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \x0C
+  
+  > 1 │  var pattern2 = /\x0C/;
+                   ^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern3 = /\x1F/; +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \x1F
+  
+  > 1 │  var pattern3 = /\x1F/;
+                   ^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern4 = /\u000C/; +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \u000C
+  
+  > 1 │  var pattern4 = /\u000C/;
+                   ^^^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern5 = /\u{C}/u; +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \u{C}
+  
+  > 1 │  var pattern5 = /\u{C}/u;
+                   ^^^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern7 = new RegExp("\x0C"); +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \x0C
+  
+  > 1 │  var pattern7 = new RegExp("\x0C");
+                   ^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +```jsx + var pattern7 = new RegExp("\\x0C"); +``` + +
nursery/noControlCharactersInRegex.js:1:17 lint/nursery/noControlCharactersInRegex ━━━━━━━━━━━━━━━━━
+
+   Unexpected control character(s) in regular expression: \x0C
+  
+  > 1 │  var pattern7 = new RegExp("\\x0C");
+                   ^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   Control characters are unusual and potentially incorrect inputs, so they are disallowed.
+  
+
+ +### Valid + +```jsx +var pattern1 = /\x20/; +var pattern2 = /\u0020/; +var pattern3 = /\u{20}/u; +var pattern4 = /\t/; +var pattern5 = /\n/; +var pattern6 = new RegExp("\x20"); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)