Skip to content

Commit

Permalink
feat(linter) eslint-plugin-unicorn - text-encoding-identifier-case
Browse files Browse the repository at this point in the history
  • Loading branch information
camc314 committed Oct 24, 2023
1 parent d8f07ca commit c76dbba
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 1 deletion.
2 changes: 1 addition & 1 deletion crates/oxc_ast/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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;
}

Expand Down Expand Up @@ -255,6 +256,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,
Expand Down
167 changes: 167 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/text_encoding_identifier_case.rs
Original file line number Diff line number Diff line change
@@ -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();
}
143 changes: 143 additions & 0 deletions crates/oxc_linter/src/snapshots/text_encoding_identifier_case.snap
Original file line number Diff line number Diff line change
@@ -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]
1fs.readFile?.(file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs?.readFile(file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1readFile(file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.readFile(...file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1new fs.readFile(file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.readFile(file, {encoding: "UTF-8"})
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.readFile("UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.readFile(file, "UTF-8", () => {})
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.readFileSync(file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs[readFile](file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs["readFile"](file, "UTF-8")
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1await fs.readFile(file, "UTF-8",)
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1fs.promises.readFile(file, "UTF-8",)
· ───────
╰────

eslint-plugin-unicorn(text-encoding-identifier-case): Prefer `utf8` over `UTF-8`.
╭─[text_encoding_identifier_case.tsx:1:1]
1whatever.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" />
· ───────
╰────


0 comments on commit c76dbba

Please sign in to comment.