diff --git a/apps/oxlint/fixtures/tsgolint/.oxlintrc.json b/apps/oxlint/fixtures/tsgolint/.oxlintrc.json index 783dc854390c5..48b235a640579 100644 --- a/apps/oxlint/fixtures/tsgolint/.oxlintrc.json +++ b/apps/oxlint/fixtures/tsgolint/.oxlintrc.json @@ -38,6 +38,7 @@ "typescript/prefer-nullish-coalescing": "error", "typescript/prefer-optional-chain": "error", "typescript/prefer-promise-reject-errors": "error", + "typescript/prefer-readonly": "error", "typescript/prefer-readonly-parameter-types": "error", "typescript/prefer-reduce-type-parameter": "error", "typescript/prefer-return-this-type": "error", diff --git a/apps/oxlint/fixtures/tsgolint/prefer-readonly.ts b/apps/oxlint/fixtures/tsgolint/prefer-readonly.ts new file mode 100644 index 0000000000000..638aa32fb7e1e --- /dev/null +++ b/apps/oxlint/fixtures/tsgolint/prefer-readonly.ts @@ -0,0 +1,12 @@ +class Example { + private value = 1; + private readonly ok = 2; + + constructor() { + this.value = 2; + } + + mutate() { + return this.ok; + } +} diff --git a/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json b/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json index 3da354c53790a..454def1734682 100644 --- a/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json +++ b/apps/oxlint/fixtures/tsgolint_rule_options/.oxlintrc.json @@ -74,6 +74,12 @@ "treatMethodsAsReadonly": true } ], + "typescript/prefer-readonly": [ + "error", + { + "onlyInlineLambdas": true + } + ], "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 88d14bfc2745f..23fab22f3fad0 100644 --- a/apps/oxlint/fixtures/tsgolint_rule_options/test.ts +++ b/apps/oxlint/fixtures/tsgolint_rule_options/test.ts @@ -102,6 +102,14 @@ function takesMutableParameter(input: MutableParameter): void { console.log(input.value); } +// Test prefer-readonly with onlyInlineLambdas option +class PreferReadonlyOptionExample { + private handler = () => 1; + getValue() { + return this.handler(); + } +} + // 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 4cb90eb6d5e1c..72cdeec3b667d 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 63 errors. -Finished in ms on 52 files with 51 rules using 1 threads. +Found 0 warnings and 64 errors. +Finished in ms on 53 files with 52 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 417348c1475dc..e3a4105c5b18b 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 52 files with 1 rules using 1 threads. +Finished in ms on 53 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 16d74d635ad98..4859b37b72e43 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 51 rules using 1 threads. +Finished in ms on 1 file with 52 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 1c1df0a44bc38..859422e383a64 100644 --- a/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures__tsgolint_--type-aware@oxlint.snap @@ -367,6 +367,14 @@ working directory: fixtures/tsgolint 6 | input.value = input.value.trim(); `---- + x typescript-eslint(prefer-readonly): Member 'value' is never reassigned; mark it as `readonly`. + ,-[prefer-readonly.ts:2:3] + 1 | class Example { + 2 | private value = 1; + : ^^^^^^^^^^^^^ + 3 | private readonly ok = 2; + `---- + x typescript-eslint(no-unnecessary-type-assertion): This assertion is unnecessary since it does not change the type of the expression. ,-[prefer-reduce-type-parameter.ts:2:13] 1 | const numbers = [1, 2, 3]; @@ -494,8 +502,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 63 errors. -Finished in ms on 52 files with 51 rules using 1 threads. +Found 0 warnings and 64 errors. +Finished in ms on 53 files with 52 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 6ec69a94d039d..00cd165c837f1 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 @@ -80,8 +80,16 @@ working directory: fixtures/tsgolint_rule_options 102 | console.log(input.value); `---- -Found 0 warnings and 9 errors. -Finished in ms on 1 file with 11 rules using 1 threads. + x typescript-eslint(prefer-readonly): Member 'handler' is never reassigned; mark it as `readonly`. + ,-[test.ts:107:3] + 106 | class PreferReadonlyOptionExample { + 107 | private handler = () => 1; + : ^^^^^^^^^^^^^^^ + 108 | getValue() { + `---- + +Found 0 warnings and 10 errors. +Finished in ms on 1 file with 12 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 2ddc54d8a1f63..8f553c2dd165c 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1899,6 +1899,11 @@ impl RuleRunner const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; } +impl RuleRunner for crate::rules::typescript::prefer_readonly::PreferReadonly { + const NODE_TYPES: Option<&AstTypesBitset> = None; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; +} + impl RuleRunner for crate::rules::typescript::prefer_readonly_parameter_types::PreferReadonlyParameterTypes { diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index 815fa06c7d33f..5b2f9d99bad01 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -518,6 +518,7 @@ pub use crate::rules::typescript::prefer_namespace_keyword::PreferNamespaceKeywo pub use crate::rules::typescript::prefer_nullish_coalescing::PreferNullishCoalescing as TypescriptPreferNullishCoalescing; pub use crate::rules::typescript::prefer_optional_chain::PreferOptionalChain as TypescriptPreferOptionalChain; pub use crate::rules::typescript::prefer_promise_reject_errors::PreferPromiseRejectErrors as TypescriptPreferPromiseRejectErrors; +pub use crate::rules::typescript::prefer_readonly::PreferReadonly as TypescriptPreferReadonly; pub use crate::rules::typescript::prefer_readonly_parameter_types::PreferReadonlyParameterTypes as TypescriptPreferReadonlyParameterTypes; pub use crate::rules::typescript::prefer_reduce_type_parameter::PreferReduceTypeParameter as TypescriptPreferReduceTypeParameter; pub use crate::rules::typescript::prefer_return_this_type::PreferReturnThisType as TypescriptPreferReturnThisType; @@ -985,6 +986,7 @@ pub enum RuleEnum { TypescriptPreferNullishCoalescing(TypescriptPreferNullishCoalescing), TypescriptPreferOptionalChain(TypescriptPreferOptionalChain), TypescriptPreferPromiseRejectErrors(TypescriptPreferPromiseRejectErrors), + TypescriptPreferReadonly(TypescriptPreferReadonly), TypescriptPreferReadonlyParameterTypes(TypescriptPreferReadonlyParameterTypes), TypescriptPreferReduceTypeParameter(TypescriptPreferReduceTypeParameter), TypescriptPreferReturnThisType(TypescriptPreferReturnThisType), @@ -1703,8 +1705,8 @@ const TYPESCRIPT_PREFER_NULLISH_COALESCING_ID: usize = const TYPESCRIPT_PREFER_OPTIONAL_CHAIN_ID: usize = TYPESCRIPT_PREFER_NULLISH_COALESCING_ID + 1usize; const TYPESCRIPT_PREFER_PROMISE_REJECT_ERRORS_ID: usize = TYPESCRIPT_PREFER_OPTIONAL_CHAIN_ID + 1usize; -const TYPESCRIPT_PREFER_READONLY_PARAMETER_TYPES_ID: usize = - TYPESCRIPT_PREFER_PROMISE_REJECT_ERRORS_ID + 1usize; +const TYPESCRIPT_PREFER_READONLY_ID: usize = TYPESCRIPT_PREFER_PROMISE_REJECT_ERRORS_ID + 1usize; +const TYPESCRIPT_PREFER_READONLY_PARAMETER_TYPES_ID: usize = TYPESCRIPT_PREFER_READONLY_ID + 1usize; const TYPESCRIPT_PREFER_REDUCE_TYPE_PARAMETER_ID: usize = TYPESCRIPT_PREFER_READONLY_PARAMETER_TYPES_ID + 1usize; const TYPESCRIPT_PREFER_RETURN_THIS_TYPE_ID: usize = @@ -2484,6 +2486,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TYPESCRIPT_PREFER_PROMISE_REJECT_ERRORS_ID } + Self::TypescriptPreferReadonly(_) => TYPESCRIPT_PREFER_READONLY_ID, Self::TypescriptPreferReadonlyParameterTypes(_) => { TYPESCRIPT_PREFER_READONLY_PARAMETER_TYPES_ID } @@ -3268,6 +3271,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::NAME } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::NAME, Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::NAME } @@ -4058,6 +4062,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::CATEGORY } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::CATEGORY, Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::CATEGORY } @@ -4863,6 +4868,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::FIX } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::FIX, Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::FIX } @@ -5708,6 +5714,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::documentation() } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::documentation(), Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::documentation() } @@ -7074,6 +7081,8 @@ impl RuleEnum { TypescriptPreferPromiseRejectErrors::config_schema(generator) .or_else(|| TypescriptPreferPromiseRejectErrors::schema(generator)) } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::config_schema(generator) + .or_else(|| TypescriptPreferReadonly::schema(generator)), Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::config_schema(generator) .or_else(|| TypescriptPreferReadonlyParameterTypes::schema(generator)) @@ -8499,6 +8508,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(_) => "typescript", Self::TypescriptPreferOptionalChain(_) => "typescript", Self::TypescriptPreferPromiseRejectErrors(_) => "typescript", + Self::TypescriptPreferReadonly(_) => "typescript", Self::TypescriptPreferReadonlyParameterTypes(_) => "typescript", Self::TypescriptPreferReduceTypeParameter(_) => "typescript", Self::TypescriptPreferReturnThisType(_) => "typescript", @@ -9816,6 +9826,9 @@ impl RuleEnum { TypescriptPreferPromiseRejectErrors::from_configuration(value)?, )) } + Self::TypescriptPreferReadonly(_) => Ok(Self::TypescriptPreferReadonly( + TypescriptPreferReadonly::from_configuration(value)?, + )), Self::TypescriptPreferReadonlyParameterTypes(_) => { Ok(Self::TypescriptPreferReadonlyParameterTypes( TypescriptPreferReadonlyParameterTypes::from_configuration(value)?, @@ -11376,6 +11389,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.to_configuration(), Self::TypescriptPreferOptionalChain(rule) => rule.to_configuration(), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.to_configuration(), + Self::TypescriptPreferReadonly(rule) => rule.to_configuration(), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.to_configuration(), Self::TypescriptPreferReduceTypeParameter(rule) => rule.to_configuration(), Self::TypescriptPreferReturnThisType(rule) => rule.to_configuration(), @@ -12064,6 +12078,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.run(node, ctx), Self::TypescriptPreferOptionalChain(rule) => rule.run(node, ctx), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.run(node, ctx), + Self::TypescriptPreferReadonly(rule) => rule.run(node, ctx), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.run(node, ctx), Self::TypescriptPreferReduceTypeParameter(rule) => rule.run(node, ctx), Self::TypescriptPreferReturnThisType(rule) => rule.run(node, ctx), @@ -12750,6 +12765,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.run_once(ctx), Self::TypescriptPreferOptionalChain(rule) => rule.run_once(ctx), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.run_once(ctx), + Self::TypescriptPreferReadonly(rule) => rule.run_once(ctx), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.run_once(ctx), Self::TypescriptPreferReduceTypeParameter(rule) => rule.run_once(ctx), Self::TypescriptPreferReturnThisType(rule) => rule.run_once(ctx), @@ -13486,6 +13502,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(rule) => { rule.run_on_jest_node(jest_node, ctx) } + Self::TypescriptPreferReadonly(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPreferReadonlyParameterTypes(rule) => { rule.run_on_jest_node(jest_node, ctx) } @@ -14214,6 +14231,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.should_run(ctx), Self::TypescriptPreferOptionalChain(rule) => rule.should_run(ctx), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.should_run(ctx), + Self::TypescriptPreferReadonly(rule) => rule.should_run(ctx), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.should_run(ctx), Self::TypescriptPreferReduceTypeParameter(rule) => rule.should_run(ctx), Self::TypescriptPreferReturnThisType(rule) => rule.should_run(ctx), @@ -15016,6 +15034,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::IS_TSGOLINT_RULE } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::IS_TSGOLINT_RULE, Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::IS_TSGOLINT_RULE } @@ -15945,6 +15964,7 @@ impl RuleEnum { Self::TypescriptPreferPromiseRejectErrors(_) => { TypescriptPreferPromiseRejectErrors::HAS_CONFIG } + Self::TypescriptPreferReadonly(_) => TypescriptPreferReadonly::HAS_CONFIG, Self::TypescriptPreferReadonlyParameterTypes(_) => { TypescriptPreferReadonlyParameterTypes::HAS_CONFIG } @@ -16715,6 +16735,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.types_info(), Self::TypescriptPreferOptionalChain(rule) => rule.types_info(), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.types_info(), + Self::TypescriptPreferReadonly(rule) => rule.types_info(), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.types_info(), Self::TypescriptPreferReduceTypeParameter(rule) => rule.types_info(), Self::TypescriptPreferReturnThisType(rule) => rule.types_info(), @@ -17401,6 +17422,7 @@ impl RuleEnum { Self::TypescriptPreferNullishCoalescing(rule) => rule.run_info(), Self::TypescriptPreferOptionalChain(rule) => rule.run_info(), Self::TypescriptPreferPromiseRejectErrors(rule) => rule.run_info(), + Self::TypescriptPreferReadonly(rule) => rule.run_info(), Self::TypescriptPreferReadonlyParameterTypes(rule) => rule.run_info(), Self::TypescriptPreferReduceTypeParameter(rule) => rule.run_info(), Self::TypescriptPreferReturnThisType(rule) => rule.run_info(), @@ -18155,6 +18177,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( RuleEnum::TypescriptPreferPromiseRejectErrors( TypescriptPreferPromiseRejectErrors::default(), ), + RuleEnum::TypescriptPreferReadonly(TypescriptPreferReadonly::default()), RuleEnum::TypescriptPreferReadonlyParameterTypes( TypescriptPreferReadonlyParameterTypes::default(), ), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index e604bfbf941d2..e7a35ad0c9ee4 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -295,6 +295,7 @@ pub(crate) mod typescript { pub mod prefer_nullish_coalescing; pub mod prefer_optional_chain; pub mod prefer_promise_reject_errors; + pub mod prefer_readonly; pub mod prefer_readonly_parameter_types; pub mod prefer_reduce_type_parameter; pub mod prefer_return_this_type; diff --git a/crates/oxc_linter/src/rules/typescript/prefer_readonly.rs b/crates/oxc_linter/src/rules/typescript/prefer_readonly.rs new file mode 100644 index 0000000000000..006e29483f58e --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/prefer_readonly.rs @@ -0,0 +1,64 @@ +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 PreferReadonly(Box); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +pub struct PreferReadonlyConfig { + /// Restrict checks to members immediately initialized with inline lambda values. + pub only_inline_lambdas: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Require class members that are never reassigned to be marked `readonly`. + /// + /// ### Why is this bad? + /// + /// Members that never change should be declared `readonly` to make class invariants explicit + /// and prevent accidental mutation. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// class Counter { + /// private value = 0; + /// + /// getValue() { + /// return this.value; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// class Counter { + /// private readonly value = 0; + /// + /// getValue() { + /// return this.value; + /// } + /// } + /// ``` + PreferReadonly(tsgolint), + typescript, + nursery, + config = PreferReadonlyConfig, +); + +impl Rule for PreferReadonly { + 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)) + } +}