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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Dart & Flutter Mobile/Client Parser
|
|
3
|
+
Parses Dart models, fromJson/toJson serializers, and HTTP/Dio API route consumers
|
|
4
|
+
in Flutter mobile, tablet, and POS applications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_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"\$([a-zA-Z0-9_]+)", ":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_dart_content(content: str, rel_path: str = "") -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Extracts Flutter Dart classes, models, fromJson field accesses,
|
|
28
|
+
and HTTP API route consumer calls from Dart source code.
|
|
29
|
+
"""
|
|
30
|
+
classes: List[Dict[str, Any]] = []
|
|
31
|
+
api_calls: List[Dict[str, Any]] = []
|
|
32
|
+
accessed_properties: List[str] = []
|
|
33
|
+
|
|
34
|
+
# 1. Parse Dart Classes and Models
|
|
35
|
+
class_regex = re.compile(r"class\s+([A-Za-z0-9_]+)(?:\s+extends\s+[A-Za-z0-9_]+)?\s*\{", re.MULTILINE)
|
|
36
|
+
for m in class_regex.finditer(content):
|
|
37
|
+
cname = m.group(1)
|
|
38
|
+
classes.append({
|
|
39
|
+
"name": cname,
|
|
40
|
+
"type": "dart_class",
|
|
41
|
+
"file": rel_path,
|
|
42
|
+
"line": content[:m.start()].count("\n") + 1,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
# 2. Parse fromJson / JSON field deserialization: json['field_name'] or map['field_name']
|
|
46
|
+
json_field_regex = re.compile(r"(?:json|map|data)\[['\"]([a-zA-Z0-9_]+)['\"]\]")
|
|
47
|
+
for jm in json_field_regex.finditer(content):
|
|
48
|
+
field_name = jm.group(1)
|
|
49
|
+
if field_name not in accessed_properties:
|
|
50
|
+
accessed_properties.append(field_name)
|
|
51
|
+
|
|
52
|
+
# 3. Parse Dart field declarations: final String name; or int? quantity;
|
|
53
|
+
field_decl_regex = re.compile(r"^\s*(?:final\s+|const\s+)?(?:[A-Za-z0-9_<>?]+\s+)+([a-zA-Z0-9_]+)\s*;", re.MULTILINE)
|
|
54
|
+
for fm in field_decl_regex.finditer(content):
|
|
55
|
+
fname = fm.group(1)
|
|
56
|
+
if fname not in ("key", "super", "override", "build") and fname not in accessed_properties:
|
|
57
|
+
accessed_properties.append(fname)
|
|
58
|
+
|
|
59
|
+
# 4. Parse HTTP and Dio API calls:
|
|
60
|
+
# http.get(Uri.parse('...')), dio.get('...'), client.get('...'), api.get('...')
|
|
61
|
+
http_call_patterns = [
|
|
62
|
+
re.compile(r"""(?:http|client|dio|apiClient|api|service)\.(get|post|put|patch|delete)\s*\(\s*(?:Uri\.parse\s*\(\s*)?['"]([^'"]+)['"]""", re.IGNORECASE),
|
|
63
|
+
re.compile(r"""['"]((?:/api/v[0-9]+|/api/|/products|/sales|/sync|/orders|/users)[^'"]*)['"]"""),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for m in http_call_patterns[0].finditer(content):
|
|
67
|
+
method = m.group(1).upper()
|
|
68
|
+
raw_url = m.group(2)
|
|
69
|
+
route_path = _normalize_route_path(raw_url)
|
|
70
|
+
api_calls.append({
|
|
71
|
+
"method": method,
|
|
72
|
+
"path": route_path,
|
|
73
|
+
"raw_url": raw_url,
|
|
74
|
+
"file": rel_path,
|
|
75
|
+
"line": content[:m.start()].count("\n") + 1,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
for m in http_call_patterns[1].finditer(content):
|
|
79
|
+
raw_url = m.group(1)
|
|
80
|
+
route_path = _normalize_route_path(raw_url)
|
|
81
|
+
if not any(c["path"] == route_path for c in api_calls):
|
|
82
|
+
api_calls.append({
|
|
83
|
+
"method": "ANY",
|
|
84
|
+
"path": route_path,
|
|
85
|
+
"raw_url": raw_url,
|
|
86
|
+
"file": rel_path,
|
|
87
|
+
"line": content[:m.start()].count("\n") + 1,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"file": rel_path,
|
|
92
|
+
"layer": "flutter_client",
|
|
93
|
+
"classes": classes,
|
|
94
|
+
"api_calls": api_calls,
|
|
95
|
+
"accessed_properties": accessed_properties,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_dart_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
|
|
100
|
+
if not os.path.exists(file_path):
|
|
101
|
+
return {"error": f"File not found: {file_path}"}
|
|
102
|
+
rel_path = os.path.relpath(file_path, repo_root) if repo_root else file_path
|
|
103
|
+
rel_path = rel_path.replace("\\", "/")
|
|
104
|
+
try:
|
|
105
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
106
|
+
content = f.read()
|
|
107
|
+
return parse_dart_content(content, rel_path)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
return {"error": str(exc), "file": rel_path}
|
|
110
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Go (Golang) AST Parser
|
|
3
|
+
Extracts Go structs, JSON / GORM field tags, REST API route handlers
|
|
4
|
+
(Gin, Fiber, Chi, standard net/http), and outbound HTTP 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 GoField:
|
|
15
|
+
name: str
|
|
16
|
+
type_name: str
|
|
17
|
+
json_name: Optional[str] = None
|
|
18
|
+
gorm_tag: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class GoStruct:
|
|
23
|
+
name: str
|
|
24
|
+
file_path: str
|
|
25
|
+
fields: List[GoField] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class GoRoute:
|
|
30
|
+
method: str
|
|
31
|
+
path: str
|
|
32
|
+
handler_name: str
|
|
33
|
+
framework: str = "gin" # gin, fiber, chi, net/http
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class GoApiCall:
|
|
38
|
+
endpoint: str
|
|
39
|
+
method: str = "GET"
|
|
40
|
+
line_number: int = 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class GoFunction:
|
|
45
|
+
name: str
|
|
46
|
+
receiver: Optional[str] = None
|
|
47
|
+
params: List[str] = field(default_factory=list)
|
|
48
|
+
return_types: List[str] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class GoParseResult:
|
|
53
|
+
structs: List[GoStruct] = field(default_factory=list)
|
|
54
|
+
routes: List[GoRoute] = field(default_factory=list)
|
|
55
|
+
api_calls: List[GoApiCall] = field(default_factory=list)
|
|
56
|
+
functions: List[GoFunction] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Struct regex
|
|
60
|
+
_RE_STRUCT = re.compile(r"type\s+([A-Za-z0-9_]+)\s+struct\s*\{([^}]*)\}", re.DOTALL)
|
|
61
|
+
_RE_STRUCT_FIELD = re.compile(r"([A-Za-z0-9_]+)\s+([A-Za-z0-9_\*\[\]\.\{\}]+)(?:\s+`([^`]*)`)?")
|
|
62
|
+
|
|
63
|
+
# Route definitions
|
|
64
|
+
# Gin: r.GET("/path", handler)
|
|
65
|
+
_RE_GIN_ROUTE = re.compile(r"(?:[A-Za-z0-9_]+)\.(GET|POST|PUT|DELETE|PATCH)\(\s*['\"`](/[^'\"`]+)['\"`]\s*,\s*([A-Za-z0-9_\.]+)")
|
|
66
|
+
# Fiber: app.Get("/path", handler)
|
|
67
|
+
_RE_FIBER_ROUTE = re.compile(r"(?:[A-Za-z0-9_]+)\.(Get|Post|Put|Delete|Patch)\(\s*['\"`](/[^'\"`]+)['\"`]\s*,\s*([A-Za-z0-9_\.]+)")
|
|
68
|
+
# Chi: r.Get("/path", handler)
|
|
69
|
+
_RE_CHI_ROUTE = re.compile(r"(?:[A-Za-z0-9_]+)\.(Get|Post|Put|Delete|Patch|Handle|HandleFunc)\(\s*['\"`](/[^'\"`]+)['\"`]\s*,\s*([A-Za-z0-9_\.]+)")
|
|
70
|
+
# net/http: http.HandleFunc("/path", handler)
|
|
71
|
+
_RE_NETHTTP_ROUTE = re.compile(r"http\.HandleFunc\(\s*['\"`](/[^'\"`]+)['\"`]\s*,\s*([A-Za-z0-9_\.]+)")
|
|
72
|
+
|
|
73
|
+
# Outbound HTTP calls in Go:
|
|
74
|
+
# http.Get("url") or http.NewRequest("GET", "url")
|
|
75
|
+
_RE_OUTBOUND_HTTP = re.compile(r"http\.(Get|Post)\(\s*['\"`]([^'\"`]+)['\"`]")
|
|
76
|
+
_RE_NEW_REQUEST = re.compile(r"http\.NewRequest(?:WithContext)?\(\s*['\"`](GET|POST|PUT|DELETE|PATCH)['\"`]\s*,\s*['\"`]([^'\"`]+)['\"`]")
|
|
77
|
+
|
|
78
|
+
# Function definition:
|
|
79
|
+
# func (s *Service) GetUser(ctx context.Context, id int) (*User, error)
|
|
80
|
+
_RE_FUNC = re.compile(r"func\s*(?:\(\s*([A-Za-z0-9_\*\s]+)\s*\))?\s*([A-Za-z0-9_]+)\s*\((.*?)\)(?:\s*(?:\((.*?)\)|([A-Za-z0-9_\*\.\[\]]+)))?")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_json_tag(tag_str: str) -> Optional[str]:
|
|
84
|
+
if not tag_str:
|
|
85
|
+
return None
|
|
86
|
+
match = re.search(r'json:"([^",]+)', tag_str)
|
|
87
|
+
return match.group(1) if match else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _extract_gorm_tag(tag_str: str) -> Optional[str]:
|
|
91
|
+
if not tag_str:
|
|
92
|
+
return None
|
|
93
|
+
match = re.search(r'gorm:"([^"]+)"', tag_str)
|
|
94
|
+
return match.group(1) if match else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_go_content(content: str, file_path: str = "service.go") -> GoParseResult:
|
|
98
|
+
"""Parses a Go source file (.go)."""
|
|
99
|
+
result = GoParseResult()
|
|
100
|
+
|
|
101
|
+
# 1. Parse Structs
|
|
102
|
+
for match in _RE_STRUCT.finditer(content):
|
|
103
|
+
struct_name = match.group(1)
|
|
104
|
+
body = match.group(2)
|
|
105
|
+
go_struct = GoStruct(name=struct_name, file_path=file_path)
|
|
106
|
+
|
|
107
|
+
for line in body.splitlines():
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line or line.startswith("//"):
|
|
110
|
+
continue
|
|
111
|
+
fmatch = _RE_STRUCT_FIELD.match(line)
|
|
112
|
+
if fmatch:
|
|
113
|
+
fname = fmatch.group(1)
|
|
114
|
+
ftype = fmatch.group(2)
|
|
115
|
+
raw_tags = fmatch.group(3) or ""
|
|
116
|
+
jname = _extract_json_tag(raw_tags)
|
|
117
|
+
gtag = _extract_gorm_tag(raw_tags)
|
|
118
|
+
go_struct.fields.append(GoField(
|
|
119
|
+
name=fname,
|
|
120
|
+
type_name=ftype,
|
|
121
|
+
json_name=jname,
|
|
122
|
+
gorm_tag=gtag
|
|
123
|
+
))
|
|
124
|
+
result.structs.append(go_struct)
|
|
125
|
+
|
|
126
|
+
# 2. Parse API Routes
|
|
127
|
+
# Gin
|
|
128
|
+
for match in _RE_GIN_ROUTE.finditer(content):
|
|
129
|
+
result.routes.append(GoRoute(
|
|
130
|
+
method=match.group(1).upper(),
|
|
131
|
+
path=match.group(2),
|
|
132
|
+
handler_name=match.group(3),
|
|
133
|
+
framework="gin"
|
|
134
|
+
))
|
|
135
|
+
# Fiber
|
|
136
|
+
for match in _RE_FIBER_ROUTE.finditer(content):
|
|
137
|
+
result.routes.append(GoRoute(
|
|
138
|
+
method=match.group(1).upper(),
|
|
139
|
+
path=match.group(2),
|
|
140
|
+
handler_name=match.group(3),
|
|
141
|
+
framework="fiber"
|
|
142
|
+
))
|
|
143
|
+
# Chi
|
|
144
|
+
for match in _RE_CHI_ROUTE.finditer(content):
|
|
145
|
+
result.routes.append(GoRoute(
|
|
146
|
+
method=match.group(1).upper(),
|
|
147
|
+
path=match.group(2),
|
|
148
|
+
handler_name=match.group(3),
|
|
149
|
+
framework="chi"
|
|
150
|
+
))
|
|
151
|
+
# net/http
|
|
152
|
+
for match in _RE_NETHTTP_ROUTE.finditer(content):
|
|
153
|
+
result.routes.append(GoRoute(
|
|
154
|
+
method="ANY",
|
|
155
|
+
path=match.group(1),
|
|
156
|
+
handler_name=match.group(2),
|
|
157
|
+
framework="net/http"
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
# 3. Parse Outbound HTTP Queries
|
|
161
|
+
for match in _RE_OUTBOUND_HTTP.finditer(content):
|
|
162
|
+
result.api_calls.append(GoApiCall(
|
|
163
|
+
method=match.group(1).upper(),
|
|
164
|
+
endpoint=match.group(2)
|
|
165
|
+
))
|
|
166
|
+
for match in _RE_NEW_REQUEST.finditer(content):
|
|
167
|
+
result.api_calls.append(GoApiCall(
|
|
168
|
+
method=match.group(1).upper(),
|
|
169
|
+
endpoint=match.group(2)
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
# 4. Parse Functions
|
|
173
|
+
for match in _RE_FUNC.finditer(content):
|
|
174
|
+
receiver = match.group(1).strip() if match.group(1) else None
|
|
175
|
+
fname = match.group(2)
|
|
176
|
+
raw_params = match.group(3) or ""
|
|
177
|
+
params = [p.strip() for p in raw_params.split(",") if p.strip()]
|
|
178
|
+
result.functions.append(GoFunction(
|
|
179
|
+
name=fname,
|
|
180
|
+
receiver=receiver,
|
|
181
|
+
params=params
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_go_file(file_path: str) -> Optional[GoParseResult]:
|
|
188
|
+
"""Reads and parses a .go file."""
|
|
189
|
+
if not os.path.exists(file_path):
|
|
190
|
+
return None
|
|
191
|
+
try:
|
|
192
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
193
|
+
content = f.read()
|
|
194
|
+
return parse_go_content(content, file_path)
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlastTrace Kotlin & Android Mobile/Tablet Parser
|
|
3
|
+
Parses Kotlin data classes, SerializedName annotations, Retrofit HTTP annotations,
|
|
4
|
+
and OkHttp/Ktor client route consumers in native Android applications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_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"\$([a-zA-Z0-9_]+)", ":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_kotlin_content(content: str, rel_path: str = "") -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Extracts Kotlin data classes, Retrofit endpoints, SerializedName mappings,
|
|
28
|
+
and HTTP API calls from Android source code.
|
|
29
|
+
"""
|
|
30
|
+
classes: List[Dict[str, Any]] = []
|
|
31
|
+
api_calls: List[Dict[str, Any]] = []
|
|
32
|
+
accessed_properties: List[str] = []
|
|
33
|
+
|
|
34
|
+
# 1. Parse Kotlin Data Classes and Classes: data class Product(...), class OrderResponse
|
|
35
|
+
class_regex = re.compile(
|
|
36
|
+
r"""(?:data\s+)?class\s+([A-Za-z0-9_]+)(?:\s*\(([^)]*)\))?""",
|
|
37
|
+
re.MULTILINE,
|
|
38
|
+
)
|
|
39
|
+
for m in class_regex.finditer(content):
|
|
40
|
+
cname = m.group(1)
|
|
41
|
+
params = m.group(2) or ""
|
|
42
|
+
classes.append({
|
|
43
|
+
"name": cname,
|
|
44
|
+
"type": "kotlin_model",
|
|
45
|
+
"file": rel_path,
|
|
46
|
+
"line": content[:m.start()].count("\n") + 1,
|
|
47
|
+
})
|
|
48
|
+
# Extract constructor properties inside data class (...)
|
|
49
|
+
for p in params.split(","):
|
|
50
|
+
p_clean = p.strip()
|
|
51
|
+
field_match = re.search(r"""(?:val|var)\s+([a-zA-Z0-9_]+)""", p_clean)
|
|
52
|
+
if field_match:
|
|
53
|
+
fname = field_match.group(1)
|
|
54
|
+
if fname not in accessed_properties:
|
|
55
|
+
accessed_properties.append(fname)
|
|
56
|
+
|
|
57
|
+
# 2. Parse @SerializedName("field") or @SerialName("field")
|
|
58
|
+
ser_regex = re.compile(r"""@(?:SerializedName|SerialName|JsonProperty)\s*\(\s*["']([^"']+)["']\s*\)""")
|
|
59
|
+
for sm in ser_regex.finditer(content):
|
|
60
|
+
prop = sm.group(1)
|
|
61
|
+
if prop not in accessed_properties:
|
|
62
|
+
accessed_properties.append(prop)
|
|
63
|
+
|
|
64
|
+
# 3. Parse general property declarations: val balance: Double or var tier: String
|
|
65
|
+
prop_regex = re.compile(r"""(?:val|var)\s+([a-zA-Z0-9_]+)\s*:\s*[A-Za-z0-9_<>?]+""", re.MULTILINE)
|
|
66
|
+
for pm in prop_regex.finditer(content):
|
|
67
|
+
pname = pm.group(1)
|
|
68
|
+
if pname not in ("tag", "context", "binding", "viewModel", "compositeDisposable") and pname not in accessed_properties:
|
|
69
|
+
accessed_properties.append(pname)
|
|
70
|
+
|
|
71
|
+
# 4. Parse Retrofit API Interface annotations: @GET("/api/v1/..."), @POST("...")
|
|
72
|
+
retrofit_regex = re.compile(
|
|
73
|
+
r"""@(GET|POST|PUT|PATCH|DELETE)\s*\(\s*["']([^"']+)["']\s*\)""",
|
|
74
|
+
re.IGNORECASE,
|
|
75
|
+
)
|
|
76
|
+
for rm in retrofit_regex.finditer(content):
|
|
77
|
+
method = rm.group(1).upper()
|
|
78
|
+
raw_url = rm.group(2)
|
|
79
|
+
route_path = _normalize_route_path(raw_url)
|
|
80
|
+
api_calls.append({
|
|
81
|
+
"method": method,
|
|
82
|
+
"path": route_path,
|
|
83
|
+
"raw_url": raw_url,
|
|
84
|
+
"file": rel_path,
|
|
85
|
+
"line": content[:rm.start()].count("\n") + 1,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# 5. Parse Ktor / OkHttp / URL string literals
|
|
89
|
+
url_literal_regex = re.compile(r"""["']((?:/api/v[0-9]+|/api/|/products|/sales|/sync|/orders|/users)[^"']*)["']""")
|
|
90
|
+
for um in url_literal_regex.finditer(content):
|
|
91
|
+
raw_url = um.group(1)
|
|
92
|
+
route_path = _normalize_route_path(raw_url)
|
|
93
|
+
if not any(c["path"] == route_path for c in api_calls):
|
|
94
|
+
api_calls.append({
|
|
95
|
+
"method": "ANY",
|
|
96
|
+
"path": route_path,
|
|
97
|
+
"raw_url": raw_url,
|
|
98
|
+
"file": rel_path,
|
|
99
|
+
"line": content[:um.start()].count("\n") + 1,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"file": rel_path,
|
|
104
|
+
"layer": "android_client",
|
|
105
|
+
"classes": classes,
|
|
106
|
+
"api_calls": api_calls,
|
|
107
|
+
"accessed_properties": accessed_properties,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_kotlin_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
|
|
112
|
+
if not os.path.exists(file_path):
|
|
113
|
+
return {"error": f"File not found: {file_path}"}
|
|
114
|
+
rel_path = os.path.relpath(file_path, repo_root) if repo_root else file_path
|
|
115
|
+
rel_path = rel_path.replace("\\", "/")
|
|
116
|
+
try:
|
|
117
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
118
|
+
content = f.read()
|
|
119
|
+
return parse_kotlin_content(content, rel_path)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
return {"error": str(exc), "file": rel_path}
|
|
122
|
+
|