diff --git a/crates/oxc_linter/src/rules/eslint/no_import_assign.rs b/crates/oxc_linter/src/rules/eslint/no_import_assign.rs index 94c0cae045b2..9b096d2bb6b2 100644 --- a/crates/oxc_linter/src/rules/eslint/no_import_assign.rs +++ b/crates/oxc_linter/src/rules/eslint/no_import_assign.rs @@ -1,10 +1,13 @@ +use oxc_ast::{ast::Expression, AstKind}; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, }; use oxc_macros::declare_oxc_lint; -use oxc_semantic::SymbolId; -use oxc_span::Span; +use oxc_semantic::{AstNodeId, SymbolId}; +use oxc_span::{GetSpan, Span}; +use oxc_syntax::operator::UnaryOperator; +use phf::phf_set; use crate::{context::LintContext, rule::Rule}; @@ -42,12 +45,49 @@ declare_oxc_lint!( nursery ); +const OBJECT_MUTATION_METHODS: phf::Set<&'static str> = + phf_set!("assign", "defineProperty", "defineProperties", "freeze", "setPrototypeOf"); + +const REFLECT_MUTATION_METHODS: phf::Set<&'static str> = + phf_set!("defineProperty", "deleteProperty", "set", "setPrototypeOf"); + impl Rule for NoImportAssign { fn run_on_symbol(&self, symbol_id: SymbolId, ctx: &LintContext<'_>) { let symbol_table = ctx.semantic().symbols(); if symbol_table.get_flag(symbol_id).is_import_binding() { + let kind = ctx.nodes().kind(symbol_table.get_declaration(symbol_id)); + let is_namespace_specifier = matches!(kind, AstKind::ImportNamespaceSpecifier(_)); for reference in symbol_table.get_resolved_references(symbol_id) { - if reference.is_write() { + if is_namespace_specifier { + let Some(parent_node) = ctx.nodes().parent_node(reference.node_id()) else { + return; + }; + if let AstKind::MemberExpression(expr) = parent_node.kind() { + let Some(parent_parent_node) = ctx.nodes().parent_node(parent_node.id()) + else { + return; + }; + let is_unary_expression_with_delete_operator = |kind| matches!(kind, AstKind::UnaryExpression(expr) if expr.operator == UnaryOperator::Delete); + let parent_parent_kind = parent_parent_node.kind(); + if matches!(parent_parent_kind, AstKind::SimpleAssignmentTarget(_)) + // delete namespace.module + || is_unary_expression_with_delete_operator(parent_parent_kind) + // delete namespace?.module + || matches!(parent_parent_kind, AstKind::ChainExpression(_) if ctx.nodes().parent_kind(parent_parent_node.id()).is_some_and(is_unary_expression_with_delete_operator)) + { + if let Some((span, _)) = expr.static_property_info() { + if span != reference.span() { + return ctx.diagnostic(NoImportAssignDiagnostic(expr.span())); + } + } + } + } + } + + if reference.is_write() + || (is_namespace_specifier + && is_argument_of_well_known_mutation_function(reference.node_id(), ctx)) + { ctx.diagnostic(NoImportAssignDiagnostic(reference.span())); } } @@ -55,6 +95,44 @@ impl Rule for NoImportAssign { } } +/// Check if a given node is at the first argument of a well-known mutation function. +/// - `Object.assign` +/// - `Object.defineProperty` +/// - `Object.defineProperties` +/// - `Object.freeze` +/// - `Object.setPrototypeOf` +/// - `Reflect.defineProperty` +/// - `Reflect.deleteProperty` +/// - `Reflect.set` +/// - `Reflect.setPrototypeOf` +fn is_argument_of_well_known_mutation_function(node_id: AstNodeId, ctx: &LintContext<'_>) -> bool { + let current_node = ctx.nodes().get_node(node_id); + let call_expression_node = + ctx.nodes().parent_node(node_id).and_then(|node| ctx.nodes().parent_kind(node.id())); + + let Some(AstKind::CallExpression(expr)) = call_expression_node else { return false }; + + let Some(member_expr) = &expr.callee.get_member_expr() else { return false }; + + if let Expression::Identifier(ident) = member_expr.object() { + let Some(property_name) = member_expr.static_property_name() else { + return false; + }; + + if ((ident.name == "Object" && OBJECT_MUTATION_METHODS.contains(property_name)) + || (ident.name == "Reflect" && REFLECT_MUTATION_METHODS.contains(property_name))) + && !ctx.scopes().has_binding(current_node.scope_id(), &ident.name) + { + return expr + .arguments + .first() + .is_some_and(|argument| argument.span() == current_node.kind().span()); + } + } + + false +} + #[test] fn test() { use crate::tester::Tester; @@ -154,32 +232,31 @@ fn test() { ("import * as mod9 from 'mod'; ({ bar: mod9 } = foo)", None), ("import * as mod10 from 'mod'; ({ bar: mod10 = 0 } = foo)", None), ("import * as mod11 from 'mod'; ({ ...mod11 } = foo)", None), - // TODO - // ("import * as mod1 from 'mod'; mod1.named = 0", None), - // ("import * as mod2 from 'mod'; mod2.named += 0", None), - // ("import * as mod3 from 'mod'; mod3.named++", None), - // ("import * as mod4 from 'mod'; for (mod4.named in foo);", None), - // ("import * as mod5 from 'mod'; for (mod5.named of foo);", None), - // ("import * as mod6 from 'mod'; [mod6.named] = foo", None), - // ("import * as mod7 from 'mod'; [mod7.named = 0] = foo", None), - // ("import * as mod8 from 'mod'; [...mod8.named] = foo", None), - // ("import * as mod9 from 'mod'; ({ bar: mod9.named } = foo)", None), - // ("import * as mod10 from 'mod'; ({ bar: mod10.named = 0 } = foo)", None), - // ("import * as mod11 from 'mod'; ({ ...mod11.named } = foo)", None), - // ("import * as mod12 from 'mod'; delete mod12.named", None), - // ("import * as mod from 'mod'; Object.assign(mod, obj)", None), - // ("import * as mod from 'mod'; Object.defineProperty(mod, key, d)", None), - // ("import * as mod from 'mod'; Object.defineProperties(mod, d)", None), - // ("import * as mod from 'mod'; Object.setPrototypeOf(mod, proto)", None), - // ("import * as mod from 'mod'; Object.freeze(mod)", None), - // ("import * as mod from 'mod'; Reflect.defineProperty(mod, key, d)", None), - // ("import * as mod from 'mod'; Reflect.deleteProperty(mod, key)", None), - // ("import * as mod from 'mod'; Reflect.set(mod, key, value)", None), - // ("import * as mod from 'mod'; Reflect.setPrototypeOf(mod, proto)", None), - // ("import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0", None), - // ("import * as mod from 'mod'; Object?.defineProperty(mod, key, d)", None), - // ("import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d)", None), - // ("import * as mod from 'mod'; delete mod?.prop", None), + ("import * as mod1 from 'mod'; mod1.named = 0", None), + ("import * as mod2 from 'mod'; mod2.named += 0", None), + ("import * as mod3 from 'mod'; mod3.named++", None), + ("import * as mod4 from 'mod'; for (mod4.named in foo);", None), + ("import * as mod5 from 'mod'; for (mod5.named of foo);", None), + ("import * as mod6 from 'mod'; [mod6.named] = foo", None), + ("import * as mod7 from 'mod'; [mod7.named = 0] = foo", None), + ("import * as mod8 from 'mod'; [...mod8.named] = foo", None), + ("import * as mod9 from 'mod'; ({ bar: mod9.named } = foo)", None), + ("import * as mod10 from 'mod'; ({ bar: mod10.named = 0 } = foo)", None), + ("import * as mod11 from 'mod'; ({ ...mod11.named } = foo)", None), + ("import * as mod12 from 'mod'; delete mod12.named", None), + ("import * as mod from 'mod'; Object.assign(mod, obj)", None), + ("import * as mod from 'mod'; Object.defineProperty(mod, key, d)", None), + ("import * as mod from 'mod'; Object.defineProperties(mod, d)", None), + ("import * as mod from 'mod'; Object.setPrototypeOf(mod, proto)", None), + ("import * as mod from 'mod'; Object.freeze(mod)", None), + ("import * as mod from 'mod'; Reflect.defineProperty(mod, key, d)", None), + ("import * as mod from 'mod'; Reflect.deleteProperty(mod, key)", None), + ("import * as mod from 'mod'; Reflect.set(mod, key, value)", None), + ("import * as mod from 'mod'; Reflect.setPrototypeOf(mod, proto)", None), + ("import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0", None), + ("import * as mod from 'mod'; Object?.defineProperty(mod, key, d)", None), + ("import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d)", None), + ("import * as mod from 'mod'; delete mod?.prop", None), ]; Tester::new(NoImportAssign::NAME, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/no_import_assign.snap b/crates/oxc_linter/src/snapshots/no_import_assign.snap index a07324986f90..1423c5be6077 100644 --- a/crates/oxc_linter/src/snapshots/no_import_assign.snap +++ b/crates/oxc_linter/src/snapshots/no_import_assign.snap @@ -239,3 +239,178 @@ expression: no_import_assign · ───── ╰──── help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:30] + 1 │ import * as mod1 from 'mod'; mod1.named = 0 + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:30] + 1 │ import * as mod2 from 'mod'; mod2.named += 0 + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:30] + 1 │ import * as mod3 from 'mod'; mod3.named++ + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:35] + 1 │ import * as mod4 from 'mod'; for (mod4.named in foo); + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:35] + 1 │ import * as mod5 from 'mod'; for (mod5.named of foo); + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:31] + 1 │ import * as mod6 from 'mod'; [mod6.named] = foo + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:31] + 1 │ import * as mod7 from 'mod'; [mod7.named = 0] = foo + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:34] + 1 │ import * as mod8 from 'mod'; [...mod8.named] = foo + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:38] + 1 │ import * as mod9 from 'mod'; ({ bar: mod9.named } = foo) + · ────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:39] + 1 │ import * as mod10 from 'mod'; ({ bar: mod10.named = 0 } = foo) + · ─────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:37] + 1 │ import * as mod11 from 'mod'; ({ ...mod11.named } = foo) + · ─────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:38] + 1 │ import * as mod12 from 'mod'; delete mod12.named + · ─────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:43] + 1 │ import * as mod from 'mod'; Object.assign(mod, obj) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:51] + 1 │ import * as mod from 'mod'; Object.defineProperty(mod, key, d) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:53] + 1 │ import * as mod from 'mod'; Object.defineProperties(mod, d) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:51] + 1 │ import * as mod from 'mod'; Object.setPrototypeOf(mod, proto) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:43] + 1 │ import * as mod from 'mod'; Object.freeze(mod) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:52] + 1 │ import * as mod from 'mod'; Reflect.defineProperty(mod, key, d) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:52] + 1 │ import * as mod from 'mod'; Reflect.deleteProperty(mod, key) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:41] + 1 │ import * as mod from 'mod'; Reflect.set(mod, key, value) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:52] + 1 │ import * as mod from 'mod'; Reflect.setPrototypeOf(mod, proto) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:51] + 1 │ import mod, * as mod_ns from 'mod'; mod.prop = 0; mod_ns.prop = 0 + · ─────────── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:52] + 1 │ import * as mod from 'mod'; Object?.defineProperty(mod, key, d) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:54] + 1 │ import * as mod from 'mod'; (Object?.defineProperty)(mod, key, d) + · ─── + ╰──── + help: imported bindings are readonly + + ⚠ eslint(no-import-assign): do not assign to imported bindings + ╭─[no_import_assign.tsx:1:36] + 1 │ import * as mod from 'mod'; delete mod?.prop + · ───────── + ╰──── + help: imported bindings are readonly