diff --git a/crates/oxc_linter/src/rules/import/named.rs b/crates/oxc_linter/src/rules/import/named.rs index 76d38ab6d..7fa69eecd 100644 --- a/crates/oxc_linter/src/rules/import/named.rs +++ b/crates/oxc_linter/src/rules/import/named.rs @@ -4,7 +4,7 @@ use oxc_diagnostics::{ }; use oxc_macros::declare_oxc_lint; use oxc_span::{Atom, Span}; -use oxc_syntax::module_record::ImportImportName; +use oxc_syntax::module_record::{ExportImportName, ImportImportName}; use crate::{context::LintContext, rule::Rule}; @@ -61,6 +61,30 @@ impl Rule for Named { import_name.span(), )); } + + for export_entry in &module_record.indirect_export_entries { + let Some(module_request) = &export_entry.module_request else { + continue; + }; + let ExportImportName::Name(import_name) = &export_entry.import_name else { + continue; + }; + let specifier = module_request.name(); + // Get remote module record + let Some(remote_module_record_ref) = module_record.loaded_modules.get(specifier) else { + continue; + }; + let remote_module_record = remote_module_record_ref.value(); + // Check remote bindings + if remote_module_record.exported_bindings.contains_key(import_name.name()) { + continue; + } + ctx.diagnostic(NamedDiagnostic( + import_name.name().clone(), + specifier.clone(), + import_name.span(), + )); + } } } @@ -79,13 +103,16 @@ fn test() { "import { destructingAssign } from './named-exports'", "import { destructingRenamedAssign } from './named-exports'", "import { ActionTypes } from './qc'", + // TODO: export * // "import {a, b, c, d} from './re-export'", // "import {a, b, c} from './re-export-common-star'", // "import {RuleTester} from './re-export-node_modules'", // "import { jsxFoo } from './jsx/AnotherComponent'", "import {a, b, d} from './common'; // eslint-disable-line named", "import { foo, bar } from './re-export-names'", + // TODO: module.exports // "import { foo, bar } from './common'", + // ignore core modules by default "import { foo } from 'crypto'", // "import { zoob } from 'a'", "import { someThing } from './test-module'", @@ -148,29 +175,28 @@ fn test() { "import { ActionTypes1 } from './qc'", "import {a, b, c, d, e} from './re-export'", "import { a } from './re-export-names'", - // "export { bar } from './bar'", - // "export bar2, { bar } from './bar'", + "export { bar } from './bar'", + "export bar2, { bar } from './bar'", // old babel parser // "import { foo, bar, baz } from './named-trampoline'", // "import { baz } from './broken-trampoline'", - // - // "const { baz } = require('./bar')", - // "let { baz } = require('./bar')", - // "const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names')", - // "const { default: defExport } = require('./named-exports')", + "const { baz } = require('./bar')", + "let { baz } = require('./bar')", + "const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names')", + "const { default: defExport } = require('./named-exports')", // flow // "import { type MyOpaqueType, MyMissingClass } from './flowtypes'", // jsnext // "/*jsnext*/ import { createSnorlax } from 'redux'", - // "import { baz } from 'es6-module'", + "import { baz } from 'es6-module'", "import { foo, bar, bap } from './re-export-default'", "import { default as barDefault } from './re-export'", // export all "import { bar } from './export-all'", // TypeScript // Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead. - // "import { NotExported } from './typescript-export-assign-object'", - // "import { FooBar } from './typescript-export-assign-object'", + "import { NotExported } from './typescript-export-assign-object'", + "import { FooBar } from './typescript-export-assign-object'", ]; Tester::new_without_config(Named::NAME, pass, fail) diff --git a/crates/oxc_linter/src/snapshots/named.snap b/crates/oxc_linter/src/snapshots/named.snap index b36b5ee52..ec5fec7a2 100644 --- a/crates/oxc_linter/src/snapshots/named.snap +++ b/crates/oxc_linter/src/snapshots/named.snap @@ -86,6 +86,68 @@ expression: named ╰──── help: does "./re-export-names" have the export "a"? + ⚠ eslint-plugin-import(named): named import "bar" not found + ╭─[named.js:1:1] + 1 │ export { bar } from './bar' + · ─── + ╰──── + help: does "./bar" have the export "bar"? + + × Unexpected token + ╭─[named.js:1:1] + 1 │ export bar2, { bar } from './bar' + · ──── + ╰──── + + ⚠ eslint-plugin-import(named): named import "baz" not found + ╭─[named.js:1:1] + 1 │ const { baz } = require('./bar') + · ─── + ╰──── + help: does "./bar" have the export "baz"? + + ⚠ eslint-plugin-import(named): named import "baz" not found + ╭─[named.js:1:1] + 1 │ let { baz } = require('./bar') + · ─── + ╰──── + help: does "./bar" have the export "baz"? + + ⚠ eslint-plugin-import(named): named import "bar" not found + ╭─[named.js:1:1] + 1 │ const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names') + · ─── + ╰──── + help: does "./bar" have the export "bar"? + + ⚠ eslint-plugin-import(named): named import "bop" not found + ╭─[named.js:1:1] + 1 │ const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names') + · ─── + ╰──── + help: does "./bar" have the export "bop"? + + ⚠ eslint-plugin-import(named): named import "a" not found + ╭─[named.js:1:1] + 1 │ const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names') + · ─ + ╰──── + help: does "./re-export-names" have the export "a"? + + ⚠ eslint-plugin-import(named): named import "defExport" not found + ╭─[named.js:1:1] + 1 │ const { default: defExport } = require('./named-exports') + · ───────── + ╰──── + help: does "./named-exports" have the export "defExport"? + + ⚠ eslint-plugin-import(named): named import "baz" not found + ╭─[named.js:1:1] + 1 │ import { baz } from 'es6-module' + · ─── + ╰──── + help: does "es6-module" have the export "baz"? + ⚠ eslint-plugin-import(named): named import "bap" not found ╭─[named.js:1:1] 1 │ import { foo, bar, bap } from './re-export-default' @@ -107,4 +169,18 @@ expression: named ╰──── help: does "./export-all" have the export "bar"? + ⚠ eslint-plugin-import(named): named import "NotExported" not found + ╭─[named.js:1:1] + 1 │ import { NotExported } from './typescript-export-assign-object' + · ─────────── + ╰──── + help: does "./typescript-export-assign-object" have the export "NotExported"? + + ⚠ eslint-plugin-import(named): named import "FooBar" not found + ╭─[named.js:1:1] + 1 │ import { FooBar } from './typescript-export-assign-object' + · ────── + ╰──── + help: does "./typescript-export-assign-object" have the export "FooBar"? + diff --git a/crates/oxc_semantic/src/module_record/builder.rs b/crates/oxc_semantic/src/module_record/builder.rs index 8e9f2024c..914562fc6 100644 --- a/crates/oxc_semantic/src/module_record/builder.rs +++ b/crates/oxc_semantic/src/module_record/builder.rs @@ -303,17 +303,16 @@ impl ModuleRecordBuilder { if let Some(decl) = &decl.declaration { decl.bound_names(&mut |ident| { + let export_name = + ExportExportName::Name(NameSpan::new(ident.name.clone(), ident.span)); + let local_name = + ExportLocalName::Name(NameSpan::new(ident.name.clone(), ident.span)); let export_entry = ExportEntry { + span: Span::default(), module_request: module_request.clone(), - export_name: ExportExportName::Name(NameSpan::new( - ident.name.clone(), - ident.span, - )), - local_name: ExportLocalName::Name(NameSpan::new( - ident.name.clone(), - ident.span, - )), - ..ExportEntry::default() + import_name: ExportImportName::Null, + export_name, + local_name, }; self.add_export_entry(export_entry); self.add_export_binding(ident.name.clone(), ident.span); @@ -321,17 +320,32 @@ impl ModuleRecordBuilder { } for specifier in &decl.specifiers { - let export_entry = ExportEntry { - module_request: module_request.clone(), - export_name: ExportExportName::Name(NameSpan::new( - specifier.exported.name().clone(), - specifier.exported.span(), - )), - local_name: ExportLocalName::Name(NameSpan::new( + let export_name = ExportExportName::Name(NameSpan::new( + specifier.exported.name().clone(), + specifier.exported.span(), + )); + let import_name = if module_request.is_some() { + ExportImportName::Name(NameSpan::new( specifier.local.name().clone(), specifier.local.span(), - )), - ..ExportEntry::default() + )) + } else { + ExportImportName::Null + }; + let local_name = if module_request.is_some() { + ExportLocalName::Null + } else { + ExportLocalName::Name(NameSpan::new( + specifier.local.name().clone(), + specifier.local.span(), + )) + }; + let export_entry = ExportEntry { + span: Span::default(), + module_request: module_request.clone(), + import_name, + export_name, + local_name, }; self.add_export_entry(export_entry); self.add_export_binding(specifier.exported.name().clone(), specifier.exported.span()); diff --git a/crates/oxc_semantic/src/module_record/mod.rs b/crates/oxc_semantic/src/module_record/mod.rs index 8e8ca24fe..5f1e4277e 100644 --- a/crates/oxc_semantic/src/module_record/mod.rs +++ b/crates/oxc_semantic/src/module_record/mod.rs @@ -150,7 +150,7 @@ mod module_record_tests { let export_entry = ExportEntry { module_request: Some(NameSpan::new("mod".into(), Span::new(18, 23))), export_name: ExportExportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), - local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + import_name: ExportImportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), ..ExportEntry::default() }; assert_eq!(module_record.indirect_export_entries.len(), 1); @@ -165,7 +165,7 @@ mod module_record_tests { let export_entry = ExportEntry { module_request: Some(NameSpan::new("mod".into(), Span::new(23, 28))), export_name: ExportExportName::Name(NameSpan::new("v".into(), Span::new(14, 15))), - local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + import_name: ExportImportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), ..ExportEntry::default() }; assert_eq!(module_record.indirect_export_entries.len(), 1);