Skip to content

Commit 81e179c

Browse files
connorsheaautofix-ci[bot]camc314
authored
fix(linter): Allow file extensions without a dot in react/jsx-filename-extension rule (#15574)
Allow `tsx` to work fine for the rule config, previously it would be thrown out. Also update the help diagnostic to note which extensions are allowed by the current config, and add a few more tests. Fixes #15508 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cameron Clark <cameron.clark@hey.com>
1 parent 3eefa15 commit 81e179c

File tree

2 files changed

+147
-37
lines changed

2 files changed

+147
-37
lines changed

crates/oxc_linter/src/rules/react/jsx_filename_extension.rs

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
use std::ffi::OsStr;
22

3+
use itertools::Itertools;
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
36
use serde_json::Value;
47

58
use oxc_ast::AstKind;
69
use oxc_diagnostics::OxcDiagnostic;
710
use oxc_macros::declare_oxc_lint;
811
use oxc_span::{CompactStr, GetSpan, Span};
9-
use schemars::JsonSchema;
10-
use serde::{Deserialize, Serialize};
1112

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

14-
fn no_jsx_with_filename_extension_diagnostic(ext: &str, span: Span) -> OxcDiagnostic {
15-
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
15+
fn no_jsx_with_filename_extension_diagnostic(
16+
ext: &str,
17+
span: Span,
18+
allowed_extensions: &[CompactStr],
19+
) -> OxcDiagnostic {
1620
OxcDiagnostic::warn(format!("JSX not allowed in files with extension '.{ext}'"))
17-
.with_help("Rename the file with a good extension.")
21+
.with_help(format!(
22+
"Rename the file to use an allowed extension: {}",
23+
allowed_extensions.iter().map(|e| format!(".{e}")).join(", ")
24+
))
1825
.with_label(span)
1926
}
2027

2128
fn extension_only_for_jsx_diagnostic(ext: &str) -> OxcDiagnostic {
22-
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
2329
OxcDiagnostic::warn(format!("Only files containing JSX may use the extension '.{ext}'"))
2430
.with_help("Rename the file with a good extension.")
2531
}
@@ -51,6 +57,7 @@ pub struct JsxFilenameExtensionConfig {
5157
/// Set this to `as-needed` to only allow JSX file extensions in files that contain JSX syntax.
5258
allow: AllowType,
5359
/// The set of allowed file extensions.
60+
/// Can include or exclude the leading dot (e.g., "jsx" and ".jsx" are both valid).
5461
extensions: Vec<CompactStr>,
5562
/// If enabled, files that do not contain code (i.e. are empty, contain only whitespaces or comments) will not be rejected.
5663
ignore_files_without_code: bool,
@@ -77,11 +84,12 @@ impl std::ops::Deref for JsxFilenameExtension {
7784
declare_oxc_lint!(
7885
/// ### What it does
7986
///
80-
/// Enforces consistent use of the JSX file extension.
87+
/// Enforces consistent use of the `.jsx` file extension.
8188
///
8289
/// ### Why is this bad?
8390
///
8491
/// Some bundlers or parsers need to know by the file extension that it contains JSX
92+
/// in order to properly handle the files.
8593
///
8694
/// ### Examples
8795
///
@@ -127,11 +135,10 @@ impl Rule for JsxFilenameExtension {
127135
.and_then(Value::as_array)
128136
.map(|v| {
129137
v.iter()
130-
.filter_map(serde_json::Value::as_str)
131-
.filter(|&s| s.starts_with('.'))
132-
.map(|s| &s[1..])
133-
.map(CompactStr::from)
134-
.collect()
138+
.filter_map(Value::as_str)
139+
.map(|s| CompactStr::from(s.strip_prefix('.').unwrap_or(s)))
140+
.unique()
141+
.collect::<Vec<_>>()
135142
})
136143
.unwrap_or(vec![CompactStr::from("jsx")]);
137144

@@ -151,6 +158,7 @@ impl Rule for JsxFilenameExtension {
151158
ctx.diagnostic(no_jsx_with_filename_extension_diagnostic(
152159
file_extension,
153160
jsx_elt.span(),
161+
&self.extensions,
154162
));
155163
}
156164
return;
@@ -184,13 +192,13 @@ fn test() {
184192
Some(PathBuf::from("foo.jsx")),
185193
),
186194
(
187-
"export default function MyComponent() { return <Comp />;}",
195+
"export default function MyComponent() { return <Comp />; }",
188196
None,
189197
None,
190198
Some(PathBuf::from("foo.jsx")),
191199
),
192200
(
193-
"export function MyComponent() { return <div><Comp /></div>;}",
201+
"export function MyComponent() { return <div><Comp /></div>; }",
194202
None,
195203
None,
196204
Some(PathBuf::from("foo.jsx")),
@@ -202,7 +210,7 @@ fn test() {
202210
Some(PathBuf::from("foo.jsx")),
203211
),
204212
(
205-
"export function MyComponent() { return <div><Comp /></div>;}",
213+
"export function MyComponent() { return <div><Comp /></div>; }",
206214
Some(serde_json::json!([{ "allow": "as-needed" }])),
207215
None,
208216
Some(PathBuf::from("foo.jsx")),
@@ -220,13 +228,13 @@ fn test() {
220228
Some(PathBuf::from("foo.jsx")),
221229
),
222230
(
223-
"export function MyComponent() { return <><Comp /><Comp /></>;}",
231+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
224232
None,
225233
None,
226234
Some(PathBuf::from("foo.jsx")),
227235
),
228236
(
229-
"export function MyComponent() { return <><Comp /><Comp /></>;}",
237+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
230238
Some(serde_json::json!([{ "allow": "as-needed" }])),
231239
None,
232240
Some(PathBuf::from("foo.jsx")),
@@ -253,7 +261,7 @@ fn test() {
253261
Some(PathBuf::from("foo.js")),
254262
),
255263
(
256-
"export function MyComponent() { return <div><Comp /></div>;}",
264+
"export function MyComponent() { return <div><Comp /></div>; }",
257265
Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])),
258266
None,
259267
Some(PathBuf::from("foo.js")),
@@ -265,7 +273,7 @@ fn test() {
265273
Some(PathBuf::from("foo.js")),
266274
),
267275
(
268-
"export function MyComponent() { return <><Comp /><Comp /></>;}",
276+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
269277
Some(serde_json::json!([{ "extensions": [".js", ".jsx"] }])),
270278
None,
271279
Some(PathBuf::from("foo.js")),
@@ -276,6 +284,25 @@ fn test() {
276284
None,
277285
Some(PathBuf::from("foo.js")),
278286
),
287+
// Test that a commented-out JSX code snippet does not count.
288+
(
289+
"// export function MyComponent() { return <><Comp /><Comp /></>;}\n",
290+
Some(serde_json::json!([{ "allow": "as-needed", "ignoreFilesWithoutCode": true }])),
291+
None,
292+
Some(PathBuf::from("foo.js")),
293+
),
294+
(
295+
"// export function MyComponent() { return <><Comp /><Comp /></>;}\nconsole.log('code');",
296+
Some(serde_json::json!([{ "allow": "as-needed" }])),
297+
None,
298+
Some(PathBuf::from("foo.js")),
299+
),
300+
(
301+
"/* export function MyComponent() { return <><Comp /><Comp /></>;} */\nconsole.log('code');",
302+
Some(serde_json::json!([{ "allow": "as-needed" }])),
303+
None,
304+
Some(PathBuf::from("foo.js")),
305+
),
279306
(
280307
"//test\n\n//comment",
281308
Some(serde_json::json!([{ "allow": "as-needed", "ignoreFilesWithoutCode": true }])),
@@ -288,6 +315,33 @@ fn test() {
288315
None,
289316
Some(PathBuf::from("foo.jsx")),
290317
),
318+
// Test that extensions without leading dot work (e.g., "tsx" instead of ".tsx")
319+
(
320+
"module.exports = function MyComponent() { return <div>jsx\n<div />\n</div>; }",
321+
Some(serde_json::json!([{ "extensions": ["tsx", ".jsx"] }])),
322+
None,
323+
Some(PathBuf::from("foo.tsx")),
324+
),
325+
(
326+
"export default function MyComponent() { return <Comp />; }",
327+
Some(serde_json::json!([{ "extensions": ["tsx"] }])),
328+
None,
329+
Some(PathBuf::from("foo.tsx")),
330+
),
331+
// Test that identical extensions are de-duplicated and still allowed
332+
(
333+
"export default function MyComponent() { return <Comp />; }",
334+
Some(serde_json::json!([{ "extensions": ["tsx", ".tsx"] }])),
335+
None,
336+
Some(PathBuf::from("foo.tsx")),
337+
),
338+
// Test that mixing extensions with and without dots works
339+
(
340+
"export function MyComponent() { return <div><Comp /></div>; }",
341+
Some(serde_json::json!([{ "extensions": [".jsx", "tsx"] }])),
342+
None,
343+
Some(PathBuf::from("baz.tsx")),
344+
),
291345
];
292346

293347
let fail = vec![
@@ -298,13 +352,13 @@ fn test() {
298352
Some(PathBuf::from("foo.js")),
299353
),
300354
(
301-
"export default function MyComponent() { return <Comp />;}",
355+
"export default function MyComponent() { return <Comp />; }",
302356
None,
303357
None,
304358
Some(PathBuf::from("foo.js")),
305359
),
306360
(
307-
"export function MyComponent() { return <div><Comp /></div>;}",
361+
"export function MyComponent() { return <div><Comp /></div>; }",
308362
None,
309363
None,
310364
Some(PathBuf::from("foo.js")),
@@ -340,7 +394,7 @@ fn test() {
340394
Some(PathBuf::from("foo.jsx")),
341395
),
342396
(
343-
"export function MyComponent() { return <><Comp /><Comp /></>;}",
397+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
344398
None,
345399
None,
346400
Some(PathBuf::from("foo.js")),
@@ -352,17 +406,44 @@ fn test() {
352406
Some(PathBuf::from("foo.js")),
353407
),
354408
(
355-
"export function MyComponent() { return <><Comp /><Comp /></>;}",
409+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
356410
Some(serde_json::json!([{ "extensions": [".js"] }])),
357411
None,
358412
Some(PathBuf::from("foo.jsx")),
359413
),
414+
// Test that the help message prints fine with multiple allowed extensions.
415+
(
416+
"export function MyComponent() { return <><Comp /><Comp /></>; }",
417+
Some(serde_json::json!([{ "extensions": [".js", ".tsx", ".ts"] }])),
418+
None,
419+
Some(PathBuf::from("foo.jsx")),
420+
),
360421
(
361422
"module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
362423
Some(serde_json::json!([{ "extensions": [".js"] }])),
363424
None,
364425
Some(PathBuf::from("foo.jsx")),
365426
),
427+
// Test that identical extensions are de-duplicated.
428+
(
429+
"module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
430+
Some(serde_json::json!([{ "extensions": [".js", "js"] }])),
431+
None,
432+
Some(PathBuf::from("foo.jsx")),
433+
),
434+
(
435+
"module.exports = function MyComponent() { return <><Comp /><Comp /></>; }",
436+
Some(serde_json::json!([{ "extensions": ["js", "js"] }])),
437+
None,
438+
Some(PathBuf::from("foo.jsx")),
439+
),
440+
// Test that extensions without leading dot work for failing cases too
441+
(
442+
"module.exports = function MyComponent() { return <div>\n<div />\n</div>; }",
443+
Some(serde_json::json!([{ "extensions": ["tsx"] }])),
444+
None,
445+
Some(PathBuf::from("foo.jsx")),
446+
),
366447
];
367448

368449
Tester::new(JsxFilenameExtension::NAME, JsxFilenameExtension::PLUGIN, pass, fail)

crates/oxc_linter/src/snapshots/react_jsx_filename_extension.snap

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,28 @@ source: crates/oxc_linter/src/tester.rs
77
2 │ │ <div />
88
3 │ ╰─▶ </div>; }
99
╰────
10-
help: Rename the file with a good extension.
10+
help: Rename the file to use an allowed extension: .jsx
1111

1212
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js'
1313
╭─[jsx_filename_extension.tsx:1:48]
14-
1export default function MyComponent() { return <Comp />;}
14+
1export default function MyComponent() { return <Comp />; }
1515
· ────────
1616
╰────
17-
help: Rename the file with a good extension.
17+
help: Rename the file to use an allowed extension: .jsx
1818

1919
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js'
2020
╭─[jsx_filename_extension.tsx:1:40]
21-
1export function MyComponent() { return <div><Comp /></div>;}
21+
1export function MyComponent() { return <div><Comp /></div>; }
2222
· ───────────────────
2323
╰────
24-
help: Rename the file with a good extension.
24+
help: Rename the file to use an allowed extension: .jsx
2525

2626
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js'
2727
╭─[jsx_filename_extension.tsx:1:28]
2828
1const MyComponent = () => (<div><Comp /></div>); export default MyComponent;
2929
· ───────────────────
3030
╰────
31-
help: Rename the file with a good extension.
31+
help: Rename the file to use an allowed extension: .jsx
3232

3333
eslint-plugin-react(jsx-filename-extension): Only files containing JSX may use the extension '.jsx'
3434
help: Rename the file with a good extension.
@@ -42,40 +42,69 @@ source: crates/oxc_linter/src/tester.rs
4242
2 │ │ <div />
4343
3 │ ╰─▶ </div>; }
4444
╰────
45-
help: Rename the file with a good extension.
45+
help: Rename the file to use an allowed extension: .jsx
4646

4747
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
4848
╭─[jsx_filename_extension.tsx:1:50]
4949
1 │ ╭─▶ module.exports = function MyComponent() { return <div>
5050
2 │ │ <div />
5151
3 │ ╰─▶ </div>; }
5252
╰────
53-
help: Rename the file with a good extension.
53+
help: Rename the file to use an allowed extension: .js
5454

5555
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js'
5656
╭─[jsx_filename_extension.tsx:1:40]
57-
1export function MyComponent() { return <><Comp /><Comp /></>;}
57+
1export function MyComponent() { return <><Comp /><Comp /></>; }
5858
· ─────────────────────
5959
╰────
60-
help: Rename the file with a good extension.
60+
help: Rename the file to use an allowed extension: .jsx
6161

6262
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.js'
6363
╭─[jsx_filename_extension.tsx:1:50]
6464
1module.exports = function MyComponent() { return <><Comp /><Comp /></>; }
6565
· ─────────────────────
6666
╰────
67-
help: Rename the file with a good extension.
67+
help: Rename the file to use an allowed extension: .jsx
6868

6969
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
7070
╭─[jsx_filename_extension.tsx:1:40]
71-
1export function MyComponent() { return <><Comp /><Comp /></>;}
71+
1export function MyComponent() { return <><Comp /><Comp /></>; }
7272
· ─────────────────────
7373
╰────
74-
help: Rename the file with a good extension.
74+
help: Rename the file to use an allowed extension: .js
75+
76+
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
77+
╭─[jsx_filename_extension.tsx:1:40]
78+
1export function MyComponent() { return <><Comp /><Comp /></>; }
79+
· ─────────────────────
80+
╰────
81+
help: Rename the file to use an allowed extension: .js, .tsx, .ts
7582

7683
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
7784
╭─[jsx_filename_extension.tsx:1:50]
7885
1module.exports = function MyComponent() { return <><Comp /><Comp /></>; }
7986
· ─────────────────────
8087
╰────
81-
help: Rename the file with a good extension.
88+
help: Rename the file to use an allowed extension: .js
89+
90+
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
91+
╭─[jsx_filename_extension.tsx:1:50]
92+
1module.exports = function MyComponent() { return <><Comp /><Comp /></>; }
93+
· ─────────────────────
94+
╰────
95+
help: Rename the file to use an allowed extension: .js
96+
97+
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
98+
╭─[jsx_filename_extension.tsx:1:50]
99+
1module.exports = function MyComponent() { return <><Comp /><Comp /></>; }
100+
· ─────────────────────
101+
╰────
102+
help: Rename the file to use an allowed extension: .js
103+
104+
eslint-plugin-react(jsx-filename-extension): JSX not allowed in files with extension '.jsx'
105+
╭─[jsx_filename_extension.tsx:1:50]
106+
1 │ ╭─▶ module.exports = function MyComponent() { return <div>
107+
2 │ │ <div />
108+
3 │ ╰─▶ </div>; }
109+
╰────
110+
help: Rename the file to use an allowed extension: .tsx

0 commit comments

Comments
 (0)