diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 942e77f5867a6..7892bd796fe79 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -24,6 +24,7 @@ mod import { pub mod no_named_as_default_member; pub mod no_self_import; pub mod no_webpack_loader_syntax; + pub mod unambiguous; } mod eslint { @@ -634,6 +635,7 @@ oxc_macros::declare_all_lint_rules! { import::no_named_as_default_member, import::no_self_import, import::no_webpack_loader_syntax, + import::unambiguous, jest::consistent_test_it, jest::expect_expect, jest::max_expects, diff --git a/crates/oxc_linter/src/rules/import/unambiguous.rs b/crates/oxc_linter/src/rules/import/unambiguous.rs new file mode 100644 index 0000000000000..810fb9b023802 --- /dev/null +++ b/crates/oxc_linter/src/rules/import/unambiguous.rs @@ -0,0 +1,82 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule}; + +fn unambiguous_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("This module could be mistakenly parsed as script instead of module") + .with_help("Add at least one import or export statement to unambiguously mark this file as a module") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct Unambiguous; + +declare_oxc_lint!( + /// ### What it does + /// + /// Warn if a `module` could be mistakenly parsed as a `script` and not pure ESM module + /// + /// ### Why is this bad? + /// + /// For ESM-only environments helps to determine files that not pure ESM modules + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// function x() {} + /// + /// (function x() { return 42 })() + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// import 'foo' + /// function x() { return 42 } + /// + /// export function x() { return 42 } + /// + /// (function x() { return 42 })() + /// export {} // simple way to mark side-effects-only file as 'module' without any imports/exports + /// ``` + Unambiguous, + restriction +); + +/// +impl Rule for Unambiguous { + fn run_once(&self, ctx: &LintContext<'_>) { + if ctx.semantic().module_record().not_esm { + ctx.diagnostic(unambiguous_diagnostic(Span::default())); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + // looks like in original rule that should pass for ecmaVersion before 2015 + // r"function x() {}", + // r#""use strict"; function y() {}"#, + r#"import y from "z"; function x() {}"#, + r#"import * as y from "z"; function x() {}"#, + r#"import { y } from "z"; function x() {}"#, + r#"import z, { y } from "z"; function x() {}"#, + "function x() {}; export {}", + "function x() {}; export { x }", + r#"function x() {}; export { y } from "z""#, + r#"function x() {}; export * as y from "z""#, + "export function x() {}", + ]; + + let fail = vec![r"function x() {}", r"(function x() { return 42 })()"]; + + Tester::new(Unambiguous::NAME, pass, fail) + .change_rule_path("index.ts") + .with_import_plugin(true) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/unambiguous.snap b/crates/oxc_linter/src/snapshots/unambiguous.snap new file mode 100644 index 0000000000000..e21dd93377e21 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/unambiguous.snap @@ -0,0 +1,16 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-import(unambiguous): This module could be mistakenly parsed as script instead of module + ╭─[index.ts:1:1] + 1 │ function x() {} + · ▲ + ╰──── + help: Add at least one import or export statement to unambiguously mark this file as a module + + ⚠ eslint-plugin-import(unambiguous): This module could be mistakenly parsed as script instead of module + ╭─[index.ts:1:1] + 1 │ (function x() { return 42 })() + · ▲ + ╰──── + help: Add at least one import or export statement to unambiguously mark this file as a module