codegraph-engine 2.3.0__py3-none-any.whl → 3.0.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.
- codegraph/__init__.py +1 -1
- codegraph/__main__.py +7 -0
- codegraph/agent_brain.py +253 -2
- codegraph/agent_capabilities.py +1348 -67
- codegraph/agent_rules.py +2 -1
- codegraph/api_drift.py +455 -0
- codegraph/architecture_rules.py +123 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/changelog.py +170 -0
- codegraph/cli.py +1605 -62
- codegraph/cli_output.py +17 -2
- codegraph/context.py +135 -24
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/dataflow_propagation.py +118 -0
- codegraph/dead_code.py +387 -0
- codegraph/epistemic.py +15 -0
- codegraph/evidence_contract.py +34 -3
- codegraph/federation.py +245 -0
- codegraph/frameworks.py +234 -12
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/graph/traversal.py +150 -7
- codegraph/graph_csr.py +218 -0
- codegraph/incremental.py +150 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/classifier.py +5 -0
- codegraph/indexing/indexer.py +292 -38
- codegraph/indexing/languages/__init__.py +1 -0
- codegraph/indexing/languages/cpp.py +457 -0
- codegraph/indexing/languages/go.py +459 -0
- codegraph/indexing/languages/jvm.py +583 -0
- codegraph/indexing/languages/rust.py +507 -0
- codegraph/indexing/models.py +15 -1
- codegraph/indexing/parser.py +385 -9
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/protocols.py +220 -0
- codegraph/indexing/scanner.py +23 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +207 -39
- codegraph/lsp.py +318 -0
- codegraph/lsp_bridge/__init__.py +16 -0
- codegraph/lsp_bridge/bridge.py +102 -0
- codegraph/lsp_bridge/client.py +207 -0
- codegraph/lsp_bridge/models.py +71 -0
- codegraph/mcp/server.py +502 -29
- codegraph/memory/trajectory.py +282 -0
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/multi_agent/__init__.py +13 -0
- codegraph/multi_agent/coordinator.py +229 -0
- codegraph/multi_agent/models.py +76 -0
- codegraph/optimizer.py +42 -5
- codegraph/polymorphism.py +95 -0
- codegraph/pr_review.py +296 -0
- codegraph/process_lifecycle.py +50 -12
- codegraph/reachability.py +198 -0
- codegraph/reachability_index.py +74 -0
- codegraph/refactor/__init__.py +48 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/patcher.py +348 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/repair.py +233 -0
- codegraph/resolver.py +1357 -161
- codegraph/retrieval_policy.py +22 -1
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +7 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +25 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/otel_receiver.py +208 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/sandbox.py +215 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/security/supply_chain.py +188 -0
- codegraph/security/taint.py +421 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/test_gen.py +437 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +876 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
- codegraph_engine-3.0.1.dist-info/RECORD +164 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Protocol Buffers (Protobuf / gRPC) and GraphQL Schema Parser for CodeGraph.
|
|
2
|
+
|
|
3
|
+
Extracts schema definitions and RPC services:
|
|
4
|
+
- Protobuf (.proto): extracts packages, messages, services, and RPC methods (rpc GetUser(Request) returns (Response))
|
|
5
|
+
- GraphQL (.graphql, .gql): extracts Query, Mutation, Subscription, and Object types
|
|
6
|
+
- Links RPC methods and GraphQL resolvers into CodeGraph's global route and endpoint topology
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from codegraph.frameworks import RouteDetection
|
|
15
|
+
from codegraph.indexing.models import Symbol
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_proto(content: str, file_path: str) -> Any:
|
|
19
|
+
"""Parse Protocol Buffers (.proto) source code into symbols and gRPC RPC route detections."""
|
|
20
|
+
from codegraph.indexing.parser import ParseResult
|
|
21
|
+
|
|
22
|
+
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
23
|
+
lines = content.splitlines()
|
|
24
|
+
|
|
25
|
+
symbols: list[Symbol] = []
|
|
26
|
+
routes: list[RouteDetection] = []
|
|
27
|
+
|
|
28
|
+
# 1. Package extraction: package order.v1;
|
|
29
|
+
pkg_name = ""
|
|
30
|
+
for line in lines[:30]:
|
|
31
|
+
m_pkg = re.match(r"^\s*package\s+([A-Za-z0-9_.]+);", line)
|
|
32
|
+
if m_pkg:
|
|
33
|
+
pkg_name = m_pkg.group(1)
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
# 2. Service & Message Extraction
|
|
37
|
+
service_re = re.compile(r"^\s*service\s+([A-Za-z0-9_]+)\s*\{")
|
|
38
|
+
rpc_re = re.compile(r"^\s*rpc\s+([A-Za-z0-9_]+)\s*\(\s*([A-Za-z0-9_.]+)\s*\)\s*returns\s*\(\s*([A-Za-z0-9_.]+)\s*\);?")
|
|
39
|
+
message_re = re.compile(r"^\s*message\s+([A-Za-z0-9_]+)\s*\{")
|
|
40
|
+
|
|
41
|
+
current_service = ""
|
|
42
|
+
for idx, line in enumerate(lines, start=1):
|
|
43
|
+
clean = line.strip()
|
|
44
|
+
if not clean or clean.startswith("//"):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Service
|
|
48
|
+
m_svc = service_re.match(clean)
|
|
49
|
+
if m_svc:
|
|
50
|
+
current_service = m_svc.group(1)
|
|
51
|
+
canon = f"proto:{pkg_name}.{current_service}" if pkg_name else f"proto:{current_service}"
|
|
52
|
+
symbols.append(
|
|
53
|
+
Symbol(
|
|
54
|
+
name=current_service,
|
|
55
|
+
qualified_name=f"{pkg_name}.{current_service}" if pkg_name else current_service,
|
|
56
|
+
kind="service",
|
|
57
|
+
start_line=idx,
|
|
58
|
+
end_line=idx,
|
|
59
|
+
file_path=file_path,
|
|
60
|
+
canonical_id=canon,
|
|
61
|
+
language="proto",
|
|
62
|
+
visibility="public",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# RPC Method
|
|
68
|
+
m_rpc = rpc_re.match(clean)
|
|
69
|
+
if m_rpc and current_service:
|
|
70
|
+
method_name = m_rpc.group(1)
|
|
71
|
+
req_type = m_rpc.group(2)
|
|
72
|
+
res_type = m_rpc.group(3)
|
|
73
|
+
canon = f"proto:{pkg_name}.{current_service}.{method_name}" if pkg_name else f"proto:{current_service}.{method_name}"
|
|
74
|
+
|
|
75
|
+
symbols.append(
|
|
76
|
+
Symbol(
|
|
77
|
+
name=method_name,
|
|
78
|
+
qualified_name=f"{current_service}.{method_name}",
|
|
79
|
+
kind="rpc_method",
|
|
80
|
+
start_line=idx,
|
|
81
|
+
end_line=idx,
|
|
82
|
+
file_path=file_path,
|
|
83
|
+
canonical_id=canon,
|
|
84
|
+
language="proto",
|
|
85
|
+
signature=f"({req_type}) -> {res_type}",
|
|
86
|
+
parameter_count=1,
|
|
87
|
+
return_type=res_type,
|
|
88
|
+
visibility="public",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Register as gRPC route
|
|
93
|
+
full_rpc_path = f"/{pkg_name}.{current_service}/{method_name}" if pkg_name else f"/{current_service}/{method_name}"
|
|
94
|
+
routes.append(
|
|
95
|
+
RouteDetection(
|
|
96
|
+
framework="grpc",
|
|
97
|
+
http_method="POST",
|
|
98
|
+
route_path=full_rpc_path,
|
|
99
|
+
normalized_route=full_rpc_path,
|
|
100
|
+
handler_name=canon,
|
|
101
|
+
handler_canonical_id=canon,
|
|
102
|
+
file_path=file_path,
|
|
103
|
+
line=idx,
|
|
104
|
+
confidence="HIGH",
|
|
105
|
+
evidence=f"gRPC RPC method '{full_rpc_path}' declared in {file_path}:{idx}",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Message definition
|
|
111
|
+
m_msg = message_re.match(clean)
|
|
112
|
+
if m_msg:
|
|
113
|
+
msg_name = m_msg.group(1)
|
|
114
|
+
canon = f"proto:{pkg_name}.{msg_name}" if pkg_name else f"proto:{msg_name}"
|
|
115
|
+
symbols.append(
|
|
116
|
+
Symbol(
|
|
117
|
+
name=msg_name,
|
|
118
|
+
qualified_name=f"{pkg_name}.{msg_name}" if pkg_name else msg_name,
|
|
119
|
+
kind="message",
|
|
120
|
+
start_line=idx,
|
|
121
|
+
end_line=idx,
|
|
122
|
+
file_path=file_path,
|
|
123
|
+
canonical_id=canon,
|
|
124
|
+
language="proto",
|
|
125
|
+
visibility="public",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return ParseResult(
|
|
130
|
+
path=file_path,
|
|
131
|
+
language="proto",
|
|
132
|
+
source_hash=source_hash,
|
|
133
|
+
symbols=tuple(symbols),
|
|
134
|
+
routes=tuple(routes),
|
|
135
|
+
parse_failed=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_graphql(content: str, file_path: str) -> Any:
|
|
140
|
+
"""Parse GraphQL Schema (.graphql, .gql) into symbols and GraphQL query/mutation routes."""
|
|
141
|
+
from codegraph.indexing.parser import ParseResult
|
|
142
|
+
|
|
143
|
+
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
144
|
+
lines = content.splitlines()
|
|
145
|
+
|
|
146
|
+
symbols: list[Symbol] = []
|
|
147
|
+
routes: list[RouteDetection] = []
|
|
148
|
+
|
|
149
|
+
type_re = re.compile(r"^\s*type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[^{]+)?\s*\{")
|
|
150
|
+
field_re = re.compile(r"^\s*([A-Za-z0-9_]+)\s*(?:\([^)]*\))?\s*:\s*([A-Za-z0-9_!\[\]]+)")
|
|
151
|
+
|
|
152
|
+
current_type = ""
|
|
153
|
+
for idx, line in enumerate(lines, start=1):
|
|
154
|
+
clean = line.strip()
|
|
155
|
+
if not clean or clean.startswith("#"):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
m_t = type_re.match(clean)
|
|
159
|
+
if m_t:
|
|
160
|
+
current_type = m_t.group(1)
|
|
161
|
+
symbols.append(
|
|
162
|
+
Symbol(
|
|
163
|
+
name=current_type,
|
|
164
|
+
qualified_name=current_type,
|
|
165
|
+
kind="graphql_type",
|
|
166
|
+
start_line=idx,
|
|
167
|
+
end_line=idx,
|
|
168
|
+
file_path=file_path,
|
|
169
|
+
canonical_id=f"graphql:{current_type}",
|
|
170
|
+
language="graphql",
|
|
171
|
+
visibility="public",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
m_f = field_re.match(clean)
|
|
177
|
+
if m_f and current_type:
|
|
178
|
+
fname = m_f.group(1)
|
|
179
|
+
ftype = m_f.group(2)
|
|
180
|
+
qname = f"{current_type}.{fname}"
|
|
181
|
+
symbols.append(
|
|
182
|
+
Symbol(
|
|
183
|
+
name=fname,
|
|
184
|
+
qualified_name=qname,
|
|
185
|
+
kind="graphql_field",
|
|
186
|
+
start_line=idx,
|
|
187
|
+
end_line=idx,
|
|
188
|
+
file_path=file_path,
|
|
189
|
+
canonical_id=f"graphql:{qname}",
|
|
190
|
+
language="graphql",
|
|
191
|
+
return_type=ftype,
|
|
192
|
+
visibility="public",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# If inside type Query or type Mutation, register as GraphQL route
|
|
197
|
+
if current_type in ("Query", "Mutation", "Subscription"):
|
|
198
|
+
routes.append(
|
|
199
|
+
RouteDetection(
|
|
200
|
+
framework="graphql",
|
|
201
|
+
http_method=current_type.upper(),
|
|
202
|
+
route_path=f"graphql:{current_type}.{fname}",
|
|
203
|
+
normalized_route=f"graphql:{fname}",
|
|
204
|
+
handler_name=qname,
|
|
205
|
+
handler_canonical_id=f"graphql:{qname}",
|
|
206
|
+
file_path=file_path,
|
|
207
|
+
line=idx,
|
|
208
|
+
confidence="HIGH",
|
|
209
|
+
evidence=f"GraphQL {current_type} field '{fname}' defined in {file_path}:{idx}",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return ParseResult(
|
|
214
|
+
path=file_path,
|
|
215
|
+
language="graphql",
|
|
216
|
+
source_hash=source_hash,
|
|
217
|
+
symbols=tuple(symbols),
|
|
218
|
+
routes=tuple(routes),
|
|
219
|
+
parse_failed=False,
|
|
220
|
+
)
|
codegraph/indexing/scanner.py
CHANGED
|
@@ -43,6 +43,29 @@ LANGUAGES = {
|
|
|
43
43
|
".tsx": "typescript",
|
|
44
44
|
".sql": "sql",
|
|
45
45
|
".prisma": "prisma",
|
|
46
|
+
".html": "html",
|
|
47
|
+
".htm": "html",
|
|
48
|
+
".jinja": "html",
|
|
49
|
+
".jinja2": "html",
|
|
50
|
+
".njk": "html",
|
|
51
|
+
".ejs": "html",
|
|
52
|
+
".lua": "luau",
|
|
53
|
+
".luau": "luau",
|
|
54
|
+
".go": "go",
|
|
55
|
+
".rs": "rust",
|
|
56
|
+
".java": "java",
|
|
57
|
+
".kt": "kotlin",
|
|
58
|
+
".kts": "kotlin",
|
|
59
|
+
".c": "c",
|
|
60
|
+
".cc": "cpp",
|
|
61
|
+
".cpp": "cpp",
|
|
62
|
+
".cxx": "cpp",
|
|
63
|
+
".h": "c_header",
|
|
64
|
+
".hpp": "cpp_header",
|
|
65
|
+
".hxx": "cpp_header",
|
|
66
|
+
".proto": "proto",
|
|
67
|
+
".graphql": "graphql",
|
|
68
|
+
".gql": "graphql",
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Deterministic HTML, Jinja2, and Web Template Parser.
|
|
2
|
+
|
|
3
|
+
Extracts:
|
|
4
|
+
1. DOM elements with IDs or semantic anchors into Symbol records.
|
|
5
|
+
2. Inline JavaScript event handlers (onclick="fn()", @click="fn") into CallRef and Symbol records.
|
|
6
|
+
3. Form submission actions (<form action="/api/..." method="POST">) into BindingRef records.
|
|
7
|
+
4. Script tag imports (<script src="...">) and stylesheet links into ImportRef records.
|
|
8
|
+
5. Embedded <script> JavaScript blocks parsed with exact file line offsets.
|
|
9
|
+
6. Jinja/Django/EJS template directives ({% block %}, {% extends %}, {% include %}).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import re
|
|
15
|
+
from html.parser import HTMLParser
|
|
16
|
+
|
|
17
|
+
from codegraph.indexing.models import (
|
|
18
|
+
BindingRef,
|
|
19
|
+
CallRef,
|
|
20
|
+
ImportRef,
|
|
21
|
+
InheritanceRef,
|
|
22
|
+
Symbol,
|
|
23
|
+
normalize_module,
|
|
24
|
+
)
|
|
25
|
+
from codegraph.indexing.parser import PARSER_VERSION, ParseResult, _parse_js_ts
|
|
26
|
+
|
|
27
|
+
# Regex for extracting JS function calls in inline handlers like:
|
|
28
|
+
# onclick="quickToggleStock(123)", onsubmit="return handleForm(this, event)"
|
|
29
|
+
_JS_CALL_PATTERN = re.compile(r"\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(")
|
|
30
|
+
|
|
31
|
+
# Regex for Jinja2 / Django / EJS template directives
|
|
32
|
+
_JINJA_EXTENDS = re.compile(r"""\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}""")
|
|
33
|
+
_JINJA_INCLUDE = re.compile(r"""\{%\s*include\s+['"]([^'"]+)['"]\s*%\}""")
|
|
34
|
+
_JINJA_BLOCK = re.compile(r"""\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}""")
|
|
35
|
+
_JINJA_URL_FOR = re.compile(r"""\{\{\s*url_for\s*\(\s*['"]([^'"]+)['"]""")
|
|
36
|
+
_DJANGO_URL = re.compile(r"""\{%\s*url\s+['"]([^'"]+)['"]""")
|
|
37
|
+
|
|
38
|
+
# HTMX HTTP request attribute mappings
|
|
39
|
+
_HTMX_VERBS = {
|
|
40
|
+
"hx-get": "GET",
|
|
41
|
+
"hx-post": "POST",
|
|
42
|
+
"hx-put": "PUT",
|
|
43
|
+
"hx-delete": "DELETE",
|
|
44
|
+
"hx-patch": "PATCH",
|
|
45
|
+
"data-hx-get": "GET",
|
|
46
|
+
"data-hx-post": "POST",
|
|
47
|
+
"data-hx-put": "PUT",
|
|
48
|
+
"data-hx-delete": "DELETE",
|
|
49
|
+
"data-hx-patch": "PATCH",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _TemplateHTMLParser(HTMLParser):
|
|
54
|
+
def __init__(self, file_path: str, module_name: str) -> None:
|
|
55
|
+
super().__init__()
|
|
56
|
+
self.file_path = file_path
|
|
57
|
+
self.module_name = module_name
|
|
58
|
+
self.symbols: list[Symbol] = []
|
|
59
|
+
self.calls: list[CallRef] = []
|
|
60
|
+
self.imports: list[ImportRef] = []
|
|
61
|
+
self.bindings: list[BindingRef] = []
|
|
62
|
+
self.script_blocks: list[tuple[int, str]] = [] # (start_line, content)
|
|
63
|
+
self._in_script = False
|
|
64
|
+
self._script_start_line = 1
|
|
65
|
+
self._script_buf: list[str] = []
|
|
66
|
+
|
|
67
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
68
|
+
line_num, _ = self.getpos()
|
|
69
|
+
attr_dict: dict[str, str] = {k.lower(): (v or "") for k, v in attrs if k}
|
|
70
|
+
|
|
71
|
+
if tag.lower() == "script":
|
|
72
|
+
src = attr_dict.get("src")
|
|
73
|
+
if src:
|
|
74
|
+
self.imports.append(
|
|
75
|
+
ImportRef(
|
|
76
|
+
source_file=self.file_path,
|
|
77
|
+
module=src,
|
|
78
|
+
line=line_num,
|
|
79
|
+
import_type="script_src",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
self._in_script = True
|
|
84
|
+
self._script_start_line = line_num
|
|
85
|
+
self._script_buf = []
|
|
86
|
+
|
|
87
|
+
if tag.lower() == "link" and attr_dict.get("rel") == "stylesheet":
|
|
88
|
+
href = attr_dict.get("href")
|
|
89
|
+
if href:
|
|
90
|
+
self.imports.append(
|
|
91
|
+
ImportRef(
|
|
92
|
+
source_file=self.file_path,
|
|
93
|
+
module=href,
|
|
94
|
+
line=line_num,
|
|
95
|
+
import_type="stylesheet",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# 1. DOM Elements with ID
|
|
100
|
+
elem_id = attr_dict.get("id")
|
|
101
|
+
if elem_id:
|
|
102
|
+
sym_name = elem_id.strip()
|
|
103
|
+
self.symbols.append(
|
|
104
|
+
Symbol(
|
|
105
|
+
name=sym_name,
|
|
106
|
+
qualified_name=f"{tag}#{sym_name}",
|
|
107
|
+
kind="html_element",
|
|
108
|
+
language="html",
|
|
109
|
+
file_path=self.file_path,
|
|
110
|
+
start_line=line_num,
|
|
111
|
+
end_line=line_num,
|
|
112
|
+
scope=tag,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 2. Inline JavaScript / Alpine.js Event Handlers (onclick, onsubmit, @click, x-on:click, x-init, etc.)
|
|
117
|
+
for attr_k, attr_v in attr_dict.items():
|
|
118
|
+
is_handler = (
|
|
119
|
+
attr_k.startswith("on")
|
|
120
|
+
or attr_k.startswith("@")
|
|
121
|
+
or attr_k.startswith("v-on:")
|
|
122
|
+
or attr_k.startswith("x-on:")
|
|
123
|
+
or attr_k in ("x-init", "x-effect")
|
|
124
|
+
or (attr_k.startswith("(") and attr_k.endswith(")"))
|
|
125
|
+
)
|
|
126
|
+
if not is_handler or not attr_v:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if attr_k.startswith(("@", "x-on:", "x-data", "x-init", "x-show")):
|
|
130
|
+
caller_label = f"{tag}#{elem_id}" if elem_id else tag
|
|
131
|
+
self.bindings.append(
|
|
132
|
+
BindingRef(
|
|
133
|
+
target_name=attr_k,
|
|
134
|
+
file_path=self.file_path,
|
|
135
|
+
line=line_num,
|
|
136
|
+
expr_kind="ALPINE_EVENT",
|
|
137
|
+
source_expr=attr_v,
|
|
138
|
+
base_expr=caller_label,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
matches = _JS_CALL_PATTERN.findall(attr_v)
|
|
143
|
+
found_fns: list[str] = list(matches)
|
|
144
|
+
|
|
145
|
+
# If attribute value is a bare identifier / method reference (e.g. @click="handleClick" or onclick="handleClick")
|
|
146
|
+
val_clean = attr_v.strip()
|
|
147
|
+
if not found_fns and re.match(r"^[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$", val_clean):
|
|
148
|
+
found_fns.append(val_clean.split(".")[-1])
|
|
149
|
+
|
|
150
|
+
for fn_name in found_fns:
|
|
151
|
+
if fn_name in ("return", "if", "for", "while", "this"):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
caller_label = f"{tag}#{elem_id}" if elem_id else tag
|
|
155
|
+
self.calls.append(
|
|
156
|
+
CallRef(
|
|
157
|
+
callee=fn_name,
|
|
158
|
+
source_file=self.file_path,
|
|
159
|
+
line=line_num,
|
|
160
|
+
end_line=line_num,
|
|
161
|
+
caller_symbol=caller_label,
|
|
162
|
+
caller_canonical_id=f"{self.module_name}.{caller_label}",
|
|
163
|
+
confidence="HIGH",
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.symbols.append(
|
|
168
|
+
Symbol(
|
|
169
|
+
name=fn_name,
|
|
170
|
+
qualified_name=f"{caller_label}[{attr_k}]->{fn_name}",
|
|
171
|
+
kind="event_handler",
|
|
172
|
+
language="html",
|
|
173
|
+
file_path=self.file_path,
|
|
174
|
+
start_line=line_num,
|
|
175
|
+
end_line=line_num,
|
|
176
|
+
scope=caller_label,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# 3. Form submissions (<form action="..." method="...">)
|
|
181
|
+
if tag.lower() == "form":
|
|
182
|
+
action = attr_dict.get("action", "").strip()
|
|
183
|
+
method = attr_dict.get("method", "GET").strip().upper()
|
|
184
|
+
if action:
|
|
185
|
+
self.bindings.append(
|
|
186
|
+
BindingRef(
|
|
187
|
+
target_name=action,
|
|
188
|
+
file_path=self.file_path,
|
|
189
|
+
line=line_num,
|
|
190
|
+
expr_kind="HTML_FORM_ACTION",
|
|
191
|
+
base_expr=method,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# 4. Modern Python Stack: HTMX attributes (hx-get, hx-post, hx-delete, hx-put, hx-patch, etc.)
|
|
196
|
+
for hx_attr, http_method in _HTMX_VERBS.items():
|
|
197
|
+
if hx_attr in attr_dict and attr_dict[hx_attr]:
|
|
198
|
+
raw_endpoint = attr_dict[hx_attr].strip()
|
|
199
|
+
caller_label = f"{tag}#{elem_id}" if elem_id else tag
|
|
200
|
+
target_sel = attr_dict.get("hx-target") or attr_dict.get("data-hx-target") or ""
|
|
201
|
+
trigger = attr_dict.get("hx-trigger") or attr_dict.get("data-hx-trigger") or ""
|
|
202
|
+
swap = attr_dict.get("hx-swap") or attr_dict.get("data-hx-swap") or ""
|
|
203
|
+
|
|
204
|
+
resolved_endpoint = raw_endpoint
|
|
205
|
+
m_django = _DJANGO_URL.search(raw_endpoint)
|
|
206
|
+
if m_django:
|
|
207
|
+
resolved_endpoint = m_django.group(1)
|
|
208
|
+
else:
|
|
209
|
+
m_jinja = _JINJA_URL_FOR.search(raw_endpoint)
|
|
210
|
+
if m_jinja:
|
|
211
|
+
resolved_endpoint = m_jinja.group(1)
|
|
212
|
+
|
|
213
|
+
meta_entries = (
|
|
214
|
+
("target", target_sel),
|
|
215
|
+
("trigger", trigger),
|
|
216
|
+
("swap", swap),
|
|
217
|
+
("element", tag),
|
|
218
|
+
("element_id", elem_id or ""),
|
|
219
|
+
("raw_url", raw_endpoint),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self.bindings.append(
|
|
223
|
+
BindingRef(
|
|
224
|
+
target_name=resolved_endpoint,
|
|
225
|
+
file_path=self.file_path,
|
|
226
|
+
line=line_num,
|
|
227
|
+
expr_kind="HTMX_REQUEST",
|
|
228
|
+
base_expr=http_method,
|
|
229
|
+
source_expr=target_sel,
|
|
230
|
+
attr_name=trigger,
|
|
231
|
+
dict_entries=meta_entries,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self.calls.append(
|
|
236
|
+
CallRef(
|
|
237
|
+
callee=resolved_endpoint,
|
|
238
|
+
qualified_callee=f"{http_method} {resolved_endpoint}",
|
|
239
|
+
source_file=self.file_path,
|
|
240
|
+
line=line_num,
|
|
241
|
+
end_line=line_num,
|
|
242
|
+
caller_symbol=caller_label,
|
|
243
|
+
caller_canonical_id=f"{self.module_name}.{caller_label}",
|
|
244
|
+
confidence="HIGH",
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.symbols.append(
|
|
249
|
+
Symbol(
|
|
250
|
+
name=resolved_endpoint,
|
|
251
|
+
qualified_name=f"{caller_label}[{hx_attr}]->{http_method} {resolved_endpoint}",
|
|
252
|
+
kind="htmx_action",
|
|
253
|
+
language="html",
|
|
254
|
+
file_path=self.file_path,
|
|
255
|
+
start_line=line_num,
|
|
256
|
+
end_line=line_num,
|
|
257
|
+
scope=caller_label,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def handle_endtag(self, tag: str) -> None:
|
|
262
|
+
if tag.lower() == "script" and self._in_script:
|
|
263
|
+
self._in_script = False
|
|
264
|
+
content = "".join(self._script_buf)
|
|
265
|
+
if content.strip():
|
|
266
|
+
self.script_blocks.append((self._script_start_line, content))
|
|
267
|
+
self._script_buf = []
|
|
268
|
+
|
|
269
|
+
def handle_data(self, data: str) -> None:
|
|
270
|
+
if self._in_script:
|
|
271
|
+
self._script_buf.append(data)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def parse_html_template(content: str, file_path: str) -> ParseResult:
|
|
275
|
+
"""Parse an HTML or web template file in a single deterministic pass."""
|
|
276
|
+
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
277
|
+
module_name = normalize_module(file_path, "html")
|
|
278
|
+
|
|
279
|
+
parser = _TemplateHTMLParser(file_path=file_path, module_name=module_name)
|
|
280
|
+
try:
|
|
281
|
+
parser.feed(content)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return ParseResult(
|
|
284
|
+
path=file_path,
|
|
285
|
+
language="html",
|
|
286
|
+
parser_version=PARSER_VERSION,
|
|
287
|
+
source_hash=source_hash,
|
|
288
|
+
parse_failed=True,
|
|
289
|
+
parse_error=f"HTML parse error: {e}",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
symbols = list(parser.symbols)
|
|
293
|
+
calls = list(parser.calls)
|
|
294
|
+
imports = list(parser.imports)
|
|
295
|
+
inheritance: list[InheritanceRef] = []
|
|
296
|
+
bindings = list(parser.bindings)
|
|
297
|
+
|
|
298
|
+
# 4. Parse Jinja / Django directives line-by-line
|
|
299
|
+
lines = content.splitlines()
|
|
300
|
+
for idx, line in enumerate(lines, 1):
|
|
301
|
+
# {% extends "base.html" %}
|
|
302
|
+
ext_match = _JINJA_EXTENDS.search(line)
|
|
303
|
+
if ext_match:
|
|
304
|
+
inheritance.append(
|
|
305
|
+
InheritanceRef(
|
|
306
|
+
source_symbol=module_name,
|
|
307
|
+
base_name=ext_match.group(1),
|
|
308
|
+
relationship="EXTENDS",
|
|
309
|
+
source_file=file_path,
|
|
310
|
+
line=idx,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# {% include "nav.html" %}
|
|
315
|
+
inc_match = _JINJA_INCLUDE.search(line)
|
|
316
|
+
if inc_match:
|
|
317
|
+
imports.append(
|
|
318
|
+
ImportRef(
|
|
319
|
+
source_file=file_path,
|
|
320
|
+
module=inc_match.group(1),
|
|
321
|
+
line=idx,
|
|
322
|
+
import_type="template_include",
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# {% block content %}
|
|
327
|
+
blk_match = _JINJA_BLOCK.search(line)
|
|
328
|
+
if blk_match:
|
|
329
|
+
blk_name = blk_match.group(1)
|
|
330
|
+
symbols.append(
|
|
331
|
+
Symbol(
|
|
332
|
+
name=blk_name,
|
|
333
|
+
qualified_name=f"block#{blk_name}",
|
|
334
|
+
kind="template_block",
|
|
335
|
+
language="html",
|
|
336
|
+
file_path=file_path,
|
|
337
|
+
start_line=idx,
|
|
338
|
+
end_line=idx,
|
|
339
|
+
scope=module_name,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# {{ url_for('user_bp.checkout') }}
|
|
344
|
+
url_match = _JINJA_URL_FOR.search(line)
|
|
345
|
+
if url_match:
|
|
346
|
+
target_endpoint = url_match.group(1)
|
|
347
|
+
calls.append(
|
|
348
|
+
CallRef(
|
|
349
|
+
callee=target_endpoint,
|
|
350
|
+
source_file=file_path,
|
|
351
|
+
line=idx,
|
|
352
|
+
end_line=idx,
|
|
353
|
+
caller_symbol=module_name,
|
|
354
|
+
caller_canonical_id=module_name,
|
|
355
|
+
confidence="HIGH",
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# 5. Parse embedded <script> tags with JS parser
|
|
360
|
+
for start_line, script_code in parser.script_blocks:
|
|
361
|
+
try:
|
|
362
|
+
js_res = _parse_js_ts(script_code, "javascript", file_path)
|
|
363
|
+
# Offset line numbers by start_line - 1
|
|
364
|
+
line_offset = max(0, start_line - 1)
|
|
365
|
+
for s in js_res.symbols:
|
|
366
|
+
adj_start = s.start_line + line_offset
|
|
367
|
+
adj_end = s.end_line + line_offset
|
|
368
|
+
symbols.append(
|
|
369
|
+
Symbol(
|
|
370
|
+
name=s.name,
|
|
371
|
+
qualified_name=s.qualified_name,
|
|
372
|
+
kind=s.kind,
|
|
373
|
+
language="javascript",
|
|
374
|
+
file_path=file_path,
|
|
375
|
+
start_line=adj_start,
|
|
376
|
+
end_line=adj_end,
|
|
377
|
+
signature=s.signature,
|
|
378
|
+
scope=s.scope,
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
for c in js_res.calls:
|
|
382
|
+
calls.append(
|
|
383
|
+
CallRef(
|
|
384
|
+
callee=c.callee,
|
|
385
|
+
source_file=file_path,
|
|
386
|
+
line=c.line + line_offset,
|
|
387
|
+
end_line=c.end_line + line_offset,
|
|
388
|
+
caller_symbol=c.caller_symbol,
|
|
389
|
+
caller_canonical_id=c.caller_canonical_id,
|
|
390
|
+
confidence=c.confidence,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
for imp in js_res.imports:
|
|
394
|
+
imports.append(
|
|
395
|
+
ImportRef(
|
|
396
|
+
source_file=file_path,
|
|
397
|
+
module=imp.module,
|
|
398
|
+
name=imp.name,
|
|
399
|
+
line=imp.line + line_offset,
|
|
400
|
+
import_type=imp.import_type,
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
except Exception:
|
|
404
|
+
# Tolerant fallback if inline script cannot be parsed
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
return ParseResult(
|
|
408
|
+
path=file_path,
|
|
409
|
+
language="html",
|
|
410
|
+
parser_version=PARSER_VERSION,
|
|
411
|
+
source_hash=source_hash,
|
|
412
|
+
symbols=tuple(symbols),
|
|
413
|
+
imports=tuple(imports),
|
|
414
|
+
calls=tuple(calls),
|
|
415
|
+
inheritance=tuple(inheritance),
|
|
416
|
+
bindings=tuple(bindings),
|
|
417
|
+
parse_failed=False,
|
|
418
|
+
)
|