diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 49f1dce591a84..11657d1bc2fd5 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -71,6 +71,7 @@ mod eslint { pub mod no_eq_null; pub mod no_eval; pub mod no_ex_assign; + pub mod no_extend_native; pub mod no_extra_boolean_cast; pub mod no_fallthrough; pub mod no_func_assign; @@ -531,6 +532,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_eq_null, eslint::no_eval, eslint::no_ex_assign, + eslint::no_extend_native, eslint::no_extra_boolean_cast, eslint::no_fallthrough, eslint::no_func_assign, diff --git a/crates/oxc_linter/src/rules/eslint/no_extend_native.rs b/crates/oxc_linter/src/rules/eslint/no_extend_native.rs new file mode 100644 index 0000000000000..ba178600d8f38 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_extend_native.rs @@ -0,0 +1,319 @@ +use oxc_ast::ast::{CallExpression, ChainElement, Expression}; +use oxc_ast::{ast::MemberExpression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::cmp::ContentEq; +use oxc_span::{CompactStr, GetSpan}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Default, Clone)] +pub struct NoExtendNative(Box); + +#[derive(Debug, Default, Clone)] +pub struct NoExtendNativeConfig { + /// A list of objects which are allowed to be exceptions to the rule. + exceptions: Vec, +} + +impl std::ops::Deref for NoExtendNative { + type Target = NoExtendNativeConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Prevents extending native global objects such as `Object`, `String`, or `Array` with new + /// properties. + /// + /// ### Why is this bad? + /// + /// Extending native objects can cause unexpected behavior and conflicts with other code. + /// + /// For example: + /// ```js + /// // Adding a new property, which might seem okay + /// Object.prototype.extra = 55; + /// + /// // Defining a user object + /// const users = { + /// "1": "user1", + /// "2": "user2", + /// }; + /// + /// for (const id in users) { + /// // This will print "extra" as well as "1" and "2": + /// console.log(id); + /// } + /// ``` + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// Object.prototype.p = 0 + /// Object.defineProperty(Array.prototype, 'p', {value: 0}) + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// x.prototype.p = 0 + /// Object.defineProperty(x.prototype, 'p', {value: 0}) + /// ``` + NoExtendNative, + suspicious, +); + +impl Rule for NoExtendNative { + fn from_configuration(value: serde_json::Value) -> Self { + let obj = value.get(0); + + Self(Box::new(NoExtendNativeConfig { + exceptions: obj + .and_then(|v| v.get("exceptions")) + .and_then(serde_json::Value::as_array) + .unwrap_or(&vec![]) + .iter() + .filter_map(serde_json::Value::as_str) + .map(CompactStr::from) + .collect(), + })) + } + + fn run_once(&self, ctx: &LintContext) { + let symbols = ctx.symbols(); + for reference_id_list in ctx.scopes().root_unresolved_references_ids() { + for reference_id in reference_id_list { + let reference = symbols.get_reference(reference_id); + let name = ctx.semantic().reference_name(reference); + // If the referenced name does not appear to be a global object, skip it. + if !ctx.env_contains_var(name) { + continue; + } + // If the referenced name is explicitly allowed, skip it. + let compact_name = CompactStr::from(name); + if self.exceptions.contains(&compact_name) { + continue; + } + // If the first letter is capital, like `Object`, we will assume it is a native object + let Some(first_char) = name.chars().next() else { + continue; + }; + if first_char.is_lowercase() { + continue; + } + let node = ctx.nodes().get_node(reference.node_id()); + // If this is not `*.prototype` access, skip it. + let Some(prop_access) = get_prototype_property_accessed(ctx, node) else { + continue; + }; + // Check if being used like `String.prototype.xyz = 0` + if let Some(prop_assign) = get_property_assignment(ctx, prop_access) { + ctx.diagnostic( + OxcDiagnostic::error(format!( + "{name} prototype is read-only, properties should not be added." + )) + .with_label(prop_assign.span()), + ); + } + // Check if being used like `Object.defineProperty(String.prototype, 'xyz', 0)` + else if let Some(define_property_call) = + get_define_property_call(ctx, prop_access) + { + ctx.diagnostic( + OxcDiagnostic::error(format!( + "{name} prototype is read-only, properties should not be added." + )) + .with_label(define_property_call.span()), + ); + } + } + } + } +} + +/// If this usage of `*.prototype` is a `Object.defineProperty` or `Object.defineProperties` call, +/// then this function returns the `CallExpression` node. +fn get_define_property_call<'a>( + ctx: &'a LintContext, + node: &AstNode<'a>, +) -> Option<&'a AstNode<'a>> { + for parent in ctx.nodes().iter_parents(node.id()).skip(1) { + if let AstKind::CallExpression(call_expr) = parent.kind() { + if is_define_property_call(call_expr) { + return Some(parent); + } + } + } + None +} + +/// Checks if a given `CallExpression` is a call to `Object.defineProperty` or `Object.defineProperties`. +fn is_define_property_call(call_expr: &CallExpression) -> bool { + let callee = call_expr.callee.without_parentheses(); + + let member_expression = if let Expression::ChainExpression(chain_expr) = callee { + chain_expr.expression.as_member_expression() + } else { + callee.as_member_expression() + }; + match member_expression { + Some(me) => { + let prop_name = me.static_property_name(); + me.object() + .get_identifier_reference() + .is_some_and(|ident_ref| ident_ref.name == "Object") + && (prop_name == Some("defineProperty") || prop_name == Some("defineProperties")) + } + _ => false, + } +} + +/// Get an assignment to the property of the given node. +/// Example: `*.prop = 0` where `*.prop` is the given node. +fn get_property_assignment<'a>( + ctx: &'a LintContext, + node: &AstNode<'a>, +) -> Option<&'a AstNode<'a>> { + for parent in ctx.nodes().iter_parents(node.id()).skip(1) { + match parent.kind() { + AstKind::AssignmentExpression(_) => return Some(parent), + AstKind::MemberExpression(MemberExpression::ComputedMemberExpression(computed)) => { + if let AstKind::MemberExpression(node_expr) = node.kind() { + // Ignore computed member expressions like `obj[Object.prototype] = 0` (i.e., the + // given node is the `expression` of the computed member expression) + if computed + .expression + .as_member_expression() + .is_some_and(|expression| expression.content_eq(node_expr)) + { + return None; + } + return None; + } + } + _ => {} + } + } + None +} + +/// Returns the ASTNode that represents a prototype property access, such as +/// `Object?.['prototype']` +fn get_prototype_property_accessed<'a>( + ctx: &'a LintContext, + node: &AstNode<'a>, +) -> Option<&'a AstNode<'a>> { + let AstKind::IdentifierReference(_) = node.kind() else { + return None; + }; + let parent = ctx.nodes().parent_node(node.id())?; + let mut prototype_node = Some(parent); + let AstKind::MemberExpression(prop_access_expr) = parent.kind() else { + return None; + }; + let prop_name = prop_access_expr.static_property_name()?; + if prop_name != "prototype" { + return None; + } + let grandparent_node = ctx.nodes().parent_node(parent.id())?; + + if let AstKind::ChainExpression(_) = grandparent_node.kind() { + prototype_node = Some(grandparent_node); + if let Some(grandparent_parent) = ctx.nodes().parent_node(grandparent_node.id()) { + prototype_node = Some(grandparent_parent); + } + } + + if is_computed_member_expression_matching(grandparent_node, prop_access_expr) { + prototype_node = Some(grandparent_node); + } + + prototype_node +} + +fn is_computed_member_expression_matching( + node: &AstNode, + prop_access_expr: &MemberExpression, +) -> bool { + match node.kind() { + AstKind::ChainExpression(chain_expr) => { + if let ChainElement::ComputedMemberExpression(computed) = &chain_expr.expression { + return computed + .object + .as_member_expression() + .is_some_and(|object| object.content_eq(prop_access_expr)); + } + } + AstKind::MemberExpression(MemberExpression::ComputedMemberExpression(computed)) => { + return computed + .object + .as_member_expression() + .is_some_and(|object| object.content_eq(prop_access_expr)); + } + _ => {} + } + false +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("x.prototype.p = 0", None), + ("x.prototype['p'] = 0", None), + ("Object.p = 0", None), + ("Object.toString.bind = 0", None), + ("Object['toString'].bind = 0", None), + ("Object.defineProperty(x, 'p', {value: 0})", None), + ("Object.defineProperty(x.prototype, 'p', {value: 0})", None), + ("Object.defineProperties(x, {p: {value: 0}})", None), + ("global.Object.prototype.toString = 0", None), + ("this.Object.prototype.toString = 0", None), + ("with(Object) { prototype.p = 0; }", None), + ("o = Object; o.prototype.toString = 0", None), + ("eval('Object.prototype.toString = 0')", None), + ("parseFloat.prototype.x = 1", None), + ("Object.prototype.g = 0", Some(serde_json::json!([{ "exceptions": ["Object"] }]))), + ("obj[Object.prototype] = 0", None), + ("Object.defineProperty()", None), + ("Object.defineProperties()", None), + ("function foo() { var Object = function() {}; Object.prototype.p = 0 }", None), + ("{ let Object = function() {}; Object.prototype.p = 0 }", None), // { "ecmaVersion": 6 } + ]; + + let fail = vec![ + ("Object.prototype.p = 0", None), + ("BigInt.prototype.p = 0", None), // { "ecmaVersion": 2020 }, + ("WeakRef.prototype.p = 0", None), // { "ecmaVersion": 2021 }, + ("FinalizationRegistry.prototype.p = 0", None), // { "ecmaVersion": 2021 }, + ("AggregateError.prototype.p = 0", None), // { "ecmaVersion": 2021 }, + ("Function.prototype['p'] = 0", None), + ("String['prototype'].p = 0", None), + ("Number['prototype']['p'] = 0", None), + ("Object.defineProperty(Array.prototype, 'p', {value: 0})", None), + ("Object['defineProperty'](Array.prototype, 'p', {value: 0})", None), + ("Object['defineProperty'](Array['prototype'], 'p', {value: 0})", None), + ("Object.defineProperties(Array.prototype, {p: {value: 0}})", None), + ("Object.defineProperties(Array.prototype, {p: {value: 0}, q: {value: 0}})", None), + ("Number['prototype']['p'] = 0", Some(serde_json::json!([{ "exceptions": ["Object"] }]))), + ("Object.prototype.p = 0; Object.prototype.q = 0", None), + ("function foo() { Object.prototype.p = 0 }", None), + ("(Object?.prototype).p = 0", None), // { "ecmaVersion": 2020 }, + ("(Object?.['prototype'])['p'] = 0", None), + ("Object.defineProperty(Object?.prototype, 'p', { value: 0 })", None), // { "ecmaVersion": 2020 }, + ("Object?.defineProperty(Object.prototype, 'p', { value: 0 })", None), // { "ecmaVersion": 2020 }, + ("Object?.['defineProperty'](Object?.['prototype'], 'p', {value: 0})", None), + ("(Object?.defineProperty)(Object.prototype, 'p', { value: 0 })", None), // { "ecmaVersion": 2020 }, + ("Array.prototype.p &&= 0", None), // { "ecmaVersion": 2021 }, + ("Array.prototype.p ||= 0", None), // { "ecmaVersion": 2021 }, + ("Array.prototype.p ??= 0", None), // { "ecmaVersion": 2021 } + ]; + + Tester::new(NoExtendNative::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_extend_native.snap b/crates/oxc_linter/src/snapshots/no_extend_native.snap new file mode 100644 index 0000000000000..d49cdb0c648b4 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_extend_native.snap @@ -0,0 +1,158 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.prototype.p = 0 + · ────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): BigInt prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ BigInt.prototype.p = 0 + · ────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): WeakRef prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ WeakRef.prototype.p = 0 + · ─────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): FinalizationRegistry prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ FinalizationRegistry.prototype.p = 0 + · ──────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): AggregateError prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ AggregateError.prototype.p = 0 + · ────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Function prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Function.prototype['p'] = 0 + · ─────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): String prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ String['prototype'].p = 0 + · ───────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Number prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Number['prototype']['p'] = 0 + · ──────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.defineProperty(Array.prototype, 'p', {value: 0}) + · ─────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object['defineProperty'](Array.prototype, 'p', {value: 0}) + · ────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object['defineProperty'](Array['prototype'], 'p', {value: 0}) + · ───────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.defineProperties(Array.prototype, {p: {value: 0}}) + · ───────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.defineProperties(Array.prototype, {p: {value: 0}, q: {value: 0}}) + · ──────────────────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Number prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Number['prototype']['p'] = 0 + · ──────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.prototype.p = 0; Object.prototype.q = 0 + · ────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:25] + 1 │ Object.prototype.p = 0; Object.prototype.q = 0 + · ────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:18] + 1 │ function foo() { Object.prototype.p = 0 } + · ────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ (Object?.prototype).p = 0 + · ───────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ (Object?.['prototype'])['p'] = 0 + · ──────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object.defineProperty(Object?.prototype, 'p', { value: 0 }) + · ─────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object?.defineProperty(Object.prototype, 'p', { value: 0 }) + · ─────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Object?.['defineProperty'](Object?.['prototype'], 'p', {value: 0}) + · ────────────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Object prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ (Object?.defineProperty)(Object.prototype, 'p', { value: 0 }) + · ───────────────────────────────────────────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Array.prototype.p &&= 0 + · ─────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Array.prototype.p ||= 0 + · ─────────────────────── + ╰──── + + ⚠ eslint(no-extend-native): Array prototype is read-only, properties should not be added. + ╭─[no_extend_native.tsx:1:1] + 1 │ Array.prototype.p ??= 0 + · ─────────────────────── + ╰────