mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter/eslint/no-control-regex): detect raw control characters (#28548)
Report raw control characters in regex literals and strings passed to `RegExp`, matching ESLint on the reported cases. Compare the source character with its code point and restrict capture-group filtering to octal escapes so raw controls remain visible. Add regression coverage for raw controls and preserve the existing octal escape behavior and diagnostics. Fixes #28276
This commit is contained in:
1 parent
7c65777da2
commit
a205cd3e1c
1 file changed
+60
-15
@@ -21,7 +21,6 @@ fn no_control_regex_diagnostic(control_chars: &[Character]) -> OxcDiagnostic {
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// Show the code point since the character itself is not printable
|
||||
format!("'U+{:04X}' is a control character.", ch.value)
|
||||
}
|
||||
};
|
||||
@@ -118,24 +117,18 @@ struct ControlCharacterFinder<'a> {
|
||||
impl<'a> Visit<'a> for ControlCharacterFinder<'a> {
|
||||
fn visit_pattern(&mut self, it: &Pattern<'a>) {
|
||||
walk::walk_pattern(self, it);
|
||||
// \1, \2, etc. are sometimes valid "control" characters as they can be
|
||||
// used to reference values from capturing groups. Note in this case
|
||||
// they're not actually control characters. However, if there's no
|
||||
// corresponding capturing group, they _are_ invalid control characters.
|
||||
//
|
||||
// Some important notes:
|
||||
// 1. Capture groups are 1-indexed.
|
||||
// 2. Capture groups can be nested.
|
||||
// 3. Capture groups may be referenced before they are defined. This is
|
||||
// why we need to do this check here, instead of filtering inside of
|
||||
// visit_character.
|
||||
if self.num_capture_groups > 0 && !self.control_chars.is_empty() {
|
||||
self.control_chars.retain(|c| !(c.value > 0x01 && c.value <= self.num_capture_groups));
|
||||
self.control_chars.retain(|c| {
|
||||
!(matches!(
|
||||
c.kind,
|
||||
CharacterKind::Octal1 | CharacterKind::Octal2 | CharacterKind::Octal3
|
||||
) && c.value > 0x01
|
||||
&& c.value <= self.num_capture_groups)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_character(&mut self, ch: &Character) {
|
||||
// Control characters are in the range 0x00 to 0x1F
|
||||
if ch.value <= 0x1F && ch.kind != CharacterKind::Null {
|
||||
let text: &str = ch.span.source_text(self.source_text);
|
||||
let is_code_point_match = text
|
||||
@@ -143,7 +136,8 @@ impl<'a> Visit<'a> for ControlCharacterFinder<'a> {
|
||||
.chars()
|
||||
.nth(0)
|
||||
.is_some_and(|c| c.to_digit(16) == Some(ch.value));
|
||||
if is_code_point_match
|
||||
if text.chars().next().is_some_and(|c| u32::from(c) == ch.value)
|
||||
|| is_code_point_match
|
||||
|| text.starts_with("\\x")
|
||||
|| text.starts_with("\\\\x")
|
||||
|| text.starts_with("\\u")
|
||||
@@ -165,6 +159,57 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::tester::Tester;
|
||||
|
||||
#[test]
|
||||
fn test_raw_control_characters() {
|
||||
let pass = vec![
|
||||
"/ /",
|
||||
"new RegExp(' ')",
|
||||
r"/\t\n\r\f\v\0/",
|
||||
r"new RegExp('\\t\\n\\r\\f\\v\\0')",
|
||||
"/\\\x08/",
|
||||
];
|
||||
let fail = vec![
|
||||
"/\x00/",
|
||||
"/a\x01b/",
|
||||
"/\x08import/",
|
||||
"/\t/",
|
||||
"/\x0b/",
|
||||
"/\x0c/",
|
||||
"/a\x1fb/",
|
||||
"/[\x08]/u",
|
||||
"new RegExp('\x08import')",
|
||||
"RegExp('\x08import')",
|
||||
"new RegExp('\x00')",
|
||||
"new RegExp('\t')",
|
||||
"new RegExp('a\x1fb')",
|
||||
"/(a)(b)\x02/",
|
||||
"new RegExp('(a)(b)\x02')",
|
||||
];
|
||||
|
||||
Tester::new(NoControlRegex::NAME, NoControlRegex::PLUGIN, pass, fail).test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_octal_escape_behavior() {
|
||||
let pass = vec![
|
||||
r"/\01/",
|
||||
r"/\10/",
|
||||
r"/(a)(b)[\2]/",
|
||||
r"new RegExp('\\01')",
|
||||
r"new RegExp('(a)(b)[\\2]')",
|
||||
];
|
||||
let fail = vec![
|
||||
r"/\1/",
|
||||
r"/\2/",
|
||||
r"/\00/",
|
||||
r"/\000/",
|
||||
r"new RegExp('\\1')",
|
||||
r"new RegExp('\\00')",
|
||||
];
|
||||
|
||||
Tester::new(NoControlRegex::NAME, NoControlRegex::PLUGIN, pass, fail).test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_literals() {
|
||||
let pass = vec![
|
||||
|
||||
Reference in new issue
Block a user