From 98279fc6ff0488fa4b5ca6c8ec22844699f4dccb Mon Sep 17 00:00:00 2001 From: RiESAEX <2597245950@qq.com> Date: Sat, 18 Nov 2023 21:57:06 +0800 Subject: [PATCH] feat(linter): eslint-plugin-unicorn: no-hex-escape (#1410) [Rule Detail](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-hex-escape.md) #684 --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/unicorn/no_hex_escape.rs | 189 ++++++++++++++++++ .../src/snapshots/no_hex_escape.snap | 11 + 3 files changed, 202 insertions(+) create mode 100644 crates/oxc_linter/src/rules/unicorn/no_hex_escape.rs create mode 100644 crates/oxc_linter/src/snapshots/no_hex_escape.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 58b4c3eef..f34708c1c 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -154,6 +154,7 @@ mod unicorn { pub mod no_console_spaces; pub mod no_document_cookie; pub mod no_empty_file; + pub mod no_hex_escape; pub mod no_instanceof_array; pub mod no_invalid_remove_event_listener; pub mod no_lonely_if; @@ -311,6 +312,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::no_console_spaces, unicorn::no_document_cookie, unicorn::no_empty_file, + unicorn::no_hex_escape, unicorn::no_instanceof_array, unicorn::no_invalid_remove_event_listener, unicorn::no_lonely_if, diff --git a/crates/oxc_linter/src/rules/unicorn/no_hex_escape.rs b/crates/oxc_linter/src/rules/unicorn/no_hex_escape.rs new file mode 100644 index 000000000..748af9a88 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/no_hex_escape.rs @@ -0,0 +1,189 @@ +use oxc_ast::{ + ast::{StringLiteral, TemplateLiteral}, + 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, Fix}; + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint-plugin-unicorn(no-hex-escape): Use Unicode escapes instead of hexadecimal escapes." +)] +#[diagnostic(severity(warning))] +struct NoHexEscapeDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoHexEscape; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces a convention of using [Unicode escapes](https://mathiasbynens.be/notes/javascript-escapes#unicode) instead of [hexadecimal escapes](https://mathiasbynens.be/notes/javascript-escapes#hexadecimal) for consistency and clarity. + /// + /// ### Example + /// ```javascript + /// // fail + /// const foo = '\x1B'; + /// const foo = `\x1B${bar}`; + /// + /// // pass + /// const foo = '\u001B'; + /// const foo = `\u001B${bar}`; + /// ``` + NoHexEscape, + correctness +); + +// \x -> \u00 +fn check_escape(value: &str) -> Option { + let mut in_escape = false; + let mut matched = Vec::new(); + for (index, c) in value.chars().enumerate() { + if c == '\\' && !in_escape { + in_escape = true; + } else if c == 'x' && in_escape { + matched.push(index); + in_escape = false; + } else { + in_escape = false; + } + } + if matched.is_empty() { + None + } else { + let mut fixed = String::with_capacity(value.len() + matched.len() * 2); + let mut last = 0; + for index in matched { + fixed.push_str(&value[last..index - 1]); + fixed.push_str("\\u00"); + last = index + 1; + } + fixed.push_str(&value[last..]); + Some(fixed) + } +} +impl Rule for NoHexEscape { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::StringLiteral(StringLiteral { span, .. }) => { + let text = span.source_text(ctx.source_text()); + if let Some(fixed) = check_escape(&text[1..text.len() - 1]) { + ctx.diagnostic_with_fix(NoHexEscapeDiagnostic(*span), || { + Fix::new(format!("'{fixed}'"), *span) + }); + } + } + AstKind::TemplateLiteral(TemplateLiteral { quasis, .. }) => { + quasis.iter().for_each(|quasi| { + if let Some(fixed) = check_escape(quasi.span.source_text(ctx.source_text())) { + ctx.diagnostic_with_fix(NoHexEscapeDiagnostic(quasi.span), || { + Fix::new(fixed, quasi.span) + }); + } + }); + } + AstKind::RegExpLiteral(regex) => { + let text = regex.span.source_text(ctx.source_text()); + if let Some(fixed) = check_escape(&text[1..text.len() - 1]) { + ctx.diagnostic_with_fix(NoHexEscapeDiagnostic(regex.span), || { + Fix::new(format!("/{fixed}/"), regex.span) + }); + } + } + _ => {} + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r"const foo = 'foo'", + r"const foo = '\u00b1'", + r"const foo = '\u00b1\u00b1'", + r"const foo = 'foo\u00b1'", + r"const foo = 'foo\u00b1foo'", + r"const foo = '\u00b1foo'", + r"const foo = '\\xb1'", + r"const foo = '\\\\xb1'", + r"const foo = 'foo\\xb1'", + r"const foo = 'foo\\\\xb1'", + r"const foo = '\\xd8\\x3d\\xdc\\xa9'", + r"const foo = 'foo\\x12foo\\x34'", + r"const foo = '\\\\xd8\\\\x3d\\\\xdc\\\\xa9'", + r"const foo = 'foo\\\\x12foo\\\\x34'", + r"const foo = 42", + r"const foo = `foo`", + r"const foo = `\u00b1`", + r"const foo = `\u00b1\u00b1`", + r"const foo = `foo\u00b1`", + r"const foo = `foo\u00b1foo`", + r"const foo = `\u00b1foo`", + r"const foo = `42`", + r"const foo = `\\xb1`", + r"const foo = `\\\\xb1`", + r"const foo = `foo\\xb1`", + r"const foo = `foo\\\\xb1`", + r"const foo = `\\xd8\\x3d\\xdc\\xa9`", + r"const foo = `foo\\x12foo\\x34`", + r"const foo = `\\\\xd8\\\\x3d\\\\xdc\\\\xa9`", + r"const foo = `foo\\\\x12foo\\\\x34`", + ]; + let fail = vec![r#"const foo = "\xb1""#]; + let fix = vec![ + (r"const foo = '\xb1'", r"const foo = '\u00b1'", None), + (r"const foo = '\\\xb1'", r"const foo = '\\\u00b1'", None), + (r"const foo = '\xb1\xb1'", r"const foo = '\u00b1\u00b1'", None), + (r"const foo = '\\\xb1\\\xb1'", r"const foo = '\\\u00b1\\\u00b1'", None), + (r"const foo = '\\\xb1\\\\xb1'", r"const foo = '\\\u00b1\\\\xb1'", None), + (r"const foo = '\\\\\xb1\\\xb1'", r"const foo = '\\\\\u00b1\\\u00b1'", None), + (r"const foo = '\xb1foo'", r"const foo = '\u00b1foo'", None), + (r"const foo = '\xd8\x3d\xdc\xa9'", r"const foo = '\u00d8\u003d\u00dc\u00a9'", None), + (r"const foo = 'foo\xb1'", r"const foo = 'foo\u00b1'", None), + (r"const foo = 'foo\\\xb1'", r"const foo = 'foo\\\u00b1'", None), + (r"const foo = 'foo\\\\\xb1'", r"const foo = 'foo\\\\\u00b1'", None), + (r"const foo = 'foo\x12foo\x34'", r"const foo = 'foo\u0012foo\u0034'", None), + (r"const foo = '42\x1242\x34'", r"const foo = '42\u001242\u0034'", None), + (r"const foo = '42\\\x1242\\\x34'", r"const foo = '42\\\u001242\\\u0034'", None), + (r"const foo = /^[\x20-\x7E]*$/", r"const foo = /^[\u0020-\u007E]*$/", None), + (r"const foo = `\xb1`", r"const foo = `\u00b1`", None), + (r"const foo = `\\\xb1`", r"const foo = `\\\u00b1`", None), + (r"const foo = `\xb1\xb1`", r"const foo = `\u00b1\u00b1`", None), + (r"const foo = `\\\xb1\\\xb1`", r"const foo = `\\\u00b1\\\u00b1`", None), + (r"const foo = `\\\\\xb1\\\xb1`", r"const foo = `\\\\\u00b1\\\u00b1`", None), + (r"const foo = `\\\\\xb1\\\\xb1`", r"const foo = `\\\\\u00b1\\\\xb1`", None), + (r"const foo = `\xb1foo`", r"const foo = `\u00b1foo`", None), + (r"const foo = `\xd8\x3d\xdc\xa9`", r"const foo = `\u00d8\u003d\u00dc\u00a9`", None), + (r"const foo = `foo\xb1`", r"const foo = `foo\u00b1`", None), + (r"const foo = `foo\\\xb1`", r"const foo = `foo\\\u00b1`", None), + (r"const foo = `foo\\\\\xb1`", r"const foo = `foo\\\\\u00b1`", None), + (r"const foo = `foo\x12foo\x34`", r"const foo = `foo\u0012foo\u0034`", None), + (r"const foo = `42\x1242\x34`", r"const foo = `42\u001242\u0034`", None), + (r"const foo = `42\\\x1242\\\x34`", r"const foo = `42\\\u001242\\\u0034`", None), + (r"const foo = `\xb1${foo}\xb1${foo}`", r"const foo = `\u00b1${foo}\u00b1${foo}`", None), + ]; + + Tester::new_without_config(NoHexEscape::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); +} + +#[test] +fn test_check_escape() { + let result = check_escape(r"\x1B").unwrap(); + assert_eq!(result, r"\u001B"); + let result = check_escape(r"a\x1B").unwrap(); + assert_eq!(result, r"a\u001B"); + assert!(check_escape(r"\\x1B").is_none()); + let result = check_escape(r"\\\x1B").unwrap(); + assert_eq!(result, r"\\\u001B"); + let result = check_escape(r"\\\a\x1B").unwrap(); + assert_eq!(result, r"\\\a\u001B"); + assert!(check_escape(r"\\xb1").is_none()); +} diff --git a/crates/oxc_linter/src/snapshots/no_hex_escape.snap b/crates/oxc_linter/src/snapshots/no_hex_escape.snap new file mode 100644 index 000000000..c07da41b7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_hex_escape.snap @@ -0,0 +1,11 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_hex_escape +--- + ⚠ eslint-plugin-unicorn(no-hex-escape): Use Unicode escapes instead of hexadecimal escapes. + ╭─[no_hex_escape.tsx:1:1] + 1 │ const foo = "\xb1" + · ────── + ╰──── + +