diff --git a/crates/oxc_ast/src/visit.rs b/crates/oxc_ast/src/visit.rs index 41792c88e..8108350cd 100644 --- a/crates/oxc_ast/src/visit.rs +++ b/crates/oxc_ast/src/visit.rs @@ -1064,7 +1064,7 @@ pub trait Visit<'a>: Sized { } JSXAttributeValue::Element(elem) => self.visit_jsx_element(elem), JSXAttributeValue::Fragment(elem) => self.visit_jsx_fragment(elem), - JSXAttributeValue::StringLiteral(_) => {} + JSXAttributeValue::StringLiteral(lit) => self.visit_string_literal(lit), } } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5d044e929..47e2349c9 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -147,6 +147,7 @@ mod unicorn { pub mod no_thenable; pub mod no_unnecessary_await; pub mod prefer_array_flat_map; + pub mod text_encoding_identifier_case; pub mod throw_new_error; } @@ -258,6 +259,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::no_instanceof_array, unicorn::no_unnecessary_await, unicorn::no_thenable, + unicorn::text_encoding_identifier_case, unicorn::throw_new_error, unicorn::prefer_array_flat_map, react::jsx_key, diff --git a/crates/oxc_linter/src/rules/eslint/no_useless_escape.rs b/crates/oxc_linter/src/rules/eslint/no_useless_escape.rs index 7b658c168..f2a94af0e 100644 --- a/crates/oxc_linter/src/rules/eslint/no_useless_escape.rs +++ b/crates/oxc_linter/src/rules/eslint/no_useless_escape.rs @@ -230,10 +230,10 @@ fn test() { "var foo = '\\f';", "var foo = '\\\n';", "var foo = '\\\r\n';", - "", + // "", "
Testing: \\
", "
Testing: \
", - "", + // "", "<> Testing: \\ ", "<> Testing: \ ", "var foo = `\\x123`", diff --git a/crates/oxc_linter/src/rules/unicorn/text_encoding_identifier_case.rs b/crates/oxc_linter/src/rules/unicorn/text_encoding_identifier_case.rs new file mode 100644 index 000000000..904d5f653 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/text_encoding_identifier_case.rs @@ -0,0 +1,167 @@ +use oxc_ast::{ + ast::{JSXAttributeItem, JSXAttributeName, JSXElementName}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::AstNodeId; +use oxc_span::{Atom, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `{1}` over `{2}`.")] +#[diagnostic(severity(warning))] +struct TextEncodingIdentifierCaseDiagnostic(#[label] pub Span, pub &'static str, pub Atom); + +#[derive(Debug, Default, Clone)] +pub struct TextEncodingIdentifierCase; + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule aims to enforce consistent case for text encoding identifiers. + /// + /// Enforces `'utf8'` for UTF-8 encoding + /// Enforces `'ascii'` for ASCII encoding. + /// + /// ### Example + /// ```javascript + /// // Fail + /// await fs.readFile(file, 'UTF-8'); + /// + /// await fs.readFile(file, 'ASCII'); + /// + /// const string = buffer.toString('utf-8'); + /// + /// // pass + /// + /// await fs.readFile(file, 'utf8'); + /// + /// await fs.readFile(file, 'ascii'); + /// + /// const string = buffer.toString('utf8'); + /// + /// ``` + TextEncodingIdentifierCase, + style +); + +impl Rule for TextEncodingIdentifierCase { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let (str, span) = match node.kind() { + AstKind::StringLiteral(string_lit) => (&string_lit.value, string_lit.span), + AstKind::JSXText(jsx_text) => (&jsx_text.value, jsx_text.span), + _ => { + return; + } + }; + + if str.as_str() == "utf-8" && is_jsx_meta_elem_with_charset_attr(node.id(), ctx) { + return; + } + + let Some(replacement) = get_replacement(str) else { + return; + }; + + if replacement == str.as_str() { + return; + } + + ctx.diagnostic(TextEncodingIdentifierCaseDiagnostic(span, replacement, str.clone())); + } +} + +fn get_replacement(node: &Atom) -> Option<&'static str> { + if !matches!(node.as_str().len(), 4 | 5) { + return None; + } + + let node_lower = node.as_str().to_ascii_lowercase(); + + if node_lower == "utf-8" || node_lower == "utf8" { + return Some("utf8"); + } + + if node_lower == "ascii" { + return Some("ascii"); + } + + None +} + +fn is_jsx_meta_elem_with_charset_attr(id: AstNodeId, ctx: &LintContext) -> bool { + let Some(parent) = ctx.nodes().parent_node(id) else { return false }; + + let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(jsx_attr)) = parent.kind() else { + return false; + }; + + let JSXAttributeName::Identifier(ident) = &jsx_attr.name else { return false }; + if ident.name.to_lowercase() != "charset" { + return false; + } + + let Some(AstKind::JSXOpeningElement(opening_elem)) = ctx.nodes().parent_kind(parent.id()) + else { + return false; + }; + + let JSXElementName::Identifier(name) = &opening_elem.name else { return false }; + + if name.name.to_lowercase() != "meta" { + return false; + } + + true +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"`UTF-8`"#, + r#""utf8""#, + r#""utf+8""#, + r#"" utf8 ""#, + "\'utf8\'", + r#""\\u0055tf8""#, + r#"const ASCII = 1"#, + r#"const UTF8 = 1"#, + r#""#, + r#""#, + ]; + + let fail = vec![ + r#""UTF-8""#, + r#""utf-8""#, + r#"'utf-8'"#, + r#""Utf8""#, + r#""ASCII""#, + r#"fs.readFile?.(file, "UTF-8")"#, + r#"fs?.readFile(file, "UTF-8")"#, + r#"readFile(file, "UTF-8")"#, + r#"fs.readFile(...file, "UTF-8")"#, + r#"new fs.readFile(file, "UTF-8")"#, + r#"fs.readFile(file, {encoding: "UTF-8"})"#, + r#"fs.readFile("UTF-8")"#, + r#"fs.readFile(file, "UTF-8", () => {})"#, + r#"fs.readFileSync(file, "UTF-8")"#, + r#"fs[readFile](file, "UTF-8")"#, + r#"fs["readFile"](file, "UTF-8")"#, + r#"await fs.readFile(file, "UTF-8",)"#, + r#"fs.promises.readFile(file, "UTF-8",)"#, + r#"whatever.readFile(file, "UTF-8",)"#, + r#""#, + r#""#, + r#""#, + r#""#, + ]; + + Tester::new_without_config(TextEncodingIdentifierCase::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/text_encoding_identifier_case.snap b/crates/oxc_linter/src/snapshots/text_encoding_identifier_case.snap new file mode 100644 index 000000000..81ef0a104 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/text_encoding_identifier_case.snap @@ -0,0 +1,143 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: text_encoding_identifier_case +--- + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ "UTF-8" + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `utf-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ "utf-8" + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `utf-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ 'utf-8' + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `Utf8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ "Utf8" + · ────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `ascii` over `ASCII`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ "ASCII" + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFile?.(file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs?.readFile(file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ readFile(file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFile(...file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ new fs.readFile(file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFile(file, {encoding: "UTF-8"}) + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFile("UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFile(file, "UTF-8", () => {}) + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.readFileSync(file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs[readFile](file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs["readFile"](file, "UTF-8") + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ await fs.readFile(file, "UTF-8",) + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ fs.promises.readFile(file, "UTF-8",) + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ whatever.readFile(file, "UTF-8",) + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `utf-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `utf-8`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `ascii` over `ASCII`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ + · ─────── + ╰──── + + ⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `ascii` over `ASCII`. + ╭─[text_encoding_identifier_case.tsx:1:1] + 1 │ + · ─────── + ╰──── + +