From f5aadc767ff886ef77dcef85ebe0356319051576 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 25 Feb 2024 00:11:48 +0800 Subject: [PATCH] feat(linter): handle cjs `module.exports = {} as default export (#2493) --- crates/oxc_ast/src/ast/js.rs | 8 ++--- crates/oxc_linter/src/rules/import/default.rs | 13 +++----- crates/oxc_linter/src/snapshots/default.snap | 12 +++---- .../oxc_semantic/src/module_record/builder.rs | 33 +++++++++++++------ 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 7fed471d2..abc8ea0bc 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -665,11 +665,7 @@ impl<'a> MemberExpression<'a> { } } - pub fn through_optional_is_specific_member_access( - &'a self, - object: &str, - property: &str, - ) -> bool { + pub fn through_optional_is_specific_member_access(&self, object: &str, property: &str) -> bool { let object_matches = match self.object().without_parenthesized() { Expression::ChainExpression(x) => match &x.expression { ChainElement::CallExpression(_) => false, @@ -686,7 +682,7 @@ impl<'a> MemberExpression<'a> { } /// Whether it is a static member access `object.property` - pub fn is_specific_member_access(&'a self, object: &str, property: &str) -> bool { + pub fn is_specific_member_access(&self, object: &str, property: &str) -> bool { self.object().is_specific_id(object) && self.static_property_name().is_some_and(|p| p == property) } diff --git a/crates/oxc_linter/src/rules/import/default.rs b/crates/oxc_linter/src/rules/import/default.rs index b7bbe0463..33a76cc5a 100644 --- a/crates/oxc_linter/src/rules/import/default.rs +++ b/crates/oxc_linter/src/rules/import/default.rs @@ -39,7 +39,9 @@ impl Rule for Default { fn run_once(&self, ctx: &LintContext<'_>) { let module_record = ctx.semantic().module_record(); for import_entry in &module_record.import_entries { - let ImportImportName::Default(_) = import_entry.import_name else { continue }; + let ImportImportName::Default(default_span) = import_entry.import_name else { + continue; + }; let specifier = import_entry.module_request.name(); let Some(remote_module_record_ref) = module_record.loaded_modules.get(specifier) else { @@ -49,10 +51,7 @@ impl Rule for Default { if remote_module_record_ref.export_default.is_none() && !remote_module_record_ref.exported_bindings.contains_key("default") { - ctx.diagnostic(DefaultDiagnostic( - specifier.to_string(), - import_entry.module_request.span(), - )); + ctx.diagnostic(DefaultDiagnostic(specifier.to_string(), default_span)); } } } @@ -72,9 +71,7 @@ fn test() { r#"import CoolClass from "./default-class""#, r#"import bar, { baz } from "./default-export""#, r#"import crypto from "crypto""#, - // TODO: module.exports - // r#"import common from "./common""#, - + r#"import common from "./common""#, // No longer valid syntax // r#"export bar from "./bar""#, // r#"export bar, { foo } from "./bar""#, diff --git a/crates/oxc_linter/src/snapshots/default.snap b/crates/oxc_linter/src/snapshots/default.snap index 9a7d3735b..fd8eec907 100644 --- a/crates/oxc_linter/src/snapshots/default.snap +++ b/crates/oxc_linter/src/snapshots/default.snap @@ -3,9 +3,9 @@ source: crates/oxc_linter/src/tester.rs expression: default --- ⚠ eslint-plugin-import(default): No default export found in imported module "./named-exports" - ╭─[index.js:1:17] + ╭─[index.js:1:8] 1 │ import baz from "./named-exports" - · ───────────────── + · ─── ╰──── help: does "./named-exports" have the default export? @@ -28,15 +28,15 @@ expression: default ╰──── ⚠ eslint-plugin-import(default): No default export found in imported module "./re-export" - ╭─[index.js:1:24] + ╭─[index.js:1:8] 1 │ import barDefault from "./re-export" - · ───────────── + · ────────── ╰──── help: does "./re-export" have the default export? ⚠ eslint-plugin-import(default): No default export found in imported module "./typescript" - ╭─[index.js:1:20] + ╭─[index.js:1:8] 1 │ import foobar from "./typescript" - · ────────────── + · ────── ╰──── help: does "./typescript" have the default export? diff --git a/crates/oxc_semantic/src/module_record/builder.rs b/crates/oxc_semantic/src/module_record/builder.rs index 16f4c8dd2..f034bfb9e 100644 --- a/crates/oxc_semantic/src/module_record/builder.rs +++ b/crates/oxc_semantic/src/module_record/builder.rs @@ -367,21 +367,34 @@ impl ModuleRecordBuilder { } } - // Add export binding `foo` of ` - // * exports.foo = bar` - // * module.exports.foo = bar` + // Add export binding for + // * exports.foo = bar + // * module.exports.foo = bar + // * module.exports = foo fn handle_cjs_export(&mut self, expr: &Expression) { let Expression::AssignmentExpression(assign_expr) = expr else { return }; let AssignmentTarget::SimpleAssignmentTarget(target) = &assign_expr.left else { return }; let SimpleAssignmentTarget::MemberAssignmentTarget(member_expr) = target else { return }; match member_expr.object() { - Expression::Identifier(ident) if ident.name == "exports" => {} - Expression::MemberExpression(member_expr) - if matches!(member_expr.object(), Expression::Identifier(ident) if ident.name == "module") - && member_expr.static_property_name() == Some("exports") => {} - _ => return, + // exports.foo = bar + Expression::Identifier(ident) if ident.name == "exports" => { + let Some((span, name)) = member_expr.static_property_info() else { return }; + self.add_export_binding(name.into(), span); + } + // module.exports = {} + Expression::Identifier(_) + if member_expr.is_specific_member_access("module", "exports") => + { + self.add_default_export(assign_expr.right.span()); + } + // module.exports.foo = bar + Expression::MemberExpression(inner_member_expr) + if inner_member_expr.is_specific_member_access("module", "exports") => + { + let Some((span, name)) = member_expr.static_property_info() else { return }; + self.add_export_binding(name.into(), span); + } + _ => {} } - let Some((span, name)) = member_expr.static_property_info() else { return }; - self.add_export_binding(name.into(), span); } }