11use std:: ffi:: OsStr ;
22
3+ use itertools:: Itertools ;
4+ use schemars:: JsonSchema ;
5+ use serde:: { Deserialize , Serialize } ;
36use serde_json:: Value ;
47
58use oxc_ast:: AstKind ;
69use oxc_diagnostics:: OxcDiagnostic ;
710use oxc_macros:: declare_oxc_lint;
811use oxc_span:: { CompactStr , GetSpan , Span } ;
9- use schemars:: JsonSchema ;
10- use serde:: { Deserialize , Serialize } ;
1112
1213use 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
2128fn 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 {
7784declare_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 /></>;}\n console.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 /></>;} */\n console.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)
0 commit comments