diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index a7639a1b7e3ae..ae3c19b21a300 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -3889,6 +3889,16 @@ impl RuleRunner for crate::rules::unicorn::require_array_join_separator::Require const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner for crate::rules::unicorn::require_module_attributes::RequireModuleAttributes { + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::ExportAllDeclaration, + AstType::ExportNamedDeclaration, + AstType::ImportDeclaration, + AstType::ImportExpression, + ])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::unicorn::require_module_specifiers::RequireModuleSpecifiers { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ AstType::ExportNamedDeclaration, diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 7f4b61969a62a..b949e05ed80f6 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -522,6 +522,7 @@ pub(crate) mod unicorn { pub mod prefer_top_level_await; pub mod prefer_type_error; pub mod require_array_join_separator; + pub mod require_module_attributes; pub mod require_module_specifiers; pub mod require_number_to_fixed_digits_argument; pub mod require_post_message_target_origin; @@ -1297,6 +1298,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::prefer_string_trim_start_end, unicorn::prefer_structured_clone, unicorn::prefer_type_error, + unicorn::require_module_attributes, unicorn::require_module_specifiers, unicorn::require_post_message_target_origin, unicorn::require_array_join_separator, diff --git a/crates/oxc_linter/src/rules/unicorn/require_module_attributes.rs b/crates/oxc_linter/src/rules/unicorn/require_module_attributes.rs new file mode 100644 index 0000000000000..ed7f4917f6340 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/require_module_attributes.rs @@ -0,0 +1,161 @@ +use oxc_ast::{ + AstKind, + ast::{Expression, PropertyKind, WithClause}, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{AstNode, context::LintContext, rule::Rule, utils::is_empty_object_expression}; + +fn require_module_attributes_diagnostic(span: Span, import_type: &str) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("{import_type} with empty attribute list is not allowed.")) + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct RequireModuleAttributes; + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule enforces non-empty attribute list in import/export statements and import() expressions. + /// + /// ### Why is this bad? + /// + /// Import attributes are meant to provide metadata about how a module should be loaded + /// (e.g., `with { type: "json" }`). An empty attribute object provides no information + /// and should be removed. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// import foo from 'foo' with {}; + /// + /// export { foo } from 'foo' with {}; + /// + /// const foo = await import('foo', {}); + /// + /// const foo = await import('foo', { with: {} }); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// import foo from 'foo'; + /// + /// export { foo } from 'foo'; + /// + /// const foo = await import('foo'); + /// + /// const foo = await import('foo'); + /// ``` + RequireModuleAttributes, + unicorn, + style, + pending, +); + +impl Rule for RequireModuleAttributes { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::ImportExpression(import_expr) => { + let Some(options) = &import_expr.options else { return }; + + let Expression::ObjectExpression(obj_expr) = options.get_inner_expression() else { + return; + }; + + if obj_expr.properties.is_empty() { + ctx.diagnostic(require_module_attributes_diagnostic( + obj_expr.span, + "import expression", + )); + return; + } + + let empty_with_prop = obj_expr.properties.iter().find_map(|prop| { + let obj_prop = prop.as_property()?; + if !obj_prop.method + && !obj_prop.shorthand + && !obj_prop.computed + && obj_prop.kind == PropertyKind::Init + && obj_prop.key.is_specific_static_name("with") + && is_empty_object_expression(obj_prop.value.get_inner_expression()) + { + Some(obj_prop) + } else { + None + } + }); + + if let Some(empty_with_prop) = empty_with_prop { + let span = empty_with_prop.value.span(); + ctx.diagnostic(require_module_attributes_diagnostic(span, "import expression")); + } + } + AstKind::ImportDeclaration(decl) => { + check_with_clause(ctx, decl.with_clause.as_deref(), "import statement"); + } + AstKind::ExportNamedDeclaration(decl) => { + check_with_clause(ctx, decl.with_clause.as_deref(), "export statement"); + } + AstKind::ExportAllDeclaration(decl) => { + check_with_clause(ctx, decl.with_clause.as_deref(), "export statement"); + } + _ => {} + } + } +} + +fn check_with_clause(ctx: &LintContext, with_clause: Option<&WithClause>, import_type: &str) { + if let Some(with_clause) = with_clause + && with_clause.with_entries.is_empty() + { + ctx.diagnostic(require_module_attributes_diagnostic(with_clause.span, import_type)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"import foo from "foo""#, + r#"export {foo} from "foo""#, + r#"export * from "foo""#, + r#"import foo from "foo" with {type: "json"}"#, + r#"export {foo} from "foo" with {type: "json"}"#, + r#"export * from "foo" with {type: "json"}"#, + "export {}", + r#"import("foo")"#, + r#"import("foo", {unknown: "unknown"})"#, + r#"import("foo", {with: {type: "json"}})"#, + r#"not_import("foo", {})"#, + r#"not_import("foo", {with:{}})"#, + ]; + + let fail = vec![ + r#"import "foo" with {}"#, + r#"import foo from "foo" with {}"#, + r#"export {foo} from "foo" with {}"#, + r#"export * from "foo" with {}"#, + r#"export * from "foo"with{}"#, + r#"export * from "foo"/* comment 1 */with/* comment 2 */{/* comment 3 */}/* comment 4 */"#, + r#"import("foo", {})"#, + r#"import("foo", (( {} )))"#, + r#"import("foo", {},)"#, + r#"import("foo", {with:{},},)"#, + r#"import("foo", {with:{}, unknown:"unknown"},)"#, + r#"import("foo", {"with":{}, unknown:"unknown"},)"#, + r#"import("foo", {unknown:"unknown", with:{}, },)"#, + r#"import("foo", {unknown:"unknown", with:{} },)"#, + r#"import("foo", {unknown:"unknown", with:{}, unknown2:"unknown2", },)"#, + r#"import("foo"/* comment 1 */, /* comment 2 */{/* comment 3 */}/* comment 4 */,/* comment 5 */)"#, + r#"import("foo", {/* comment 1 */"with"/* comment 2 */:/* comment 3 */{/* comment 4 */}, }/* comment 5 */,)"#, + r#"import("foo", {with: (({}))})"#, + ]; + + Tester::new(RequireModuleAttributes::NAME, RequireModuleAttributes::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/unicorn_require_module_attributes.snap b/crates/oxc_linter/src/snapshots/unicorn_require_module_attributes.snap new file mode 100644 index 0000000000000..c65724a33c3d8 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/unicorn_require_module_attributes.snap @@ -0,0 +1,110 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-unicorn(require-module-attributes): import statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:19] + 1 │ import "foo" with {} + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:28] + 1 │ import foo from "foo" with {} + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): export statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:30] + 1 │ export {foo} from "foo" with {} + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): export statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:26] + 1 │ export * from "foo" with {} + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): export statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:24] + 1 │ export * from "foo"with{} + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): export statement with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:54] + 1 │ export * from "foo"/* comment 1 */with/* comment 2 */{/* comment 3 */}/* comment 4 */ + · ───────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:15] + 1 │ import("foo", {}) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:18] + 1 │ import("foo", (( {} ))) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:15] + 1 │ import("foo", {},) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:21] + 1 │ import("foo", {with:{},},) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:21] + 1 │ import("foo", {with:{}, unknown:"unknown"},) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:23] + 1 │ import("foo", {"with":{}, unknown:"unknown"},) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:40] + 1 │ import("foo", {unknown:"unknown", with:{}, },) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:40] + 1 │ import("foo", {unknown:"unknown", with:{} },) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:40] + 1 │ import("foo", {unknown:"unknown", with:{}, unknown2:"unknown2", },) + · ── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:45] + 1 │ import("foo"/* comment 1 */, /* comment 2 */{/* comment 3 */}/* comment 4 */,/* comment 5 */) + · ───────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:68] + 1 │ import("foo", {/* comment 1 */"with"/* comment 2 */:/* comment 3 */{/* comment 4 */}, }/* comment 5 */,) + · ───────────────── + ╰──── + + ⚠ eslint-plugin-unicorn(require-module-attributes): import expression with empty attribute list is not allowed. + ╭─[require_module_attributes.tsx:1:22] + 1 │ import("foo", {with: (({}))}) + · ────── + ╰────