From b9315a60ed94243eedaa88be5572dd6bd64f8836 Mon Sep 17 00:00:00 2001 From: Artur A Date: Sat, 9 Mar 2024 10:47:54 +0300 Subject: [PATCH] feat(es/lints): add `no-dupe-class-members` rule --- .../lints/no-dupe-class-members/es6/.swcrc | 8 + .../lints/no-dupe-class-members/es6/input.js | 202 ++++++++++++ .../es6/output.swc-stderr | 308 ++++++++++++++++++ .../lints/no-dupe-class-members/ts/.swcrc | 11 + .../lints/no-dupe-class-members/ts/input.ts | 16 + .../ts/output.swc-stderr | 23 ++ crates/swc_ecma_lints/src/config.rs | 4 + crates/swc_ecma_lints/src/rules/mod.rs | 5 + .../src/rules/no_dupe_class_members.rs | 185 +++++++++++ 9 files changed, 762 insertions(+) create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/es6/.swcrc create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/es6/input.js create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/es6/output.swc-stderr create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/ts/.swcrc create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/ts/input.ts create mode 100644 crates/swc/tests/errors/lints/no-dupe-class-members/ts/output.swc-stderr create mode 100644 crates/swc_ecma_lints/src/rules/no_dupe_class_members.rs diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/es6/.swcrc b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/.swcrc new file mode 100644 index 000000000000..fa21009c0a04 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/.swcrc @@ -0,0 +1,8 @@ +{ + "jsc": { + "target": "EsNext", + "lints": { + "noDupeClassMembers": ["error"] + } + } +} diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/es6/input.js b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/input.js new file mode 100644 index 000000000000..04b522326ce3 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/input.js @@ -0,0 +1,202 @@ +() => { + class A { + foo() {} + foo() {} + } +}; + +() => { + !class A { foo() {} foo() {} }; +}; + +() => { + class A { 'foo'() {} 'foo'() {} } +}; + +() => { + class A { 10() {} 1e1() {} } +}; + +() => { + class A { ['foo']() {} ['foo']() {} } +}; + + +() => { + class A { static ['foo']() {} static foo() {} } +}; + + +() => { + class A { + set foo(value) {} + set ['foo'](val) {} + } +} + +() => { + class A { ''() {} ['']() {} } +}; + +() => { + class A { [`foo`]() {} [`foo`]() {} } +}; + +() => { + class A { static get [`foo`]() {} static get ['foo']() {} } +} + +() => { + class A { foo() {} [`foo`]() {} } +} + +() => { + class A { get [`foo`]() {} 'foo'() {} } +} + +() => { + class A { static 'foo'() {} static [`foo`]() {} } +} + +() => { + class A { ['constructor']() {} ['constructor']() {} } +} + +() => { + class A { [123]() {} [123]() {} } +} + +() => { + class A { [0x10]() {} 16() {} } +} + +() => { + class A { [123.00]() {} [`123`]() {} } +}; + +() => { + class A { [null]() {} 'null'() {} } +} + +() => { + class A { foo() {} foo() {} foo() {} } +} + +() => { + class A { static foo() {} static foo() {} } +} + +() => { + class A { foo() {} get foo() {} } +} + +() => { + class A { set foo(value) {} foo() {} } +} + +() => { + class A { foo; foo; } +} + +() => { + class A { #priv() {} #priv() {} } +} + +() => { + class A { #priv; #priv() {} } +} + +// Valid + +() => { + class A { foo() {} bar() {} } +} + +() => { + class A { static foo() {} foo() {} } +} + +() => { + class A { get foo() {} set foo(value) {} } +} + +() => { + class A { static foo() {} get foo() {} set foo(value) {} } +} + +() => { + class A { foo() { } } + class B { foo() { } } +} + +() => { + class A { [foo]() {} foo() {} } +} + +() => { + class A { 'foo'() {} 'bar'() {} baz() {} } +} + +() => { + class A { *'foo'() {} *'bar'() {} *baz() {} } +} + +() => { + class A { get 'foo'() {} get 'bar'() {} get baz() {} } +} + +() => { + class A { 1() {} 2() {} } +} + +() => { + class A { ['foo']() {} ['bar']() {} } +} + +() => { + class A { [`foo`]() {} [`bar`]() {} } +} + +() => { + class A { [12]() {} [123]() {} } +} + +() => { + class A { [1.0]() {} ['1.0']() {} } +} + +() => { + class A { [0x1]() {} [`0x1`]() {} } +} + +() => { + class A { [null]() {} ['']() {} } +} + +() => { + class A { get ['foo']() {} set ['foo'](value) {} } +} + +() => { + class A { ['foo']() {} static ['foo']() {} } +} + +() => { + class A { ['constructor']() {} constructor() {} } +} + +() => { + class A { 'constructor'() {} [`constructor`]() {} } +} + +() => { + class A { foo; static foo; } +} + +() => { + class A { foo; #foo; } +} + +() => { + class A { '#foo'; #foo; } +} diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/es6/output.swc-stderr b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/output.swc-stderr new file mode 100644 index 000000000000..4c1e3fc6a057 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/es6/output.swc-stderr @@ -0,0 +1,308 @@ + + x Duplicate name 'foo' + ,-[1:1] + 1 | () => { + 2 | class A { + 3 | foo() {} + 4 | foo() {} + : ^^^^^^^^ + 5 | } + 6 | }; + `---- + + x Duplicate name 'foo' + ,-[6:1] + 6 | }; + 7 | + 8 | () => { + 9 | !class A { foo() {} foo() {} }; + : ^^^^^^^^ + 10 | }; + 11 | + 12 | () => { + `---- + + x Duplicate name 'foo' + ,-[10:1] + 10 | }; + 11 | + 12 | () => { + 13 | class A { 'foo'() {} 'foo'() {} } + : ^^^^^^^^^^ + 14 | }; + 15 | + 16 | () => { + `---- + + x Duplicate name '10' + ,-[14:1] + 14 | }; + 15 | + 16 | () => { + 17 | class A { 10() {} 1e1() {} } + : ^^^^^^^^ + 18 | }; + 19 | + 20 | () => { + `---- + + x Duplicate name 'foo' + ,-[18:1] + 18 | }; + 19 | + 20 | () => { + 21 | class A { ['foo']() {} ['foo']() {} } + : ^^^^^^^^^^^^ + 22 | }; + 23 | + `---- + + x Duplicate name 'foo' + ,-[23:1] + 23 | + 24 | + 25 | () => { + 26 | class A { static ['foo']() {} static foo() {} } + : ^^^^^^^^^^^^^^^ + 27 | }; + 28 | + `---- + + x Duplicate name 'foo' + ,-[30:1] + 30 | () => { + 31 | class A { + 32 | set foo(value) {} + 33 | set ['foo'](val) {} + : ^^^^^^^^^^^^^^^^^^^ + 34 | } + 35 | } + `---- + + x Duplicate name '' + ,-[35:1] + 35 | } + 36 | + 37 | () => { + 38 | class A { ''() {} ['']() {} } + : ^^^^^^^^^ + 39 | }; + 40 | + 41 | () => { + `---- + + x Duplicate name 'foo' + ,-[39:1] + 39 | }; + 40 | + 41 | () => { + 42 | class A { [`foo`]() {} [`foo`]() {} } + : ^^^^^^^^^^^^ + 43 | }; + 44 | + 45 | () => { + `---- + + x Duplicate name 'foo' + ,-[43:1] + 43 | }; + 44 | + 45 | () => { + 46 | class A { static get [`foo`]() {} static get ['foo']() {} } + : ^^^^^^^^^^^^^^^^^^^^^^^ + 47 | } + 48 | + 49 | () => { + `---- + + x Duplicate name 'foo' + ,-[47:1] + 47 | } + 48 | + 49 | () => { + 50 | class A { foo() {} [`foo`]() {} } + : ^^^^^^^^^^^^ + 51 | } + 52 | + 53 | () => { + `---- + + x Duplicate name 'foo' + ,-[51:1] + 51 | } + 52 | + 53 | () => { + 54 | class A { get [`foo`]() {} 'foo'() {} } + : ^^^^^^^^^^ + 55 | } + 56 | + 57 | () => { + `---- + + x Duplicate name 'foo' + ,-[55:1] + 55 | } + 56 | + 57 | () => { + 58 | class A { static 'foo'() {} static [`foo`]() {} } + : ^^^^^^^^^^^^^^^^^^^ + 59 | } + 60 | + 61 | () => { + `---- + + x Duplicate name 'constructor' + ,-[59:1] + 59 | } + 60 | + 61 | () => { + 62 | class A { ['constructor']() {} ['constructor']() {} } + : ^^^^^^^^^^^^^^^^^^^^ + 63 | } + 64 | + 65 | () => { + `---- + + x Duplicate name '123' + ,-[63:1] + 63 | } + 64 | + 65 | () => { + 66 | class A { [123]() {} [123]() {} } + : ^^^^^^^^^^ + 67 | } + 68 | + 69 | () => { + `---- + + x Duplicate name '16' + ,-[67:1] + 67 | } + 68 | + 69 | () => { + 70 | class A { [0x10]() {} 16() {} } + : ^^^^^^^ + 71 | } + 72 | + 73 | () => { + `---- + + x Duplicate name '123' + ,-[71:1] + 71 | } + 72 | + 73 | () => { + 74 | class A { [123.00]() {} [`123`]() {} } + : ^^^^^^^^^^^^ + 75 | }; + 76 | + 77 | () => { + `---- + + x Duplicate name 'null' + ,-[75:1] + 75 | }; + 76 | + 77 | () => { + 78 | class A { [null]() {} 'null'() {} } + : ^^^^^^^^^^^ + 79 | } + 80 | + 81 | () => { + `---- + + x Duplicate name 'foo' + ,-[79:1] + 79 | } + 80 | + 81 | () => { + 82 | class A { foo() {} foo() {} foo() {} } + : ^^^^^^^^ + 83 | } + 84 | + 85 | () => { + `---- + + x Duplicate name 'foo' + ,-[79:1] + 79 | } + 80 | + 81 | () => { + 82 | class A { foo() {} foo() {} foo() {} } + : ^^^^^^^^ + 83 | } + 84 | + 85 | () => { + `---- + + x Duplicate name 'foo' + ,-[83:1] + 83 | } + 84 | + 85 | () => { + 86 | class A { static foo() {} static foo() {} } + : ^^^^^^^^^^^^^^^ + 87 | } + 88 | + 89 | () => { + `---- + + x Duplicate name 'foo' + ,-[87:1] + 87 | } + 88 | + 89 | () => { + 90 | class A { foo() {} get foo() {} } + : ^^^^^^^^^^^^ + 91 | } + 92 | + 93 | () => { + `---- + + x Duplicate name 'foo' + ,-[91:1] + 91 | } + 92 | + 93 | () => { + 94 | class A { set foo(value) {} foo() {} } + : ^^^^^^^^ + 95 | } + 96 | + 97 | () => { + `---- + + x Duplicate name 'foo' + ,-[95:1] + 95 | } + 96 | + 97 | () => { + 98 | class A { foo; foo; } + : ^^^^ + 99 | } + 100 | + 101 | () => { + `---- + + x Duplicate name 'priv' + ,-[99:1] + 99 | } + 100 | + 101 | () => { + 102 | class A { #priv() {} #priv() {} } + : ^^^^^^^^^^ + 103 | } + 104 | + 105 | () => { + `---- + + x Duplicate name 'priv' + ,-[103:1] + 103 | } + 104 | + 105 | () => { + 106 | class A { #priv; #priv() {} } + : ^^^^^^^^^^ + 107 | } + 108 | + 109 | // Valid + `---- diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/ts/.swcrc b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/.swcrc new file mode 100644 index 000000000000..917c328412d7 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/.swcrc @@ -0,0 +1,11 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "EsNext", + "lints": { + "noDupeClassMembers": ["error"] + } + } +} diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/ts/input.ts b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/input.ts new file mode 100644 index 000000000000..a8c7675bf0f0 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/input.ts @@ -0,0 +1,16 @@ +() => { + class A { + foo() {} + foo() {} + foo() {} + } +} + +() => { + class Foo { + foo(a: string): string; + foo(a: string): string; + foo(a: number): number; + foo(a: any): any {} + } +} diff --git a/crates/swc/tests/errors/lints/no-dupe-class-members/ts/output.swc-stderr b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/output.swc-stderr new file mode 100644 index 000000000000..231283b27d11 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-dupe-class-members/ts/output.swc-stderr @@ -0,0 +1,23 @@ + + x Duplicate name 'foo' + ,-[1:1] + 1 | () => { + 2 | class A { + 3 | foo() {} + 4 | foo() {} + : ^^^^^^^^ + 5 | foo() {} + 6 | } + 7 | } + `---- + + x Duplicate name 'foo' + ,-[2:1] + 2 | class A { + 3 | foo() {} + 4 | foo() {} + 5 | foo() {} + : ^^^^^^^^ + 6 | } + 7 | } + `---- diff --git a/crates/swc_ecma_lints/src/config.rs b/crates/swc_ecma_lints/src/config.rs index 8da49264ba36..0b2dc1ecaacd 100644 --- a/crates/swc_ecma_lints/src/config.rs +++ b/crates/swc_ecma_lints/src/config.rs @@ -219,4 +219,8 @@ pub struct LintConfig { #[cfg(feature = "non_critical_lints")] #[serde(default, alias = "noNewObject")] pub no_new_object: RuleConfig<()>, + + #[cfg(feature = "non_critical_lints")] + #[serde(default, alias = "noDupeClassMembers")] + pub no_dupe_class_members: RuleConfig<()>, } diff --git a/crates/swc_ecma_lints/src/rules/mod.rs b/crates/swc_ecma_lints/src/rules/mod.rs index fb43e27db696..6b38c9000340 100644 --- a/crates/swc_ecma_lints/src/rules/mod.rs +++ b/crates/swc_ecma_lints/src/rules/mod.rs @@ -27,6 +27,7 @@ pub(crate) mod non_critical_lints { pub mod no_cond_assign; pub mod no_console; pub mod no_debugger; + pub mod no_dupe_class_members; pub mod no_empty_function; pub mod no_empty_pattern; pub mod no_loop_func; @@ -200,6 +201,10 @@ pub fn all(lint_params: LintParams) -> Vec> { unresolved_ctxt, &lint_config.no_new_object, )); + + rules.extend(no_dupe_class_members::no_dupe_class_members( + &lint_config.no_dupe_class_members, + )); } rules diff --git a/crates/swc_ecma_lints/src/rules/no_dupe_class_members.rs b/crates/swc_ecma_lints/src/rules/no_dupe_class_members.rs new file mode 100644 index 000000000000..362a51bfb024 --- /dev/null +++ b/crates/swc_ecma_lints/src/rules/no_dupe_class_members.rs @@ -0,0 +1,185 @@ +use swc_atoms::JsWord; +use swc_common::{collections::AHashSet, errors::HANDLER, Span, Spanned}; +use swc_ecma_ast::*; +use swc_ecma_visit::{noop_visit_type, Visit, VisitWith}; + +use crate::{ + config::{LintRuleReaction, RuleConfig}, + rule::{visitor_rule, Rule}, +}; + +#[derive(Debug, Hash, PartialEq, Eq)] +enum MemberType { + ES6Private, + StaticES6Private, + Getter, + Setter, + StaticOther, + Other, +} + +type Member = (MemberType, JsWord); + +pub fn no_dupe_class_members(config: &RuleConfig<()>) -> Option> { + let rule_reaction = config.get_rule_reaction(); + + match rule_reaction { + LintRuleReaction::Off => None, + _ => Some(visitor_rule(NoDupeClassMembers::new(rule_reaction))), + } +} + +#[derive(Debug, Default)] +struct NoDupeClassMembers { + expected_reaction: LintRuleReaction, + class_methods: AHashSet, +} + +impl NoDupeClassMembers { + fn new(expected_reaction: LintRuleReaction) -> Self { + Self { + expected_reaction, + class_methods: Default::default(), + } + } + + fn emit_report(&self, span: Span, member_name: &str) { + let message = format!("Duplicate name '{}'", member_name); + + HANDLER.with(|handler| match self.expected_reaction { + LintRuleReaction::Error => { + handler.struct_span_err(span, &message).emit(); + } + LintRuleReaction::Warning => { + handler.struct_span_warn(span, &message).emit(); + } + _ => {} + }); + } + + fn get_method_type(kind: &MethodKind, is_static: bool) -> MemberType { + match kind { + MethodKind::Getter => MemberType::Getter, + MethodKind::Setter => MemberType::Setter, + _ => { + if is_static { + MemberType::StaticOther + } else { + MemberType::Other + } + } + } + } + + fn extract_prop_name(prop_name: &PropName) -> Option { + match prop_name { + PropName::Ident(ident) => Some(ident.sym.clone()), + PropName::Str(lit_str) => Some(lit_str.value.clone()), + PropName::Num(num) => Some(JsWord::new(num.value.to_string())), + PropName::Computed(computed_prop_name) => match computed_prop_name.expr.as_ref() { + Expr::Lit(Lit::Str(lit_str)) => Some(lit_str.value.clone()), + Expr::Lit(Lit::Num(num)) => Some(JsWord::new(num.value.to_string())), + Expr::Lit(Lit::Null(_)) => Some(JsWord::new("null")), + Expr::Tpl(tpl) => { + if tpl.exprs.is_empty() && tpl.quasis.len() == 1 { + Some(tpl.quasis[0].raw.clone()) + } else { + None + } + } + _ => None, + }, + _ => None, + } + } + + fn extract_name(class_member: &ClassMember) -> Option { + match class_member { + ClassMember::Method(method) => { + if method.function.body.is_some() { + Self::extract_prop_name(&method.key) + .map(|name| (Self::get_method_type(&method.kind, method.is_static), name)) + } else { + None + } + } + ClassMember::ClassProp(class_prop) => { + Self::extract_prop_name(&class_prop.key).map(|name| { + let member_type = if class_prop.is_static { + MemberType::StaticOther + } else { + MemberType::Other + }; + + (member_type, name) + }) + } + ClassMember::PrivateMethod(private_method) => { + let member_type = if private_method.is_static { + MemberType::StaticES6Private + } else { + MemberType::ES6Private + }; + + Some((member_type, private_method.key.id.sym.clone())) + } + ClassMember::PrivateProp(private_prop) => { + let member_type = if private_prop.is_static { + MemberType::StaticES6Private + } else { + MemberType::ES6Private + }; + + Some((member_type, private_prop.key.id.sym.clone())) + } + _ => None, + } + } + + fn check(&self, span: Span, member: &Member) { + let duplicates = match &member.0 { + MemberType::Other => { + self.class_methods.contains(member) + || self + .class_methods + .contains(&(MemberType::Getter, member.1.clone())) + || self + .class_methods + .contains(&(MemberType::Setter, member.1.clone())) + } + MemberType::Getter | MemberType::Setter => { + self.class_methods.contains(member) + || self + .class_methods + .contains(&(MemberType::Other, member.1.clone())) + } + _ => self.class_methods.contains(member), + }; + + if duplicates { + self.emit_report(span, member.1.as_str()); + } + } +} + +impl Visit for NoDupeClassMembers { + noop_visit_type!(); + + fn visit_class(&mut self, class: &Class) { + let prev_class_methods = std::mem::take(&mut self.class_methods); + + class.visit_children_with(self); + + self.class_methods = prev_class_methods; + } + + fn visit_class_member(&mut self, class_member: &ClassMember) { + if let Some(member) = Self::extract_name(class_member) { + self.check(class_member.span(), &member); + + self.class_methods.insert(member); + } + + class_member.visit_children_with(self); + } +}