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
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ working directory: fixtures/overrides_with_plugin
`----
help: Consider removing this declaration.

x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/valid-title.html\eslint-plugin-jest(valid-title)]8;;\: "Should not have an empty title"
,-[index.test.ts:4:6]
! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/expect-expect.html\eslint-plugin-jest(expect-expect)]8;;\: Test has no assertions
,-[index.test.ts:4:3]
3 |
4 | it("", () => {});
: ^^
: ^^
5 | // ^ jest/no-valid-title error as explicitly set in the `.test.ts` override
`----
help: "Write a meaningful title for your test"
help: Add assertion(s) in this Test

! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/expect-expect.html\eslint-plugin-jest(expect-expect)]8;;\: Test has no assertions
,-[index.test.ts:4:3]
x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/valid-title.html\eslint-plugin-jest(valid-title)]8;;\: "Should not have an empty title"
,-[index.test.ts:4:6]
3 |
4 | it("", () => {});
: ^^
: ^^
5 | // ^ jest/no-valid-title error as explicitly set in the `.test.ts` override
`----
help: Add assertion(s) in this Test
help: "Write a meaningful title for your test"

Found 2 warnings and 2 errors.
Finished in <variable>ms on 2 files with 87 rules using 1 threads.
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod import {
pub mod no_self_import;
pub mod no_unassigned_import;
pub mod no_webpack_loader_syntax;
pub mod prefer_default_export;
pub mod unambiguous;
}

Expand Down Expand Up @@ -743,6 +744,7 @@ oxc_macros::declare_all_lint_rules! {
import::no_named_as_default_member,
import::no_self_import,
import::no_webpack_loader_syntax,
import::prefer_default_export,
import::unambiguous,
jest::consistent_test_it,
jest::expect_expect,
Expand Down
320 changes: 320 additions & 0 deletions crates/oxc_linter/src/rules/import/prefer_default_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use serde_json::Value;

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

fn prefer_default_export_diagnostic(span: Span, target: Target) -> OxcDiagnostic {
let msg = if target == Target::Single {
"Prefer default export on a file with single export."
} else {
"Prefer default export to be present on every file that has export."
};
OxcDiagnostic::warn(msg).with_help("Prefer a default export").with_label(span)
}

#[derive(Debug, Default, PartialEq, Clone, Copy)]
enum Target {
#[default]
Single,
Any,
}

impl From<&str> for Target {
fn from(raw: &str) -> Self {
if raw == "any" { Self::Any } else { Self::Single }
}
}

#[derive(Debug, Default, Clone)]
pub struct PreferDefaultExport {
target: Target,
}

declare_oxc_lint!(
/// ### What it does
///
/// In exporting files, this rule checks if there is default export or not.
///
/// ### Why is this bad?
///
/// This rule exists to standardize module exports by preferring default exports
/// when a module only has one export, enhancing readability, maintainability.
///
/// ### Examples
///
/// Examples of **incorrect** code for the `{ target: "single" }` option:
/// ```js
/// export const foo = 'foo';
/// ```
///
/// Examples of **correct** code for the `{ target: "single" }` option:
/// ```js
/// export const foo = 'foo';
/// const bar = 'bar';
/// export default bar;
/// ```
///
/// Examples of **incorrect** code for the `{ target: "any" }` option:
/// ```js
/// export const foo = 'foo';
/// export const baz = 'baz';
/// ```
///
/// Examples of **correct** code for the `{ target: "any" }` option:
/// ```js
/// export default function bar() {};
/// ```
PreferDefaultExport,
import,
style,
);

impl Rule for PreferDefaultExport {
fn from_configuration(value: Value) -> Self {
let obj = value.get(0);
Self {
target: obj
.and_then(|v| v.get("target"))
.and_then(Value::as_str)
.map(Target::from)
.unwrap_or_default(),
}
}

fn run_once(&self, ctx: &LintContext<'_>) {
let module_record = ctx.module_record();
if module_record.export_default.is_some() {
return;
}
let star_export_entries = &module_record.star_export_entries;

if !star_export_entries.is_empty() {
return;
}
let indirect_entries = &module_record.indirect_export_entries;
let local_entries = &module_record.local_export_entries;

if exist_type(indirect_entries) || exist_type(local_entries) {
return;
}

if self.target == Target::Single {
if indirect_entries.len() + local_entries.len() == 1 {
for entry in indirect_entries {
ctx.diagnostic(prefer_default_export_diagnostic(entry.span, self.target));
}
for entry in local_entries {
ctx.diagnostic(prefer_default_export_diagnostic(
entry.statement_span,
self.target,
));
}
}
} else {
// find the last export statement
if let Some(last_export_span) = indirect_entries
.iter()
.chain(local_entries.iter())
.max_by_key(|entry| entry.statement_span.start)
{
ctx.diagnostic(prefer_default_export_diagnostic(
last_export_span.statement_span,
self.target,
));
}
}
}
}

fn exist_type(export_entries: &[ExportEntry]) -> bool {
export_entries.iter().any(|entry| entry.is_type)
}

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

let pass = vec![
("export default a", None),
("export const { foo, bar } = item;", None),
("export const { foo, bar: baz } = item;", None),
("export const { foo: { bar, baz } } = item;", None),
("export const [a, b] = item;", None),
(
"
let item;
export const foo = item;
export { item };
",
None,
),
(
"
let foo;
export { foo as default }
",
None,
),
("export const [CounterProvider,, withCounter] = func()", None),
("export * from './foo'", None),
("export default function bar() {};", None),
(
"
export const foo = 'foo';
export const bar = 'bar';
",
None,
),
(
"
export const foo = 'foo';
export function bar() {};
",
None,
),
(
"
export const foo = 'foo';
export default bar;
",
None,
),
(
"
let foo, bar;
export { foo, bar }
",
None,
),
(
"
export default a;
export * from './foo'
export const a = 3;
export { MemoryValue } from './Memory'
",
None,
),
("export type foo = string", None),
("import * as foo from './foo'", None),
("export type UserId = number;", None),
("export { a, b } from 'foo.js'", None),
("let foo; export { foo as 'default' };", None),
("export default function bar() {};", Some(json!([{"target": "any"}]))),
(
"
export const foo = 'foo';
export const bar = 'bar';
export default 42;
",
Some(json!([{"target": "any"}])),
),
("export default a = 2;", Some(json!([{"target": "any"}]))),
(
"
export const a = 2;
export default function foo() {};
",
Some(json!([{"target": "any"}])),
),
(
"
export const a = 5;
export function bar(){};
let foo;
export { foo as default }
",
Some(json!([{"target": "any"}])),
),
("export * from './foo';", Some(json!([{"target": "any"}]))),
("import * as foo from './foo';", Some(json!([{"target": "any"}]))),
("const a = 5;", Some(json!([{"target": "any"}]))),
(
"export const a = 4; let foo; export { foo as 'default' };",
Some(json!([{"target": "any"}])),
),
(
"
export type foo = string;
export type bar = number;
",
None,
),
(
"
export const a = 2;
export { a } from './c';
export type bar = number;
",
Some(json!([{"target": "any"}])),
),
];

let fail = vec![
("export const a = 3", None),
("export { MemoryValue } from './Memory'", None),
("export const a = 3", Some(json!([{"target": "any"}]))),
("export function bar() {}", None),
("export const foo = 'foo';", None),
("const foo = 'foo'; export { foo };", None),
("export const { foo } = { foo: 'bar' };", None),
("export const { foo: { bar } } = { foo: { bar: 'baz' } };", None),
("export const [a] = ['foo']", None),
(
"
export const foo = 'foo'
export const bar = 'bar';
",
Some(json!([{"target": "any"}])),
),
(
"
export const foo = 'foo';
export function bar() {};
",
Some(json!([{"target": "any"}])),
),
(
"
let foo, bar;
export { foo, bar }
",
Some(json!([{"target": "any"}])),
),
(
"
let item;
export const foo = item;
export { item };
",
Some(json!([{"target": "any"}])),
),
("export { a, b } from 'foo.js'", Some(json!([{"target": "any"}]))),
(
"
const foo = 'foo';
export { foo };
",
Some(json!([{"target": "any"}])),
),
("export const { foo } = { foo: 'bar' };", Some(json!([{"target": "any"}]))),
(
"export const { foo: { bar } } = { foo: { bar: 'baz' } };",
Some(json!([{"target": "any"}])),
),
(
"
export const b = 3;
export const r = 3;
export { last } from './Memory'
",
Some(json!([{"target": "any"}])),
),
];

Tester::new(PreferDefaultExport::NAME, PreferDefaultExport::PLUGIN, pass, fail)
.test_and_snapshot();
}
Loading
Loading