11import { defineRule } from "@oxlint/plugins" ;
22
3+ /**
4+ * Walk a function body looking for a ReturnStatement whose argument is
5+ * JSXElement or JSXFragment. Only traverses control flow nodes — does NOT
6+ * recurse into call arguments or JSX attribute values, preventing false
7+ * positives on functions that pass JSX as a prop/argument. Stops at nested
8+ * function boundaries so inner helpers returning JSX don't count.
9+ */
10+ function containsJSXReturn ( node ) {
11+ if ( ! node || typeof node !== "object" || ! node . type ) return false ;
12+
13+ // Stop at nested function boundaries
14+ if (
15+ node . type === "FunctionDeclaration" ||
16+ node . type === "FunctionExpression" ||
17+ node . type === "ArrowFunctionExpression"
18+ ) {
19+ return false ;
20+ }
21+
22+ // Arrow with concise body: const Foo = () => <div />
23+ if ( node . type === "JSXElement" || node . type === "JSXFragment" ) return true ;
24+
25+ // return <...>
26+ if ( node . type === "ReturnStatement" ) {
27+ return (
28+ node . argument ?. type === "JSXElement" ||
29+ node . argument ?. type === "JSXFragment"
30+ ) ;
31+ }
32+
33+ // Only recurse through control flow / block nodes, not into expressions
34+ const CONTROL_FLOW_KEYS = {
35+ BlockStatement : [ "body" ] ,
36+ Program : [ "body" ] ,
37+ IfStatement : [ "consequent" , "alternate" ] ,
38+ SwitchStatement : [ "cases" ] ,
39+ SwitchCase : [ "consequent" ] ,
40+ TryStatement : [ "block" , "handler" , "finalizer" ] ,
41+ CatchClause : [ "body" ] ,
42+ WhileStatement : [ "body" ] ,
43+ DoWhileStatement : [ "body" ] ,
44+ ForStatement : [ "body" ] ,
45+ ForInStatement : [ "body" ] ,
46+ ForOfStatement : [ "body" ] ,
47+ LabeledStatement : [ "body" ] ,
48+ } ;
49+
50+ const keys = CONTROL_FLOW_KEYS [ node . type ] ;
51+ if ( ! keys ) return false ;
52+
53+ for ( const key of keys ) {
54+ const child = node [ key ] ;
55+ if ( ! child ) continue ;
56+ if ( Array . isArray ( child ) ) {
57+ for ( const item of child ) {
58+ if ( containsJSXReturn ( item ) ) return true ;
59+ }
60+ } else if ( containsJSXReturn ( child ) ) {
61+ return true ;
62+ }
63+ }
64+ return false ;
65+ }
66+
367const plugin = {
468 meta : {
569 name : "monkeytype-rules" ,
@@ -24,28 +88,24 @@ const plugin = {
2488 hasSuggestions : true ,
2589 } ,
2690 createOnce ( context ) {
27- const isPascalCase = ( name ) => / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / . test ( name ) ;
28-
2991 const getComponentAncestor = ( node ) => {
3092 let current = node . parent ;
3193 while ( current ) {
32- // function ComponentName () { ... }
94+ // function Foo () { return < ...> }
3395 if (
3496 current . type === "FunctionDeclaration" &&
35- current . id ?. name &&
36- isPascalCase ( current . id . name )
97+ containsJSXReturn ( current . body )
3798 ) {
38- return current . id . name ;
99+ return current . id ? .name ?? "component" ;
39100 }
40- // const ComponentName = () => { ... } or const ComponentName = function() { ... }
101+ // const Foo = () => { return < ...> } or const Foo = function() { return < ...> }
41102 if (
42103 ( current . type === "ArrowFunctionExpression" ||
43104 current . type === "FunctionExpression" ) &&
44- current . parent ?. type === "VariableDeclarator" &&
45- current . parent . id ?. name &&
46- isPascalCase ( current . parent . id . name )
105+ containsJSXReturn ( current . body ?? current ) &&
106+ current . parent ?. type === "VariableDeclarator"
47107 ) {
48- return current . parent . id . name ;
108+ return current . parent . id ? .name ?? "component" ;
49109 }
50110 current = current . parent ;
51111 }
@@ -90,6 +150,95 @@ const plugin = {
90150 } ;
91151 } ,
92152 } ) ,
153+ "one-component-per-file" : defineRule ( {
154+ createOnce ( context ) {
155+ let exportedComponents ;
156+
157+ return {
158+ before ( ) {
159+ exportedComponents = [ ] ;
160+ } ,
161+ ExportNamedDeclaration ( node ) {
162+ // export function Foo() { return <...> }
163+ if (
164+ node . declaration ?. type === "FunctionDeclaration" &&
165+ node . declaration . id ?. name &&
166+ containsJSXReturn ( node . declaration . body )
167+ ) {
168+ exportedComponents . push ( {
169+ name : node . declaration . id . name ,
170+ node,
171+ } ) ;
172+ return ;
173+ }
174+ // export const Foo = () => <...> or export const Foo = function() { return <...> }
175+ if ( node . declaration ?. type === "VariableDeclaration" ) {
176+ for ( const decl of node . declaration . declarations ) {
177+ if (
178+ decl . id ?. name &&
179+ ( decl . init ?. type === "ArrowFunctionExpression" ||
180+ decl . init ?. type === "FunctionExpression" ) &&
181+ containsJSXReturn ( decl . init . body ?? decl . init )
182+ ) {
183+ exportedComponents . push ( { name : decl . id . name , node } ) ;
184+ }
185+ }
186+ }
187+ } ,
188+ "Program:exit" ( ) {
189+ if ( exportedComponents . length > 1 ) {
190+ for ( const { name, node } of exportedComponents . slice ( 1 ) ) {
191+ context . report ( {
192+ node,
193+ message : `Only one exported component per file. Move \`${ name } \` to its own file.` ,
194+ } ) ;
195+ }
196+ }
197+ } ,
198+ } ;
199+ } ,
200+ } ) ,
201+ "component-pascal-case" : defineRule ( {
202+ createOnce ( context ) {
203+ const isPascalCase = ( name ) => / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / . test ( name ) ;
204+
205+ return {
206+ FunctionDeclaration ( node ) {
207+ const isTopLevel =
208+ node . parent ?. type === "Program" ||
209+ node . parent ?. type === "ExportNamedDeclaration" ;
210+ if ( ! isTopLevel || ! node . id ) return ;
211+ const name = node . id . name ;
212+ if ( ! isPascalCase ( name ) && containsJSXReturn ( node . body ) ) {
213+ context . report ( {
214+ node : node . id ,
215+ message : `Component \`${ name } \` should be PascalCase.` ,
216+ } ) ;
217+ }
218+ } ,
219+ VariableDeclarator ( node ) {
220+ const isTopLevel =
221+ node . parent ?. parent ?. type === "Program" ||
222+ node . parent ?. parent ?. type === "ExportNamedDeclaration" ;
223+ if (
224+ ! isTopLevel ||
225+ node . id ?. type !== "Identifier" ||
226+ ( node . init ?. type !== "ArrowFunctionExpression" &&
227+ node . init ?. type !== "FunctionExpression" )
228+ ) {
229+ return ;
230+ }
231+ const name = node . id . name ;
232+ if ( ! isPascalCase ( name ) && containsJSXReturn ( node . init ) ) {
233+ context . report ( {
234+ node : node . id ,
235+ message : `Component \`${ name } \` should be PascalCase.` ,
236+ } ) ;
237+ }
238+ } ,
239+ } ;
240+ } ,
241+ } ) ,
93242 } ,
94243} ;
95244
0 commit comments