Skip to content

Commit

Permalink
feat(linter): no-barrel-file
Browse files Browse the repository at this point in the history
closes #3069
closes #3215
  • Loading branch information
rzvxa authored and Boshen committed May 15, 2024
1 parent 754d9f4 commit 4bbfa7c
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 122 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ oxc_traverse = { version = "0.13.0", path = "crates/oxc_traverse" }
oxc_macros = { path = "crates/oxc_macros" }
oxc_linter = { path = "crates/oxc_linter" }
oxc_prettier = { path = "crates/oxc_prettier" }
oxc_module_lexer = { path = "crates/oxc_module_lexer" }
oxc_tasks_common = { path = "tasks/common" }

napi = "2"
Expand Down
10 changes: 10 additions & 0 deletions crates/oxc_ast/src/ast/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2660,6 +2660,16 @@ macro_rules! match_module_declaration {
pub use match_module_declaration;

impl<'a> ModuleDeclaration<'a> {
pub fn is_type(&self) -> bool {
match self {
Self::TSExportAssignment(_) | Self::TSNamespaceExportDeclaration(_) => true,
Self::ExportNamedDeclaration(decl) => decl.export_kind.is_type(),
Self::ExportAllDeclaration(decl) => decl.export_kind.is_type(),
Self::ImportDeclaration(decl) => decl.import_kind.is_type(),
Self::ExportDefaultDeclaration(_) => false,
}
}

pub fn is_import(&self) -> bool {
matches!(self, Self::ImportDeclaration(_))
}
Expand Down
20 changes: 10 additions & 10 deletions crates/oxc_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ workspace = true
doctest = false

[dependencies]
oxc_allocator = { workspace = true }
oxc_parser = { workspace = true }
oxc_span = { workspace = true }
oxc_ast = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_macros = { workspace = true }
oxc_semantic = { workspace = true }
oxc_syntax = { workspace = true }
oxc_codegen = { workspace = true }
oxc_resolver = { workspace = true }
oxc_allocator = { workspace = true }
oxc_parser = { workspace = true }
oxc_span = { workspace = true }
oxc_ast = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_macros = { workspace = true }
oxc_semantic = { workspace = true }
oxc_syntax = { workspace = true }
oxc_codegen = { workspace = true }
oxc_resolver = { workspace = true }

rayon = { workspace = true }
lazy_static = { workspace = true } # used in oxc_macros
Expand Down
167 changes: 98 additions & 69 deletions crates/oxc_linter/src/rules/oxc/no_barrel_file.rs
Original file line number Diff line number Diff line change
@@ -1,115 +1,144 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;

use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use oxc_semantic::ModuleRecord;
use oxc_syntax::module_graph_visitor::{ModuleGraphVisitorBuilder, VisitFoldWhile};

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

fn no_barrel_file_diagnostic(span: Span, x1: u32) -> OxcDiagnostic {
OxcDiagnostic::warning(format!(
"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 {x1} modules."
))
.with_help("For more information visit this link: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/>")
.with_label(span)
#[derive(Debug, Clone)]
pub struct NoBarrelFile {
threshold: usize,
}

/// Minimum amount of exports to consider module as barrelfile
const AMOUNT_OF_EXPORTS_TO_CONSIDER_MODULE_AS_BARREL: u8 = 3;

/// <https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/avoid-barrel-files.md>
#[derive(Debug, Default, Clone)]
pub struct NoBarrelFile;
impl Default for NoBarrelFile {
fn default() -> Self {
Self { threshold: 100 }
}
}

declare_oxc_lint!(
/// ### What it does
///
/// Disallow the use of barrel files.
/// Disallow the use of barrel files where the file contains `export *` statements,
/// and the total number of modules exceed a threshold.
///
/// The default threshold is 100;
///
/// References:
///
/// * <https://github.com/thepassle/eslint-plugin-barrel-files>
/// * <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7>
///
/// ### Example
///
/// Invalid:
///
/// ```javascript
/// export { foo } from 'foo';
/// export { bar } from 'bar';
/// export { baz } from 'baz';
/// export { qux } from 'qux';
/// export * from 'foo'; // where `foo` loads a subtree of 100 modules
/// import * as ns from 'foo'; // where `foo` loads a subtree of 100 modules
/// ```
///
/// Valid:
///
/// ```javascript
/// export type { foo } from './foo.js';
/// export { foo } from 'foo';
/// ```
NoBarrelFile,
nursery
restriction
);

impl Rule for NoBarrelFile {
#[allow(clippy::cast_possible_truncation)]
fn from_configuration(value: serde_json::Value) -> Self {
Self {
threshold: value
.get(0)
.and_then(|config| config.get("threshold"))
.and_then(serde_json::Value::as_u64)
.map_or(NoBarrelFile::default().threshold, |n| n as usize),
}
}

fn run_once(&self, ctx: &LintContext<'_>) {
let semantic = ctx.semantic();
let module_record = semantic.module_record();
let Some(root) = semantic.nodes().root_node() else {
// Return early if the semantic's root node isn't set.
// It usually means we are running on an empty or invalid file.

if module_record.not_esm {
return;
};

let AstKind::Program(program) = root.kind() else { unreachable!() };

let declarations =
program
.body
.iter()
.fold(0, |acc, node| if node.is_declaration() { acc + 1 } else { 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(no_barrel_file_diagnostic(program.span, loaded_modules_count));
}

let module_requests = module_record
.indirect_export_entries
.iter()
.chain(module_record.star_export_entries.iter())
.filter_map(|export_entry| {
if let Some(module_request) = &export_entry.module_request {
let import_name = &export_entry.import_name;
if import_name.is_all() || import_name.is_all_but_default() {
return Some(module_request);
}
}
None
})
.collect::<Vec<_>>();

let mut labels = vec![];
let mut total: usize = 0;

for module_request in module_requests {
if let Some(remote_module) = module_record.loaded_modules.get(module_request.name()) {
if let Some(count) = count_loaded_modules(remote_module.value()) {
total += count;
labels.push(LabeledSpan::new_with_span(
Some(format!("{count} modules")),
module_request.span(),
));
}
};
}

if total >= self.threshold {
let diagnostic = OxcDiagnostic::warning(format!(
"oxc(no-barrel-file): Barrel file detected, {total} modules are loaded."
))
.with_help(format!("Loading {total} modules is slow for runtimes and bundlers.\nSee also: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7>."))
.with_labels(labels);
ctx.diagnostic(diagnostic);
}
}
}

fn count_loaded_modules(module_record: &ModuleRecord) -> Option<usize> {
if module_record.loaded_modules.is_empty() {
return None;
}
Some(
ModuleGraphVisitorBuilder::default()
.visit_fold(0, module_record, |acc, _, _| VisitFoldWhile::Next(acc + 1))
.result,
)
}

#[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 };"#,
(r#"export type * from "foo";"#, None),
(r#"export type { foo } from "foo";"#, None),
(r#"export type * from "foo"; export type { bar } from "bar";"#, None),
(r#"import { foo, bar, baz } from "../import/export-star/models";"#, None),
];

let fail = vec![
let settings = Some(serde_json::json!([{"threshold": 1}]));

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";"#,
];
settings,
)];

Tester::new(NoBarrelFile::NAME, pass, fail)
.change_rule_path("index.ts")
Expand Down
52 changes: 14 additions & 38 deletions crates/oxc_linter/src/snapshots/no_barrel_file.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,18 @@
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";
oxc(no-barrel-file): Barrel file detected, 6 modules are loaded.
╭─[index.ts:1:15]
1export * from "./deep/a.js";
· ──────┬──────
· ╰── 3 modules
2export * from "./deep/b.js";
· ──────┬──────
· ╰── 2 modules
3export * from "./deep/c.js";
· ──────┬──────
· ╰── 1 modules
4export * from "./deep/d.js";
╰────
help: For more information visit this link: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/>

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: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/>

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: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/>

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: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/>
help: Loading 6 modules is slow for runtimes and bundlers.
See also: <https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7>.
15 changes: 10 additions & 5 deletions crates/oxc_syntax/src/module_graph_visitor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::{collections::HashSet, marker::PhantomData, path::PathBuf, sync::Arc};
use std::{marker::PhantomData, path::PathBuf, sync::Arc};

use rustc_hash::FxHashSet;

use oxc_span::CompactStr;

Expand Down Expand Up @@ -92,8 +94,11 @@ impl<'a, T> ModuleGraphVisitorBuilder<'a, T> {
module: &ModuleRecord,
visit: V,
) -> ModuleGraphVisitResult<T> {
let mut visitor =
ModuleGraphVisitor { traversed: HashSet::new(), depth: 0, max_depth: self.max_depth };
let mut visitor = ModuleGraphVisitor {
traversed: FxHashSet::default(),
depth: 0,
max_depth: self.max_depth,
};
let filter = self.filter.unwrap_or_else(|| Box::new(|_, _| true));
let event = self.event.unwrap_or_else(|| Box::new(|_, _, _| {}));
let enter = self.enter.unwrap_or_else(|| Box::new(|_, _| {}));
Expand All @@ -120,7 +125,7 @@ impl<'a, T> Default for ModuleGraphVisitorBuilder<'a, T> {

pub struct ModuleGraphVisitResult<T> {
pub result: T,
pub traversed: HashSet<PathBuf>,
pub traversed: FxHashSet<PathBuf>,
pub max_depth: u32,
}

Expand All @@ -132,7 +137,7 @@ impl<T> ModuleGraphVisitResult<T> {

#[derive(Debug)]
struct ModuleGraphVisitor {
traversed: HashSet<PathBuf>,
traversed: FxHashSet<PathBuf>,
depth: u32,
max_depth: u32,
}
Expand Down

0 comments on commit 4bbfa7c

Please sign in to comment.