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_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)

View file

@ -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"?

View file

@ -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());

View file

@ -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);