From ba9c750f68e9189b5ad439c33f95af565f50aa83 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 17 Jan 2026 09:10:49 +0000 Subject: [PATCH] feat(span)!: use `ModuleKind::CommonJS` for `.cjs` and `.cts` file extensions (#18117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Updates `SourceType::from_path()` to return `ModuleKind::CommonJS` for `.cjs`/`.cts` files (previously returned `ModuleKind::Script`) - Fixes `eslint/no-eval` to check `!is_module()` instead of `is_script()` to handle both scripts and CommonJS (matching ESLint's `sourceType: "commonjs"` behavior) - Fixes `eslint/no-redeclare` to run for both scripts and CommonJS files - Updates tests to use proper CommonJS assertions This follows up on #18089 which added `ModuleKind::CommonJS` support but didn't update the file extension mapping. ## Test plan - [x] `cargo test -p oxc_span` - [x] `cargo test -p oxc_linter` ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) --- .../fixtures/languageOptions/output.snap.md | 4 +- .../src/loader/partial_loader/vue.rs | 5 ++- crates/oxc_linter/src/rules/eslint/no_eval.rs | 4 +- .../src/rules/eslint/no_redeclare.rs | 6 +-- crates/oxc_span/src/source_type.rs | 15 +++++-- tasks/coverage/snapshots/parser_misc.snap | 40 +------------------ tasks/coverage/snapshots/semantic_misc.snap | 14 +------ 7 files changed, 26 insertions(+), 62 deletions(-) diff --git a/apps/oxlint/test/fixtures/languageOptions/output.snap.md b/apps/oxlint/test/fixtures/languageOptions/output.snap.md index 4abc4cf6b799d..74125083ac3ae 100644 --- a/apps/oxlint/test/fixtures/languageOptions/output.snap.md +++ b/apps/oxlint/test/fixtures/languageOptions/output.snap.md @@ -4,9 +4,9 @@ # stdout ``` x language-options-plugin(lang): languageOptions: - | sourceType: script + | sourceType: commonjs | ecmaVersion: 2026 - | parserOptions: {"sourceType":"script"} + | parserOptions: {"sourceType":"commonjs"} | globals: {} | env: {"builtin":true} ,-[files/index.cjs:1:1] diff --git a/crates/oxc_linter/src/loader/partial_loader/vue.rs b/crates/oxc_linter/src/loader/partial_loader/vue.rs index 14804f6870666..c66b3510ee4bf 100644 --- a/crates/oxc_linter/src/loader/partial_loader/vue.rs +++ b/crates/oxc_linter/src/loader/partial_loader/vue.rs @@ -316,7 +316,10 @@ mod test { let cases = [ ("", Some(SourceType::mjs())), ("", Some(SourceType::tsx())), - (r#""#, Some(SourceType::cjs())), + ( + r#""#, + Some(SourceType::from_extension("cjs").unwrap()), + ), ("", Some(SourceType::tsx())), ("", None), (r#""#, None), diff --git a/crates/oxc_linter/src/rules/eslint/no_eval.rs b/crates/oxc_linter/src/rules/eslint/no_eval.rs index b31affd1b8acd..fa966421209b5 100644 --- a/crates/oxc_linter/src/rules/eslint/no_eval.rs +++ b/crates/oxc_linter/src/rules/eslint/no_eval.rs @@ -200,7 +200,9 @@ impl Rule for NoEval { } let is_valid = if scope_flags.is_top() { - ctx.semantic().source_type().is_script() + // In scripts and CommonJS, `this` at top level refers to the global object + // In ES modules, `this` at top level is undefined + !ctx.semantic().source_type().is_module() } else { let node = ctx.nodes().get_node(ctx.scoping().get_node_id(scope_id)); ast_util::is_default_this_binding(ctx, node, true) diff --git a/crates/oxc_linter/src/rules/eslint/no_redeclare.rs b/crates/oxc_linter/src/rules/eslint/no_redeclare.rs index caa0049a1bd48..ec188fcf016a9 100644 --- a/crates/oxc_linter/src/rules/eslint/no_redeclare.rs +++ b/crates/oxc_linter/src/rules/eslint/no_redeclare.rs @@ -2,7 +2,7 @@ use javascript_globals::GLOBALS; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::{ModuleKind, Span}; +use oxc_span::Span; use schemars::JsonSchema; use serde::Deserialize; @@ -122,8 +122,8 @@ impl Rule for NoRedeclare { } fn should_run(&self, ctx: &ContextHost) -> bool { - // Modules run in their own scope, and don't conflict with existing globals - ctx.source_type().module_kind() == ModuleKind::Script + // ES modules run in their own scope, and don't conflict with existing globals + !ctx.source_type().is_module() } } diff --git a/crates/oxc_span/src/source_type.rs b/crates/oxc_span/src/source_type.rs index 04720dd960841..84ba3f7509014 100644 --- a/crates/oxc_span/src/source_type.rs +++ b/crates/oxc_span/src/source_type.rs @@ -194,7 +194,7 @@ impl From for SourceType { let module_kind = match file_ext { Js | Tsx | Ts | Jsx | Mts | Mjs => ModuleKind::Module, - Cjs | Cts => ModuleKind::Script, + Cjs | Cts => ModuleKind::CommonJS, }; let variant = match file_ext { @@ -665,9 +665,14 @@ mod tests { assert!(!ts.is_script()); assert!(!mts.is_script()); - assert!(cts.is_script()); + assert!(!cts.is_script()); assert!(!tsx.is_script()); + assert!(!ts.is_commonjs()); + assert!(!mts.is_commonjs()); + assert!(cts.is_commonjs()); + assert!(!tsx.is_commonjs()); + assert!(ts.is_strict()); assert!(mts.is_strict()); assert!(!cts.is_strict()); @@ -700,7 +705,11 @@ mod tests { assert!(!dts.is_script()); assert!(!dmts.is_script()); - assert!(dcts.is_script()); + assert!(!dcts.is_script()); + + assert!(!dts.is_commonjs()); + assert!(!dmts.is_commonjs()); + assert!(dcts.is_commonjs()); assert!(dts.is_strict()); assert!(dmts.is_strict()); diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index 9d516d25503cf..f1dc25bcf3de6 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -1,47 +1,9 @@ parser_misc Summary: AST Parsed : 59/59 (100.00%) -Positive Passed: 55/59 (93.22%) +Positive Passed: 59/59 (100.00%) Negative Passed: 128/129 (99.22%) Expect Syntax Error: tasks/coverage/misc/fail/script-top-level-using.js -Expect to Parse: tasks/coverage/misc/pass/commonjs-top-level-new-target.cjs - - ร— Unexpected new.target expression - โ•ญโ”€[misc/pass/commonjs-top-level-new-target.cjs:2:1] - 1 โ”‚ // CommonJS allows top-level new.target because the file is wrapped in a function - 2 โ”‚ new.target; - ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โ•ฐโ”€โ”€โ”€โ”€ - help: new.target is only allowed in constructors and functions invoked using the `new` operator - -Expect to Parse: tasks/coverage/misc/pass/commonjs-top-level-new-target.cts - - ร— Unexpected new.target expression - โ•ญโ”€[misc/pass/commonjs-top-level-new-target.cts:2:1] - 1 โ”‚ // TypeScript CommonJS allows top-level new.target - 2 โ”‚ new.target; - ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โ•ฐโ”€โ”€โ”€โ”€ - help: new.target is only allowed in constructors and functions invoked using the `new` operator - -Expect to Parse: tasks/coverage/misc/pass/commonjs-top-level-return.cjs - - ร— TS(1108): A 'return' statement can only be used within a function body. - โ•ญโ”€[misc/pass/commonjs-top-level-return.cjs:2:1] - 1 โ”‚ // CommonJS allows top-level return because the file is wrapped in a function - 2 โ”‚ return 42; - ยท โ”€โ”€โ”€โ”€โ”€โ”€ - โ•ฐโ”€โ”€โ”€โ”€ - -Expect to Parse: tasks/coverage/misc/pass/commonjs-top-level-return.cts - - ร— TS(1108): A 'return' statement can only be used within a function body. - โ•ญโ”€[misc/pass/commonjs-top-level-return.cts:2:1] - 1 โ”‚ // TypeScript CommonJS allows top-level return - 2 โ”‚ return 42; - ยท โ”€โ”€โ”€โ”€โ”€โ”€ - โ•ฐโ”€โ”€โ”€โ”€ - ร— Cannot assign to 'arguments' in strict mode โ•ญโ”€[misc/fail/arguments-eval.ts:1:10] diff --git a/tasks/coverage/snapshots/semantic_misc.snap b/tasks/coverage/snapshots/semantic_misc.snap index 4515290530afa..82e4ad41286a6 100644 --- a/tasks/coverage/snapshots/semantic_misc.snap +++ b/tasks/coverage/snapshots/semantic_misc.snap @@ -1,18 +1,6 @@ semantic_misc Summary: AST Parsed : 59/59 (100.00%) -Positive Passed: 36/59 (61.02%) -semantic Error: tasks/coverage/misc/pass/commonjs-top-level-new-target.cjs -Unexpected new.target expression - -semantic Error: tasks/coverage/misc/pass/commonjs-top-level-new-target.cts -Unexpected new.target expression - -semantic Error: tasks/coverage/misc/pass/commonjs-top-level-return.cjs -A 'return' statement can only be used within a function body. - -semantic Error: tasks/coverage/misc/pass/commonjs-top-level-return.cts -A 'return' statement can only be used within a function body. - +Positive Passed: 40/59 (67.80%) semantic Error: tasks/coverage/misc/pass/declare-let-private.ts Bindings mismatch: after transform: ScopeId(0): ["private"]