mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): add eslint-plugin-import(export) rule (#1654)
This commit is contained in:
parent
c49a1f6b32
commit
90524c83f7
4 changed files with 492 additions and 0 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
/// <https://github.com/import-js/eslint-plugin-import>
|
/// <https://github.com/import-js/eslint-plugin-import>
|
||||||
mod import {
|
mod import {
|
||||||
pub mod default;
|
pub mod default;
|
||||||
|
pub mod export;
|
||||||
pub mod named;
|
pub mod named;
|
||||||
pub mod no_amd;
|
pub mod no_amd;
|
||||||
pub mod no_cycle;
|
pub mod no_cycle;
|
||||||
|
|
@ -436,6 +437,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
import::no_cycle,
|
import::no_cycle,
|
||||||
import::no_self_import,
|
import::no_self_import,
|
||||||
import::no_amd,
|
import::no_amd,
|
||||||
|
import::export,
|
||||||
jsx_a11y::alt_text,
|
jsx_a11y::alt_text,
|
||||||
jsx_a11y::anchor_has_content,
|
jsx_a11y::anchor_has_content,
|
||||||
jsx_a11y::anchor_is_valid,
|
jsx_a11y::anchor_is_valid,
|
||||||
|
|
|
||||||
341
crates/oxc_linter/src/rules/import/export.rs
Normal file
341
crates/oxc_linter/src/rules/import/export.rs
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::{self, Error},
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_semantic::ModuleRecord;
|
||||||
|
use oxc_span::{Atom, Span};
|
||||||
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
|
||||||
|
use crate::{context::LintContext, rule::Rule};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
enum ExportDiagnostic {
|
||||||
|
#[error("eslint-plugin-import(export): Multiple exports of name '{1}'.")]
|
||||||
|
#[diagnostic(severity(warning))]
|
||||||
|
MultipleNamedExport(#[label] Span, Atom),
|
||||||
|
#[error("eslint-plugin-import(export): No named exports found in module '{1}'")]
|
||||||
|
#[diagnostic(severity(warning))]
|
||||||
|
NoNamedExport(#[label] Span, Atom),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/export.md>
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Export;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
/// Reports funny business with exports, like repeated exports of names or defaults.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```javascript
|
||||||
|
/// let foo;
|
||||||
|
/// export { foo }; // Multiple exports of name 'foo'.
|
||||||
|
/// export * from "./export-all" // export-all.js also export foo
|
||||||
|
/// ```
|
||||||
|
Export,
|
||||||
|
nursery
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for Export {
|
||||||
|
fn run_once(&self, ctx: &LintContext<'_>) {
|
||||||
|
let module_record = ctx.semantic().module_record();
|
||||||
|
let named_export = &module_record.exported_bindings;
|
||||||
|
let mut duplicated_named_export = FxHashMap::default();
|
||||||
|
if module_record.star_export_entries.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for export_entry in &module_record.star_export_entries {
|
||||||
|
let Some(module_request) = &export_entry.module_request else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(remote_module_record_ref) =
|
||||||
|
module_record.loaded_modules.get(module_request.name())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let remote_module_record = remote_module_record_ref.value();
|
||||||
|
let mut all_export_names = FxHashSet::default();
|
||||||
|
let mut visited = FxHashSet::default();
|
||||||
|
walk_exported_recursive(remote_module_record, &mut all_export_names, &mut visited);
|
||||||
|
if all_export_names.is_empty() {
|
||||||
|
ctx.diagnostic(ExportDiagnostic::NoNamedExport(
|
||||||
|
module_request.span(),
|
||||||
|
module_request.name().clone(),
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in &all_export_names {
|
||||||
|
if let Some(span) = named_export.get(name) {
|
||||||
|
duplicated_named_export.entry(*span).or_insert_with(|| name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (span, name) in duplicated_named_export {
|
||||||
|
ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(span, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_exported_recursive(
|
||||||
|
module_record: &ModuleRecord,
|
||||||
|
result: &mut FxHashSet<Atom>,
|
||||||
|
visited: &mut FxHashSet<PathBuf>,
|
||||||
|
) {
|
||||||
|
let path = &module_record.resolved_absolute_path;
|
||||||
|
if path.components().any(|c| match c {
|
||||||
|
std::path::Component::Normal(p) => p == std::ffi::OsStr::new("node_modules"),
|
||||||
|
_ => false,
|
||||||
|
}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !visited.insert(path.clone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for name in module_record.exported_bindings.keys() {
|
||||||
|
result.insert(name.clone());
|
||||||
|
}
|
||||||
|
for export_entry in &module_record.star_export_entries {
|
||||||
|
let Some(module_request) = &export_entry.module_request else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(remote_module_record_ref) =
|
||||||
|
module_record.loaded_modules.get(module_request.name())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
walk_exported_recursive(remote_module_record_ref.value(), result, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
let mut tester =
|
||||||
|
Tester::new_without_config::<String>(Export::NAME, vec![], vec![]).with_import_plugin(true);
|
||||||
|
|
||||||
|
{
|
||||||
|
let pass = vec![
|
||||||
|
(r#"import "./malformed.js""#),
|
||||||
|
(r#"var foo = "foo"; export default foo;"#),
|
||||||
|
(r#"export var foo = "foo"; export var bar = "bar";"#),
|
||||||
|
(r#"export var foo = "foo", bar = "bar";"#),
|
||||||
|
("export var { foo, bar } = object;"),
|
||||||
|
("export var [ foo, bar ] = array;"),
|
||||||
|
("let foo; export { foo, foo as bar }"),
|
||||||
|
(r#"let bar; export { bar }; export * from "./export-all""#),
|
||||||
|
(r#"export * from "./export-all""#),
|
||||||
|
(r#"export * from "./does-not-exist""#),
|
||||||
|
(r#"export default foo; export * from "./bar""#),
|
||||||
|
// SYNTAX_CASES doesn't need to be tested
|
||||||
|
("
|
||||||
|
import * as A from './named-export-collision/a';
|
||||||
|
import * as B from './named-export-collision/b';
|
||||||
|
export { A, B };
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export * as A from './named-export-collision/a';
|
||||||
|
export * as B from './named-export-collision/b';
|
||||||
|
"),
|
||||||
|
// ("
|
||||||
|
// export default function foo(param: string): boolean;
|
||||||
|
// export default function foo(param: string, param1: number): boolean;
|
||||||
|
// export default function foo(param: string, param1?: number): boolean {
|
||||||
|
// return param && param1;
|
||||||
|
// }
|
||||||
|
// "),
|
||||||
|
// Typescript
|
||||||
|
("
|
||||||
|
export const Foo = 1;
|
||||||
|
export type Foo = number;
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export const Foo = 1;
|
||||||
|
export interface Foo {}
|
||||||
|
"),
|
||||||
|
// ("
|
||||||
|
// export function fff(a: string);
|
||||||
|
// export function fff(a: number);
|
||||||
|
// "),
|
||||||
|
// ("
|
||||||
|
// export function fff(a: string);
|
||||||
|
// export function fff(a: number);
|
||||||
|
// export function fff(a: string|number) {};
|
||||||
|
// "),
|
||||||
|
("
|
||||||
|
export const Bar = 1;
|
||||||
|
export namespace Foo {
|
||||||
|
export const Bar = 1;
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export type Bar = string;
|
||||||
|
export namespace Foo {
|
||||||
|
export type Bar = string;
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export const Bar = 1;
|
||||||
|
export type Bar = string;
|
||||||
|
export namespace Foo {
|
||||||
|
export const Bar = 1;
|
||||||
|
export type Bar = string;
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export namespace Foo {
|
||||||
|
export const Foo = 1;
|
||||||
|
export namespace Bar {
|
||||||
|
export const Foo = 2;
|
||||||
|
}
|
||||||
|
export namespace Baz {
|
||||||
|
export const Foo = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export class Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
export namespace Foo {
|
||||||
|
export class Bar {}
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
// ("
|
||||||
|
// export function Foo();
|
||||||
|
// export namespace Foo { }
|
||||||
|
// "),
|
||||||
|
// ("
|
||||||
|
// export function Foo(a: string);
|
||||||
|
// export namespace Foo { }
|
||||||
|
// "),
|
||||||
|
// ("
|
||||||
|
// export function Foo(a: string);
|
||||||
|
// export function Foo(a: number);
|
||||||
|
// export namespace Foo { }
|
||||||
|
// "),
|
||||||
|
("
|
||||||
|
export enum Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
(r#"export * from "./file1.ts""#),
|
||||||
|
("
|
||||||
|
export * as A from './named-export-collision/a';
|
||||||
|
export * as B from './named-export-collision/b';
|
||||||
|
"),
|
||||||
|
// (r#"
|
||||||
|
// declare module "a" {
|
||||||
|
// const Foo = 1;
|
||||||
|
// export {Foo as default};
|
||||||
|
// }
|
||||||
|
// declare module "b" {
|
||||||
|
// const Bar = 2;
|
||||||
|
// export {Bar as default};
|
||||||
|
// }
|
||||||
|
// "#),
|
||||||
|
// (r#"
|
||||||
|
// declare module "a" {
|
||||||
|
// const Foo = 1;
|
||||||
|
// export {Foo as default};
|
||||||
|
// }
|
||||||
|
// const Bar = 2;
|
||||||
|
// export {Bar as default};
|
||||||
|
// "#),
|
||||||
|
];
|
||||||
|
let fail = vec![
|
||||||
|
(r#"let foo; export { foo }; export * from "./export-all""#),
|
||||||
|
// (r#"export * from "./malformed.js""#),
|
||||||
|
(r#"export * from "./default-export""#),
|
||||||
|
(r#"let foo; export { foo as "foo" }; export * from "./export-all""#),
|
||||||
|
("
|
||||||
|
export type Foo = string;
|
||||||
|
export type Foo = number;
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export const a = 1
|
||||||
|
export namespace Foo {
|
||||||
|
export const a = 2;
|
||||||
|
export const a = 3;
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
declare module 'foo' {
|
||||||
|
const Foo = 1;
|
||||||
|
export default Foo;
|
||||||
|
export default Foo;
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export namespace Foo {
|
||||||
|
export namespace Bar {
|
||||||
|
export const Foo = 1;
|
||||||
|
export const Foo = 2;
|
||||||
|
}
|
||||||
|
export namespace Baz {
|
||||||
|
export const Bar = 3;
|
||||||
|
export const Bar = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export class Foo { }
|
||||||
|
export class Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
// ("
|
||||||
|
// export enum Foo { }
|
||||||
|
// export enum Foo { }
|
||||||
|
// export namespace Foo { }
|
||||||
|
// "),
|
||||||
|
("
|
||||||
|
export enum Foo { }
|
||||||
|
export class Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export const Foo = 'bar';
|
||||||
|
export class Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export function Foo();
|
||||||
|
export class Foo { }
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
("
|
||||||
|
export const Foo = 'bar';
|
||||||
|
export function Foo();
|
||||||
|
export namespace Foo { }
|
||||||
|
"),
|
||||||
|
// ("
|
||||||
|
// export const Foo = 'bar';
|
||||||
|
// export namespace Foo { }
|
||||||
|
// "),
|
||||||
|
(r#"
|
||||||
|
declare module "a" {
|
||||||
|
const Foo = 1;
|
||||||
|
export {Foo as default};
|
||||||
|
}
|
||||||
|
const Bar = 2;
|
||||||
|
export {Bar as default};
|
||||||
|
const Baz = 3;
|
||||||
|
export {Baz as default};
|
||||||
|
"#),
|
||||||
|
];
|
||||||
|
|
||||||
|
tester = tester.change_rule_path("index.js").update_expect_pass_fail(pass, fail);
|
||||||
|
tester.test();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let pass = vec!["export * from './module'"];
|
||||||
|
let fail = vec![];
|
||||||
|
tester =
|
||||||
|
tester.change_rule_path("export-star-4/index.js").update_expect_pass_fail(pass, fail);
|
||||||
|
tester.test_and_snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
141
crates/oxc_linter/src/snapshots/export.snap
Normal file
141
crates/oxc_linter/src/snapshots/export.snap
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
expression: export
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-import(export): Multiple exports of name 'foo'.
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │ let foo; export { foo }; export * from "./export-all"
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-import(export): No named exports found in module './default-export'
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │ export * from "./default-export"
|
||||||
|
· ──────────────────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-import(export): Multiple exports of name 'foo'.
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │ let foo; export { foo as "foo" }; export * from "./export-all"
|
||||||
|
· ─────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `Foo` has already been declared
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ export type Foo = string;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Foo` has already been declared here
|
||||||
|
3 │ export type Foo = number;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
4 │
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `a` has already been declared
|
||||||
|
╭─[index.js:3:1]
|
||||||
|
3 │ export namespace Foo {
|
||||||
|
4 │ export const a = 2;
|
||||||
|
· ┬
|
||||||
|
· ╰── `a` has already been declared here
|
||||||
|
5 │ export const a = 3;
|
||||||
|
· ┬
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
6 │ }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Expected a semicolon or an implicit semicolon after a statement, but found none
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ declare module 'foo' {
|
||||||
|
· ─
|
||||||
|
3 │ const Foo = 1;
|
||||||
|
╰────
|
||||||
|
help: Try insert a semicolon here
|
||||||
|
|
||||||
|
× Identifier `Foo` has already been declared
|
||||||
|
╭─[index.js:3:1]
|
||||||
|
3 │ export namespace Bar {
|
||||||
|
4 │ export const Foo = 1;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Foo` has already been declared here
|
||||||
|
5 │ export const Foo = 2;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
6 │ }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `Bar` has already been declared
|
||||||
|
╭─[index.js:7:1]
|
||||||
|
7 │ export namespace Baz {
|
||||||
|
8 │ export const Bar = 3;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Bar` has already been declared here
|
||||||
|
9 │ export const Bar = 4;
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
10 │ }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `Foo` has already been declared
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ export class Foo { }
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Foo` has already been declared here
|
||||||
|
3 │ export class Foo { }
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
4 │ export namespace Foo { }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `Foo` has already been declared
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ export enum Foo { }
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Foo` has already been declared here
|
||||||
|
3 │ export class Foo { }
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
4 │ export namespace Foo { }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Identifier `Foo` has already been declared
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ export const Foo = 'bar';
|
||||||
|
· ─┬─
|
||||||
|
· ╰── `Foo` has already been declared here
|
||||||
|
3 │ export class Foo { }
|
||||||
|
· ─┬─
|
||||||
|
· ╰── It can not be redeclared here
|
||||||
|
4 │ export namespace Foo { }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Unexpected token
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ export function Foo();
|
||||||
|
· ─
|
||||||
|
3 │ export class Foo { }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Unexpected token
|
||||||
|
╭─[index.js:2:1]
|
||||||
|
2 │ export const Foo = 'bar';
|
||||||
|
3 │ export function Foo();
|
||||||
|
· ─
|
||||||
|
4 │ export namespace Foo { }
|
||||||
|
╰────
|
||||||
|
|
||||||
|
× Expected a semicolon or an implicit semicolon after a statement, but found none
|
||||||
|
╭─[index.js:1:1]
|
||||||
|
1 │
|
||||||
|
2 │ declare module "a" {
|
||||||
|
· ─
|
||||||
|
3 │ const Foo = 1;
|
||||||
|
╰────
|
||||||
|
help: Try insert a semicolon here
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -187,6 +187,14 @@ impl ExportExportName {
|
||||||
pub fn is_null(&self) -> bool {
|
pub fn is_null(&self) -> bool {
|
||||||
matches!(self, Self::Null)
|
matches!(self, Self::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn span(&self) -> Option<Span> {
|
||||||
|
match self {
|
||||||
|
Self::Name(name) => Some(name.span()),
|
||||||
|
Self::Default(span) => Some(*span),
|
||||||
|
Self::Null => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `LocalName` for `ExportEntry`
|
/// `LocalName` for `ExportEntry`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue