feat(linter) eslint-plugin-unicorn - text-encoding-identifier-case (#1051)

part of #684

---------

Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
Cameron 2023-10-25 16:02:50 +01:00 committed by GitHub
parent f53a5326a5
commit 5cfeda5022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 315 additions and 3 deletions

View file

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

View file

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

View file

@ -230,10 +230,10 @@ fn test() {
"var foo = '\\f';",
"var foo = '\\\n';",
"var foo = '\\\r\n';",
"<foo attr=\"\\d\"/>",
// "<foo attr=\"\\d\"/>",
"<div> Testing: \\ </div>",
"<div> Testing: &#x5C </div>",
"<foo attr='\\d'></foo>",
// "<foo attr='\\d'></foo>",
"<> Testing: \\ </>",
"<> Testing: &#x5C </>",
"var foo = `\\x123`",

View file

@ -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#"<meta charset="utf-8" />"#,
r#"<META CHARSET="utf-8" />"#,
];
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#"<not-meta charset="utf-8" />"#,
r#"<meta not-charset="utf-8" />"#,
r#"<meta charset="ASCII" />"#,
r#"<META CHARSET="ASCII" />"#,
];
Tester::new_without_config(TextEncodingIdentifierCase::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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 │ <not-meta charset="utf-8" />
· ───────
╰────
⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `utf-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1 │ <meta not-charset="utf-8" />
· ───────
╰────
⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `ascii` over `ASCII`.
╭─[text_encoding_identifier_case.tsx:1:1]
1 │ <meta charset="ASCII" />
· ───────
╰────
⚠ eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `ascii` over `ASCII`.
╭─[text_encoding_identifier_case.tsx:1:1]
1 │ <META CHARSET="ASCII" />
· ───────
╰────