mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +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>
|
||||
mod import {
|
||||
pub mod default;
|
||||
pub mod export;
|
||||
pub mod named;
|
||||
pub mod no_amd;
|
||||
pub mod no_cycle;
|
||||
|
|
@ -436,6 +437,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
import::no_cycle,
|
||||
import::no_self_import,
|
||||
import::no_amd,
|
||||
import::export,
|
||||
jsx_a11y::alt_text,
|
||||
jsx_a11y::anchor_has_content,
|
||||
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 {
|
||||
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`
|
||||
|
|
|
|||
Loading…
Reference in a new issue