blasttrace 1.2.0__py2.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.
Potentially problematic release.
This version of blasttrace might be problematic. Click here for more details.
- blasttrace/__init__.py +7 -0
- blasttrace/classifier.py +415 -0
- blasttrace/install_agent.py +220 -0
- blasttrace/license.py +331 -0
- blasttrace/parsers/__init__.py +4 -0
- blasttrace/parsers/api_route_parser.py +318 -0
- blasttrace/parsers/contract_linker.py +815 -0
- blasttrace/parsers/dart_flutter_parser.py +110 -0
- blasttrace/parsers/go_parser.py +196 -0
- blasttrace/parsers/kotlin_android_parser.py +122 -0
- blasttrace/parsers/python_ast_parser.py +340 -0
- blasttrace/parsers/sql_schema_parser.py +376 -0
- blasttrace/parsers/swift_ios_parser.py +127 -0
- blasttrace/parsers/test_mapper.py +182 -0
- blasttrace/parsers/ts_react_parser.py +299 -0
- blasttrace/parsers/vue_nuxt_parser.py +175 -0
- blasttrace/remediation.py +220 -0
- blasttrace/server.py +935 -0
- blasttrace/visualizer.py +165 -0
- blasttrace-1.2.0.dist-info/METADATA +125 -0
- blasttrace-1.2.0.dist-info/RECORD +24 -0
- blasttrace-1.2.0.dist-info/WHEEL +6 -0
- blasttrace-1.2.0.dist-info/entry_points.txt +3 -0
- blasttrace-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace TypeScript & React Frontend Parser
|
|
3
|
+
Parses TypeScript/JavaScript functions, React components, interfaces/types, ES imports,
|
|
4
|
+
API consumer calls (fetch, axios, useSWR, useQuery), and response property accesses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_url_to_route_path(raw_url: str) -> str:
|
|
13
|
+
cleaned = raw_url.strip().strip("\"'`")
|
|
14
|
+
cleaned = re.sub(r"^https?://[^/]+", "", cleaned)
|
|
15
|
+
cleaned = cleaned.split("?")[0]
|
|
16
|
+
cleaned = re.sub(r"\$\{[^}]+\}", ":param", cleaned)
|
|
17
|
+
cleaned = re.sub(r"\{[^}]+\}", ":param", cleaned)
|
|
18
|
+
if not cleaned.startswith("/"):
|
|
19
|
+
cleaned = "/" + cleaned
|
|
20
|
+
if len(cleaned) > 1 and cleaned.endswith("/"):
|
|
21
|
+
cleaned = cleaned.rstrip("/")
|
|
22
|
+
return cleaned
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_ts_params(param_str: str) -> List[Dict[str, Any]]:
|
|
26
|
+
params: List[Dict[str, Any]] = []
|
|
27
|
+
raw = param_str.strip()
|
|
28
|
+
if not raw:
|
|
29
|
+
return params
|
|
30
|
+
# Handle destructured props like `{ userId, amount = 0 }: CheckoutProps`
|
|
31
|
+
if raw.startswith("{"):
|
|
32
|
+
destruct_match = re.match(r"\{([^}]*)\}(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?", raw)
|
|
33
|
+
if destruct_match:
|
|
34
|
+
inner = destruct_match.group(1)
|
|
35
|
+
prop_type = (destruct_match.group(2) or "").strip()
|
|
36
|
+
for part in inner.split(","):
|
|
37
|
+
p = part.strip()
|
|
38
|
+
if not p:
|
|
39
|
+
continue
|
|
40
|
+
has_def = "=" in p
|
|
41
|
+
p_name = p.split("=")[0].split(":")[0].strip()
|
|
42
|
+
def_val = p.split("=", 1)[1].strip() if has_def else None
|
|
43
|
+
params.append({
|
|
44
|
+
"name": p_name,
|
|
45
|
+
"annotation": prop_type,
|
|
46
|
+
"required": not has_def,
|
|
47
|
+
"default": def_val,
|
|
48
|
+
})
|
|
49
|
+
return params
|
|
50
|
+
|
|
51
|
+
for part in raw.split(","):
|
|
52
|
+
p = part.strip()
|
|
53
|
+
if not p:
|
|
54
|
+
continue
|
|
55
|
+
has_def = "=" in p
|
|
56
|
+
left = p.split("=", 1)[0].strip()
|
|
57
|
+
def_val = p.split("=", 1)[1].strip() if has_def else None
|
|
58
|
+
optional_q = "?" in left.split(":")[0]
|
|
59
|
+
if ":" in left:
|
|
60
|
+
p_name, p_ann = left.split(":", 1)
|
|
61
|
+
p_name = p_name.strip().rstrip("?")
|
|
62
|
+
p_ann = p_ann.strip()
|
|
63
|
+
else:
|
|
64
|
+
p_name = left.rstrip("?")
|
|
65
|
+
p_ann = ""
|
|
66
|
+
params.append({
|
|
67
|
+
"name": p_name,
|
|
68
|
+
"annotation": p_ann,
|
|
69
|
+
"required": not (has_def or optional_q),
|
|
70
|
+
"default": def_val,
|
|
71
|
+
})
|
|
72
|
+
return params
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_ts_react_content(content: str, rel_file: str) -> Dict[str, Any]:
|
|
76
|
+
symbols: Dict[str, Dict[str, Any]] = {}
|
|
77
|
+
interfaces: Dict[str, Dict[str, Any]] = {}
|
|
78
|
+
imports: List[Dict[str, str]] = []
|
|
79
|
+
api_calls: List[Dict[str, Any]] = []
|
|
80
|
+
property_accesses: List[str] = []
|
|
81
|
+
nodes: List[Dict[str, Any]] = []
|
|
82
|
+
edges: List[Dict[str, str]] = []
|
|
83
|
+
|
|
84
|
+
module_id = f"fe:{rel_file}"
|
|
85
|
+
nodes.append({
|
|
86
|
+
"id": module_id,
|
|
87
|
+
"type": "frontend_module",
|
|
88
|
+
"name": rel_file,
|
|
89
|
+
"file": rel_file,
|
|
90
|
+
"layer": "frontend",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# 1. ES Imports
|
|
94
|
+
import_pattern = re.compile(
|
|
95
|
+
r"import\s+(?:type\s+)?(?:\{([^}]+)\}|([A-Za-z0-9_$]+))\s+from\s+[`\"']([^`\"']+)[`\"']",
|
|
96
|
+
re.MULTILINE,
|
|
97
|
+
)
|
|
98
|
+
for match in import_pattern.finditer(content):
|
|
99
|
+
named_group = match.group(1)
|
|
100
|
+
default_imp = match.group(2)
|
|
101
|
+
src_mod = match.group(3)
|
|
102
|
+
if named_group:
|
|
103
|
+
for item in named_group.split(","):
|
|
104
|
+
sym = item.strip().split(" as ")[0].strip()
|
|
105
|
+
if sym:
|
|
106
|
+
imports.append({"module": src_mod, "symbol": sym})
|
|
107
|
+
elif default_imp:
|
|
108
|
+
imports.append({"module": src_mod, "symbol": default_imp.strip()})
|
|
109
|
+
|
|
110
|
+
# 2. Interfaces & Type Aliases
|
|
111
|
+
iface_pattern = re.compile(r"(?:export\s+)?interface\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}", re.DOTALL)
|
|
112
|
+
for match in iface_pattern.finditer(content):
|
|
113
|
+
iname = match.group(1).strip()
|
|
114
|
+
ibody = match.group(2)
|
|
115
|
+
fields: Dict[str, Dict[str, Any]] = {}
|
|
116
|
+
for line in ibody.splitlines():
|
|
117
|
+
line_clean = line.strip().rstrip(";,")
|
|
118
|
+
if not line_clean or line_clean.startswith("//"):
|
|
119
|
+
continue
|
|
120
|
+
if ":" in line_clean:
|
|
121
|
+
fname, ftype = line_clean.split(":", 1)
|
|
122
|
+
fname_clean = fname.strip().rstrip("?")
|
|
123
|
+
fields[fname_clean] = {
|
|
124
|
+
"name": fname_clean,
|
|
125
|
+
"type": ftype.strip(),
|
|
126
|
+
"required": not fname.strip().endswith("?"),
|
|
127
|
+
}
|
|
128
|
+
iface_id = f"fe:{rel_file}:{iname}"
|
|
129
|
+
interfaces[iname] = {"name": iname, "fields": fields, "file": rel_file}
|
|
130
|
+
nodes.append({
|
|
131
|
+
"id": iface_id,
|
|
132
|
+
"type": "ts_interface",
|
|
133
|
+
"name": iname,
|
|
134
|
+
"fields": list(fields.keys()),
|
|
135
|
+
"file": rel_file,
|
|
136
|
+
"layer": "frontend",
|
|
137
|
+
})
|
|
138
|
+
edges.append({"source": module_id, "target": iface_id, "relation": "defines_interface"})
|
|
139
|
+
|
|
140
|
+
# 3. Functions, Arrow Functions, and React Components
|
|
141
|
+
fn_pattern = re.compile(
|
|
142
|
+
r"(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?",
|
|
143
|
+
)
|
|
144
|
+
for match in fn_pattern.finditer(content):
|
|
145
|
+
fname = match.group(1).strip()
|
|
146
|
+
raw_params = match.group(2)
|
|
147
|
+
ret_type = (match.group(3) or "").strip()
|
|
148
|
+
params = _parse_ts_params(raw_params)
|
|
149
|
+
is_component = fname[0].isupper()
|
|
150
|
+
sym_id = f"fe:{rel_file}:{fname}"
|
|
151
|
+
symbols[fname] = {
|
|
152
|
+
"name": fname,
|
|
153
|
+
"kind": "react_component" if is_component else "ts_function",
|
|
154
|
+
"params": params,
|
|
155
|
+
"return_type": ret_type,
|
|
156
|
+
"signature_string": f"function {fname}({raw_params})" + (f": {ret_type}" if ret_type else ""),
|
|
157
|
+
"file": rel_file,
|
|
158
|
+
}
|
|
159
|
+
nodes.append({
|
|
160
|
+
"id": sym_id,
|
|
161
|
+
"type": "react_component" if is_component else "ts_function",
|
|
162
|
+
"name": fname,
|
|
163
|
+
"params": params,
|
|
164
|
+
"return_type": ret_type,
|
|
165
|
+
"file": rel_file,
|
|
166
|
+
"layer": "frontend",
|
|
167
|
+
})
|
|
168
|
+
edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
|
|
169
|
+
|
|
170
|
+
arrow_pattern = re.compile(
|
|
171
|
+
r"(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*(?::\s*[^=]+)?=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([A-Za-z0-9_<>[\]|&]+))?\s*=>",
|
|
172
|
+
)
|
|
173
|
+
for match in arrow_pattern.finditer(content):
|
|
174
|
+
fname = match.group(1).strip()
|
|
175
|
+
if fname in symbols:
|
|
176
|
+
continue
|
|
177
|
+
raw_params = match.group(2)
|
|
178
|
+
ret_type = (match.group(3) or "").strip()
|
|
179
|
+
params = _parse_ts_params(raw_params)
|
|
180
|
+
is_component = fname[0].isupper()
|
|
181
|
+
sym_id = f"fe:{rel_file}:{fname}"
|
|
182
|
+
symbols[fname] = {
|
|
183
|
+
"name": fname,
|
|
184
|
+
"kind": "react_component" if is_component else "ts_function",
|
|
185
|
+
"params": params,
|
|
186
|
+
"return_type": ret_type,
|
|
187
|
+
"signature_string": f"const {fname} = ({raw_params}) =>",
|
|
188
|
+
"file": rel_file,
|
|
189
|
+
}
|
|
190
|
+
nodes.append({
|
|
191
|
+
"id": sym_id,
|
|
192
|
+
"type": "react_component" if is_component else "ts_function",
|
|
193
|
+
"name": fname,
|
|
194
|
+
"params": params,
|
|
195
|
+
"return_type": ret_type,
|
|
196
|
+
"file": rel_file,
|
|
197
|
+
"layer": "frontend",
|
|
198
|
+
})
|
|
199
|
+
edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
|
|
200
|
+
|
|
201
|
+
# 4. Property accesses on response/state objects (e.g. data.balance, user.balance, profile.email)
|
|
202
|
+
prop_pattern = re.compile(r"\b(?:data|user|res|response|payload|item|account|profile)\.([a-zA-Z_][a-zA-Z0-9_]*)\b")
|
|
203
|
+
for p_match in prop_pattern.finditer(content):
|
|
204
|
+
prop_name = p_match.group(1)
|
|
205
|
+
if prop_name not in ("json", "status", "ok", "headers", "then", "catch", "map", "filter", "length"):
|
|
206
|
+
if prop_name not in property_accesses:
|
|
207
|
+
property_accesses.append(prop_name)
|
|
208
|
+
|
|
209
|
+
# 5. API calls: fetch('/api/...'), axios.get('/api/...'), useSWR('/api/...')
|
|
210
|
+
fetch_pattern = re.compile(
|
|
211
|
+
r"\bfetch\s*\(\s*[`\"']([^`\"']+)[`\"'](?:\s*,\s*\{([^}]*)\})?",
|
|
212
|
+
re.DOTALL,
|
|
213
|
+
)
|
|
214
|
+
for match in fetch_pattern.finditer(content):
|
|
215
|
+
raw_url = match.group(1)
|
|
216
|
+
opts = match.group(2) or ""
|
|
217
|
+
m_match = re.search(r"method\s*:\s*[`\"']([A-Za-z]+)[`\"']", opts, re.IGNORECASE)
|
|
218
|
+
method = m_match.group(1).upper() if m_match else "GET"
|
|
219
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
220
|
+
api_calls.append({
|
|
221
|
+
"client": "fetch",
|
|
222
|
+
"method": method,
|
|
223
|
+
"raw_url": raw_url,
|
|
224
|
+
"normalized_path": norm_path,
|
|
225
|
+
"accessed_fields": property_accesses,
|
|
226
|
+
"file": rel_file,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
axios_pattern = re.compile(
|
|
230
|
+
r"\baxios\.(get|post|put|delete|patch)\s*(?:<[^>]+>)?\s*\(\s*[`\"']([^`\"']+)[`\"']",
|
|
231
|
+
re.IGNORECASE,
|
|
232
|
+
)
|
|
233
|
+
for match in axios_pattern.finditer(content):
|
|
234
|
+
method = match.group(1).upper()
|
|
235
|
+
raw_url = match.group(2)
|
|
236
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
237
|
+
api_calls.append({
|
|
238
|
+
"client": "axios",
|
|
239
|
+
"method": method,
|
|
240
|
+
"raw_url": raw_url,
|
|
241
|
+
"normalized_path": norm_path,
|
|
242
|
+
"accessed_fields": property_accesses,
|
|
243
|
+
"file": rel_file,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
swr_pattern = re.compile(r"\b(?:useSWR|useQuery)\s*(?:<[^>]+>)?\s*\(\s*[`\"'](/[^`\"']+)[`\"']")
|
|
247
|
+
for match in swr_pattern.finditer(content):
|
|
248
|
+
raw_url = match.group(1)
|
|
249
|
+
norm_path = _normalize_url_to_route_path(raw_url)
|
|
250
|
+
api_calls.append({
|
|
251
|
+
"client": "hook",
|
|
252
|
+
"method": "GET",
|
|
253
|
+
"raw_url": raw_url,
|
|
254
|
+
"normalized_path": norm_path,
|
|
255
|
+
"accessed_fields": property_accesses,
|
|
256
|
+
"file": rel_file,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"file": rel_file,
|
|
261
|
+
"symbols": symbols,
|
|
262
|
+
"interfaces": interfaces,
|
|
263
|
+
"imports": imports,
|
|
264
|
+
"api_calls": api_calls,
|
|
265
|
+
"property_accesses": property_accesses,
|
|
266
|
+
"nodes": nodes,
|
|
267
|
+
"edges": edges,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def parse_ts_react_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
|
|
272
|
+
if not os.path.exists(file_path):
|
|
273
|
+
return {
|
|
274
|
+
"error": f"Frontend file not found: {file_path}",
|
|
275
|
+
"symbols": {},
|
|
276
|
+
"interfaces": {},
|
|
277
|
+
"imports": [],
|
|
278
|
+
"api_calls": [],
|
|
279
|
+
"property_accesses": [],
|
|
280
|
+
"nodes": [],
|
|
281
|
+
"edges": [],
|
|
282
|
+
}
|
|
283
|
+
rel_file = os.path.relpath(file_path, repo_root).replace("\\", "/") if repo_root else os.path.basename(file_path)
|
|
284
|
+
try:
|
|
285
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
286
|
+
content = f.read()
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
return {
|
|
289
|
+
"error": str(exc),
|
|
290
|
+
"symbols": {},
|
|
291
|
+
"interfaces": {},
|
|
292
|
+
"imports": [],
|
|
293
|
+
"api_calls": [],
|
|
294
|
+
"property_accesses": [],
|
|
295
|
+
"nodes": [],
|
|
296
|
+
"edges": [],
|
|
297
|
+
}
|
|
298
|
+
return parse_ts_react_content(content, rel_file)
|
|
299
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Vue.js & Nuxt 3 AST Parser
|
|
3
|
+
Extracts component definitions, script interfaces, Pinia state bindings,
|
|
4
|
+
template property references, and HTTP / useFetch / $fetch API route queries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Any, List, Optional, Set
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class VueApiCall:
|
|
15
|
+
endpoint: str
|
|
16
|
+
method: str = "GET"
|
|
17
|
+
line_number: int = 1
|
|
18
|
+
response_fields_accessed: List[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class VueComponent:
|
|
23
|
+
name: str
|
|
24
|
+
file_path: str
|
|
25
|
+
props: List[str] = field(default_factory=list)
|
|
26
|
+
interfaces: List[str] = field(default_factory=list)
|
|
27
|
+
state_properties: List[str] = field(default_factory=list)
|
|
28
|
+
template_properties: List[str] = field(default_factory=list)
|
|
29
|
+
api_calls: List[VueApiCall] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class VueParseResult:
|
|
34
|
+
components: List[VueComponent] = field(default_factory=list)
|
|
35
|
+
api_calls: List[VueApiCall] = field(default_factory=list)
|
|
36
|
+
interfaces: Dict[str, List[str]] = field(default_factory=dict)
|
|
37
|
+
all_accessed_properties: Set[str] = field(default_factory=set)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Regex patterns for Vue 2/3 and Nuxt 3
|
|
41
|
+
_RE_SCRIPT = re.compile(r"<script[^>]*>(.*?)</script>", re.DOTALL | re.IGNORECASE)
|
|
42
|
+
_RE_TEMPLATE = re.compile(r"<template[^>]*>(.*?)</template>", re.DOTALL | re.IGNORECASE)
|
|
43
|
+
|
|
44
|
+
# TypeScript interfaces inside Vue script
|
|
45
|
+
_RE_INTERFACE = re.compile(r"(?:export\s+)?interface\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}", re.DOTALL)
|
|
46
|
+
_RE_TYPE_ALIAS = re.compile(r"(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*=\s*\{([^}]*)\}", re.DOTALL)
|
|
47
|
+
|
|
48
|
+
# Vue defineProps
|
|
49
|
+
_RE_DEFINE_PROPS = re.compile(r"defineProps<\s*\{([^}]*)\}\s*>", re.DOTALL)
|
|
50
|
+
_RE_DEFINE_PROPS_ARR = re.compile(r"defineProps\(\s*\[([^\]]+)\]\s*\)")
|
|
51
|
+
|
|
52
|
+
# Pinia stores: const store = useProductStore()
|
|
53
|
+
_RE_PINIA = re.compile(r"(?:const|let|var)\s+([A-Za-z0-9_]+)\s*=\s*(use[A-Za-z0-9_]+Store)\(\)")
|
|
54
|
+
|
|
55
|
+
# API Fetching in Vue / Nuxt:
|
|
56
|
+
# 1. Nuxt: useFetch('/api/v1/user') or useAsyncData('key', () => $fetch('/api/...'))
|
|
57
|
+
# 2. $fetch('/api/v1/user')
|
|
58
|
+
# 3. axios.get('/api/v1/user')
|
|
59
|
+
# 4. fetch('/api/v1/user')
|
|
60
|
+
_RE_NUXT_USE_FETCH = re.compile(r"useFetch(?:<[^>]+>)?\(\s*['\"`](/[^'\"`]+)['\"`]")
|
|
61
|
+
_RE_DOLLAR_FETCH = re.compile(r"\$fetch(?:<[^>]+>)?\(\s*['\"`](/[^'\"`]+)['\"`]")
|
|
62
|
+
_RE_AXIOS = re.compile(r"axios\.(get|post|put|delete|patch)\(\s*['\"`](/[^'\"`]+)['\"`]", re.IGNORECASE)
|
|
63
|
+
_RE_STANDARD_FETCH = re.compile(r"fetch\(\s*['\"`](/[^'\"`]+)['\"`]")
|
|
64
|
+
|
|
65
|
+
# Template property accesses: {{ item.price }}, :value="user.balance", v-if="order.tax"
|
|
66
|
+
_RE_TEMPLATE_INTERPOLATION = re.compile(r"\{\{\s*([^}]+)\s*\}\}")
|
|
67
|
+
_RE_TEMPLATE_ATTR_BINDING = re.compile(r"(?::|v-bind:|v-model|v-if|v-for=)[a-zA-Z0-9_\-\.]*=['\"]([^'\"]+)['\"]")
|
|
68
|
+
_RE_PROP_ACCESS = re.compile(r"(?:[A-Za-z0-9_]+)\.([A-Za-z0-9_]+)")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_vue_content(content: str, file_path: str = "Component.vue") -> VueParseResult:
|
|
72
|
+
"""Parses single-file Vue / Nuxt component (.vue)."""
|
|
73
|
+
result = VueParseResult()
|
|
74
|
+
basename = os.path.basename(file_path)
|
|
75
|
+
comp_name = os.path.splitext(basename)[0]
|
|
76
|
+
|
|
77
|
+
component = VueComponent(name=comp_name, file_path=file_path)
|
|
78
|
+
|
|
79
|
+
# 1. Extract Scripts
|
|
80
|
+
scripts = _RE_SCRIPT.findall(content)
|
|
81
|
+
script_text = "\n".join(scripts) if scripts else ""
|
|
82
|
+
|
|
83
|
+
# Parse Interfaces
|
|
84
|
+
for match in _RE_INTERFACE.finditer(script_text):
|
|
85
|
+
ifname = match.group(1)
|
|
86
|
+
body = match.group(2)
|
|
87
|
+
fields = [f.split(":")[0].strip().replace("?", "") for f in body.split(";") if ":" in f]
|
|
88
|
+
component.interfaces.append(ifname)
|
|
89
|
+
result.interfaces[ifname] = fields
|
|
90
|
+
|
|
91
|
+
for match in _RE_TYPE_ALIAS.finditer(script_text):
|
|
92
|
+
tname = match.group(1)
|
|
93
|
+
body = match.group(2)
|
|
94
|
+
fields = [f.split(":")[0].strip().replace("?", "") for f in body.split(";") if ":" in f]
|
|
95
|
+
component.interfaces.append(tname)
|
|
96
|
+
result.interfaces[tname] = fields
|
|
97
|
+
|
|
98
|
+
# Parse defineProps
|
|
99
|
+
for match in _RE_DEFINE_PROPS.finditer(script_text):
|
|
100
|
+
body = match.group(1)
|
|
101
|
+
props = [p.split(":")[0].strip().replace("?", "") for p in body.split(";") if ":" in p]
|
|
102
|
+
component.props.extend(props)
|
|
103
|
+
|
|
104
|
+
for match in _RE_DEFINE_PROPS_ARR.finditer(script_text):
|
|
105
|
+
raw_props = match.group(1)
|
|
106
|
+
props = [p.strip().strip("'\"") for p in raw_props.split(",") if p.strip()]
|
|
107
|
+
component.props.extend(props)
|
|
108
|
+
|
|
109
|
+
# Parse API calls
|
|
110
|
+
# Nuxt useFetch
|
|
111
|
+
for match in _RE_NUXT_USE_FETCH.finditer(script_text):
|
|
112
|
+
endpoint = match.group(1)
|
|
113
|
+
call = VueApiCall(endpoint=endpoint, method="GET")
|
|
114
|
+
component.api_calls.append(call)
|
|
115
|
+
result.api_calls.append(call)
|
|
116
|
+
|
|
117
|
+
# $fetch
|
|
118
|
+
for match in _RE_DOLLAR_FETCH.finditer(script_text):
|
|
119
|
+
endpoint = match.group(1)
|
|
120
|
+
call = VueApiCall(endpoint=endpoint, method="GET")
|
|
121
|
+
component.api_calls.append(call)
|
|
122
|
+
result.api_calls.append(call)
|
|
123
|
+
|
|
124
|
+
# Axios
|
|
125
|
+
for match in _RE_AXIOS.finditer(script_text):
|
|
126
|
+
method = match.group(1).upper()
|
|
127
|
+
endpoint = match.group(2)
|
|
128
|
+
call = VueApiCall(endpoint=endpoint, method=method)
|
|
129
|
+
component.api_calls.append(call)
|
|
130
|
+
result.api_calls.append(call)
|
|
131
|
+
|
|
132
|
+
# Standard fetch
|
|
133
|
+
for match in _RE_STANDARD_FETCH.finditer(script_text):
|
|
134
|
+
endpoint = match.group(1)
|
|
135
|
+
call = VueApiCall(endpoint=endpoint, method="GET")
|
|
136
|
+
component.api_calls.append(call)
|
|
137
|
+
result.api_calls.append(call)
|
|
138
|
+
|
|
139
|
+
# 2. Extract Templates
|
|
140
|
+
templates = _RE_TEMPLATE.findall(content)
|
|
141
|
+
template_text = "\n".join(templates) if templates else ""
|
|
142
|
+
|
|
143
|
+
# Check property bindings in template
|
|
144
|
+
accessed: Set[str] = set()
|
|
145
|
+
for match in _RE_TEMPLATE_INTERPOLATION.finditer(template_text):
|
|
146
|
+
expr = match.group(1)
|
|
147
|
+
for pmatch in _RE_PROP_ACCESS.finditer(expr):
|
|
148
|
+
accessed.add(pmatch.group(1))
|
|
149
|
+
|
|
150
|
+
for match in _RE_TEMPLATE_ATTR_BINDING.finditer(template_text):
|
|
151
|
+
expr = match.group(1)
|
|
152
|
+
for pmatch in _RE_PROP_ACCESS.finditer(expr):
|
|
153
|
+
accessed.add(pmatch.group(1))
|
|
154
|
+
|
|
155
|
+
# Also extract property accesses in script
|
|
156
|
+
for pmatch in _RE_PROP_ACCESS.finditer(script_text):
|
|
157
|
+
accessed.add(pmatch.group(1))
|
|
158
|
+
|
|
159
|
+
component.template_properties = sorted(list(accessed))
|
|
160
|
+
result.all_accessed_properties = accessed
|
|
161
|
+
result.components.append(component)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parse_vue_file(file_path: str) -> Optional[VueParseResult]:
|
|
167
|
+
"""Reads and parses a .vue file."""
|
|
168
|
+
if not os.path.exists(file_path):
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
172
|
+
content = f.read()
|
|
173
|
+
return parse_vue_content(content, file_path)
|
|
174
|
+
except Exception:
|
|
175
|
+
return None
|