raggiecode 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. Agent/__init__.py +0 -0
  2. Agent/agent.py +891 -0
  3. Agent/chat_history_db.py +1500 -0
  4. Agent/command.py +49 -0
  5. Agent/config.py +46 -0
  6. Agent/effort_levels.py +33 -0
  7. Agent/git_manager.py +727 -0
  8. Agent/tools.py +35 -0
  9. Commands/__init__.py +18 -0
  10. Commands/effort.py +42 -0
  11. Commands/global_todo.py +23 -0
  12. Commands/help.py +22 -0
  13. Commands/reasoning.py +24 -0
  14. Commands/redo.py +11 -0
  15. Commands/reindex.py +27 -0
  16. Commands/shell.py +28 -0
  17. Commands/stream.py +24 -0
  18. Commands/undo.py +13 -0
  19. Commands/unlimited_effort.py +8 -0
  20. Commands/window_size.py +29 -0
  21. RAG/__init__.py +0 -0
  22. RAG/document.py +119 -0
  23. RAG/find.py +408 -0
  24. RAG/graph.py +231 -0
  25. Tools/GetFileCodeStructure.py +43 -0
  26. Tools/GetSymbolSourceCode.py +27 -0
  27. Tools/__init__.py +39 -0
  28. Tools/ask_user.py +102 -0
  29. Tools/dispatch_subagent.py +215 -0
  30. Tools/document.py +35 -0
  31. Tools/edit_symbol.py +250 -0
  32. Tools/fuzzy_search.py +119 -0
  33. Tools/list_dir.py +51 -0
  34. Tools/read.py +49 -0
  35. Tools/read_image.py +75 -0
  36. Tools/remove.py +75 -0
  37. Tools/replace.py +305 -0
  38. Tools/search.py +41 -0
  39. Tools/shell.py +149 -0
  40. Tools/shell_kill.py +87 -0
  41. Tools/temp_background_service.py +113 -0
  42. Tools/todo_list.py +481 -0
  43. Tools/utils.py +116 -0
  44. Tools/view_changes.py +179 -0
  45. Tools/walk_call_tree.py +30 -0
  46. Tools/web_fetch.py +175 -0
  47. Tools/web_search.py +69 -0
  48. Tools/write.py +48 -0
  49. cli.py +111 -0
  50. config/__init__.py +0 -0
  51. config/coder_system_prompt.md +119 -0
  52. config/roles.json +43 -0
  53. config/tools.json +709 -0
  54. indexing/__init__.py +0 -0
  55. indexing/cli.py +128 -0
  56. indexing/code_index_sdk.py +832 -0
  57. indexing/code_indexer.py +1763 -0
  58. indexing/db_schema.py +396 -0
  59. indexing/export_to_json.py +346 -0
  60. indexing/extractors.py +189 -0
  61. indexing/file_utils.py +97 -0
  62. indexing/frontend/__init__.py +0 -0
  63. indexing/frontend/css_extractor.py +195 -0
  64. indexing/frontend/css_parser.py +387 -0
  65. indexing/frontend/css_selector_utils.py +226 -0
  66. indexing/frontend/edit_safety.py +573 -0
  67. indexing/frontend/graph.py +838 -0
  68. indexing/frontend/html_extractor.py +496 -0
  69. indexing/frontend/html_parser.py +314 -0
  70. indexing/frontend/jsx_extractor.py +1204 -0
  71. indexing/frontend/location_lookup.py +247 -0
  72. indexing/frontend/resolver.py +485 -0
  73. indexing/frontend/runtime_resolver.py +862 -0
  74. indexing/frontend/semantic_output.py +705 -0
  75. indexing/frontend/source_location.py +69 -0
  76. indexing/frontend_config.py +72 -0
  77. indexing/frontend_models.py +347 -0
  78. indexing/language_config.py +360 -0
  79. indexing/models.py +284 -0
  80. indexing/node_utils.py +1112 -0
  81. indexing/parse_worker.py +1082 -0
  82. indexing/queries.py +1542 -0
  83. indexing/sdk_examples.py +426 -0
  84. interactive.py +248 -0
  85. raggie.py +673 -0
  86. raggiecode-0.2.1.dist-info/METADATA +944 -0
  87. raggiecode-0.2.1.dist-info/RECORD +93 -0
  88. raggiecode-0.2.1.dist-info/WHEEL +5 -0
  89. raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
  90. raggiecode-0.2.1.dist-info/top_level.txt +10 -0
  91. skills/__init__.py +3 -0
  92. skills/manager.py +114 -0
  93. skills/tool.py +121 -0
@@ -0,0 +1,1204 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ JSX/TSX semantic extractor.
4
+ Extracts React component definitions, markup trees, events, bindings,
5
+ render relationships, and selector matches from JSX/TSX files.
6
+ """
7
+
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, List, Optional, Tuple
11
+
12
+ from indexing.language_config import LANGUAGE_CONFIG
13
+ from indexing.node_utils import create_parser
14
+ from indexing.frontend.source_location import SourceLocation, node_to_location, extract_range
15
+
16
+
17
+ # Event handler attributes
18
+ EVENT_ATTR_PATTERN = re.compile(r"^on([A-Z][a-zA-Z]*)$")
19
+
20
+ # Known wrapper functions that wrap components
21
+ WRAPPER_FUNCTIONS = {"memo", "forwardRef", "connect", "withRouter", "observer"}
22
+
23
+ # Known non-component capitalized identifiers (built-in React APIs)
24
+ NON_COMPONENT_CAPS = {"React", "Fragment", "Suspense", "StrictMode", "Profiler",
25
+ "Children", "createElement", "cloneElement", "isValidElement",
26
+ "createRef", "createContext", "createFactory", "lazy", "Component",
27
+ "PureComponent"}
28
+
29
+
30
+ @dataclass
31
+ class JSXComponent:
32
+ name: str
33
+ component_type: str # 'function', 'arrow', 'class', 'memo', 'forwardRef'
34
+ location: SourceLocation
35
+ is_exported: bool = False
36
+ is_default_export: bool = False
37
+ impl_function_name: Optional[str] = None
38
+ impl_class_name: Optional[str] = None
39
+ wrapper_name: Optional[str] = None # e.g. 'memo', 'forwardRef'
40
+
41
+
42
+ @dataclass
43
+ class JSXMarkupElement:
44
+ tag_name: str
45
+ element_type: str # 'native', 'custom_component', 'fragment', 'expression', 'text'
46
+ location: SourceLocation
47
+ component_index: Optional[int] = None # which component this belongs to
48
+ parent_index: Optional[int] = None
49
+ element_id_attr: Optional[str] = None
50
+ static_classes: List[str] = field(default_factory=list)
51
+ attributes: Dict[str, str] = field(default_factory=dict)
52
+ is_conditional: bool = False
53
+ is_repeated: bool = False
54
+ conditional_expr: Optional[str] = None
55
+ repeated_expr: Optional[str] = None
56
+ expression_text: Optional[str] = None # for expression elements
57
+ class_expression: Optional[str] = None # for dynamic className expressions
58
+
59
+
60
+ @dataclass
61
+ class JSXEvent:
62
+ component_index: int
63
+ element_index: int
64
+ event_name: str
65
+ handler_type: str # 'direct', 'inline', 'member'
66
+ handler_expression: str
67
+ resolution_status: str # 'exact', 'inferred', 'unresolved'
68
+ location: SourceLocation
69
+
70
+
71
+ @dataclass
72
+ class JSXBinding:
73
+ component_index: int
74
+ element_index: int
75
+ binding_name: str
76
+ binding_type: str # 'property', 'ref', 'spread', 'style'
77
+ expression: str
78
+ resolution_status: str
79
+ location: SourceLocation
80
+
81
+
82
+ @dataclass
83
+ class JSXRenderRelationship:
84
+ parent_component_index: int
85
+ child_component_name: str
86
+ render_type: str # 'direct', 'conditional', 'repeated'
87
+ element_index: int
88
+
89
+
90
+ @dataclass
91
+ class JSXDocument:
92
+ components: List[JSXComponent] = field(default_factory=list)
93
+ markup_elements: List[JSXMarkupElement] = field(default_factory=list)
94
+ events: List[JSXEvent] = field(default_factory=list)
95
+ bindings: List[JSXBinding] = field(default_factory=list)
96
+ render_relationships: List[JSXRenderRelationship] = field(default_factory=list)
97
+ selector_matches: List[dict] = field(default_factory=list)
98
+ diagnostics: List[dict] = field(default_factory=list)
99
+ source_bytes: bytes = b""
100
+
101
+
102
+ _jsx_parsers = {}
103
+
104
+ def _get_parser(language):
105
+ """Get or create a parser for the given language (cached per process)."""
106
+ if language not in _jsx_parsers:
107
+ lang_module = LANGUAGE_CONFIG.get(language, {}).get("language_module")
108
+ if lang_module is None:
109
+ _jsx_parsers[language] = None
110
+ else:
111
+ _jsx_parsers[language] = create_parser(lang_module)
112
+ return _jsx_parsers[language]
113
+
114
+
115
+ def _has_jsx_nodes(root_node) -> bool:
116
+ """Check if the AST contains any JSX nodes."""
117
+ stack = [root_node]
118
+ while stack:
119
+ node = stack.pop()
120
+ if node.type.startswith("jsx_"):
121
+ return True
122
+ for i in range(node.child_count):
123
+ stack.append(node.child(i))
124
+ return False
125
+
126
+
127
+ def _get_function_name(func_node, source_bytes) -> Optional[str]:
128
+ """Extract the name from a function/arrow function node by looking at parent."""
129
+ parent = func_node.parent
130
+ if parent is None:
131
+ return None
132
+
133
+ # function_declaration has identifier child
134
+ if func_node.type == "function_declaration":
135
+ for i in range(func_node.child_count):
136
+ c = func_node.child(i)
137
+ if c.type in ("identifier", "type_identifier"):
138
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
139
+
140
+ # class_declaration has type_identifier child
141
+ if func_node.type == "class_declaration":
142
+ for i in range(func_node.child_count):
143
+ c = func_node.child(i)
144
+ if c.type in ("identifier", "type_identifier"):
145
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
146
+
147
+ # Arrow function or function expression — name is in parent variable_declarator
148
+ if func_node.type in ("arrow_function", "function_expression", "function"):
149
+ if parent.type == "variable_declarator":
150
+ for i in range(parent.child_count):
151
+ c = parent.child(i)
152
+ if c.type in ("identifier", "type_identifier"):
153
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
154
+ # Named function expression: function Inner() { ... }
155
+ if func_node.type == "function_expression":
156
+ for i in range(func_node.child_count):
157
+ c = func_node.child(i)
158
+ if c.type == "identifier":
159
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
160
+
161
+ return None
162
+
163
+
164
+ def _is_exported(func_node) -> Tuple[bool, bool]:
165
+ """Check if a function/component is exported (and default)."""
166
+ parent = func_node.parent
167
+ while parent:
168
+ if parent.type == "export_statement":
169
+ for i in range(parent.child_count):
170
+ c = parent.child(i)
171
+ if c.type == "default":
172
+ return (True, True)
173
+ return (True, False)
174
+ if parent.type == "lexical_declaration":
175
+ grandparent = parent.parent
176
+ if grandparent and grandparent.type == "export_statement":
177
+ for i in range(grandparent.child_count):
178
+ c = grandparent.child(i)
179
+ if c.type == "default":
180
+ return (True, True)
181
+ return (True, False)
182
+ parent = parent.parent
183
+ return (False, False)
184
+
185
+
186
+ def _function_returns_jsx(func_node, source_bytes) -> bool:
187
+ """Check if a function/arrow function returns JSX."""
188
+ # Look for return statements containing JSX, or arrow with direct JSX body
189
+ stack = [func_node]
190
+ while stack:
191
+ node = stack.pop()
192
+ if node.type == "return_statement":
193
+ # Check if return contains JSX
194
+ for i in range(node.child_count):
195
+ c = node.child(i)
196
+ if c.type.startswith("jsx_"):
197
+ return True
198
+ # Check inside parenthesized_expression
199
+ if c.type == "parenthesized_expression":
200
+ for j in range(c.child_count):
201
+ cc = c.child(j)
202
+ if cc.type.startswith("jsx_"):
203
+ return True
204
+ # Deeper nesting
205
+ stack2 = [cc]
206
+ while stack2:
207
+ n2 = stack2.pop()
208
+ if n2.type.startswith("jsx_"):
209
+ return True
210
+ for k in range(n2.child_count):
211
+ stack2.append(n2.child(k))
212
+ # Arrow function with expression body (no statement_block)
213
+ if node.type == "arrow_function" and node != func_node:
214
+ pass # Don't recurse into nested arrows
215
+ for i in range(node.child_count):
216
+ child = node.child(i)
217
+ if child.type == "statement_block":
218
+ # Look inside the block for returns
219
+ for j in range(child.child_count):
220
+ stack.append(child.child(j))
221
+ elif child.type.startswith("jsx_") and func_node.type == "arrow_function":
222
+ # Arrow function with direct JSX return
223
+ return True
224
+
225
+ # Also check if arrow function body is directly JSX
226
+ if func_node.type == "arrow_function":
227
+ found_arrow = False
228
+ for i in range(func_node.child_count):
229
+ c = func_node.child(i)
230
+ if c.type == "=>":
231
+ found_arrow = True
232
+ continue
233
+ if found_arrow and c.type.startswith("jsx_"):
234
+ return True
235
+ if found_arrow and c.type == "parenthesized_expression":
236
+ for j in range(c.child_count):
237
+ cc = c.child(j)
238
+ if cc.type.startswith("jsx_"):
239
+ return True
240
+
241
+ return False
242
+
243
+
244
+ def _detect_wrapper(func_node, source_bytes) -> Optional[str]:
245
+ """Detect if a function is wrapped in memo(), forwardRef(), etc."""
246
+ parent = func_node.parent
247
+ if parent and parent.type == "arguments":
248
+ grandparent = parent.parent
249
+ if grandparent and grandparent.type == "call_expression":
250
+ for i in range(grandparent.child_count):
251
+ c = grandparent.child(i)
252
+ if c.type == "function_name" or c.type == "identifier":
253
+ name = extract_range(source_bytes, c.start_byte, c.end_byte)
254
+ if name in WRAPPER_FUNCTIONS:
255
+ return name
256
+ if c.type == "member_expression":
257
+ name = extract_range(source_bytes, c.start_byte, c.end_byte)
258
+ parts = name.rsplit(".", 1)
259
+ if len(parts) == 2 and parts[1] in WRAPPER_FUNCTIONS:
260
+ return parts[1]
261
+ return None
262
+
263
+
264
+ def _find_components(root_node, source_bytes) -> Tuple[List[JSXComponent], Dict[str, object]]:
265
+ """Find all React component definitions in the AST.
266
+
267
+ Returns (components, node_map) where node_map maps target_name -> AST node
268
+ so callers can avoid re-traversing the tree per component.
269
+ """
270
+ components = []
271
+ node_map = {} # target_name -> AST node
272
+
273
+ stack = [root_node]
274
+ while stack:
275
+ node = stack.pop()
276
+
277
+ if node.type == "function_declaration":
278
+ name = _get_function_name(node, source_bytes)
279
+ if name and name[0].isupper() and name not in NON_COMPONENT_CAPS:
280
+ if _function_returns_jsx(node, source_bytes):
281
+ is_exp, is_default = _is_exported(node)
282
+ components.append(JSXComponent(
283
+ name=name,
284
+ component_type="function",
285
+ location=node_to_location(node),
286
+ is_exported=is_exp,
287
+ is_default_export=is_default,
288
+ impl_function_name=name,
289
+ ))
290
+ node_map[name] = node
291
+
292
+ elif node.type in ("arrow_function", "function_expression"):
293
+ name = _get_function_name(node, source_bytes)
294
+ if name and name[0].isupper() and name not in NON_COMPONENT_CAPS:
295
+ if _function_returns_jsx(node, source_bytes):
296
+ wrapper = _detect_wrapper(node, source_bytes)
297
+ ctype = "arrow"
298
+ if wrapper:
299
+ ctype = wrapper
300
+ is_exp, is_default = _is_exported(node)
301
+ components.append(JSXComponent(
302
+ name=name,
303
+ component_type=ctype,
304
+ location=node_to_location(node),
305
+ is_exported=is_exp,
306
+ is_default_export=is_default,
307
+ impl_function_name=name,
308
+ wrapper_name=wrapper,
309
+ ))
310
+ node_map[name] = node
311
+
312
+ # Class declarations (React.Component)
313
+ elif node.type == "class_declaration":
314
+ name = _get_function_name(node, source_bytes)
315
+ if name and name[0].isupper():
316
+ # Check if extends React.Component or similar
317
+ has_react_base = False
318
+ for i in range(node.child_count):
319
+ c = node.child(i)
320
+ if c.type in ("class_heritage", "extends_clause"):
321
+ text = extract_range(source_bytes, c.start_byte, c.end_byte)
322
+ if "Component" in text or "PureComponent" in text:
323
+ has_react_base = True
324
+ # Also check nested extends_clause inside class_heritage
325
+ for j in range(c.child_count):
326
+ cc = c.child(j)
327
+ if cc.type == "extends_clause":
328
+ text2 = extract_range(source_bytes, cc.start_byte, cc.end_byte)
329
+ if "Component" in text2 or "PureComponent" in text2:
330
+ has_react_base = True
331
+ if has_react_base:
332
+ is_exp, is_default = _is_exported(node)
333
+ components.append(JSXComponent(
334
+ name=name,
335
+ component_type="class",
336
+ location=node_to_location(node),
337
+ is_exported=is_exp,
338
+ is_default_export=is_default,
339
+ impl_class_name=name,
340
+ ))
341
+ node_map[name] = node
342
+
343
+ for i in range(node.child_count):
344
+ stack.append(node.child(i))
345
+
346
+ return components, node_map
347
+
348
+
349
+ def _get_jsx_tag_name(opening_node, source_bytes) -> str:
350
+ """Extract the tag name from a jsx_opening_element or jsx_self_closing_element."""
351
+ for i in range(opening_node.child_count):
352
+ c = opening_node.child(i)
353
+ if c.type == "identifier":
354
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
355
+ if c.type == "member_expression":
356
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
357
+ if c.type == "namespace_function":
358
+ return extract_range(source_bytes, c.start_byte, c.end_byte)
359
+ return ""
360
+
361
+
362
+ def _is_custom_component(tag_name: str) -> bool:
363
+ """Check if a tag name represents a custom component (capitalized)."""
364
+ if not tag_name:
365
+ return False
366
+ # Member expressions like Card.Sub — check first part
367
+ first_part = tag_name.split(".")[0]
368
+ return first_part[0].isupper() if first_part else False
369
+
370
+
371
+ def _extract_jsx_attributes(opening_node, source_bytes) -> List[dict]:
372
+ """Extract JSX attributes from an opening element node."""
373
+ attrs = []
374
+ for i in range(opening_node.child_count):
375
+ c = opening_node.child(i)
376
+ if c.type == "jsx_attribute":
377
+ attr_name = None
378
+ attr_value_node = None
379
+ for j in range(c.child_count):
380
+ cc = c.child(j)
381
+ if cc.type == "property_identifier":
382
+ attr_name = extract_range(source_bytes, cc.start_byte, cc.end_byte)
383
+ elif cc.type == "string":
384
+ raw = extract_range(source_bytes, cc.start_byte, cc.end_byte)
385
+ if len(raw) >= 2 and raw[0] in ('"', "'") and raw[-1] == raw[0]:
386
+ attr_value_node = ("string", raw[1:-1])
387
+ elif cc.type == "jsx_expression":
388
+ # Expression attribute
389
+ expr_text = ""
390
+ for k in range(cc.child_count):
391
+ ec = cc.child(k)
392
+ if ec.type not in ("{", "}"):
393
+ expr_text += extract_range(source_bytes, ec.start_byte, ec.end_byte)
394
+ attr_value_node = ("expression", expr_text.strip())
395
+ if attr_name:
396
+ attrs.append({
397
+ "name": attr_name,
398
+ "value": attr_value_node,
399
+ "location": node_to_location(c),
400
+ })
401
+
402
+ # Spread attributes: {...props}
403
+ if c.type == "jsx_expression":
404
+ for j in range(c.child_count):
405
+ cc = c.child(j)
406
+ if cc.type == "spread_element":
407
+ attrs.append({
408
+ "name": "...spread",
409
+ "value": ("spread", extract_range(source_bytes, cc.start_byte, cc.end_byte)),
410
+ "location": node_to_location(c),
411
+ })
412
+
413
+ return attrs
414
+
415
+
416
+ def _extract_class_names(attr_value, source_bytes) -> Tuple[List[str], str, str]:
417
+ """Extract static class names from a className attribute value.
418
+
419
+ Returns: (static_classes, resolution_status, expression_text)
420
+ """
421
+ if attr_value is None:
422
+ return [], "unresolved", ""
423
+
424
+ value_type, value_text = attr_value
425
+
426
+ if value_type == "string":
427
+ # Static string: "card primary"
428
+ return value_text.split(), "exact", value_text
429
+
430
+ if value_type == "expression":
431
+ # Check for different expression types
432
+ expr = value_text.strip()
433
+
434
+ # CSS module reference: styles.className
435
+ if "." in expr and not expr.startswith("(") and not expr.startswith("cn"):
436
+ parts = expr.split(".")
437
+ if len(parts) == 2 and parts[0].isidentifier():
438
+ return [], "unresolved", expr # CSS module — Phase 6 resolution
439
+
440
+ # Ternary: cond ? 'a' : 'b' — dynamic, not static
441
+ if "?" in expr and ":" in expr:
442
+ return [], "unresolved", expr
443
+
444
+ # Function call: cn('btn', loading && 'loading') — dynamic, not static
445
+ if "(" in expr and ")" in expr:
446
+ return [], "unresolved", expr
447
+
448
+ # Template literal: `btn ${variant}`
449
+ if expr.startswith("`"):
450
+ static_parts = re.findall(r"([a-zA-Z][\w-]*)", expr)
451
+ return static_parts, "unresolved", expr
452
+
453
+ return [], "unresolved", expr
454
+
455
+ return [], "unresolved", ""
456
+
457
+
458
+ def _process_jsx_element_node(node, source_bytes, component_index, parent_index,
459
+ markup_elements, events, bindings, render_relationships,
460
+ components, conditional, repeated, conditional_expr, repeated_expr):
461
+ """Process a single jsx_element or jsx_self_closing_element node.
462
+
463
+ Returns a list of child nodes to push onto the walk stack.
464
+ """
465
+ children_to_walk = []
466
+
467
+ if node.type == "jsx_element":
468
+ opening = None
469
+ children_nodes = []
470
+
471
+ for i in range(node.child_count):
472
+ c = node.child(i)
473
+ if c.type == "jsx_opening_element":
474
+ opening = c
475
+ elif c.type == "jsx_closing_element":
476
+ pass
477
+ else:
478
+ children_nodes.append(c)
479
+
480
+ if opening is None:
481
+ return []
482
+
483
+ tag_name = _get_jsx_tag_name(opening, source_bytes)
484
+ is_custom = _is_custom_component(tag_name)
485
+ elem_type = "custom_component" if is_custom else "native"
486
+
487
+ if tag_name == "" and opening.child_count == 2:
488
+ types = [opening.child(i).type for i in range(opening.child_count)]
489
+ if "<" in types and ">" in types:
490
+ elem_type = "fragment"
491
+ tag_name = "Fragment"
492
+
493
+ attrs = _extract_jsx_attributes(opening, source_bytes)
494
+
495
+ element_id = None
496
+ static_classes = []
497
+ class_resolution = "exact"
498
+ class_expr = ""
499
+ class_expression = None
500
+ attrs_dict = {}
501
+
502
+ elem_index = len(markup_elements)
503
+
504
+ for attr in attrs:
505
+ attr_name = attr["name"]
506
+ attr_value = attr["value"]
507
+
508
+ if attr_name == "id" and attr_value and attr_value[0] == "string":
509
+ element_id = attr_value[1]
510
+ attrs_dict["id"] = attr_value[1]
511
+ elif attr_name in ("className", "class"):
512
+ classes, resolution, expr = _extract_class_names(attr_value, source_bytes)
513
+ static_classes = classes
514
+ class_resolution = resolution
515
+ class_expr = expr
516
+ class_expression = expr if resolution != "exact" else None
517
+ if attr_value:
518
+ attrs_dict[attr_name] = attr_value[1] if attr_value[0] == "string" else expr
519
+ elif attr_name == "style":
520
+ if attr_value and attr_value[0] == "expression":
521
+ bindings.append(JSXBinding(
522
+ component_index=component_index,
523
+ element_index=elem_index,
524
+ binding_name="style",
525
+ binding_type="style",
526
+ expression=attr_value[1],
527
+ resolution_status="unresolved",
528
+ location=attr["location"],
529
+ ))
530
+ elif attr_name == "ref":
531
+ if attr_value:
532
+ bindings.append(JSXBinding(
533
+ component_index=component_index,
534
+ element_index=elem_index,
535
+ binding_name="ref",
536
+ binding_type="ref",
537
+ expression=attr_value[1] if attr_value[0] == "expression" else attr_value[1],
538
+ resolution_status="unresolved",
539
+ location=attr["location"],
540
+ ))
541
+ elif attr_name == "...spread":
542
+ bindings.append(JSXBinding(
543
+ component_index=component_index,
544
+ element_index=elem_index,
545
+ binding_name="...props",
546
+ binding_type="spread",
547
+ expression=attr_value[1] if attr_value else "",
548
+ resolution_status="unresolved",
549
+ location=attr["location"],
550
+ ))
551
+ else:
552
+ event_match = EVENT_ATTR_PATTERN.match(attr_name)
553
+ if event_match and attr_value:
554
+ event_name = event_match.group(1).lower()
555
+ handler_expr = attr_value[1] if attr_value[0] == "expression" else attr_value[1]
556
+
557
+ handler_type = "direct"
558
+ resolution = "exact"
559
+ if attr_value[0] == "expression":
560
+ expr = attr_value[1].strip()
561
+ if expr.startswith("()") or expr.startswith("("):
562
+ handler_type = "inline"
563
+ resolution = "inferred"
564
+ elif "." in expr and "(" not in expr:
565
+ handler_type = "member"
566
+ resolution = "inferred"
567
+ elif expr.isidentifier():
568
+ handler_type = "direct"
569
+ resolution = "exact"
570
+ else:
571
+ handler_type = "inline"
572
+ resolution = "inferred"
573
+
574
+ events.append(JSXEvent(
575
+ component_index=component_index,
576
+ element_index=elem_index,
577
+ event_name=event_name,
578
+ handler_type=handler_type,
579
+ handler_expression=handler_expr,
580
+ resolution_status=resolution,
581
+ location=attr["location"],
582
+ ))
583
+ elif attr_value:
584
+ bindings.append(JSXBinding(
585
+ component_index=component_index,
586
+ element_index=elem_index,
587
+ binding_name=attr_name,
588
+ binding_type="property",
589
+ expression=attr_value[1] if attr_value[0] == "expression" else attr_value[1],
590
+ resolution_status="exact" if attr_value[0] == "string" else "unresolved",
591
+ location=attr["location"],
592
+ ))
593
+
594
+ if attr_name not in ("id", "className", "class", "style", "ref", "...spread"):
595
+ if attr_value:
596
+ attrs_dict[attr_name] = attr_value[1] if attr_value[0] == "string" else (attr_value[1] if attr_value[0] == "expression" else "")
597
+
598
+ markup_elem = JSXMarkupElement(
599
+ tag_name=tag_name,
600
+ element_type=elem_type,
601
+ location=node_to_location(node),
602
+ component_index=component_index,
603
+ parent_index=parent_index,
604
+ element_id_attr=element_id,
605
+ static_classes=static_classes,
606
+ attributes=attrs_dict,
607
+ is_conditional=conditional,
608
+ is_repeated=repeated,
609
+ conditional_expr=conditional_expr,
610
+ repeated_expr=repeated_expr,
611
+ class_expression=class_expression,
612
+ )
613
+ markup_elements.append(markup_elem)
614
+
615
+ if is_custom:
616
+ render_type = "conditional" if conditional else ("repeated" if repeated else "direct")
617
+ render_relationships.append(JSXRenderRelationship(
618
+ parent_component_index=component_index,
619
+ child_component_name=tag_name,
620
+ render_type=render_type,
621
+ element_index=elem_index,
622
+ ))
623
+
624
+ for child in children_nodes:
625
+ if child.type in ("jsx_element", "jsx_self_closing_element", "jsx_expression"):
626
+ children_to_walk.append((child, elem_index, False, False, None, None))
627
+ elif child.type == "jsx_text":
628
+ text = extract_range(source_bytes, child.start_byte, child.end_byte).strip()
629
+ if text:
630
+ markup_elements.append(JSXMarkupElement(
631
+ tag_name="#text",
632
+ element_type="text",
633
+ location=node_to_location(child),
634
+ component_index=component_index,
635
+ parent_index=elem_index,
636
+ expression_text=text,
637
+ ))
638
+
639
+ elif node.type == "jsx_self_closing_element":
640
+ tag_name = _get_jsx_tag_name(node, source_bytes)
641
+ is_custom = _is_custom_component(tag_name)
642
+ elem_type = "custom_component" if is_custom else "native"
643
+
644
+ attrs = _extract_jsx_attributes(node, source_bytes)
645
+
646
+ element_id = None
647
+ static_classes = []
648
+ class_expression = None
649
+ attrs_dict = {}
650
+
651
+ elem_index = len(markup_elements)
652
+
653
+ for attr in attrs:
654
+ attr_name = attr["name"]
655
+ attr_value = attr["value"]
656
+
657
+ if attr_name == "id" and attr_value and attr_value[0] == "string":
658
+ element_id = attr_value[1]
659
+ attrs_dict["id"] = attr_value[1]
660
+ elif attr_name in ("className", "class"):
661
+ classes, resolution, expr = _extract_class_names(attr_value, source_bytes)
662
+ static_classes = classes
663
+ class_expression = expr if resolution != "exact" else None
664
+ if attr_value:
665
+ attrs_dict[attr_name] = attr_value[1] if attr_value[0] == "string" else expr
666
+ elif attr_name == "style":
667
+ if attr_value and attr_value[0] == "expression":
668
+ bindings.append(JSXBinding(
669
+ component_index=component_index,
670
+ element_index=elem_index,
671
+ binding_name="style",
672
+ binding_type="style",
673
+ expression=attr_value[1],
674
+ resolution_status="unresolved",
675
+ location=attr["location"],
676
+ ))
677
+ elif attr_name == "ref":
678
+ if attr_value:
679
+ bindings.append(JSXBinding(
680
+ component_index=component_index,
681
+ element_index=elem_index,
682
+ binding_name="ref",
683
+ binding_type="ref",
684
+ expression=attr_value[1] if attr_value[0] == "expression" else attr_value[1],
685
+ resolution_status="unresolved",
686
+ location=attr["location"],
687
+ ))
688
+ elif attr_name == "...spread":
689
+ bindings.append(JSXBinding(
690
+ component_index=component_index,
691
+ element_index=elem_index,
692
+ binding_name="...props",
693
+ binding_type="spread",
694
+ expression=attr_value[1] if attr_value else "",
695
+ resolution_status="unresolved",
696
+ location=attr["location"],
697
+ ))
698
+ else:
699
+ event_match = EVENT_ATTR_PATTERN.match(attr_name)
700
+ if event_match and attr_value:
701
+ event_name = event_match.group(1).lower()
702
+ handler_expr = attr_value[1] if attr_value[0] == "expression" else attr_value[1]
703
+ handler_type = "direct"
704
+ resolution = "exact"
705
+ if attr_value[0] == "expression":
706
+ expr = attr_value[1].strip()
707
+ if expr.startswith("()") or expr.startswith("("):
708
+ handler_type = "inline"
709
+ resolution = "inferred"
710
+ elif "." in expr and "(" not in expr:
711
+ handler_type = "member"
712
+ resolution = "inferred"
713
+ elif expr.isidentifier():
714
+ handler_type = "direct"
715
+ resolution = "exact"
716
+ else:
717
+ handler_type = "inline"
718
+ resolution = "inferred"
719
+
720
+ events.append(JSXEvent(
721
+ component_index=component_index,
722
+ element_index=elem_index,
723
+ event_name=event_name,
724
+ handler_type=handler_type,
725
+ handler_expression=handler_expr,
726
+ resolution_status=resolution,
727
+ location=attr["location"],
728
+ ))
729
+ elif attr_value:
730
+ bindings.append(JSXBinding(
731
+ component_index=component_index,
732
+ element_index=elem_index,
733
+ binding_name=attr_name,
734
+ binding_type="property",
735
+ expression=attr_value[1] if attr_value[0] == "expression" else attr_value[1],
736
+ resolution_status="exact" if attr_value[0] == "string" else "unresolved",
737
+ location=attr["location"],
738
+ ))
739
+
740
+ if attr_name not in ("id", "className", "class", "style", "ref", "...spread"):
741
+ if attr_value:
742
+ attrs_dict[attr_name] = attr_value[1] if attr_value[0] == "string" else (attr_value[1] if attr_value[0] == "expression" else "")
743
+
744
+ markup_elem = JSXMarkupElement(
745
+ tag_name=tag_name,
746
+ element_type=elem_type,
747
+ location=node_to_location(node),
748
+ component_index=component_index,
749
+ parent_index=parent_index,
750
+ element_id_attr=element_id,
751
+ static_classes=static_classes,
752
+ attributes=attrs_dict,
753
+ is_conditional=conditional,
754
+ is_repeated=repeated,
755
+ conditional_expr=conditional_expr,
756
+ repeated_expr=repeated_expr,
757
+ class_expression=class_expression,
758
+ )
759
+ markup_elements.append(markup_elem)
760
+
761
+ if is_custom:
762
+ render_type = "conditional" if conditional else ("repeated" if repeated else "direct")
763
+ render_relationships.append(JSXRenderRelationship(
764
+ parent_component_index=component_index,
765
+ child_component_name=tag_name,
766
+ render_type=render_type,
767
+ element_index=elem_index,
768
+ ))
769
+
770
+ return children_to_walk
771
+
772
+
773
+ def _process_jsx_expression_node(node, source_bytes, component_index, parent_index,
774
+ markup_elements, events, bindings, render_relationships,
775
+ components):
776
+ """Process a jsx_expression node, detecting conditional/repeated rendering.
777
+
778
+ Returns a list of child nodes to push onto the walk stack.
779
+ """
780
+ children_to_walk = []
781
+
782
+ expr_node = None
783
+ for i in range(node.child_count):
784
+ c = node.child(i)
785
+ if c.type not in ("{", "}"):
786
+ expr_node = c
787
+ break
788
+
789
+ if expr_node is None:
790
+ return []
791
+
792
+ expr_text = extract_range(source_bytes, expr_node.start_byte, expr_node.end_byte)
793
+
794
+ if expr_node.type == "binary_expression" and "&&" in expr_text:
795
+ for i in range(expr_node.child_count):
796
+ c = expr_node.child(i)
797
+ if c.type.startswith("jsx_"):
798
+ cond_expr = expr_text.split("&&")[0].strip()
799
+ children_to_walk.append((c, parent_index, True, False, cond_expr, None))
800
+
801
+ elif expr_node.type == "ternary_expression":
802
+ cond_expr = None
803
+ for i in range(expr_node.child_count):
804
+ c = expr_node.child(i)
805
+ if c.type == "?":
806
+ cond_expr = extract_range(source_bytes, expr_node.start_byte, c.start_byte).strip()
807
+ if c.type.startswith("jsx_"):
808
+ children_to_walk.append((c, parent_index, True, False, cond_expr, None))
809
+
810
+ elif expr_node.type == "call_expression":
811
+ is_map = False
812
+ for i in range(expr_node.child_count):
813
+ c = expr_node.child(i)
814
+ if c.type == "member_expression":
815
+ member_text = extract_range(source_bytes, c.start_byte, c.end_byte)
816
+ if ".map" in member_text:
817
+ is_map = True
818
+
819
+ if is_map:
820
+ for i in range(expr_node.child_count):
821
+ c = expr_node.child(i)
822
+ if c.type == "arguments":
823
+ for j in range(c.child_count):
824
+ arg = c.child(j)
825
+ stack = [arg]
826
+ while stack:
827
+ n = stack.pop()
828
+ if n.type.startswith("jsx_"):
829
+ children_to_walk.append((n, parent_index, False, True, None, expr_text))
830
+ for k in range(n.child_count):
831
+ stack.append(n.child(k))
832
+ else:
833
+ markup_elements.append(JSXMarkupElement(
834
+ tag_name="#expression",
835
+ element_type="expression",
836
+ location=node_to_location(node),
837
+ component_index=component_index,
838
+ parent_index=parent_index,
839
+ expression_text=expr_text,
840
+ ))
841
+
842
+ else:
843
+ markup_elements.append(JSXMarkupElement(
844
+ tag_name="#expression",
845
+ element_type="expression",
846
+ location=node_to_location(node),
847
+ component_index=component_index,
848
+ parent_index=parent_index,
849
+ expression_text=expr_text,
850
+ ))
851
+
852
+ return children_to_walk
853
+
854
+
855
+ def _walk_jsx_elements(node, source_bytes, component_index, parent_index,
856
+ markup_elements, events, bindings, render_relationships,
857
+ components, conditional=False, repeated=False,
858
+ conditional_expr=None, repeated_expr=None):
859
+ """Iteratively walk JSX nodes and build markup element list.
860
+
861
+ Uses an explicit stack instead of recursion.
862
+ """
863
+ # Stack entries: (node, parent_idx, conditional, repeated, conditional_expr, repeated_expr)
864
+ stack = [(node, parent_index, conditional, repeated, conditional_expr, repeated_expr)]
865
+
866
+ while stack:
867
+ entry = stack.pop()
868
+ n, p_idx, cond, rep, cond_e, rep_e = entry
869
+
870
+ if n.type in ("jsx_element", "jsx_self_closing_element"):
871
+ children = _process_jsx_element_node(
872
+ n, source_bytes, component_index, p_idx,
873
+ markup_elements, events, bindings, render_relationships,
874
+ components, cond, rep, cond_e, rep_e,
875
+ )
876
+ # Push children in reverse order so they're processed left-to-right
877
+ for child in reversed(children):
878
+ stack.append(child)
879
+
880
+ elif n.type == "jsx_expression":
881
+ children = _process_jsx_expression_node(
882
+ n, source_bytes, component_index, p_idx,
883
+ markup_elements, events, bindings, render_relationships,
884
+ components,
885
+ )
886
+ for child in reversed(children):
887
+ stack.append(child)
888
+
889
+
890
+ def _find_component_jsx(func_node, source_bytes) -> List:
891
+ """Find all top-level JSX elements returned by a component function."""
892
+ jsx_roots = []
893
+
894
+ stack = [func_node]
895
+ while stack:
896
+ node = stack.pop()
897
+ if node.type == "return_statement":
898
+ for i in range(node.child_count):
899
+ c = node.child(i)
900
+ if c.type.startswith("jsx_"):
901
+ jsx_roots.append(c)
902
+ elif c.type == "parenthesized_expression":
903
+ for j in range(c.child_count):
904
+ cc = c.child(j)
905
+ if cc.type.startswith("jsx_"):
906
+ jsx_roots.append(cc)
907
+ # Arrow function direct JSX body
908
+ if node.type == "arrow_function" and node == func_node:
909
+ found_arrow = False
910
+ for i in range(node.child_count):
911
+ c = node.child(i)
912
+ if c.type == "=>":
913
+ found_arrow = True
914
+ continue
915
+ if found_arrow and c.type.startswith("jsx_"):
916
+ jsx_roots.append(c)
917
+ if found_arrow and c.type == "parenthesized_expression":
918
+ for j in range(c.child_count):
919
+ cc = c.child(j)
920
+ if cc.type.startswith("jsx_"):
921
+ jsx_roots.append(cc)
922
+ if node.type == "statement_block":
923
+ for i in range(node.child_count):
924
+ stack.append(node.child(i))
925
+ elif node.type == "arrow_function" and node != func_node:
926
+ pass # Don't recurse into nested arrows
927
+ else:
928
+ for i in range(node.child_count):
929
+ child = node.child(i)
930
+ if child.type not in ("arrow_function",) or child == func_node:
931
+ stack.append(child)
932
+
933
+ return jsx_roots
934
+
935
+
936
+ def extract_jsx_semantics(source_bytes: bytes, language: str = "tsx", config=None, tree=None) -> dict:
937
+ """Extract all semantic entities from a JSX/TSX file.
938
+
939
+ Args:
940
+ source_bytes: Raw source file content as bytes.
941
+ language: 'tsx' or 'javascript' (for .jsx files).
942
+ config: Optional FrontendConfig instance for extraction limits.
943
+ tree: Optional pre-parsed tree-sitter Tree. If provided, skips re-parsing.
944
+
945
+ Returns:
946
+ dict with keys:
947
+ - frontend_components: list of component dicts
948
+ - markup_elements: list of element dicts
949
+ - frontend_events: list of event handler dicts
950
+ - frontend_bindings: list of binding dicts
951
+ - render_relationships: list of render relationship dicts
952
+ - style_selector_matches: list of selector match dicts
953
+ - frontend_diagnostics: list of diagnostic dicts
954
+ """
955
+ if tree is not None:
956
+ root = tree.root_node
957
+ else:
958
+ parser = _get_parser(language)
959
+ if parser is None:
960
+ return {
961
+ "frontend_components": [],
962
+ "markup_elements": [],
963
+ "frontend_events": [],
964
+ "frontend_bindings": [],
965
+ "render_relationships": [],
966
+ "style_selector_matches": [],
967
+ "frontend_diagnostics": [{
968
+ "diagnostic_type": "missing_parser",
969
+ "severity": "fatal",
970
+ "message": f"Parser for {language} not available",
971
+ }],
972
+ }
973
+ tree = parser.parse(source_bytes)
974
+ root = tree.root_node
975
+
976
+ # Portal detection (works even without JSX nodes)
977
+ source_text = source_bytes.decode("utf-8", errors="replace") if isinstance(source_bytes, bytes) else source_bytes
978
+ portal_diags = []
979
+ if "createPortal" in source_text:
980
+ portal_diags.append({
981
+ "diagnostic_type": "portal_detected",
982
+ "severity": "unsupported",
983
+ "message": "React portal detected — portal children may not be in the normal render tree",
984
+ })
985
+
986
+ # If no JSX nodes, return empty (with portal diagnostic if applicable)
987
+ if not _has_jsx_nodes(root):
988
+ diags = list(portal_diags)
989
+ # Check for parse errors (truncated/incomplete files)
990
+ if root.has_error:
991
+ diags.append({
992
+ "diagnostic_type": "parse_error",
993
+ "severity": "recoverable",
994
+ "message": "JSX/TSX file has syntax errors — partial or no extraction performed",
995
+ })
996
+ return {
997
+ "frontend_components": [],
998
+ "markup_elements": [],
999
+ "frontend_events": [],
1000
+ "frontend_bindings": [],
1001
+ "render_relationships": [],
1002
+ "style_selector_matches": [],
1003
+ "frontend_diagnostics": diags,
1004
+ }
1005
+
1006
+ doc = JSXDocument(source_bytes=source_bytes)
1007
+
1008
+ # Find components and build a node map to avoid per-component root scans
1009
+ doc.components, component_node_map = _find_components(root, source_bytes)
1010
+
1011
+ # For each component, find its JSX and extract markup
1012
+ for comp_idx, component in enumerate(doc.components):
1013
+ # Use the node map from _find_components to avoid re-traversing the AST
1014
+ target_name = component.impl_function_name or component.impl_class_name
1015
+ comp_node = component_node_map.get(target_name)
1016
+ if comp_node is None:
1017
+ comp_node = _find_component_node(root, component, source_bytes)
1018
+ if comp_node is None:
1019
+ continue
1020
+
1021
+ jsx_roots = _find_component_jsx(comp_node, source_bytes)
1022
+ for jsx_root in jsx_roots:
1023
+ _walk_jsx_elements(
1024
+ jsx_root, source_bytes, comp_idx, None,
1025
+ doc.markup_elements, doc.events, doc.bindings,
1026
+ doc.render_relationships, doc.components,
1027
+ )
1028
+
1029
+ # Compute selector matches for static className strings
1030
+ # (The global FrontendResolver also recomputes these, but the local computation
1031
+ # is needed for standalone extractor usage and is cheap for individual files.)
1032
+ for i, elem in enumerate(doc.markup_elements):
1033
+ if elem.static_classes and elem.element_type in ("native", "custom_component"):
1034
+ for cls in elem.static_classes:
1035
+ doc.selector_matches.append({
1036
+ "element_index": i,
1037
+ "selector_text": f".{cls}",
1038
+ "match_type": "static",
1039
+ "confidence": "high",
1040
+ "source_range": elem.location.to_dict(),
1041
+ })
1042
+
1043
+ # Build result dict
1044
+ result = {
1045
+ "frontend_components": [],
1046
+ "markup_elements": [],
1047
+ "frontend_events": [],
1048
+ "frontend_bindings": [],
1049
+ "render_relationships": [],
1050
+ "style_selector_matches": [],
1051
+ "frontend_diagnostics": [],
1052
+ }
1053
+
1054
+ # Components
1055
+ for comp in doc.components:
1056
+ result["frontend_components"].append({
1057
+ "name": comp.name,
1058
+ "component_type": comp.component_type,
1059
+ "source_range": comp.location.to_dict(),
1060
+ "is_exported": comp.is_exported,
1061
+ "is_default_export": comp.is_default_export,
1062
+ "impl_function_name": comp.impl_function_name,
1063
+ "impl_class_name": comp.impl_class_name,
1064
+ "wrapper_name": comp.wrapper_name,
1065
+ })
1066
+
1067
+ # Markup elements
1068
+ for elem in doc.markup_elements:
1069
+ result["markup_elements"].append({
1070
+ "tag_name": elem.tag_name,
1071
+ "element_type": elem.element_type,
1072
+ "source_range": elem.location.to_dict(),
1073
+ "component_index": elem.component_index,
1074
+ "parent_index": elem.parent_index,
1075
+ "element_id_attr": elem.element_id_attr,
1076
+ "static_classes": elem.static_classes if elem.static_classes else None,
1077
+ "attributes": elem.attributes if elem.attributes else None,
1078
+ "is_conditional": elem.is_conditional,
1079
+ "is_repeated": elem.is_repeated,
1080
+ "conditional_expr": elem.conditional_expr,
1081
+ "repeated_expr": elem.repeated_expr,
1082
+ "expression_text": elem.expression_text,
1083
+ })
1084
+
1085
+ # Events
1086
+ for ev in doc.events:
1087
+ result["frontend_events"].append({
1088
+ "component_index": ev.component_index,
1089
+ "element_index": ev.element_index,
1090
+ "event_name": ev.event_name,
1091
+ "handler_type": ev.handler_type,
1092
+ "handler_expression": ev.handler_expression,
1093
+ "resolution_status": ev.resolution_status,
1094
+ "source_range": ev.location.to_dict(),
1095
+ })
1096
+
1097
+ # Bindings
1098
+ for b in doc.bindings:
1099
+ result["frontend_bindings"].append({
1100
+ "component_index": b.component_index,
1101
+ "element_index": b.element_index,
1102
+ "binding_name": b.binding_name,
1103
+ "binding_type": b.binding_type,
1104
+ "expression": b.expression,
1105
+ "resolution_status": b.resolution_status,
1106
+ "source_range": b.location.to_dict(),
1107
+ })
1108
+
1109
+ # Render relationships
1110
+ for rr in doc.render_relationships:
1111
+ result["render_relationships"].append({
1112
+ "parent_component_index": rr.parent_component_index,
1113
+ "child_component_name": rr.child_component_name,
1114
+ "render_type": rr.render_type,
1115
+ "element_index": rr.element_index,
1116
+ })
1117
+
1118
+ # Selector matches
1119
+ result["style_selector_matches"] = doc.selector_matches
1120
+
1121
+ # Diagnostics
1122
+ if root.has_error:
1123
+ result["frontend_diagnostics"].append({
1124
+ "diagnostic_type": "parse_error",
1125
+ "severity": "recoverable",
1126
+ "message": "JSX/TSX file contains parse errors",
1127
+ })
1128
+
1129
+ # Unresolved render relationships (component referenced but not found in file)
1130
+ component_names = {c.name for c in doc.components}
1131
+ for rr in doc.render_relationships:
1132
+ if rr.child_component_name not in component_names:
1133
+ result["frontend_diagnostics"].append({
1134
+ "diagnostic_type": "unresolved_component",
1135
+ "severity": "unresolved",
1136
+ "message": f"Component '{rr.child_component_name}' is rendered but not defined in this file",
1137
+ })
1138
+
1139
+ # Dynamic class expressions
1140
+ for elem in doc.markup_elements:
1141
+ if elem.class_expression and elem.element_type in ("native", "custom_component"):
1142
+ result["frontend_diagnostics"].append({
1143
+ "diagnostic_type": "dynamic_class_expression",
1144
+ "severity": "unresolved",
1145
+ "message": f"className expression '{elem.class_expression[:50]}' could not be statically resolved",
1146
+ "source_range": elem.location.to_dict(),
1147
+ })
1148
+
1149
+ # Spread props
1150
+ for b in doc.bindings:
1151
+ if b.binding_type == "spread":
1152
+ result["frontend_diagnostics"].append({
1153
+ "diagnostic_type": "spread_props",
1154
+ "severity": "unsupported",
1155
+ "message": f"Spread props '{b.expression[:50]}' — individual props cannot be resolved",
1156
+ "source_range": b.location.to_dict(),
1157
+ })
1158
+
1159
+ # Include portal diagnostic if detected
1160
+ result["frontend_diagnostics"].extend(portal_diags)
1161
+
1162
+ return result
1163
+
1164
+
1165
+ def _find_component_node(root_node, component: JSXComponent, source_bytes):
1166
+ """Find the AST node corresponding to a component."""
1167
+ target_name = component.impl_function_name or component.impl_class_name
1168
+ if target_name is None:
1169
+ return None
1170
+
1171
+ stack = [root_node]
1172
+ while stack:
1173
+ node = stack.pop()
1174
+
1175
+ if node.type == "function_declaration":
1176
+ for i in range(node.child_count):
1177
+ c = node.child(i)
1178
+ if c.type in ("identifier", "type_identifier"):
1179
+ name = extract_range(source_bytes, c.start_byte, c.end_byte)
1180
+ if name == target_name:
1181
+ return node
1182
+
1183
+ elif node.type in ("arrow_function", "function_expression"):
1184
+ parent = node.parent
1185
+ if parent and parent.type == "variable_declarator":
1186
+ for i in range(parent.child_count):
1187
+ c = parent.child(i)
1188
+ if c.type in ("identifier", "type_identifier"):
1189
+ name = extract_range(source_bytes, c.start_byte, c.end_byte)
1190
+ if name == target_name:
1191
+ return node
1192
+
1193
+ elif node.type == "class_declaration":
1194
+ for i in range(node.child_count):
1195
+ c = node.child(i)
1196
+ if c.type in ("identifier", "type_identifier"):
1197
+ name = extract_range(source_bytes, c.start_byte, c.end_byte)
1198
+ if name == target_name:
1199
+ return node
1200
+
1201
+ for i in range(node.child_count):
1202
+ stack.append(node.child(i))
1203
+
1204
+ return None