@@ -217,6 +217,34 @@ fn isRegexLiteral(value: []const u8) bool {
217217 return false ;
218218}
219219
220+ fn isJsxTagNameChar (c : u8 ) bool {
221+ return ch .isIdentChar (c ) or c == '.' or c == ':' or c == '-' ;
222+ }
223+
224+ /// Detect a complete JSX element or fragment without assuming a particular JSX
225+ /// runtime. This deliberately requires a matching outer close or a self-close,
226+ /// keeping angle-bracket assertions and generic arrow functions distinct.
227+ fn isJsxExpression (value : []const u8 ) bool {
228+ const expression = trim (value );
229+ if (expression .len < 3 or expression [0 ] != '<' ) return false ;
230+ if (ch .startsWith (expression , "<>" )) return ch .endsWith (expression , "</>" );
231+ if (! ch .isIdentStart (expression [1 ])) return false ;
232+
233+ var tag_end : usize = 2 ;
234+ while (tag_end < expression .len and isJsxTagNameChar (expression [tag_end ])) tag_end += 1 ;
235+ const tag_name = expression [1.. tag_end ];
236+ if (tag_name .len == 0 or tag_end >= expression .len ) return false ;
237+ const delimiter = expression [tag_end ];
238+ if (! ch .isWhitespace (delimiter ) and delimiter != '>' and delimiter != '/' ) return false ;
239+ if (ch .endsWith (expression , "/>" )) return true ;
240+
241+ const close_start = std .mem .lastIndexOf (u8 , expression , "</" ) orelse return false ;
242+ var close_end = close_start + 2 ;
243+ while (close_end < expression .len and isJsxTagNameChar (expression [close_end ])) close_end += 1 ;
244+ return std .mem .eql (u8 , expression [close_start + 2 .. close_end ], tag_name ) and
245+ close_end < expression .len and expression [close_end ] == '>' and close_end == expression .len - 1 ;
246+ }
247+
220248fn inferAccessType (alloc : std.mem.Allocator , value : []const u8 ) InferError ! ? []const u8 {
221249 if (value .len > 3 and value [value .len - 1 ] == ']' ) {
222250 const bracket = std .mem .lastIndexOfScalar (u8 , value , '[' ) orelse return null ;
@@ -990,6 +1018,11 @@ pub fn inferFunctionBodyReturnType(alloc: std.mem.Allocator, body: []const u8, p
9901018 if (expression_depth == 0 ) break ;
9911019 expression_depth -= 1 ;
9921020 } else if (c == ';' and expression_depth == 0 ) break ;
1021+ if ((c == '\n ' or c == '\r ' ) and expression_depth == 0 ) {
1022+ var next = i + 1 ;
1023+ while (next < content .len and ch .isWhitespace (content [next ])) next += 1 ;
1024+ if (ch .startsWith (content [next .. ], "return" ) and (next + 6 >= content .len or ! ch .isIdentChar (content [next + 6 ]))) break ;
1025+ }
9931026 }
9941027 const expression = trim (content [expression_start .. i ]);
9951028 const inferred = try inferBodyExpressionType (alloc , expression , parameters , depth + 1 );
@@ -1012,6 +1045,7 @@ fn inferBodyExpressionType(alloc: std.mem.Allocator, expression: []const u8, par
10121045 while (value .len >= 2 and value [0 ] == '(' and value [value .len - 1 ] == ')' and findMatchingBracket (value , 0 , '(' , ')' ) == value .len - 1 ) {
10131046 value = trim (value [1 .. value .len - 1 ]);
10141047 }
1048+ if (isJsxExpression (value )) return "JSX.Element" ;
10151049 if (findParameterType (parameters , value )) | parameter_type | return parameter_type ;
10161050 if (try inferBodyCallType (alloc , value , parameters )) | call_type | return call_type ;
10171051
@@ -1114,6 +1148,8 @@ pub fn inferNarrowType(alloc: std.mem.Allocator, value: []const u8, is_const: bo
11141148 return inferNarrowType (alloc , trim (trimmed [1 .. trimmed .len - 1 ]), is_const , in_union , depth + 1 );
11151149 }
11161150
1151+ if (isJsxExpression (trimmed )) return "JSX.Element" ;
1152+
11171153 // The comma operator evaluates to its final operand. Reuse the balanced
11181154 // element splitter so commas inside calls, arrays, and objects are ignored.
11191155 if (findTopLevelComma (trimmed )) | comma | return inferNarrowType (alloc , trim (trimmed [comma + 1 .. ]), is_const , in_union , depth + 1 );
@@ -2562,6 +2598,18 @@ test "arithmetic and update expressions emit valid result types" {
25622598 try std .testing .expectEqualStrings ("typeof state.count" , try inferNarrowType (alloc , "--state.count" , false , false , 0 ));
25632599}
25642600
2601+ test "JSX expressions infer portable element types" {
2602+ var arena = std .heap .ArenaAllocator .init (std .testing .allocator );
2603+ defer arena .deinit ();
2604+ const alloc = arena .allocator ();
2605+ try std .testing .expectEqualStrings ("JSX.Element" , try inferNarrowType (alloc , "<button>Save</button>" , false , false , 0 ));
2606+ try std .testing .expectEqualStrings ("JSX.Element" , try inferNarrowType (alloc , "<Form.Field name='email' />" , false , false , 0 ));
2607+ try std .testing .expectEqualStrings ("JSX.Element" , try inferNarrowType (alloc , "<><span>One</span><span>Two</span></>" , false , false , 0 ));
2608+ try std .testing .expectEqualStrings ("JSX.Element" , try inferFunctionBodyReturnType (alloc , "{ return <Card title={title} /> }" , "(title: string)" , 0 ));
2609+ try std .testing .expectEqualStrings ("null | JSX.Element" , try inferFunctionBodyReturnType (alloc , "{ if (hidden) return null; return <Panel /> }" , "(hidden: boolean)" , 0 ));
2610+ try std .testing .expectEqualStrings ("boolean" , try inferNarrowType (alloc , "<Value>input" , false , false , 0 ));
2611+ }
2612+
25652613test "extractSatisfiesType" {
25662614 try std .testing .expectEqualStrings ("Config" , extractSatisfiesType ("{ port: 3000 } satisfies Config" ).? );
25672615 try std .testing .expect (extractSatisfiesType ("just a value without it" ) == null );
0 commit comments