Skip to content

Commit 6bc8aec

Browse files
committed
fix(linter): Fix the behavior of import/extensions rule for a file that has multiple extensions. (#18919)
See #18918 for details and a test repo. This fixes the problematic behavior of the Oxlint implementation so we match the implementation from the original import plugin. Basically, we want to have `tsx: never` in the rule config, and in that case it should disallow this: ```tsx import Foo from "./Foo.stories.tsx"; ``` It correctly did that, but if you _removed_ the `tsx` extension, you would end up with an error about not allowing the `stories` extension, which was nonsensical. Now we correctly handle this behavior, so `import Foo from "./Foo.stories";` is allowed for this config setting. Built with Claude Code, reviewed and tested by me. [Another user also confirmed that this fixes the problem for them](#18681 (comment)). Fixes #18918 and #18681.
1 parent 1bf569b commit 6bc8aec

File tree

5 files changed

+58
-3
lines changed

5 files changed

+58
-3
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const Component = () => <div>Component</div>;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function helper() {
2+
return 'helper';
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function testUtil() {
2+
return 'test';
3+
}

crates/oxc_linter/src/rules/import/extensions.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -540,15 +540,27 @@ impl Extensions {
540540
return;
541541
}
542542

543+
// Determine if the extension being checked is actually written in the import
544+
// For files with multiple extensions (e.g., foo.stories.tsx), we need to check
545+
// if the ACTUAL file extension (resolved) matches what's written in the import.
546+
// If resolved is "tsx" but written is "stories", then tsx is NOT written.
547+
let extension_is_written = if let Some(resolved) = resolved_extension {
548+
// If we have a resolved extension, check if it matches the written extension
549+
written_extension == Some(resolved)
550+
} else {
551+
// Otherwise, just check if there's any written extension
552+
written_extension.is_some()
553+
};
554+
543555
if config.should_flag_extension(
544556
ext_str,
545-
written_extension.is_some(),
557+
extension_is_written,
546558
resolved_extension.is_some(),
547559
require_extension,
548560
) {
549-
if let Some(ext) = written_extension {
561+
if extension_is_written {
550562
ctx.diagnostic(extension_should_not_be_included_in_diagnostic(
551-
span, ext, is_import,
563+
span, ext_str, is_import,
552564
));
553565
} else {
554566
ctx.diagnostic(extension_missing_diagnostic(span, is_import));
@@ -1217,6 +1229,11 @@ fn test() {
12171229
r#"import data from "./data.json" with { type: "json" };"#,
12181230
Some(json!(["ignorePackages"])),
12191231
),
1232+
// Files with multiple extensions (e.g., Component.stories.tsx)
1233+
// These test cases use actual fixture files for proper module resolution
1234+
(r"import { Component } from './Component.stories';", Some(json!([{ "tsx": "never" }]))),
1235+
(r"import { testUtil } from './utils.test';", Some(json!([{ "ts": "never" }]))),
1236+
(r"import { helper } from './helper.spec';", Some(json!([{ "js": "never" }]))),
12201237
// Subpath imports
12211238
// https://nodejs.org/api/packages.html#subpath-imports
12221239
// (
@@ -1685,6 +1702,16 @@ fn test() {
16851702
r"import useState from '@foo/bar/useState.ts';",
16861703
Some(json!(["never", { "ignorePackages": true }])),
16871704
),
1705+
// Files with multiple extensions - should fail when actual extension IS included
1706+
(
1707+
r"import Component from './Component.stories.tsx';",
1708+
Some(json!(["never", { "tsx": "never" }])),
1709+
),
1710+
(
1711+
r"import Component from './Component.test.ts';",
1712+
Some(json!(["never", { "ts": "never" }])),
1713+
),
1714+
(r"import utils from './utils.spec.js';", Some(json!(["never", { "js": "never" }]))),
16881715
// TODO: This should probably fail? Needs further investigation.
16891716
// (
16901717
// r"import useState from '@foo/bar/useState';",

crates/oxc_linter/src/snapshots/import_extensions.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,24 @@ source: crates/oxc_linter/src/tester.rs
693693
· ────────────────────────────────────────────
694694
╰────
695695
help: Remove the file extension from this import.
696+
697+
eslint-plugin-import(extensions): File extension "tsx" should not be included in the import declaration.
698+
╭─[extensions.tsx:1:1]
699+
1import Component from './Component.stories.tsx';
700+
· ────────────────────────────────────────────────
701+
╰────
702+
help: Remove the file extension from this import.
703+
704+
eslint-plugin-import(extensions): File extension "ts" should not be included in the import declaration.
705+
╭─[extensions.tsx:1:1]
706+
1import Component from './Component.test.ts';
707+
· ────────────────────────────────────────────
708+
╰────
709+
help: Remove the file extension from this import.
710+
711+
eslint-plugin-import(extensions): File extension "js" should not be included in the import declaration.
712+
╭─[extensions.tsx:1:1]
713+
1import utils from './utils.spec.js';
714+
· ────────────────────────────────────
715+
╰────
716+
help: Remove the file extension from this import.

0 commit comments

Comments
 (0)