diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index 5edd251a949..24a4862b146 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -46,6 +46,7 @@ define_dategories! { "lint/nursery/noAssignInExpressions": "https://docs.rome.tools/lint/rules/noAssignInExpressions", "lint/nursery/noWith": "https://docs.rome.tools/lint/rules/noWith", "lint/nursery/noBannedTypes":"https://docs.rome.tools/lint/rules/noBannedTypes", + "lint/nursery/noClassAssign": "https://docs.rome.tools/lint/rules/noClassAssign", "lint/nursery/noCommaOperator": "https://docs.rome.tools/lint/rules/noCommaOperator", "lint/nursery/noConstEnum": "https://docs.rome.tools/lint/rules/noConstEnum", "lint/nursery/noConstructorReturn": "https://docs.rome.tools/lint/rules/noConstructorReturn", diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs index 0af250e65c8..853f29d4933 100644 --- a/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery.rs @@ -1,10 +1,11 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use rome_analyze::declare_group; +mod no_class_assign; mod no_restricted_globals; mod no_var; mod use_camel_case; mod use_const; mod use_exhaustive_dependencies; mod use_hook_at_top_level; -declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_var :: NoVar , self :: use_camel_case :: UseCamelCase , self :: use_const :: UseConst , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel ,] } } +declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_class_assign :: NoClassAssign , self :: no_restricted_globals :: NoRestrictedGlobals , self :: no_var :: NoVar , self :: use_camel_case :: UseCamelCase , self :: use_const :: UseConst , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel ,] } } diff --git a/crates/rome_js_analyze/src/semantic_analyzers/nursery/no_class_assign.rs b/crates/rome_js_analyze/src/semantic_analyzers/nursery/no_class_assign.rs new file mode 100644 index 00000000000..1719b1951bd --- /dev/null +++ b/crates/rome_js_analyze/src/semantic_analyzers/nursery/no_class_assign.rs @@ -0,0 +1,116 @@ +use rome_analyze::context::RuleContext; +use rome_analyze::{declare_rule, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_js_semantic::{Reference, ReferencesExtensions}; +use rome_js_syntax::AnyJsClass; + +use crate::semantic_services::Semantic; + +declare_rule! { + /// Disallow reassigning class members. + /// + /// A class declaration creates a variable that we can modify, however, the modification is a mistake in most cases. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// class A {} + /// A = 0; + /// ``` + /// + /// ```js,expect_diagnostic + /// A = 0; + /// class A {} + /// ``` + /// + /// ```js,expect_diagnostic + /// class A { + /// b() { + /// A = 0; + /// } + /// } + /// ``` + /// + /// ```js,expect_diagnostic + /// let A = class A { + /// b() { + /// A = 0; + /// // `let A` is shadowed by the class name. + /// } + /// } + /// ``` + /// + /// ### Valid + /// + /// ```js + /// let A = class A {} + /// A = 0; // A is a variable. + /// ``` + /// + /// ```js + /// let A = class { + /// b() { + /// A = 0; // A is a variable. + /// } + /// } + /// ``` + /// + /// ```js + /// class A { + /// b(A) { + /// A = 0; // A is a parameter. + /// } + /// } + /// ``` + /// + pub(crate) NoClassAssign { + version: "12.0.0", + name: "noClassAssign", + recommended: true, + } +} + +impl Rule for NoClassAssign { + type Query = Semantic; + type State = Reference; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let model = ctx.model(); + + if let Ok(Some(id)) = node.id() { + if let Some(id_binding) = id.as_js_identifier_binding() { + return id_binding.all_writes(model).collect(); + } + } + + Vec::new() + } + + fn diagnostic(ctx: &RuleContext, reference: &Self::State) -> Option { + let binding = ctx + .query() + .id() + .ok()?? + .as_js_identifier_binding()? + .name_token() + .ok()?; + let class_name = binding.text_trimmed(); + + Some( + RuleDiagnostic::new( + rule_category!(), + reference.syntax().text_trimmed_range(), + markup! {"'"{class_name}"' is a class."}, + ) + .detail( + binding.text_trimmed_range(), + markup! {"'"{class_name}"' is defined here."}, + ), + ) + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js b/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js new file mode 100644 index 00000000000..410ef0e3135 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js @@ -0,0 +1,110 @@ +/* Valid */ +function case1() { + class A { } + foo(A); +} + +function case2() { + let A = class A { }; + foo(A); +} + +function case3() { + class A { + b(A) { + A = 0; + } + } +} + +function case4() { + class A { + b() { + let A; + A = 0; + } + } +} + +function case5() { + let A = class { + b() { + A = 0; + } + } +} + +// /* Ignores non class. */ +function case6() { + var x = 0; + x = 1; +} + +function case7() { + let x = 0; + x = 1; +} + +function case8() { + const x = 0; + x = 1; +} + +function case9() { + function x() {} + x = 1; +} + +function case10(x) { + x = 1; +} + +function case11() { + try {} + catch (x) { + x = 1; + } +} + +// /* Invalid */ +function case12() { + class A { } + A = 0; +} + +function case13() { + class B { } + ({B} = 0); +} + +function case14() { + class C { } + ({b: C = 0} = {}); +} + +function case15() { + D = 0; + class D { } +} + +function case16() { + class E { + b() { + E = 0; + } + } +} + +function case17() { + let F = class F { + b() { + F = 0; + } + } +} + +function case18() { + class G { } + G = 0; + G = 1; +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js.snap b/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js.snap new file mode 100644 index 00000000000..732003cf06b --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/noClassAssign.js.snap @@ -0,0 +1,307 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +assertion_line: 92 +expression: noClassAssign.js +--- +# Input +```js +/* Valid */ +function case1() { + class A { } + foo(A); +} + +function case2() { + let A = class A { }; + foo(A); +} + +function case3() { + class A { + b(A) { + A = 0; + } + } +} + +function case4() { + class A { + b() { + let A; + A = 0; + } + } +} + +function case5() { + let A = class { + b() { + A = 0; + } + } +} + +// /* Ignores non class. */ +function case6() { + var x = 0; + x = 1; +} + +function case7() { + let x = 0; + x = 1; +} + +function case8() { + const x = 0; + x = 1; +} + +function case9() { + function x() {} + x = 1; +} + +function case10(x) { + x = 1; +} + +function case11() { + try {} + catch (x) { + x = 1; + } +} + +// /* Invalid */ +function case12() { + class A { } + A = 0; +} + +function case13() { + class B { } + ({B} = 0); +} + +function case14() { + class C { } + ({b: C = 0} = {}); +} + +function case15() { + D = 0; + class D { } +} + +function case16() { + class E { + b() { + E = 0; + } + } +} + +function case17() { + let F = class F { + b() { + F = 0; + } + } +} + +function case18() { + class G { } + G = 0; + G = 1; +} + +``` + +# Diagnostics +``` +noClassAssign.js:72:2 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'A' is a class. + + 70 │ function case12() { + 71 │ class A { } + > 72 │ A = 0; + │ ^ + 73 │ } + 74 │ + + i 'A' is defined here. + + 69 │ // /* Invalid */ + 70 │ function case12() { + > 71 │ class A { } + │ ^ + 72 │ A = 0; + 73 │ } + + +``` + +``` +noClassAssign.js:77:4 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'B' is a class. + + 75 │ function case13() { + 76 │ class B { } + > 77 │ ({B} = 0); + │ ^ + 78 │ } + 79 │ + + i 'B' is defined here. + + 75 │ function case13() { + > 76 │ class B { } + │ ^ + 77 │ ({B} = 0); + 78 │ } + + +``` + +``` +noClassAssign.js:82:7 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'C' is a class. + + 80 │ function case14() { + 81 │ class C { } + > 82 │ ({b: C = 0} = {}); + │ ^ + 83 │ } + 84 │ + + i 'C' is defined here. + + 80 │ function case14() { + > 81 │ class C { } + │ ^ + 82 │ ({b: C = 0} = {}); + 83 │ } + + +``` + +``` +noClassAssign.js:86:2 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'D' is a class. + + 85 │ function case15() { + > 86 │ D = 0; + │ ^ + 87 │ class D { } + 88 │ } + + i 'D' is defined here. + + 85 │ function case15() { + 86 │ D = 0; + > 87 │ class D { } + │ ^ + 88 │ } + 89 │ + + +``` + +``` +noClassAssign.js:93:4 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'E' is a class. + + 91 │ class E { + 92 │ b() { + > 93 │ E = 0; + │ ^ + 94 │ } + 95 │ } + + i 'E' is defined here. + + 90 │ function case16() { + > 91 │ class E { + │ ^ + 92 │ b() { + 93 │ E = 0; + + +``` + +``` +noClassAssign.js:101:4 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'F' is a class. + + 99 │ let F = class F { + 100 │ b() { + > 101 │ F = 0; + │ ^ + 102 │ } + 103 │ } + + i 'F' is defined here. + + 98 │ function case17() { + > 99 │ let F = class F { + │ ^ + 100 │ b() { + 101 │ F = 0; + + +``` + +``` +noClassAssign.js:108:2 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'G' is a class. + + 106 │ function case18() { + 107 │ class G { } + > 108 │ G = 0; + │ ^ + 109 │ G = 1; + 110 │ } + + i 'G' is defined here. + + 106 │ function case18() { + > 107 │ class G { } + │ ^ + 108 │ G = 0; + 109 │ G = 1; + + +``` + +``` +noClassAssign.js:109:2 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! 'G' is a class. + + 107 │ class G { } + 108 │ G = 0; + > 109 │ G = 1; + │ ^ + 110 │ } + 111 │ + + i 'G' is defined here. + + 106 │ function case18() { + > 107 │ class G { } + │ ^ + 108 │ G = 0; + 109 │ G = 1; + + +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 852f812f328..7ce52e1a462 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -729,6 +729,8 @@ struct NurserySchema { no_assign_in_expressions: Option, #[doc = "Disallow certain types."] no_banned_types: Option, + #[doc = "Disallow reassigning class members."] + no_class_assign: Option, #[doc = "Disallow comma operator."] no_comma_operator: Option, #[doc = "Disallow TypeScript const enum"] @@ -798,10 +800,11 @@ struct NurserySchema { } impl Nursery { const CATEGORY_NAME: &'static str = "nursery"; - pub(crate) const CATEGORY_RULES: [&'static str; 36] = [ + pub(crate) const CATEGORY_RULES: [&'static str; 37] = [ "noAccessKey", "noAssignInExpressions", "noBannedTypes", + "noClassAssign", "noCommaOperator", "noConstEnum", "noConstructorReturn", @@ -836,9 +839,10 @@ impl Nursery { "useHookAtTopLevel", "useNumericLiterals", ]; - const RECOMMENDED_RULES: [&'static str; 27] = [ + const RECOMMENDED_RULES: [&'static str; 28] = [ "noAssignInExpressions", "noBannedTypes", + "noClassAssign", "noCommaOperator", "noConstEnum", "noConstructorReturn", @@ -865,7 +869,7 @@ impl Nursery { "useExhaustiveDependencies", "useNumericLiterals", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 27] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 28] = [ RuleFilter::Rule("nursery", Self::CATEGORY_RULES[1]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[2]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[3]), @@ -877,8 +881,8 @@ impl Nursery { RuleFilter::Rule("nursery", Self::CATEGORY_RULES[9]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[10]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[11]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[14]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[17]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[12]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[15]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[18]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[19]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[20]), @@ -886,13 +890,14 @@ impl Nursery { RuleFilter::Rule("nursery", Self::CATEGORY_RULES[22]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[23]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[24]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[26]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[28]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[25]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[27]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[29]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[30]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[31]), RuleFilter::Rule("nursery", Self::CATEGORY_RULES[32]), - RuleFilter::Rule("nursery", Self::CATEGORY_RULES[35]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[33]), + RuleFilter::Rule("nursery", Self::CATEGORY_RULES[36]), ]; pub(crate) fn is_recommended(&self) -> bool { !matches!(self.recommended, Some(false)) } pub(crate) fn get_enabled_rules(&self) -> IndexSet { @@ -919,7 +924,7 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 27] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 28] { Self::RECOMMENDED_RULES_AS_FILTERS } } diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 5afcf83f6e5..56c699ae22b 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -610,6 +610,17 @@ } ] }, + "noClassAssign": { + "description": "Disallow reassigning class members.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, "noCommaOperator": { "description": "Disallow comma operator.", "anyOf": [ diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index f7bf2a1acbb..5463b2a33e0 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -297,6 +297,10 @@ export interface Nursery { * Disallow certain types. */ noBannedTypes?: RuleConfiguration; + /** + * Disallow reassigning class members. + */ + noClassAssign?: RuleConfiguration; /** * Disallow comma operator. */ @@ -706,6 +710,7 @@ export type Category = | "lint/nursery/noAssignInExpressions" | "lint/nursery/noWith" | "lint/nursery/noBannedTypes" + | "lint/nursery/noClassAssign" | "lint/nursery/noCommaOperator" | "lint/nursery/noConstEnum" | "lint/nursery/noConstructorReturn" diff --git a/npm/rome/configuration_schema.json b/npm/rome/configuration_schema.json index 5afcf83f6e5..56c699ae22b 100644 --- a/npm/rome/configuration_schema.json +++ b/npm/rome/configuration_schema.json @@ -610,6 +610,17 @@ } ] }, + "noClassAssign": { + "description": "Disallow reassigning class members.", + "anyOf": [ + { + "$ref": "#/definitions/RuleConfiguration" + }, + { + "type": "null" + } + ] + }, "noCommaOperator": { "description": "Disallow comma operator.", "anyOf": [ diff --git a/website/src/pages/lint/rules/index.mdx b/website/src/pages/lint/rules/index.mdx index b91d9919f18..0ebdb9c92d3 100644 --- a/website/src/pages/lint/rules/index.mdx +++ b/website/src/pages/lint/rules/index.mdx @@ -492,6 +492,12 @@ Disallow assignments in expressions. Disallow certain types.
+

+ noClassAssign +

+Disallow reassigning class members. +
+

noCommaOperator

diff --git a/website/src/pages/lint/rules/noClassAssign.md b/website/src/pages/lint/rules/noClassAssign.md new file mode 100644 index 00000000000..72092990167 --- /dev/null +++ b/website/src/pages/lint/rules/noClassAssign.md @@ -0,0 +1,141 @@ +--- +title: Lint Rule noClassAssign +parent: lint/rules/index +--- + +# noClassAssign (since v12.0.0) + +Disallow reassigning class members. + +A class declaration creates a variable that we can modify, however, the modification is a mistake in most cases. + +## Examples + +### Invalid + +```jsx +class A {} +A = 0; +``` + +
nursery/noClassAssign.js:2:1 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   'A' is a class.
+  
+    1 │ class A {}
+  > 2 │ A = 0;
+   ^
+    3 │ 
+  
+   'A' is defined here.
+  
+  > 1 │ class A {}
+         ^
+    2 │ A = 0;
+    3 │ 
+  
+
+ +```jsx +A = 0; +class A {} +``` + +
nursery/noClassAssign.js:1:1 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   'A' is a class.
+  
+  > 1 │ A = 0;
+   ^
+    2 │ class A {}
+    3 │ 
+  
+   'A' is defined here.
+  
+    1 │ A = 0;
+  > 2 │ class A {}
+         ^
+    3 │ 
+  
+
+ +```jsx +class A { + b() { + A = 0; + } +} +``` + +
nursery/noClassAssign.js:3:3 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   'A' is a class.
+  
+    1 │ class A {
+    2 │ 	b() {
+  > 3 │ 		A = 0;
+   		^
+    4 │ 	}
+    5 │ }
+  
+   'A' is defined here.
+  
+  > 1 │ class A {
+         ^
+    2 │ 	b() {
+    3 │ 		A = 0;
+  
+
+ +```jsx +let A = class A { + b() { + A = 0; + // `let A` is shadowed by the class name. + } +} +``` + +
nursery/noClassAssign.js:3:3 lint/nursery/noClassAssign ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   'A' is a class.
+  
+    1 │ let A = class A {
+    2 │ 	b() {
+  > 3 │ 		A = 0;
+   		^
+    4 │ 		// `let A` is shadowed by the class name.
+    5 │ 	}
+  
+   'A' is defined here.
+  
+  > 1 │ let A = class A {
+                 ^
+    2 │ 	b() {
+    3 │ 		A = 0;
+  
+
+ +### Valid + +```jsx +let A = class A {} +A = 0; // A is a variable. +``` + +```jsx +let A = class { + b() { + A = 0; // A is a variable. + } +} +``` + +```jsx +class A { + b(A) { + A = 0; // A is a parameter. + } +} +``` +