Skip to content

Commit

Permalink
feat(linter) eslint plugin unicorn prefer string trim start end (#1148)
Browse files Browse the repository at this point in the history
  • Loading branch information
camc314 committed Nov 4, 2023
1 parent 3bf374e commit f8be0b4
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ mod unicorn {
pub mod prefer_array_flat_map;
pub mod prefer_logical_operator_over_ternary;
pub mod prefer_query_selector;
pub mod prefer_string_trim_start_end;
pub mod require_number_to_fixed_digits_argument;
pub mod switch_case_braces;
pub mod text_encoding_identifier_case;
Expand Down Expand Up @@ -275,6 +276,7 @@ oxc_macros::declare_all_lint_rules! {
unicorn::switch_case_braces,
unicorn::text_encoding_identifier_case,
unicorn::throw_new_error,
unicorn::prefer_string_trim_start_end,
unicorn::prefer_query_selector,
react::jsx_key,
react::jsx_no_comment_text_nodes,
Expand Down
115 changes: 115 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/prefer_string_trim_start_end.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use oxc_ast::{ast::MemberExpression, AstKind};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::{self, Error},
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};

use crate::{context::LintContext, rule::Rule, AstNode};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `{1}` over `{2}`")]
#[diagnostic(severity(warning), help("Replace with `{1}`"))]
struct PreferStringTrimStartEndDiagnostic(#[label] pub Span, Atom, &'static str);

#[derive(Debug, Default, Clone)]
pub struct PreferStringTrimStartEnd;

declare_oxc_lint!(
/// ### What it does
///
/// [`String#trimLeft()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimLeft) and [`String#trimRight()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimRight) are aliases of [`String#trimStart()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart) and [`String#trimEnd()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd). This is to ensure consistency and use [direction](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Handling_different_text_directions)-independent wording.
///
/// ### Why is this bad?
///
/// The `trimLeft` and `trimRight` names are confusing and inconsistent with the rest of the language.
///
/// ### Example
/// ```javascript
/// // Bad
/// str.trimLeft();
/// str.trimRight();
///
/// // Good
/// str.trimStart();
/// str.trimEnd();
/// ```
PreferStringTrimStartEnd,
style
);

impl Rule for PreferStringTrimStartEnd {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};

if call_expr.optional {
return;
}

let Some(member_expr) = call_expr.callee.get_member_expr() else { return };

let (span, name) = match member_expr {
MemberExpression::StaticMemberExpression(v) => {
if !matches!(v.property.name.as_str(), "trimLeft" | "trimRight") {
return;
}
(v.property.span, &v.property.name)
}
_ => return,
};

if !call_expr.arguments.is_empty() {
return;
}

ctx.diagnostic(PreferStringTrimStartEndDiagnostic(
span,
name.clone(),
get_replacement(name.as_str()),
));
}
}

fn get_replacement(name: &str) -> &'static str {
match name {
"trimLeft" => "trimStart",
"trimRight" => "trimEnd",
_ => unreachable!(),
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
r#"foo.trimStart()"#,
r#"foo.trimStart?.()"#,
r#"foo.trimEnd()"#,
r#"new foo.trimLeft();"#,
r#"trimLeft();"#,
r#"foo['trimLeft']();"#,
r#"foo[trimLeft]();"#,
r#"foo.bar();"#,
r#"foo.trimLeft(extra);"#,
r#"foo.trimLeft(...argumentsArray)"#,
r#"foo.bar(trimLeft)"#,
r#"foo.bar(foo.trimLeft)"#,
r#"trimLeft.foo()"#,
r#"foo.trimLeft.bar()"#,
];

let fail = vec![
r#"foo.trimLeft()"#,
r#"foo.trimRight()"#,
r#"trimLeft.trimRight()"#,
r#"foo.trimLeft.trimRight()"#,
r#""foo".trimLeft()"#,
r#"foo?.trimLeft()"#,
];

Tester::new_without_config(PreferStringTrimStartEnd::NAME, pass, fail).test_and_snapshot();
}
47 changes: 47 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_string_trim_start_end.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
source: crates/oxc_linter/src/tester.rs
expression: prefer_string_trim_start_end
---
eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimLeft` over `trimStart`
╭─[prefer_string_trim_start_end.tsx:1:1]
1foo.trimLeft()
· ────────
╰────
help: Replace with `trimLeft`

eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimRight` over `trimEnd`
╭─[prefer_string_trim_start_end.tsx:1:1]
1foo.trimRight()
· ─────────
╰────
help: Replace with `trimRight`

eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimRight` over `trimEnd`
╭─[prefer_string_trim_start_end.tsx:1:1]
1trimLeft.trimRight()
· ─────────
╰────
help: Replace with `trimRight`

eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimRight` over `trimEnd`
╭─[prefer_string_trim_start_end.tsx:1:1]
1foo.trimLeft.trimRight()
· ─────────
╰────
help: Replace with `trimRight`

eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimLeft` over `trimStart`
╭─[prefer_string_trim_start_end.tsx:1:1]
1"foo".trimLeft()
· ────────
╰────
help: Replace with `trimLeft`

eslint-plugin-unicorn(prefer-string-trim-start-end): Prefer `trimLeft` over `trimStart`
╭─[prefer_string_trim_start_end.tsx:1:1]
1foo?.trimLeft()
· ────────
╰────
help: Replace with `trimLeft`


0 comments on commit f8be0b4

Please sign in to comment.