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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- 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
|