diff --git a/apps/oxlint/fixtures/tsgolint/.oxlintrc.json b/apps/oxlint/fixtures/tsgolint/.oxlintrc.json index 680a7f20c0f34..d49c40e940f61 100644 --- a/apps/oxlint/fixtures/tsgolint/.oxlintrc.json +++ b/apps/oxlint/fixtures/tsgolint/.oxlintrc.json @@ -43,6 +43,7 @@ "typescript/prefer-regexp-exec": "error", "typescript/prefer-reduce-type-parameter": "error", "typescript/prefer-return-this-type": "error", + "typescript/prefer-string-starts-ends-with": "error", "typescript/promise-function-async": "error", "typescript/related-getter-setter-pairs": "error", "typescript/require-array-sort-compare": "error", diff --git a/apps/oxlint/fixtures/tsgolint/prefer-string-starts-ends-with.ts b/apps/oxlint/fixtures/tsgolint/prefer-string-starts-ends-with.ts new file mode 100644 index 0000000000000..cbc713d367925 --- /dev/null +++ b/apps/oxlint/fixtures/tsgolint/prefer-string-starts-ends-with.ts @@ -0,0 +1,9 @@ +// Examples of incorrect code for prefer-string-starts-ends-with rule + +function startsCase(s: string): boolean { + return s[0] === 'a'; +} + +function endsCase(s: string): boolean { + return s.slice(-3) === 'bar'; +} diff --git a/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json b/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json index 454def1734682..a94920d2d26c9 100644 --- a/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json +++ b/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json @@ -80,6 +80,12 @@ "onlyInlineLambdas": true } ], + "typescript/prefer-string-starts-ends-with": [ + "error", + { + "allowSingleElementEquality": "always" + } + ], "typescript/only-throw-error": [ "error", { diff --git a/apps/oxlint/fixtures/tsgolint_rule_options/test.ts b/apps/oxlint/fixtures/tsgolint_rule_options/test.ts index 23fab22f3fad0..85efba542b55f 100644 --- a/apps/oxlint/fixtures/tsgolint_rule_options/test.ts +++ b/apps/oxlint/fixtures/tsgolint_rule_options/test.ts @@ -110,6 +110,13 @@ class PreferReadonlyOptionExample { } } +// Test prefer-string-starts-ends-with with allowSingleElementEquality option +declare const boundaryText: string; +// This should NOT error because single element equality is allowed +const boundaryCharMatch = boundaryText[0] === 'a'; +// This SHOULD error because startsWith is preferred here +const boundarySliceMatch = boundaryText.slice(0, 3) === 'foo'; + // Test only-throw-error with allowRethrowing option // When allowRethrowing is false, rethrowing a caught error SHOULD error try { diff --git a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware --silent@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware --silent@oxlint.snap index 1d03462f89117..1e859c6771277 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware --silent@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware --silent@oxlint.snap @@ -6,8 +6,8 @@ arguments: --type-aware --silent working directory: fixtures/tsgolint ---------- -Found 0 warnings and 67 errors. -Finished in ms on 54 files with 53 rules using 1 threads. +Found 0 warnings and 69 errors. +Finished in ms on 55 files with 54 rules using 1 threads. ---------- CLI result: LintFoundErrors ---------- diff --git a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware -c config-test.json@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware -c config-test.json@oxlint.snap index e24f72d57c4ce..959dca36be452 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware -c config-test.json@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware -c config-test.json@oxlint.snap @@ -40,7 +40,7 @@ working directory: fixtures/tsgolint help: Remove the debugger statement Found 2 warnings and 2 errors. -Finished in ms on 54 files with 1 rules using 1 threads. +Finished in ms on 55 files with 1 rules using 1 threads. ---------- CLI result: LintFoundErrors ---------- diff --git a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware test.svelte@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware test.svelte@oxlint.snap index 3974e388f27c5..fdd27170eb7f7 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware test.svelte@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware test.svelte@oxlint.snap @@ -16,7 +16,7 @@ working directory: fixtures/tsgolint help: Remove the debugger statement Found 0 warnings and 1 error. -Finished in ms on 1 file with 53 rules using 1 threads. +Finished in ms on 1 file with 54 rules using 1 threads. ---------- CLI result: LintFoundErrors ---------- diff --git a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap index 250f930e0d471..42b854bab461b 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap @@ -415,6 +415,22 @@ working directory: fixtures/tsgolint 4 | this.value = value; `---- + x typescript-eslint(prefer-string-starts-ends-with): Use 'String#startsWith' method instead. + ,-[prefer-string-starts-ends-with.ts:4:10] + 3 | function startsCase(s: string): boolean { + 4 | return s[0] === 'a'; + : ^^^^^^^^^^^^ + 5 | } + `---- + + x typescript-eslint(prefer-string-starts-ends-with): Use 'String#endsWith' method instead. + ,-[prefer-string-starts-ends-with.ts:8:10] + 7 | function endsCase(s: string): boolean { + 8 | return s.slice(-3) === 'bar'; + : ^^^^^^^^^^^^^^^^^^^^^ + 9 | } + `---- + x typescript-eslint(promise-function-async): Functions that return promises must be async. ,-[promise-function-async.ts:2:1] 1 | declare function fetch(url: string): Promise; @@ -526,8 +542,8 @@ working directory: fixtures/tsgolint `---- help: If your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead. -Found 0 warnings and 67 errors. -Finished in ms on 54 files with 53 rules using 1 threads. +Found 0 warnings and 69 errors. +Finished in ms on 55 files with 54 rules using 1 threads. ---------- CLI result: LintFoundErrors ---------- diff --git a/apps/oxlint/src/snapshots/fixtures__tsgolint_rule_options_--type-aware@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__tsgolint_rule_options_--type-aware@oxlint.snap index 00cd165c837f1..2b144ef77c239 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_rule_options_--type-aware@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_rule_options_--type-aware@oxlint.snap @@ -88,8 +88,16 @@ working directory: fixtures/tsgolint_rule_options 108 | getValue() { `---- -Found 0 warnings and 10 errors. -Finished in ms on 1 file with 12 rules using 1 threads. + x typescript-eslint(prefer-string-starts-ends-with): Use 'String#startsWith' method instead. + ,-[test.ts:118:28] + 117 | // This SHOULD error because startsWith is preferred here + 118 | const boundarySliceMatch = boundaryText.slice(0, 3) === 'foo'; + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 119 | + `---- + +Found 0 warnings and 11 errors. +Finished in ms on 1 file with 13 rules using 1 threads. ---------- CLI result: LintFoundErrors ---------- diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 3da030b3a115b..9cc990cdb18f8 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1928,6 +1928,13 @@ impl RuleRunner for crate::rules::typescript::prefer_return_this_type::PreferRet const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; } +impl RuleRunner + for crate::rules::typescript::prefer_string_starts_ends_with::PreferStringStartsEndsWith +{ + const NODE_TYPES: Option<&AstTypesBitset> = None; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; +} + impl RuleRunner for crate::rules::typescript::prefer_ts_expect_error::PreferTsExpectError { const NODE_TYPES: Option<&AstTypesBitset> = None; const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce; diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index 28b546f7ae328..6587328e17183 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -523,6 +523,7 @@ pub use crate::rules::typescript::prefer_readonly_parameter_types::PreferReadonl pub use crate::rules::typescript::prefer_reduce_type_parameter::PreferReduceTypeParameter as TypescriptPreferReduceTypeParameter; pub use crate::rules::typescript::prefer_regexp_exec::PreferRegexpExec as TypescriptPreferRegexpExec; pub use crate::rules::typescript::prefer_return_this_type::PreferReturnThisType as TypescriptPreferReturnThisType; +pub use crate::rules::typescript::prefer_string_starts_ends_with::PreferStringStartsEndsWith as TypescriptPreferStringStartsEndsWith; pub use crate::rules::typescript::prefer_ts_expect_error::PreferTsExpectError as TypescriptPreferTsExpectError; pub use crate::rules::typescript::promise_function_async::PromiseFunctionAsync as TypescriptPromiseFunctionAsync; pub use crate::rules::typescript::related_getter_setter_pairs::RelatedGetterSetterPairs as TypescriptRelatedGetterSetterPairs; @@ -992,6 +993,7 @@ pub enum RuleEnum { TypescriptPreferReduceTypeParameter(TypescriptPreferReduceTypeParameter), TypescriptPreferRegexpExec(TypescriptPreferRegexpExec), TypescriptPreferReturnThisType(TypescriptPreferReturnThisType), + TypescriptPreferStringStartsEndsWith(TypescriptPreferStringStartsEndsWith), TypescriptPreferTsExpectError(TypescriptPreferTsExpectError), TypescriptPromiseFunctionAsync(TypescriptPromiseFunctionAsync), TypescriptRelatedGetterSetterPairs(TypescriptRelatedGetterSetterPairs), @@ -1713,7 +1715,10 @@ const TYPESCRIPT_PREFER_REDUCE_TYPE_PARAMETER_ID: usize = TYPESCRIPT_PREFER_READONLY_PARAMETER_TYPES_ID + 1usize; const TYPESCRIPT_PREFER_REGEXP_EXEC_ID: usize = TYPESCRIPT_PREFER_REDUCE_TYPE_PARAMETER_ID + 1usize; const TYPESCRIPT_PREFER_RETURN_THIS_TYPE_ID: usize = TYPESCRIPT_PREFER_REGEXP_EXEC_ID + 1usize; -const TYPESCRIPT_PREFER_TS_EXPECT_ERROR_ID: usize = TYPESCRIPT_PREFER_RETURN_THIS_TYPE_ID + 1usize; +const TYPESCRIPT_PREFER_STRING_STARTS_ENDS_WITH_ID: usize = + TYPESCRIPT_PREFER_RETURN_THIS_TYPE_ID + 1usize; +const TYPESCRIPT_PREFER_TS_EXPECT_ERROR_ID: usize = + TYPESCRIPT_PREFER_STRING_STARTS_ENDS_WITH_ID + 1usize; const TYPESCRIPT_PROMISE_FUNCTION_ASYNC_ID: usize = TYPESCRIPT_PREFER_TS_EXPECT_ERROR_ID + 1usize; const TYPESCRIPT_RELATED_GETTER_SETTER_PAIRS_ID: usize = TYPESCRIPT_PROMISE_FUNCTION_ASYNC_ID + 1usize; @@ -2497,6 +2502,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(_) => TYPESCRIPT_PREFER_REGEXP_EXEC_ID, Self::TypescriptPreferReturnThisType(_) => TYPESCRIPT_PREFER_RETURN_THIS_TYPE_ID, + Self::TypescriptPreferStringStartsEndsWith(_) => { + TYPESCRIPT_PREFER_STRING_STARTS_ENDS_WITH_ID + } Self::TypescriptPreferTsExpectError(_) => TYPESCRIPT_PREFER_TS_EXPECT_ERROR_ID, Self::TypescriptPromiseFunctionAsync(_) => TYPESCRIPT_PROMISE_FUNCTION_ASYNC_ID, Self::TypescriptRelatedGetterSetterPairs(_) => { @@ -3283,6 +3291,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(_) => TypescriptPreferRegexpExec::NAME, Self::TypescriptPreferReturnThisType(_) => TypescriptPreferReturnThisType::NAME, + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::NAME + } Self::TypescriptPreferTsExpectError(_) => TypescriptPreferTsExpectError::NAME, Self::TypescriptPromiseFunctionAsync(_) => TypescriptPromiseFunctionAsync::NAME, Self::TypescriptRelatedGetterSetterPairs(_) => TypescriptRelatedGetterSetterPairs::NAME, @@ -4075,6 +4086,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(_) => TypescriptPreferRegexpExec::CATEGORY, Self::TypescriptPreferReturnThisType(_) => TypescriptPreferReturnThisType::CATEGORY, + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::CATEGORY + } Self::TypescriptPreferTsExpectError(_) => TypescriptPreferTsExpectError::CATEGORY, Self::TypescriptPromiseFunctionAsync(_) => TypescriptPromiseFunctionAsync::CATEGORY, Self::TypescriptRelatedGetterSetterPairs(_) => { @@ -4882,6 +4896,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(_) => TypescriptPreferRegexpExec::FIX, Self::TypescriptPreferReturnThisType(_) => TypescriptPreferReturnThisType::FIX, + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::FIX + } Self::TypescriptPreferTsExpectError(_) => TypescriptPreferTsExpectError::FIX, Self::TypescriptPromiseFunctionAsync(_) => TypescriptPromiseFunctionAsync::FIX, Self::TypescriptRelatedGetterSetterPairs(_) => TypescriptRelatedGetterSetterPairs::FIX, @@ -5731,6 +5748,9 @@ impl RuleEnum { Self::TypescriptPreferReturnThisType(_) => { TypescriptPreferReturnThisType::documentation() } + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::documentation() + } Self::TypescriptPreferTsExpectError(_) => { TypescriptPreferTsExpectError::documentation() } @@ -7106,6 +7126,10 @@ impl RuleEnum { TypescriptPreferReturnThisType::config_schema(generator) .or_else(|| TypescriptPreferReturnThisType::schema(generator)) } + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::config_schema(generator) + .or_else(|| TypescriptPreferStringStartsEndsWith::schema(generator)) + } Self::TypescriptPreferTsExpectError(_) => { TypescriptPreferTsExpectError::config_schema(generator) .or_else(|| TypescriptPreferTsExpectError::schema(generator)) @@ -8524,6 +8548,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(_) => "typescript", Self::TypescriptPreferRegexpExec(_) => "typescript", Self::TypescriptPreferReturnThisType(_) => "typescript", + Self::TypescriptPreferStringStartsEndsWith(_) => "typescript", Self::TypescriptPreferTsExpectError(_) => "typescript", Self::TypescriptPromiseFunctionAsync(_) => "typescript", Self::TypescriptRelatedGetterSetterPairs(_) => "typescript", @@ -9857,6 +9882,11 @@ impl RuleEnum { Self::TypescriptPreferReturnThisType(_) => Ok(Self::TypescriptPreferReturnThisType( TypescriptPreferReturnThisType::from_configuration(value)?, )), + Self::TypescriptPreferStringStartsEndsWith(_) => { + Ok(Self::TypescriptPreferStringStartsEndsWith( + TypescriptPreferStringStartsEndsWith::from_configuration(value)?, + )) + } Self::TypescriptPreferTsExpectError(_) => Ok(Self::TypescriptPreferTsExpectError( TypescriptPreferTsExpectError::from_configuration(value)?, )), @@ -11409,6 +11439,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.to_configuration(), Self::TypescriptPreferRegexpExec(rule) => rule.to_configuration(), Self::TypescriptPreferReturnThisType(rule) => rule.to_configuration(), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.to_configuration(), Self::TypescriptPreferTsExpectError(rule) => rule.to_configuration(), Self::TypescriptPromiseFunctionAsync(rule) => rule.to_configuration(), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.to_configuration(), @@ -12099,6 +12130,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.run(node, ctx), Self::TypescriptPreferRegexpExec(rule) => rule.run(node, ctx), Self::TypescriptPreferReturnThisType(rule) => rule.run(node, ctx), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.run(node, ctx), Self::TypescriptPreferTsExpectError(rule) => rule.run(node, ctx), Self::TypescriptPromiseFunctionAsync(rule) => rule.run(node, ctx), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.run(node, ctx), @@ -12787,6 +12819,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.run_once(ctx), Self::TypescriptPreferRegexpExec(rule) => rule.run_once(ctx), Self::TypescriptPreferReturnThisType(rule) => rule.run_once(ctx), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.run_once(ctx), Self::TypescriptPreferTsExpectError(rule) => rule.run_once(ctx), Self::TypescriptPromiseFunctionAsync(rule) => rule.run_once(ctx), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.run_once(ctx), @@ -13529,6 +13562,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPreferReturnThisType(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::TypescriptPreferStringStartsEndsWith(rule) => { + rule.run_on_jest_node(jest_node, ctx) + } Self::TypescriptPreferTsExpectError(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPromiseFunctionAsync(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.run_on_jest_node(jest_node, ctx), @@ -14255,6 +14291,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.should_run(ctx), Self::TypescriptPreferRegexpExec(rule) => rule.should_run(ctx), Self::TypescriptPreferReturnThisType(rule) => rule.should_run(ctx), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.should_run(ctx), Self::TypescriptPreferTsExpectError(rule) => rule.should_run(ctx), Self::TypescriptPromiseFunctionAsync(rule) => rule.should_run(ctx), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.should_run(ctx), @@ -15065,6 +15102,9 @@ impl RuleEnum { Self::TypescriptPreferReturnThisType(_) => { TypescriptPreferReturnThisType::IS_TSGOLINT_RULE } + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::IS_TSGOLINT_RULE + } Self::TypescriptPreferTsExpectError(_) => { TypescriptPreferTsExpectError::IS_TSGOLINT_RULE } @@ -15994,6 +16034,9 @@ impl RuleEnum { } Self::TypescriptPreferRegexpExec(_) => TypescriptPreferRegexpExec::HAS_CONFIG, Self::TypescriptPreferReturnThisType(_) => TypescriptPreferReturnThisType::HAS_CONFIG, + Self::TypescriptPreferStringStartsEndsWith(_) => { + TypescriptPreferStringStartsEndsWith::HAS_CONFIG + } Self::TypescriptPreferTsExpectError(_) => TypescriptPreferTsExpectError::HAS_CONFIG, Self::TypescriptPromiseFunctionAsync(_) => TypescriptPromiseFunctionAsync::HAS_CONFIG, Self::TypescriptRelatedGetterSetterPairs(_) => { @@ -16762,6 +16805,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.types_info(), Self::TypescriptPreferRegexpExec(rule) => rule.types_info(), Self::TypescriptPreferReturnThisType(rule) => rule.types_info(), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.types_info(), Self::TypescriptPreferTsExpectError(rule) => rule.types_info(), Self::TypescriptPromiseFunctionAsync(rule) => rule.types_info(), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.types_info(), @@ -17450,6 +17494,7 @@ impl RuleEnum { Self::TypescriptPreferReduceTypeParameter(rule) => rule.run_info(), Self::TypescriptPreferRegexpExec(rule) => rule.run_info(), Self::TypescriptPreferReturnThisType(rule) => rule.run_info(), + Self::TypescriptPreferStringStartsEndsWith(rule) => rule.run_info(), Self::TypescriptPreferTsExpectError(rule) => rule.run_info(), Self::TypescriptPromiseFunctionAsync(rule) => rule.run_info(), Self::TypescriptRelatedGetterSetterPairs(rule) => rule.run_info(), @@ -18210,6 +18255,9 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( ), RuleEnum::TypescriptPreferRegexpExec(TypescriptPreferRegexpExec::default()), RuleEnum::TypescriptPreferReturnThisType(TypescriptPreferReturnThisType::default()), + RuleEnum::TypescriptPreferStringStartsEndsWith( + TypescriptPreferStringStartsEndsWith::default(), + ), RuleEnum::TypescriptPreferTsExpectError(TypescriptPreferTsExpectError::default()), RuleEnum::TypescriptPromiseFunctionAsync(TypescriptPromiseFunctionAsync::default()), RuleEnum::TypescriptRelatedGetterSetterPairs(TypescriptRelatedGetterSetterPairs::default()), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c14c257a533ac..3c9417e1f13a6 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -300,6 +300,7 @@ pub(crate) mod typescript { pub mod prefer_reduce_type_parameter; pub mod prefer_regexp_exec; pub mod prefer_return_this_type; + pub mod prefer_string_starts_ends_with; pub mod prefer_ts_expect_error; pub mod promise_function_async; pub mod related_getter_setter_pairs; diff --git a/crates/oxc_linter/src/rules/typescript/prefer_string_starts_ends_with.rs b/crates/oxc_linter/src/rules/typescript/prefer_string_starts_ends_with.rs new file mode 100644 index 0000000000000..99d2c402aca69 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/prefer_string_starts_ends_with.rs @@ -0,0 +1,61 @@ +use oxc_macros::declare_oxc_lint; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::rule::{DefaultRuleConfig, Rule}; + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct PreferStringStartsEndsWith(Box); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +pub enum AllowSingleElementEquality { + Always, + Never, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +pub struct PreferStringStartsEndsWithConfig { + /// Whether equality checks against the first/last character are allowed. + pub allow_single_element_equality: Option, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefer `startsWith` and `endsWith` over manual string boundary checks. + /// + /// ### Why is this bad? + /// + /// Boundary checks written with `slice`, `indexOf`, regex anchors, or manual indexing are + /// harder to read and maintain than `startsWith`/`endsWith`. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// value.slice(0, 3) === 'foo'; + /// value.slice(-3) === 'bar'; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// value.startsWith('foo'); + /// value.endsWith('bar'); + /// ``` + PreferStringStartsEndsWith(tsgolint), + typescript, + nursery, + config = PreferStringStartsEndsWithConfig, +); + +impl Rule for PreferStringStartsEndsWith { + fn from_configuration(value: serde_json::Value) -> Result { + serde_json::from_value::>(value).map(DefaultRuleConfig::into_inner) + } + + fn to_configuration(&self) -> Option> { + Some(serde_json::to_value(&*self.0)) + } +}