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
This commit is contained in:
RiESAEX 2023-11-18 21:57:06 +08:00 committed by GitHub
parent bc3069ec76
commit 98279fc6ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 0 deletions

View file

@ -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,

View file

@ -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<String> {
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());
}

View file

@ -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"
· ──────
╰────