From a5dbb17612327c66366086f99b44c6731d125ffc Mon Sep 17 00:00:00 2001 From: Artur Date: Mon, 11 Mar 2024 07:17:40 +0300 Subject: [PATCH] feat(es/lints): Add `no-prototype-builtins` rule (#8684) --- .../no-prototype-builtins/default/.swcrc | 9 + .../no-prototype-builtins/default/input.js | 56 +++++ .../default/output.swc-stderr | 194 ++++++++++++++++++ crates/swc_ecma_lints/src/config.rs | 2 + crates/swc_ecma_lints/src/rules/mod.rs | 5 + .../src/rules/no_prototype_builtins.rs | 153 ++++++++++++++ 6 files changed, 419 insertions(+) create mode 100644 crates/swc/tests/errors/lints/no-prototype-builtins/default/.swcrc create mode 100644 crates/swc/tests/errors/lints/no-prototype-builtins/default/input.js create mode 100644 crates/swc/tests/errors/lints/no-prototype-builtins/default/output.swc-stderr create mode 100644 crates/swc_ecma_lints/src/rules/no_prototype_builtins.rs diff --git a/crates/swc/tests/errors/lints/no-prototype-builtins/default/.swcrc b/crates/swc/tests/errors/lints/no-prototype-builtins/default/.swcrc new file mode 100644 index 00000000000..f96a69541bf --- /dev/null +++ b/crates/swc/tests/errors/lints/no-prototype-builtins/default/.swcrc @@ -0,0 +1,9 @@ +{ + "jsc": { + "lints": { + "noPrototypeBuiltins": [ + "error" + ] + } + } +} diff --git a/crates/swc/tests/errors/lints/no-prototype-builtins/default/input.js b/crates/swc/tests/errors/lints/no-prototype-builtins/default/input.js new file mode 100644 index 00000000000..8cf9ce7d0b4 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-prototype-builtins/default/input.js @@ -0,0 +1,56 @@ +foo.hasOwnProperty('bar'); + +foo.isPrototypeOf('bar'); + +foo.propertyIsEnumerable('bar'); + +foo.bar.hasOwnProperty('bar'); + +foo.bar.baz.isPrototypeOf('bar'); + +bar?.foo?.hasOwnProperty('bar'); + +foo?.bar.hasOwnProperty('baz'); + +foo.hasOwnProperty?.('bar'); + +foo?.hasOwnProperty('bar').baz; + +foo.hasOwnProperty('bar')?.baz; + +(a,b).hasOwnProperty('bar'); + +(foo?.hasOwnProperty)('bar'); + +(((foo?.hasOwnProperty)))('dlya-tex-kto-dumaet-cho-on-samiy-umniy'); + +(foo?.anotherProp, foo?.hasOwnProperty)('bar'); + +(foo.hasOwnProperty('ok'), foo?.hasOwnProperty)('bar'); + +foo['hasOwnProperty']('bar'); + +foo[`isPrototypeOf`]('bar').baz; + +// valid + +Object.prototype.hasOwnProperty.call(foo, 'bar'); +Object.prototype.isPrototypeOf.call(foo, 'bar'); +Object.prototype.propertyIsEnumerable.call(foo, 'bar'); +Object.prototype.hasOwnProperty.apply(foo, ['bar']); +Object.prototype.isPrototypeOf.apply(foo, ['bar']); +Object.prototype.propertyIsEnumerable.apply(foo, ['bar']); +foo.hasOwnProperty; +foo.hasOwnProperty.bar(); +foo(hasOwnProperty); +hasOwnProperty(foo, 'bar'); +isPrototypeOf(foo, 'bar'); +propertyIsEnumerable(foo, 'bar'); +({}.hasOwnProperty.call(foo, 'bar')); +({}.isPrototypeOf.call(foo, 'bar')); +({}.propertyIsEnumerable.call(foo, 'bar')); +({}.hasOwnProperty.apply(foo, ['bar'])); +({}.isPrototypeOf.apply(foo, ['bar'])); +({}.propertyIsEnumerable.apply(foo, ['bar'])); +foo[hasOwnProperty]('bar'); +foo['HasOwnProperty']('bar'); diff --git a/crates/swc/tests/errors/lints/no-prototype-builtins/default/output.swc-stderr b/crates/swc/tests/errors/lints/no-prototype-builtins/default/output.swc-stderr new file mode 100644 index 00000000000..d42c1fef6d4 --- /dev/null +++ b/crates/swc/tests/errors/lints/no-prototype-builtins/default/output.swc-stderr @@ -0,0 +1,194 @@ + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[1:1] + 1 | foo.hasOwnProperty('bar'); + : ^^^^^^^^^^^^^^ + 2 | + 3 | foo.isPrototypeOf('bar'); + `---- + + x Do not access Object.prototype method 'isPrototypeOf' from target object + ,-[1:1] + 1 | foo.hasOwnProperty('bar'); + 2 | + 3 | foo.isPrototypeOf('bar'); + : ^^^^^^^^^^^^^ + 4 | + 5 | foo.propertyIsEnumerable('bar'); + `---- + + x Do not access Object.prototype method 'propertyIsEnumerable' from target object + ,-[2:1] + 2 | + 3 | foo.isPrototypeOf('bar'); + 4 | + 5 | foo.propertyIsEnumerable('bar'); + : ^^^^^^^^^^^^^^^^^^^^ + 6 | + 7 | foo.bar.hasOwnProperty('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[4:1] + 4 | + 5 | foo.propertyIsEnumerable('bar'); + 6 | + 7 | foo.bar.hasOwnProperty('bar'); + : ^^^^^^^^^^^^^^ + 8 | + 9 | foo.bar.baz.isPrototypeOf('bar'); + `---- + + x Do not access Object.prototype method 'isPrototypeOf' from target object + ,-[6:1] + 6 | + 7 | foo.bar.hasOwnProperty('bar'); + 8 | + 9 | foo.bar.baz.isPrototypeOf('bar'); + : ^^^^^^^^^^^^^ + 10 | + 11 | bar?.foo?.hasOwnProperty('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[8:1] + 8 | + 9 | foo.bar.baz.isPrototypeOf('bar'); + 10 | + 11 | bar?.foo?.hasOwnProperty('bar'); + : ^^^^^^^^^^^^^^ + 12 | + 13 | foo?.bar.hasOwnProperty('baz'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[10:1] + 10 | + 11 | bar?.foo?.hasOwnProperty('bar'); + 12 | + 13 | foo?.bar.hasOwnProperty('baz'); + : ^^^^^^^^^^^^^^ + 14 | + 15 | foo.hasOwnProperty?.('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[12:1] + 12 | + 13 | foo?.bar.hasOwnProperty('baz'); + 14 | + 15 | foo.hasOwnProperty?.('bar'); + : ^^^^^^^^^^^^^^ + 16 | + 17 | foo?.hasOwnProperty('bar').baz; + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[14:1] + 14 | + 15 | foo.hasOwnProperty?.('bar'); + 16 | + 17 | foo?.hasOwnProperty('bar').baz; + : ^^^^^^^^^^^^^^ + 18 | + 19 | foo.hasOwnProperty('bar')?.baz; + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[16:1] + 16 | + 17 | foo?.hasOwnProperty('bar').baz; + 18 | + 19 | foo.hasOwnProperty('bar')?.baz; + : ^^^^^^^^^^^^^^ + 20 | + 21 | (a,b).hasOwnProperty('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[18:1] + 18 | + 19 | foo.hasOwnProperty('bar')?.baz; + 20 | + 21 | (a,b).hasOwnProperty('bar'); + : ^^^^^^^^^^^^^^ + 22 | + 23 | (foo?.hasOwnProperty)('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[20:1] + 20 | + 21 | (a,b).hasOwnProperty('bar'); + 22 | + 23 | (foo?.hasOwnProperty)('bar'); + : ^^^^^^^^^^^^^^ + 24 | + 25 | (((foo?.hasOwnProperty)))('dlya-tex-kto-dumaet-cho-on-samiy-umniy'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[22:1] + 22 | + 23 | (foo?.hasOwnProperty)('bar'); + 24 | + 25 | (((foo?.hasOwnProperty)))('dlya-tex-kto-dumaet-cho-on-samiy-umniy'); + : ^^^^^^^^^^^^^^ + 26 | + 27 | (foo?.anotherProp, foo?.hasOwnProperty)('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[24:1] + 24 | + 25 | (((foo?.hasOwnProperty)))('dlya-tex-kto-dumaet-cho-on-samiy-umniy'); + 26 | + 27 | (foo?.anotherProp, foo?.hasOwnProperty)('bar'); + : ^^^^^^^^^^^^^^ + 28 | + 29 | (foo.hasOwnProperty('ok'), foo?.hasOwnProperty)('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[26:1] + 26 | + 27 | (foo?.anotherProp, foo?.hasOwnProperty)('bar'); + 28 | + 29 | (foo.hasOwnProperty('ok'), foo?.hasOwnProperty)('bar'); + : ^^^^^^^^^^^^^^ + 30 | + 31 | foo['hasOwnProperty']('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[26:1] + 26 | + 27 | (foo?.anotherProp, foo?.hasOwnProperty)('bar'); + 28 | + 29 | (foo.hasOwnProperty('ok'), foo?.hasOwnProperty)('bar'); + : ^^^^^^^^^^^^^^ + 30 | + 31 | foo['hasOwnProperty']('bar'); + `---- + + x Do not access Object.prototype method 'hasOwnProperty' from target object + ,-[28:1] + 28 | + 29 | (foo.hasOwnProperty('ok'), foo?.hasOwnProperty)('bar'); + 30 | + 31 | foo['hasOwnProperty']('bar'); + : ^^^^^^^^^^^^^^^^ + 32 | + 33 | foo[`isPrototypeOf`]('bar').baz; + `---- + + x Do not access Object.prototype method 'isPrototypeOf' from target object + ,-[30:1] + 30 | + 31 | foo['hasOwnProperty']('bar'); + 32 | + 33 | foo[`isPrototypeOf`]('bar').baz; + : ^^^^^^^^^^^^^^^ + 34 | + 35 | // valid + `---- diff --git a/crates/swc_ecma_lints/src/config.rs b/crates/swc_ecma_lints/src/config.rs index 8da49264ba3..91dc62907c9 100644 --- a/crates/swc_ecma_lints/src/config.rs +++ b/crates/swc_ecma_lints/src/config.rs @@ -217,6 +217,8 @@ pub struct LintConfig { pub no_cond_assign: RuleConfig<()>, #[cfg(feature = "non_critical_lints")] + #[serde(default, alias = "noPrototypeBuiltins")] + pub no_prototype_builtins: RuleConfig<()>, #[serde(default, alias = "noNewObject")] pub no_new_object: RuleConfig<()>, } diff --git a/crates/swc_ecma_lints/src/rules/mod.rs b/crates/swc_ecma_lints/src/rules/mod.rs index fb43e27db69..cccca217c8d 100644 --- a/crates/swc_ecma_lints/src/rules/mod.rs +++ b/crates/swc_ecma_lints/src/rules/mod.rs @@ -35,6 +35,7 @@ pub(crate) mod non_critical_lints { pub mod no_new_symbol; pub mod no_obj_calls; pub mod no_param_reassign; + pub mod no_prototype_builtins; pub mod no_restricted_syntax; pub mod no_sparse_arrays; pub mod no_throw_literal; @@ -196,6 +197,10 @@ pub fn all(lint_params: LintParams) -> Vec> { rules.extend(no_cond_assign::no_cond_assign(&lint_config.no_cond_assign)); + rules.extend(no_prototype_builtins::no_prototype_builtins( + &lint_config.no_prototype_builtins, + )); + rules.extend(no_new_object::no_new_object( unresolved_ctxt, &lint_config.no_new_object, diff --git a/crates/swc_ecma_lints/src/rules/no_prototype_builtins.rs b/crates/swc_ecma_lints/src/rules/no_prototype_builtins.rs new file mode 100644 index 00000000000..40b32b120e7 --- /dev/null +++ b/crates/swc_ecma_lints/src/rules/no_prototype_builtins.rs @@ -0,0 +1,153 @@ +use swc_atoms::Atom; +use swc_common::{errors::HANDLER, Span}; +use swc_ecma_ast::*; +use swc_ecma_utils::ExprExt; +use swc_ecma_visit::{noop_visit_type, Visit, VisitWith}; + +use crate::{ + config::{LintRuleReaction, RuleConfig}, + rule::{visitor_rule, Rule}, +}; + +const METHODS: [&str; 3] = ["hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable"]; + +pub fn no_prototype_builtins(config: &RuleConfig<()>) -> Option> { + let rule_reaction = config.get_rule_reaction(); + + match rule_reaction { + LintRuleReaction::Off => None, + _ => Some(visitor_rule(NoPrototypeBuiltins::new(rule_reaction))), + } +} + +#[derive(Debug, Default)] +struct CallInfo { + chain: Vec, + method_span: Option, +} + +#[derive(Debug, Default)] +struct NoPrototypeBuiltins { + expected_reaction: LintRuleReaction, + call_info: CallInfo, +} + +impl NoPrototypeBuiltins { + fn new(expected_reaction: LintRuleReaction) -> Self { + Self { + expected_reaction, + call_info: Default::default(), + } + } + + fn emit_error(&self, span: Span, method: &str) { + let message = format!( + "Do not access Object.prototype method '{}' from target object", + method + ); + + 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 extend_chain(&mut self, span: Span, atom: Atom) { + if self.call_info.method_span.is_none() { + self.call_info.method_span = Some(span); + } + + self.call_info.chain.push(atom); + } + + fn extract_path(&mut self, expr: &Expr) { + match expr { + Expr::Member(member) => { + match &member.prop { + MemberProp::Ident(ident) => { + self.extend_chain(ident.span, ident.sym.clone()); + } + MemberProp::Computed(computed_prop_name) => { + match computed_prop_name.expr.as_ref() { + Expr::Lit(_) | Expr::Tpl(_) | Expr::Paren(_) | Expr::Seq(_) => { + self.extract_path(&computed_prop_name.expr); + } + _ => {} + } + } + _ => {} + } + + self.extract_path(member.obj.as_ref()); + } + Expr::OptChain(OptChainExpr { base, .. }) => { + if let Some(member_expr) = base.as_member() { + if let Some(ident) = member_expr.prop.as_ident() { + self.extend_chain(ident.span, ident.sym.clone()); + } + + self.extract_path(member_expr.obj.as_ref()); + } + } + Expr::Paren(ParenExpr { expr, .. }) => { + self.extract_path(expr.as_ref()); + } + Expr::Seq(SeqExpr { exprs, .. }) => { + self.extract_path(exprs.last().unwrap().as_ref()); + } + Expr::Lit(Lit::Str(lit_str)) => { + self.extend_chain(lit_str.span, lit_str.value.clone()); + } + Expr::Tpl(tpl) => { + if tpl.exprs.is_empty() && tpl.quasis.len() == 1 { + self.extend_chain(tpl.span, tpl.quasis[0].raw.clone()); + } + } + Expr::Ident(ident) => { + self.extend_chain(ident.span, ident.sym.clone()); + } + _ => {} + } + } + + fn check(&mut self, expr: &Expr) { + let prev_call_info = std::mem::take(&mut self.call_info); + + self.extract_path(expr); + + if self.call_info.chain.len() > 1 { + let method_name = self.call_info.chain[0].as_str(); + + if METHODS.contains(&method_name) { + self.emit_error(self.call_info.method_span.unwrap(), method_name); + } + } + + self.call_info = prev_call_info; + } +} + +impl Visit for NoPrototypeBuiltins { + noop_visit_type!(); + + fn visit_opt_chain_base(&mut self, opt_chain_base: &OptChainBase) { + if let OptChainBase::Call(opt_call) = opt_chain_base { + self.check(opt_call.callee.as_expr()); + } + + opt_chain_base.visit_children_with(self); + } + + fn visit_call_expr(&mut self, call_expr: &CallExpr) { + if let Some(expr) = call_expr.callee.as_expr() { + self.check(expr.as_ref()); + } + + call_expr.visit_children_with(self); + } +}