Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

/// <https://github.com/import-js/eslint-plugin-import>
mod import {
pub mod exports_last;
pub mod no_absolute_path;
pub mod no_mutable_exports;
// pub mod no_deprecated;
Expand Down Expand Up @@ -694,6 +695,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::yoda,
import::default,
import::export,
import::exports_last,
import::first,
import::no_absolute_path,
import::no_mutable_exports,
Expand Down
176 changes: 176 additions & 0 deletions crates/oxc_linter/src/rules/import/exports_last.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use itertools::Itertools;
use oxc_ast::{
AstKind,
ast::{ModuleDeclaration, Statement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

use crate::{context::LintContext, rule::Rule};

fn exports_last_diagnostic(span: Span) -> OxcDiagnostic {
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
OxcDiagnostic::warn("Export statements should appear at the end of the file").with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct ExportsLast;

declare_oxc_lint!(
/// ### What it does
///
/// This rule enforces that all exports are declared at the bottom of the file.
/// This rule will report any export declarations that comes before any non-export statements.
///
/// ### Why is this bad?
///
/// Exports scattered throughout the file can lead to poor code readability
/// and increase the cost of locating the export quickly
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// const bool = true
/// export const foo = 'bar'
/// const str = 'foo'
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// const arr = ['bar']
/// export const bool = true
/// export const str = 'foo'
/// export function func() {
/// console.log('Hello World')
/// }
/// ```
ExportsLast,
import,
style
);

impl Rule for ExportsLast {
fn run_once(&self, ctx: &LintContext<'_>) {
// find last non export declaration index
let Some(root) = ctx.nodes().root_node() else {
return;
};
if let AstKind::Program(program) = root.kind() {
let body = &program.body;
let find_res =
body.iter().rev().find_position(|statement| !is_exports_declaration(statement));
if let Some((index, _)) = find_res {
let end = body.len() - index;
for statement in &body[0..end] {
if is_exports_declaration(statement) {
ctx.diagnostic(exports_last_diagnostic(statement.span()));
}
}
}
}
}
}

fn is_exports_declaration(statement: &Statement) -> bool {
statement.as_module_declaration().is_some_and(|declaration| {
matches!(
declaration,
ModuleDeclaration::ExportAllDeclaration(_)
| ModuleDeclaration::ExportDefaultDeclaration(_)
| ModuleDeclaration::ExportNamedDeclaration(_)
)
})
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
"// comment",
r"
const foo = 'bar'
const bar = 'baz'
",
r"
const arr = ['bar']
export const bool = true
",
r"
const foo = 'bar'
export {foo}
",
r"
const foo = 'bar'
export default foo
",
r"
export default foo
export const bar = true
",
r"
const foo = 'bar'
export default foo
export const bar = true
",
r"
const foo = 'bar'
export default function bar () {
const very = 'multiline'
}
export const baz = true
",
r"
const foo = 'bar'
export default foo
export const so = 'many'
export const exports = ':)'
export const i = 'cant'
export const even = 'count'
export const how = 'many'
",
"export * from './foo'",
r"
const bool = true
const str = 'foo'
export default bool
",
r"
export = 4
let a = 4;
",
];

let fail = vec![
r"
const bool = true
export default bool
const str = 'foo'
",
r"
export const bool = true
const str = 'foo'
",
r"
export const foo = 'bar'
const bar = true
",
r"
export default 'such foo many bar'
export const so = 'many'
const foo = 'bar'
export const exports = ':)'
export const i = 'cant'
export const even = 'count'
export const how = 'many'
",
r"
export * from './foo' ;
const bar = true
",
];

Tester::new(ExportsLast::NAME, ExportsLast::PLUGIN, pass, fail).test_and_snapshot();
}
50 changes: 50 additions & 0 deletions crates/oxc_linter/src/snapshots/import_exports_last.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:3:13]
2 │ const bool = true
3 │ export default bool
· ───────────────────
4 │ const str = 'foo'
╰────

⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:2:13]
1 │
2 │ export const bool = true
· ────────────────────────
3 │ const str = 'foo'
╰────

⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:2:13]
1 │
2 │ export const foo = 'bar'
· ────────────────────────
3 │ const bar = true
╰────

⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:2:13]
1 │
2 │ export default 'such foo many bar'
· ──────────────────────────────────
3 │ export const so = 'many'
╰────

⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:3:13]
2 │ export default 'such foo many bar'
3 │ export const so = 'many'
· ────────────────────────
4 │ const foo = 'bar'
╰────

⚠ eslint-plugin-import(exports-last): Export statements should appear at the end of the file
╭─[exports_last.tsx:2:13]
1 │
2 │ export * from './foo' ;
· ───────────────────────────────
3 │ const bar = true
╰────