feat(linter): implement re-exports (#877)

This commit is contained in:
Boshen 2023-09-09 18:28:49 +08:00 committed by GitHub
parent 1d03ec32ff
commit 4e5f63a67d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 147 additions and 31 deletions

View file

@ -4,7 +4,7 @@ use oxc_diagnostics::{
}; };
use oxc_macros::declare_oxc_lint; use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span}; use oxc_span::{Atom, Span};
use oxc_syntax::module_record::ImportImportName; use oxc_syntax::module_record::{ExportImportName, ImportImportName};
use crate::{context::LintContext, rule::Rule}; use crate::{context::LintContext, rule::Rule};
@ -61,6 +61,30 @@ impl Rule for Named {
import_name.span(), 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 { destructingAssign } from './named-exports'",
"import { destructingRenamedAssign } from './named-exports'", "import { destructingRenamedAssign } from './named-exports'",
"import { ActionTypes } from './qc'", "import { ActionTypes } from './qc'",
// TODO: export *
// "import {a, b, c, d} from './re-export'", // "import {a, b, c, d} from './re-export'",
// "import {a, b, c} from './re-export-common-star'", // "import {a, b, c} from './re-export-common-star'",
// "import {RuleTester} from './re-export-node_modules'", // "import {RuleTester} from './re-export-node_modules'",
// "import { jsxFoo } from './jsx/AnotherComponent'", // "import { jsxFoo } from './jsx/AnotherComponent'",
"import {a, b, d} from './common'; // eslint-disable-line named", "import {a, b, d} from './common'; // eslint-disable-line named",
"import { foo, bar } from './re-export-names'", "import { foo, bar } from './re-export-names'",
// TODO: module.exports
// "import { foo, bar } from './common'", // "import { foo, bar } from './common'",
// ignore core modules by default
"import { foo } from 'crypto'", "import { foo } from 'crypto'",
// "import { zoob } from 'a'", // "import { zoob } from 'a'",
"import { someThing } from './test-module'", "import { someThing } from './test-module'",
@ -148,29 +175,28 @@ fn test() {
"import { ActionTypes1 } from './qc'", "import { ActionTypes1 } from './qc'",
"import {a, b, c, d, e} from './re-export'", "import {a, b, c, d, e} from './re-export'",
"import { a } from './re-export-names'", "import { a } from './re-export-names'",
// "export { bar } from './bar'", "export { bar } from './bar'",
// "export bar2, { bar } from './bar'", "export bar2, { bar } from './bar'",
// old babel parser // old babel parser
// "import { foo, bar, baz } from './named-trampoline'", // "import { foo, bar, baz } from './named-trampoline'",
// "import { baz } from './broken-trampoline'", // "import { baz } from './broken-trampoline'",
// "const { baz } = require('./bar')",
// "const { baz } = require('./bar')", "let { baz } = require('./bar')",
// "let { baz } = require('./bar')", "const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names')",
// "const { baz: bar, bop } = require('./bar'), { a } = require('./re-export-names')", "const { default: defExport } = require('./named-exports')",
// "const { default: defExport } = require('./named-exports')",
// flow // flow
// "import { type MyOpaqueType, MyMissingClass } from './flowtypes'", // "import { type MyOpaqueType, MyMissingClass } from './flowtypes'",
// jsnext // jsnext
// "/*jsnext*/ import { createSnorlax } from 'redux'", // "/*jsnext*/ import { createSnorlax } from 'redux'",
// "import { baz } from 'es6-module'", "import { baz } from 'es6-module'",
"import { foo, bar, bap } from './re-export-default'", "import { foo, bar, bap } from './re-export-default'",
"import { default as barDefault } from './re-export'", "import { default as barDefault } from './re-export'",
// export all // export all
"import { bar } from './export-all'", "import { bar } from './export-all'",
// TypeScript // TypeScript
// Export assignment cannot be used when targeting ECMAScript modules. Consider using 'export default' or another module format instead. // 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 { NotExported } from './typescript-export-assign-object'",
// "import { FooBar } from './typescript-export-assign-object'", "import { FooBar } from './typescript-export-assign-object'",
]; ];
Tester::new_without_config(Named::NAME, pass, fail) Tester::new_without_config(Named::NAME, pass, fail)

View file

@ -86,6 +86,68 @@ expression: named
╰──── ╰────
help: does "./re-export-names" have the export "a"? 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 ⚠ eslint-plugin-import(named): named import "bap" not found
╭─[named.js:1:1] ╭─[named.js:1:1]
1 │ import { foo, bar, bap } from './re-export-default' 1 │ import { foo, bar, bap } from './re-export-default'
@ -107,4 +169,18 @@ expression: named
╰──── ╰────
help: does "./export-all" have the export "bar"? 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"?

View file

@ -303,17 +303,16 @@ impl ModuleRecordBuilder {
if let Some(decl) = &decl.declaration { if let Some(decl) = &decl.declaration {
decl.bound_names(&mut |ident| { 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 { let export_entry = ExportEntry {
span: Span::default(),
module_request: module_request.clone(), module_request: module_request.clone(),
export_name: ExportExportName::Name(NameSpan::new( import_name: ExportImportName::Null,
ident.name.clone(), export_name,
ident.span, local_name,
)),
local_name: ExportLocalName::Name(NameSpan::new(
ident.name.clone(),
ident.span,
)),
..ExportEntry::default()
}; };
self.add_export_entry(export_entry); self.add_export_entry(export_entry);
self.add_export_binding(ident.name.clone(), ident.span); self.add_export_binding(ident.name.clone(), ident.span);
@ -321,17 +320,32 @@ impl ModuleRecordBuilder {
} }
for specifier in &decl.specifiers { for specifier in &decl.specifiers {
let export_entry = ExportEntry { let export_name = ExportExportName::Name(NameSpan::new(
module_request: module_request.clone(), specifier.exported.name().clone(),
export_name: ExportExportName::Name(NameSpan::new( specifier.exported.span(),
specifier.exported.name().clone(), ));
specifier.exported.span(), let import_name = if module_request.is_some() {
)), ExportImportName::Name(NameSpan::new(
local_name: ExportLocalName::Name(NameSpan::new(
specifier.local.name().clone(), specifier.local.name().clone(),
specifier.local.span(), 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_entry(export_entry);
self.add_export_binding(specifier.exported.name().clone(), specifier.exported.span()); self.add_export_binding(specifier.exported.name().clone(), specifier.exported.span());

View file

@ -150,7 +150,7 @@ mod module_record_tests {
let export_entry = ExportEntry { let export_entry = ExportEntry {
module_request: Some(NameSpan::new("mod".into(), Span::new(18, 23))), module_request: Some(NameSpan::new("mod".into(), Span::new(18, 23))),
export_name: ExportExportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), 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() ..ExportEntry::default()
}; };
assert_eq!(module_record.indirect_export_entries.len(), 1); assert_eq!(module_record.indirect_export_entries.len(), 1);
@ -165,7 +165,7 @@ mod module_record_tests {
let export_entry = ExportEntry { let export_entry = ExportEntry {
module_request: Some(NameSpan::new("mod".into(), Span::new(23, 28))), module_request: Some(NameSpan::new("mod".into(), Span::new(23, 28))),
export_name: ExportExportName::Name(NameSpan::new("v".into(), Span::new(14, 15))), 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() ..ExportEntry::default()
}; };
assert_eq!(module_record.indirect_export_entries.len(), 1); assert_eq!(module_record.indirect_export_entries.len(), 1);