Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): support check ImportNamespaceSpecifier in no_import_assign #2617

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
135 changes: 106 additions & 29 deletions 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};

Expand Down Expand Up @@ -42,19 +45,94 @@ 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()));
}
}
}
}
}

/// 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;
Expand Down Expand Up @@ -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();
Expand Down
175 changes: 175 additions & 0 deletions crates/oxc_linter/src/snapshots/no_import_assign.snap
Expand Up @@ -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