From 5cf55c212ebd81e862fe3e97429dc65d4dd21cb7 Mon Sep 17 00:00:00 2001
From: Ali Rezvani <3788964+rzvxa@users.noreply.github.com>
Date: Mon, 22 Apr 2024 05:48:33 +0330
Subject: [PATCH] feat(linter): no barrel file. (#3030)
closes #3004
I've based it on [this plugin](https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/lib/rules/avoid-barrel-files.js) instead of [biome](https://github.com/biomejs/biome/blob/4130ff0e4c56c4944236b11f2b4f34ca6e8ca03a/crates/biome_js_analyze/src/lint/performance/no_barrel_file.rs#L70) Since the original plugin is less likely to detect a false positive.
I didn't understand your statement [here](https://github.com/oxc-project/oxc/issues/3004#issue-2245574895), Where you've mentioned:
> In oxlint, we can do better when --import-plugin is enabled, by displaying the total number of dependencies pulled into a given file by walking
I would appreciate it if you expand upon it.
------
#### Edit:
I've added it under `eslint-plugin-import` even though it is not; I wasn't sure If I should create a whole new category for this single rule, It is a different story if we would like to adopt the other rules in [here](https://github.com/thepassle/eslint-plugin-barrel-files/tree/main/lib/rules).
Also, check my diagnosis messages; It is my first contribution to the linters so I'm just not familiar enough with the conventions of messages, rules, etc.
---
crates/oxc_linter/src/rules.rs | 2 +
.../src/rules/oxc/no_barrel_file.rs | 114 ++++++++++++++++++
.../src/snapshots/no_barrel_file.snap | 43 +++++++
3 files changed, 159 insertions(+)
create mode 100644 crates/oxc_linter/src/rules/oxc/no_barrel_file.rs
create mode 100644 crates/oxc_linter/src/snapshots/no_barrel_file.snap
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index a293dc7b4..041969d1e 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -333,6 +333,7 @@ mod oxc {
pub mod erasing_op;
pub mod misrefactored_assign_op;
pub mod no_accumulating_spread;
+ pub mod no_barrel_file;
pub mod only_used_in_recursion;
}
@@ -671,6 +672,7 @@ oxc_macros::declare_all_lint_rules! {
oxc::erasing_op,
oxc::misrefactored_assign_op,
oxc::no_accumulating_spread,
+ oxc::no_barrel_file,
oxc::only_used_in_recursion,
nextjs::google_font_display,
nextjs::google_font_preconnect,
diff --git a/crates/oxc_linter/src/rules/oxc/no_barrel_file.rs b/crates/oxc_linter/src/rules/oxc/no_barrel_file.rs
new file mode 100644
index 000000000..4e248965f
--- /dev/null
+++ b/crates/oxc_linter/src/rules/oxc/no_barrel_file.rs
@@ -0,0 +1,114 @@
+use oxc_ast::{ast::Statement, AstKind};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+use oxc_syntax::module_graph_visitor::{ModuleGraphVisitorBuilder, VisitFoldWhile};
+
+use crate::{context::LintContext, rule::Rule};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error(
+ "oxc(no-barrel-file): \
+ Avoid barrel files, they slow down performance, \
+ and cause large module graphs with modules that go unused.\n\
+ Loading this barrel file results in importing {1:?} modules."
+)]
+#[diagnostic(severity(warning), help("For more information visit this link: "))]
+struct NoBarrelFileDiagnostic(#[label] pub Span, pub u32);
+
+/// Minimum amount of exports to consider module as barrelfile
+const AMOUNT_OF_EXPORTS_TO_CONSIDER_MODULE_AS_BARREL: u8 = 3;
+
+///
+#[derive(Debug, Default, Clone)]
+pub struct NoBarrelFile;
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// Disallow the use of barrel files.
+ ///
+ /// ### Example
+ ///
+ /// Invalid:
+ /// ```javascript
+ /// export { foo } from 'foo';
+ /// export { bar } from 'bar';
+ /// export { baz } from 'baz';
+ /// export { qux } from 'qux';
+ /// ```
+ /// Valid:
+ /// ```javascript
+ /// export type { foo } from './foo.js';
+ /// ```
+ NoBarrelFile,
+ nursery
+);
+
+impl Rule for NoBarrelFile {
+ fn run_once(&self, ctx: &LintContext<'_>) {
+ let semantic = ctx.semantic();
+ let module_record = semantic.module_record();
+ let root = semantic.nodes().root_node();
+
+ let AstKind::Program(program) = root.kind() else { unreachable!() };
+
+ let declarations = program.body.iter().fold(0, |acc, node| match node {
+ Statement::Declaration(_) => acc + 1,
+ _ => acc,
+ });
+ let exports =
+ module_record.star_export_entries.len() + module_record.indirect_export_entries.len();
+
+ if exports > declarations
+ && exports > AMOUNT_OF_EXPORTS_TO_CONSIDER_MODULE_AS_BARREL as usize
+ {
+ let loaded_modules_count = ModuleGraphVisitorBuilder::default()
+ .visit_fold(0, module_record, |acc, _, _| VisitFoldWhile::Next(acc + 1))
+ .result;
+ ctx.diagnostic(NoBarrelFileDiagnostic(program.span, loaded_modules_count));
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ r#"export type * from "foo";"#,
+ r#"export type { foo } from "foo";"#,
+ r#"export type * from "foo";
+ export type { bar } from "bar";"#,
+ r#"import { foo, bar, baz } from "../feature";
+ export { foo };
+ export { bar };"#,
+ ];
+
+ let fail = vec![
+ r#"export * from "./deep/a.js";
+ export * from "./deep/b.js";
+ export * from "./deep/c.js";
+ export * from "./deep/d.js";"#,
+ r#"export { foo } from "foo";
+ export { bar } from "bar";
+ export { baz } from "baz";
+ export { qux } from "qux";"#,
+ r#"export { default as module1 } from "./module1";
+ export { default as module2 } from "./module2";
+ export { default as module3 } from "./module3";
+ export { default as module4 } from "./module4";"#,
+ r#"export { foo, type Foo } from "foo";
+ export { bar, type Bar } from "bar";
+ export { baz, type Baz } from "baz";
+ export { qux, type Qux } from "qux";"#,
+ ];
+
+ Tester::new(NoBarrelFile::NAME, pass, fail)
+ .change_rule_path("index.ts")
+ .with_import_plugin(true)
+ .test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/no_barrel_file.snap b/crates/oxc_linter/src/snapshots/no_barrel_file.snap
new file mode 100644
index 000000000..38fb1c178
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/no_barrel_file.snap
@@ -0,0 +1,43 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: no_barrel_file
+---
+ ⚠ oxc(no-barrel-file): Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.
+ │ Loading this barrel file results in importing 4 modules.
+ ╭─[index.ts:1:1]
+ 1 │ ╭─▶ export * from "./deep/a.js";
+ 2 │ │ export * from "./deep/b.js";
+ 3 │ │ export * from "./deep/c.js";
+ 4 │ ╰─▶ export * from "./deep/d.js";
+ ╰────
+ help: For more information visit this link:
+
+ ⚠ oxc(no-barrel-file): Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.
+ │ Loading this barrel file results in importing 0 modules.
+ ╭─[index.ts:1:1]
+ 1 │ ╭─▶ export { foo } from "foo";
+ 2 │ │ export { bar } from "bar";
+ 3 │ │ export { baz } from "baz";
+ 4 │ ╰─▶ export { qux } from "qux";
+ ╰────
+ help: For more information visit this link:
+
+ ⚠ oxc(no-barrel-file): Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.
+ │ Loading this barrel file results in importing 0 modules.
+ ╭─[index.ts:1:1]
+ 1 │ ╭─▶ export { default as module1 } from "./module1";
+ 2 │ │ export { default as module2 } from "./module2";
+ 3 │ │ export { default as module3 } from "./module3";
+ 4 │ ╰─▶ export { default as module4 } from "./module4";
+ ╰────
+ help: For more information visit this link:
+
+ ⚠ oxc(no-barrel-file): Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.
+ │ Loading this barrel file results in importing 0 modules.
+ ╭─[index.ts:1:1]
+ 1 │ ╭─▶ export { foo, type Foo } from "foo";
+ 2 │ │ export { bar, type Bar } from "bar";
+ 3 │ │ export { baz, type Baz } from "baz";
+ 4 │ ╰─▶ export { qux, type Qux } from "qux";
+ ╰────
+ help: For more information visit this link: