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
codegraph/indexing/parser.py
CHANGED
|
@@ -16,6 +16,7 @@ import json
|
|
|
16
16
|
import posixpath
|
|
17
17
|
import re
|
|
18
18
|
from dataclasses import asdict, dataclass
|
|
19
|
+
from typing import cast
|
|
19
20
|
|
|
20
21
|
from codegraph.frameworks import (
|
|
21
22
|
AnalysisContext,
|
|
@@ -27,6 +28,9 @@ from codegraph.frameworks import (
|
|
|
27
28
|
analyze_js_ts_mounts,
|
|
28
29
|
get_python_analyzers,
|
|
29
30
|
)
|
|
31
|
+
from codegraph.frameworks_nestjs import analyze_nestjs_file
|
|
32
|
+
from codegraph.frameworks_nextjs import analyze_nextjs_file
|
|
33
|
+
from codegraph.frameworks_react import analyze_react_file
|
|
30
34
|
from codegraph.semantic_decorators import analyze_symbol_decorators
|
|
31
35
|
|
|
32
36
|
from .models import (
|
|
@@ -215,6 +219,30 @@ def parse(content: str, language: str, file_path: str) -> ParseResult:
|
|
|
215
219
|
"""Parse a source file in a single scoped pass."""
|
|
216
220
|
if language == "python":
|
|
217
221
|
return _parse_python(content, file_path)
|
|
222
|
+
if language == "luau" or file_path.lower().endswith((".lua", ".luau")):
|
|
223
|
+
from codegraph.roblox import parse_luau
|
|
224
|
+
return parse_luau(content, file_path)
|
|
225
|
+
if language == "go" or file_path.lower().endswith(".go"):
|
|
226
|
+
from codegraph.indexing.languages.go import parse_go
|
|
227
|
+
return cast(ParseResult, parse_go(content, file_path))
|
|
228
|
+
if language == "rust" or file_path.lower().endswith(".rs"):
|
|
229
|
+
from codegraph.indexing.languages.rust import parse_rust
|
|
230
|
+
return cast(ParseResult, parse_rust(content, file_path))
|
|
231
|
+
if language in ("java", "kotlin") or file_path.lower().endswith((".java", ".kt", ".kts")):
|
|
232
|
+
from codegraph.indexing.languages.jvm import parse_jvm
|
|
233
|
+
return cast(ParseResult, parse_jvm(content, file_path))
|
|
234
|
+
if language in ("c", "cpp", "c_header", "cpp_header") or file_path.lower().endswith((".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx")):
|
|
235
|
+
from codegraph.indexing.languages.cpp import parse_cpp
|
|
236
|
+
return cast(ParseResult, parse_cpp(content, file_path))
|
|
237
|
+
if language == "proto" or file_path.lower().endswith(".proto"):
|
|
238
|
+
from codegraph.indexing.protocols import parse_proto
|
|
239
|
+
return cast(ParseResult, parse_proto(content, file_path))
|
|
240
|
+
if language == "graphql" or file_path.lower().endswith((".graphql", ".gql")):
|
|
241
|
+
from codegraph.indexing.protocols import parse_graphql
|
|
242
|
+
return cast(ParseResult, parse_graphql(content, file_path))
|
|
243
|
+
if language == "html" or file_path.lower().endswith((".html", ".htm", ".jinja", ".jinja2", ".njk", ".ejs")):
|
|
244
|
+
from codegraph.indexing.templates import parse_html_template
|
|
245
|
+
return parse_html_template(content, file_path)
|
|
218
246
|
if language in ("sql", "prisma"):
|
|
219
247
|
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
220
248
|
return ParseResult(
|
|
@@ -434,6 +462,7 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
434
462
|
self.bindings: list[BindingRef] = []
|
|
435
463
|
self.exports: list[str] = []
|
|
436
464
|
self.conditional_depth: int = 0
|
|
465
|
+
self.di_type_aliases: dict[str, str] = {}
|
|
437
466
|
|
|
438
467
|
# Scope stack: (name, kind, qualified_name, canonical_id)
|
|
439
468
|
self.scope_stack: list[tuple[str, str, str, str]] = []
|
|
@@ -825,6 +854,36 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
825
854
|
dep_expr = ast.unparse(elts[0]).split("[")[0].strip()
|
|
826
855
|
break
|
|
827
856
|
|
|
857
|
+
# Check if annotation refers to an Annotated type alias: e.g. session: SessionDep, current_user: CurrentUser
|
|
858
|
+
if not dep_expr and arg.annotation and hasattr(ast, "unparse"):
|
|
859
|
+
ann_text = ast.unparse(arg.annotation).strip()
|
|
860
|
+
ann_short = ann_text.split("[")[0].strip().split(".")[-1]
|
|
861
|
+
if ann_short in self.di_type_aliases:
|
|
862
|
+
dep_expr = self.di_type_aliases[ann_short]
|
|
863
|
+
elif (
|
|
864
|
+
ann_short not in _PYTHON_BUILTINS
|
|
865
|
+
and not ann_short.startswith("_")
|
|
866
|
+
and ann_short not in (
|
|
867
|
+
"Any", "Optional", "Union", "List", "Dict", "Set", "Tuple",
|
|
868
|
+
"int", "str", "float", "bool", "bytes", "dict", "list", "set", "tuple",
|
|
869
|
+
)
|
|
870
|
+
):
|
|
871
|
+
self.bindings.append(
|
|
872
|
+
BindingRef(
|
|
873
|
+
target_name=canon_id,
|
|
874
|
+
file_path=self.file_path,
|
|
875
|
+
line=node.lineno,
|
|
876
|
+
column=getattr(node, "col_offset", None),
|
|
877
|
+
scope=self.current_scope_qname,
|
|
878
|
+
expr_kind="PARAM_TYPE_DI",
|
|
879
|
+
source_expr=ann_text,
|
|
880
|
+
is_conditional=is_cond,
|
|
881
|
+
base_expr="fastapi",
|
|
882
|
+
attr_name="Depends",
|
|
883
|
+
subscript_key=json.dumps({"param": arg.arg, "alias": ann_short, "framework": "fastapi"}),
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
|
|
828
887
|
if dep_expr:
|
|
829
888
|
self.bindings.append(
|
|
830
889
|
BindingRef(
|
|
@@ -1354,12 +1413,104 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
1354
1413
|
)
|
|
1355
1414
|
)
|
|
1356
1415
|
|
|
1416
|
+
# 10. Modern FastAPI / typing Annotated DI alias
|
|
1417
|
+
# e.g., SessionDep = Annotated[Session, Depends(get_db)]
|
|
1418
|
+
# CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
1419
|
+
# TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
|
1420
|
+
if isinstance(value_node, ast.Subscript) and hasattr(ast, "unparse"):
|
|
1421
|
+
val_id = getattr(value_node.value, "id", None) or (
|
|
1422
|
+
getattr(value_node.value, "attr", None) if isinstance(value_node.value, ast.Attribute) else ""
|
|
1423
|
+
)
|
|
1424
|
+
val_unparse = ast.unparse(value_node.value) if hasattr(ast, "unparse") else ""
|
|
1425
|
+
if val_id in ("Annotated", "typing.Annotated", "typing_extensions.Annotated") or "Annotated" in val_unparse:
|
|
1426
|
+
slice_node = value_node.slice
|
|
1427
|
+
elts = slice_node.elts if isinstance(slice_node, (ast.Tuple, ast.List)) else [slice_node]
|
|
1428
|
+
for elt in elts[1:]:
|
|
1429
|
+
if isinstance(elt, ast.Call):
|
|
1430
|
+
f_nm = getattr(elt.func, "id", None) or (
|
|
1431
|
+
getattr(elt.func, "attr", None) if isinstance(elt.func, ast.Attribute) else ""
|
|
1432
|
+
)
|
|
1433
|
+
if f_nm == "Depends":
|
|
1434
|
+
elt_arg: ast.expr | None = elt.args[0] if elt.args else None
|
|
1435
|
+
if elt_arg is None:
|
|
1436
|
+
elt_kw = next((kw for kw in elt.keywords if kw.arg == "dependency"), None)
|
|
1437
|
+
if elt_kw is not None:
|
|
1438
|
+
elt_arg = elt_kw.value
|
|
1439
|
+
dep_alias_str: str | None = None
|
|
1440
|
+
if elt_arg is not None:
|
|
1441
|
+
if isinstance(elt_arg, (ast.Name, ast.Attribute)):
|
|
1442
|
+
dep_alias_str = ast.unparse(elt_arg)
|
|
1443
|
+
else:
|
|
1444
|
+
dep_alias_str = "<DYNAMIC>"
|
|
1445
|
+
elif hasattr(ast, "unparse"):
|
|
1446
|
+
dep_alias_str = ast.unparse(elts[0]).split("[")[0].strip()
|
|
1447
|
+
if dep_alias_str:
|
|
1448
|
+
self.di_type_aliases[target_name] = dep_alias_str
|
|
1449
|
+
self.bindings.append(
|
|
1450
|
+
BindingRef(
|
|
1451
|
+
target_name=target_name,
|
|
1452
|
+
file_path=self.file_path,
|
|
1453
|
+
line=line,
|
|
1454
|
+
column=col,
|
|
1455
|
+
scope=scope,
|
|
1456
|
+
expr_kind="DI_ALIAS_INJECTS",
|
|
1457
|
+
source_expr=dep_alias_str,
|
|
1458
|
+
is_conditional=is_cond,
|
|
1459
|
+
base_expr="fastapi",
|
|
1460
|
+
attr_name="Depends",
|
|
1461
|
+
subscript_key=json.dumps({"alias": target_name, "framework": "fastapi"}),
|
|
1462
|
+
)
|
|
1463
|
+
)
|
|
1464
|
+
break
|
|
1465
|
+
|
|
1357
1466
|
def visit_Assign(self, node: ast.Assign) -> None:
|
|
1358
1467
|
for target in node.targets:
|
|
1359
1468
|
if isinstance(target, ast.Name):
|
|
1360
1469
|
self._extract_binding_from_assign(
|
|
1361
1470
|
target.id, node.value, node.lineno, getattr(node, "col_offset", None)
|
|
1362
1471
|
)
|
|
1472
|
+
if target.id in ("template_name", "template") and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
1473
|
+
self.bindings.append(
|
|
1474
|
+
BindingRef(
|
|
1475
|
+
target_name=node.value.value,
|
|
1476
|
+
file_path=self.file_path,
|
|
1477
|
+
line=node.lineno,
|
|
1478
|
+
column=getattr(node, "col_offset", None),
|
|
1479
|
+
scope=self.current_scope_qname or "",
|
|
1480
|
+
expr_kind="RENDERS_TEMPLATE",
|
|
1481
|
+
base_expr=self.current_scope_canonical_id or "",
|
|
1482
|
+
source_expr=node.value.value,
|
|
1483
|
+
)
|
|
1484
|
+
)
|
|
1485
|
+
elif isinstance(node.value, ast.BinOp) and isinstance(node.value.op, ast.BitOr):
|
|
1486
|
+
# Unfold bitwise OR chain (e.g. prompt | llm | output_parser)
|
|
1487
|
+
pipe_steps: list[str] = []
|
|
1488
|
+
curr: ast.expr = node.value
|
|
1489
|
+
is_valid_pipe = True
|
|
1490
|
+
while isinstance(curr, ast.BinOp) and isinstance(curr.op, ast.BitOr):
|
|
1491
|
+
r_text = ast.unparse(curr.right).strip() if hasattr(ast, "unparse") else ""
|
|
1492
|
+
if not r_text or isinstance(curr.right, ast.Constant):
|
|
1493
|
+
is_valid_pipe = False
|
|
1494
|
+
break
|
|
1495
|
+
pipe_steps.append(r_text)
|
|
1496
|
+
curr = curr.left
|
|
1497
|
+
l_text = ast.unparse(curr).strip() if hasattr(ast, "unparse") else ""
|
|
1498
|
+
if is_valid_pipe and l_text and not isinstance(curr, ast.Constant):
|
|
1499
|
+
pipe_steps.append(l_text)
|
|
1500
|
+
pipe_steps.reverse()
|
|
1501
|
+
if len(pipe_steps) >= 2:
|
|
1502
|
+
self.bindings.append(
|
|
1503
|
+
BindingRef(
|
|
1504
|
+
target_name=target.id,
|
|
1505
|
+
file_path=self.file_path,
|
|
1506
|
+
line=node.lineno,
|
|
1507
|
+
column=getattr(node, "col_offset", None),
|
|
1508
|
+
scope=self.current_scope_qname or "",
|
|
1509
|
+
expr_kind="LCEL_PIPELINE",
|
|
1510
|
+
base_expr="|".join(pipe_steps),
|
|
1511
|
+
source_expr=json.dumps(pipe_steps),
|
|
1512
|
+
)
|
|
1513
|
+
)
|
|
1363
1514
|
elif (
|
|
1364
1515
|
isinstance(target, ast.Attribute)
|
|
1365
1516
|
and isinstance(target.value, ast.Name)
|
|
@@ -1575,6 +1726,39 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
1575
1726
|
caller_canon = self.current_scope_canonical_id
|
|
1576
1727
|
end_ln = node.end_lineno or node.lineno
|
|
1577
1728
|
|
|
1729
|
+
# Python View -> Template rendering (Flask render_template, Django render, FastAPI TemplateResponse)
|
|
1730
|
+
template_name: str | None = None
|
|
1731
|
+
if isinstance(node.func, ast.Name):
|
|
1732
|
+
if node.func.id == "render_template" and node.args:
|
|
1733
|
+
if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
1734
|
+
template_name = node.args[0].value
|
|
1735
|
+
elif node.func.id == "render" and len(node.args) >= 2:
|
|
1736
|
+
if isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
|
|
1737
|
+
template_name = node.args[1].value
|
|
1738
|
+
elif isinstance(node.func, ast.Attribute):
|
|
1739
|
+
if node.func.attr == "TemplateResponse" and node.args:
|
|
1740
|
+
if isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
1741
|
+
template_name = node.args[0].value
|
|
1742
|
+
elif len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
|
|
1743
|
+
template_name = node.args[1].value
|
|
1744
|
+
for kw in node.keywords:
|
|
1745
|
+
if kw.arg in ("name", "template_name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
1746
|
+
template_name = kw.value.value
|
|
1747
|
+
|
|
1748
|
+
if template_name:
|
|
1749
|
+
self.bindings.append(
|
|
1750
|
+
BindingRef(
|
|
1751
|
+
target_name=template_name,
|
|
1752
|
+
file_path=self.file_path,
|
|
1753
|
+
line=node.lineno,
|
|
1754
|
+
column=getattr(node, "col_offset", None),
|
|
1755
|
+
scope=caller_qname or "",
|
|
1756
|
+
expr_kind="RENDERS_TEMPLATE",
|
|
1757
|
+
base_expr=caller_canon or "",
|
|
1758
|
+
source_expr=template_name,
|
|
1759
|
+
)
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1578
1762
|
if isinstance(node.func, ast.Name):
|
|
1579
1763
|
callee_name = node.func.id
|
|
1580
1764
|
if callee_name == "subscribe" and len(node.args) >= 2 and hasattr(ast, "unparse"):
|
|
@@ -2010,6 +2194,106 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
2010
2194
|
subscript_key=ev_str,
|
|
2011
2195
|
)
|
|
2012
2196
|
)
|
|
2197
|
+
# Celery / Background task / Ray remote task invocation (.delay(), .apply_async(), .s(), .si(), .signature(), .remote())
|
|
2198
|
+
if attr_name in ("delay", "apply_async", "s", "si", "signature", "remote"):
|
|
2199
|
+
task_callee = recv_clean.split(".")[-1]
|
|
2200
|
+
if task_callee and task_callee not in ("self", "cls"):
|
|
2201
|
+
self.calls.append(
|
|
2202
|
+
CallRef(
|
|
2203
|
+
callee=task_callee,
|
|
2204
|
+
qualified_callee=recv_clean,
|
|
2205
|
+
line=node.lineno,
|
|
2206
|
+
end_line=end_ln,
|
|
2207
|
+
source_file=self.file_path,
|
|
2208
|
+
confidence="HIGH",
|
|
2209
|
+
caller_symbol=caller_qname,
|
|
2210
|
+
caller_canonical_id=caller_canon,
|
|
2211
|
+
receiver=recv_clean if "." in recv_clean else None,
|
|
2212
|
+
)
|
|
2213
|
+
)
|
|
2214
|
+
dispatch_kind = "RAY_REMOTE" if attr_name == "remote" else "CELERY_DISPATCH"
|
|
2215
|
+
self.bindings.append(
|
|
2216
|
+
BindingRef(
|
|
2217
|
+
target_name=task_callee,
|
|
2218
|
+
file_path=self.file_path,
|
|
2219
|
+
line=node.lineno,
|
|
2220
|
+
column=getattr(node, "col_offset", None),
|
|
2221
|
+
scope=caller_qname or "",
|
|
2222
|
+
expr_kind=dispatch_kind,
|
|
2223
|
+
base_expr=attr_name,
|
|
2224
|
+
source_expr=recv_clean,
|
|
2225
|
+
)
|
|
2226
|
+
)
|
|
2227
|
+
elif attr_name == "send_task" and node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
2228
|
+
full_task = node.args[0].value
|
|
2229
|
+
task_short = full_task.split(".")[-1]
|
|
2230
|
+
self.calls.append(
|
|
2231
|
+
CallRef(
|
|
2232
|
+
callee=task_short,
|
|
2233
|
+
qualified_callee=full_task,
|
|
2234
|
+
line=node.lineno,
|
|
2235
|
+
end_line=end_ln,
|
|
2236
|
+
source_file=self.file_path,
|
|
2237
|
+
confidence="HIGH",
|
|
2238
|
+
caller_symbol=caller_qname,
|
|
2239
|
+
caller_canonical_id=caller_canon,
|
|
2240
|
+
receiver=full_task if "." in full_task else None,
|
|
2241
|
+
)
|
|
2242
|
+
)
|
|
2243
|
+
self.bindings.append(
|
|
2244
|
+
BindingRef(
|
|
2245
|
+
target_name=task_short,
|
|
2246
|
+
file_path=self.file_path,
|
|
2247
|
+
line=node.lineno,
|
|
2248
|
+
column=getattr(node, "col_offset", None),
|
|
2249
|
+
scope=caller_qname or "",
|
|
2250
|
+
expr_kind="CELERY_DISPATCH",
|
|
2251
|
+
base_expr="send_task",
|
|
2252
|
+
source_expr=full_task,
|
|
2253
|
+
)
|
|
2254
|
+
)
|
|
2255
|
+
elif attr_name == "connect" and node.args and hasattr(ast, "unparse"):
|
|
2256
|
+
receiver_node = node.args[0]
|
|
2257
|
+
receiver_name = ast.unparse(receiver_node).strip()
|
|
2258
|
+
sender_name = ""
|
|
2259
|
+
for kw in node.keywords:
|
|
2260
|
+
if kw.arg == "sender":
|
|
2261
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
2262
|
+
sender_name = kw.value.value
|
|
2263
|
+
elif isinstance(kw.value, (ast.Name, ast.Attribute)):
|
|
2264
|
+
sender_name = ast.unparse(kw.value).strip()
|
|
2265
|
+
self.bindings.append(
|
|
2266
|
+
BindingRef(
|
|
2267
|
+
target_name=receiver_name,
|
|
2268
|
+
file_path=self.file_path,
|
|
2269
|
+
line=node.lineno,
|
|
2270
|
+
column=getattr(node, "col_offset", None),
|
|
2271
|
+
scope=self.current_scope_qname or "",
|
|
2272
|
+
expr_kind="DJANGO_SIGNAL_RECEIVER",
|
|
2273
|
+
base_expr=recv_clean,
|
|
2274
|
+
source_expr=sender_name,
|
|
2275
|
+
)
|
|
2276
|
+
)
|
|
2277
|
+
elif attr_name in ("send", "send_robust") and hasattr(ast, "unparse"):
|
|
2278
|
+
sender_name = ""
|
|
2279
|
+
for kw in node.keywords:
|
|
2280
|
+
if kw.arg == "sender":
|
|
2281
|
+
if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
2282
|
+
sender_name = kw.value.value
|
|
2283
|
+
elif isinstance(kw.value, (ast.Name, ast.Attribute)):
|
|
2284
|
+
sender_name = ast.unparse(kw.value).strip()
|
|
2285
|
+
self.bindings.append(
|
|
2286
|
+
BindingRef(
|
|
2287
|
+
target_name=recv_clean,
|
|
2288
|
+
file_path=self.file_path,
|
|
2289
|
+
line=node.lineno,
|
|
2290
|
+
column=getattr(node, "col_offset", None),
|
|
2291
|
+
scope=self.current_scope_qname or "",
|
|
2292
|
+
expr_kind="DJANGO_SIGNAL_SEND",
|
|
2293
|
+
base_expr=attr_name,
|
|
2294
|
+
source_expr=sender_name,
|
|
2295
|
+
)
|
|
2296
|
+
)
|
|
2013
2297
|
|
|
2014
2298
|
q_callee = f"{resolved_recv}.{attr_name}" if resolved_recv else attr_name
|
|
2015
2299
|
self.calls.append(
|
|
@@ -2042,6 +2326,26 @@ class _PythonScopeVisitor(ast.NodeVisitor):
|
|
|
2042
2326
|
)
|
|
2043
2327
|
)
|
|
2044
2328
|
|
|
2329
|
+
# Agent tool registration (e.g. AgentExecutor(tools=[...]), create_openai_tools_agent(..., tools=[...]))
|
|
2330
|
+
for kw in node.keywords:
|
|
2331
|
+
if kw.arg in ("tools", "functions") and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
2332
|
+
func_label = ast.unparse(node.func).strip() if hasattr(ast, "unparse") else ""
|
|
2333
|
+
for elt in kw.value.elts:
|
|
2334
|
+
t_name = ast.unparse(elt).strip() if hasattr(ast, "unparse") else ""
|
|
2335
|
+
if t_name:
|
|
2336
|
+
self.bindings.append(
|
|
2337
|
+
BindingRef(
|
|
2338
|
+
target_name=t_name,
|
|
2339
|
+
file_path=self.file_path,
|
|
2340
|
+
line=node.lineno,
|
|
2341
|
+
column=getattr(node, "col_offset", None),
|
|
2342
|
+
scope=caller_qname or "",
|
|
2343
|
+
expr_kind="AGENT_TOOL_REGISTRATION",
|
|
2344
|
+
base_expr=func_label,
|
|
2345
|
+
source_expr=t_name,
|
|
2346
|
+
)
|
|
2347
|
+
)
|
|
2348
|
+
|
|
2045
2349
|
self.generic_visit(node)
|
|
2046
2350
|
|
|
2047
2351
|
|
|
@@ -2096,19 +2400,47 @@ def _extract_all_python_imports(
|
|
|
2096
2400
|
return imports
|
|
2097
2401
|
|
|
2098
2402
|
|
|
2403
|
+
def _repair_python_syntax(content: str) -> str:
|
|
2404
|
+
"""Repair common syntax patterns from newer or experimental Python versions (e.g. unparenthesized multiple except)."""
|
|
2405
|
+
lines = content.splitlines()
|
|
2406
|
+
repaired = False
|
|
2407
|
+
new_lines = []
|
|
2408
|
+
for line in lines:
|
|
2409
|
+
# Match 'except A, B:' or 'except A, B as e:' or 'except* A, B:' without enclosing parentheses
|
|
2410
|
+
m = re.match(
|
|
2411
|
+
r'^(\s*except\*?\s+)([A-Za-z0-9_.\s]+,\s*[A-Za-z0-9_.,\s]+?)(\s+as\s+[A-Za-z0-9_]+)?(\s*:.*)$',
|
|
2412
|
+
line,
|
|
2413
|
+
)
|
|
2414
|
+
if m:
|
|
2415
|
+
prefix, types, as_part, suffix = m.group(1), m.group(2).strip(), m.group(3) or "", m.group(4)
|
|
2416
|
+
if not types.startswith("("):
|
|
2417
|
+
line = f"{prefix}({types}){as_part}{suffix}"
|
|
2418
|
+
repaired = True
|
|
2419
|
+
new_lines.append(line)
|
|
2420
|
+
return "\n".join(new_lines) if repaired else content
|
|
2421
|
+
|
|
2422
|
+
|
|
2099
2423
|
def _parse_python(content: str, file_path: str) -> ParseResult:
|
|
2100
2424
|
source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
2425
|
+
tree: ast.Module | None = None
|
|
2101
2426
|
try:
|
|
2102
2427
|
tree = ast.parse(content)
|
|
2103
2428
|
except SyntaxError as exc:
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2429
|
+
repaired = _repair_python_syntax(content)
|
|
2430
|
+
if repaired != content:
|
|
2431
|
+
try:
|
|
2432
|
+
tree = ast.parse(repaired)
|
|
2433
|
+
except SyntaxError:
|
|
2434
|
+
tree = None
|
|
2435
|
+
if tree is None:
|
|
2436
|
+
return ParseResult(
|
|
2437
|
+
path=file_path,
|
|
2438
|
+
language="python",
|
|
2439
|
+
source_hash=source_hash,
|
|
2440
|
+
imports=tuple(_py_imports_fallback(content, file_path)),
|
|
2441
|
+
parse_failed=True,
|
|
2442
|
+
parse_error=f"SyntaxError at line {exc.lineno}: {exc.msg}",
|
|
2443
|
+
)
|
|
2112
2444
|
|
|
2113
2445
|
# First collect import module names cheaply from top-level imports to configure analyzers
|
|
2114
2446
|
import_mods: set[str] = set()
|
|
@@ -2732,7 +3064,7 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
|
|
|
2732
3064
|
)
|
|
2733
3065
|
|
|
2734
3066
|
# Route detection hook per line (Express and Next.js)
|
|
2735
|
-
detected_routes = analyze_js_ts_line(line, lineno, file_path, module, import_mods_set)
|
|
3067
|
+
detected_routes = analyze_js_ts_line(line, lineno, file_path, module, import_mods_set, lines, idx)
|
|
2736
3068
|
if detected_routes:
|
|
2737
3069
|
routes.extend(detected_routes)
|
|
2738
3070
|
detected_mounts = analyze_js_ts_mounts(line, lineno, file_path, module)
|
|
@@ -3013,6 +3345,50 @@ def _parse_js_ts(content: str, language: str, file_path: str) -> ParseResult:
|
|
|
3013
3345
|
)
|
|
3014
3346
|
)
|
|
3015
3347
|
|
|
3348
|
+
# Framework file-level enhancements (NestJS and Next.js)
|
|
3349
|
+
if "@" in content:
|
|
3350
|
+
nest_res = analyze_nestjs_file(content, file_path, module)
|
|
3351
|
+
if nest_res.routes:
|
|
3352
|
+
routes.extend(nest_res.routes)
|
|
3353
|
+
if nest_res.mounts:
|
|
3354
|
+
mounts.extend(nest_res.mounts)
|
|
3355
|
+
if nest_res.bindings:
|
|
3356
|
+
bindings.extend(nest_res.bindings)
|
|
3357
|
+
|
|
3358
|
+
next_res = analyze_nextjs_file(content, file_path, module)
|
|
3359
|
+
if next_res.routes:
|
|
3360
|
+
seen_ep_ids = {r.endpoint_id for r in routes}
|
|
3361
|
+
for nr in next_res.routes:
|
|
3362
|
+
if nr.endpoint_id not in seen_ep_ids:
|
|
3363
|
+
routes.append(nr)
|
|
3364
|
+
seen_ep_ids.add(nr.endpoint_id)
|
|
3365
|
+
if next_res.bindings:
|
|
3366
|
+
bindings.extend(next_res.bindings)
|
|
3367
|
+
|
|
3368
|
+
# React and JSX component architecture (Pillar 4)
|
|
3369
|
+
react_res = analyze_react_file(content, file_path, module)
|
|
3370
|
+
sym_indices = {s.canonical_id: i for i, s in enumerate(symbols)}
|
|
3371
|
+
for sym in react_res.components:
|
|
3372
|
+
if sym.canonical_id in sym_indices:
|
|
3373
|
+
symbols[sym_indices[sym.canonical_id]] = sym
|
|
3374
|
+
else:
|
|
3375
|
+
seen_canonical.add(sym.canonical_id)
|
|
3376
|
+
symbols.append(sym)
|
|
3377
|
+
for h_sym in react_res.hooks:
|
|
3378
|
+
if h_sym.canonical_id in sym_indices:
|
|
3379
|
+
symbols[sym_indices[h_sym.canonical_id]] = h_sym
|
|
3380
|
+
else:
|
|
3381
|
+
seen_canonical.add(h_sym.canonical_id)
|
|
3382
|
+
symbols.append(h_sym)
|
|
3383
|
+
if react_res.bindings:
|
|
3384
|
+
bindings.extend(react_res.bindings)
|
|
3385
|
+
|
|
3386
|
+
if language == "typescript" and ("@Service" in content or "@Controller" in content or "Dependency<" in content):
|
|
3387
|
+
from codegraph.roblox import extract_flamework_bindings
|
|
3388
|
+
fw_bindings = extract_flamework_bindings(content, file_path, module)
|
|
3389
|
+
if fw_bindings:
|
|
3390
|
+
bindings.extend(fw_bindings)
|
|
3391
|
+
|
|
3016
3392
|
symbols.sort(key=lambda s: (s.start_line, s.canonical_id))
|
|
3017
3393
|
return ParseResult(
|
|
3018
3394
|
path=file_path,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Thread-safe SQLite connection pool for concurrent MCP and CLI execution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import queue
|
|
5
|
+
import sqlite3
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .classifier import make_compat_row
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLiteConnectionPool:
|
|
14
|
+
"""Thread-safe, re-entrant connection pool for SQLite databases.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Thread-safe LIFO pool to maximize SQLite cache reuse.
|
|
18
|
+
- WAL mode, synchronous=NORMAL, foreign_keys=ON, busy_timeout=30000ms.
|
|
19
|
+
- check_same_thread=False for cross-thread pool handoff.
|
|
20
|
+
- Re-entrant per-thread acquisition (nesting depth tracking via thread-local state).
|
|
21
|
+
- Optional initializer hook for idempotent schema / setup execution.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
db_path: Path | str,
|
|
27
|
+
max_size: int = 16,
|
|
28
|
+
timeout: float = 30.0,
|
|
29
|
+
initializer: Callable[[sqlite3.Connection], None] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.db_path = Path(db_path) if isinstance(db_path, str) else db_path
|
|
32
|
+
self.max_size = max(1, max_size)
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
self.initializer = initializer
|
|
35
|
+
self._pool: queue.LifoQueue[sqlite3.Connection] = queue.LifoQueue()
|
|
36
|
+
self._created = 0
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
self._local = threading.local()
|
|
39
|
+
|
|
40
|
+
def _create_connection(self) -> sqlite3.Connection:
|
|
41
|
+
con = sqlite3.connect(
|
|
42
|
+
self.db_path,
|
|
43
|
+
check_same_thread=False,
|
|
44
|
+
timeout=self.timeout,
|
|
45
|
+
)
|
|
46
|
+
con.row_factory = make_compat_row
|
|
47
|
+
con.execute("PRAGMA journal_mode = WAL")
|
|
48
|
+
con.execute("PRAGMA synchronous = NORMAL")
|
|
49
|
+
con.execute("PRAGMA foreign_keys = ON")
|
|
50
|
+
con.execute("PRAGMA busy_timeout = 30000")
|
|
51
|
+
con.execute("PRAGMA cache_size = -64000")
|
|
52
|
+
con.execute("PRAGMA temp_store = MEMORY")
|
|
53
|
+
if self.initializer is not None:
|
|
54
|
+
self.initializer(con)
|
|
55
|
+
return con
|
|
56
|
+
|
|
57
|
+
def acquire(self) -> tuple[sqlite3.Connection, bool]:
|
|
58
|
+
"""Acquire a connection. Returns (connection, is_owner).
|
|
59
|
+
|
|
60
|
+
If the current thread already holds a connection from this pool,
|
|
61
|
+
it increments the re-entrancy depth and returns (connection, False).
|
|
62
|
+
"""
|
|
63
|
+
active_con = getattr(self._local, "con", None)
|
|
64
|
+
if active_con is not None:
|
|
65
|
+
self._local.nesting = getattr(self._local, "nesting", 0) + 1
|
|
66
|
+
return active_con, False
|
|
67
|
+
|
|
68
|
+
con: sqlite3.Connection | None = None
|
|
69
|
+
try:
|
|
70
|
+
con = self._pool.get_nowait()
|
|
71
|
+
except queue.Empty:
|
|
72
|
+
with self._lock:
|
|
73
|
+
if self._created < self.max_size:
|
|
74
|
+
con = self._create_connection()
|
|
75
|
+
self._created += 1
|
|
76
|
+
|
|
77
|
+
if con is None:
|
|
78
|
+
con = self._pool.get(timeout=self.timeout)
|
|
79
|
+
|
|
80
|
+
self._local.con = con
|
|
81
|
+
self._local.nesting = 1
|
|
82
|
+
return con, True
|
|
83
|
+
|
|
84
|
+
def release(
|
|
85
|
+
self,
|
|
86
|
+
con: sqlite3.Connection,
|
|
87
|
+
is_owner: bool,
|
|
88
|
+
rollback: bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Release a connection back to the pool."""
|
|
91
|
+
if not is_owner:
|
|
92
|
+
cur_nesting = getattr(self._local, "nesting", 1) - 1
|
|
93
|
+
self._local.nesting = max(0, cur_nesting)
|
|
94
|
+
if rollback:
|
|
95
|
+
try:
|
|
96
|
+
con.rollback()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
self._local.con = None
|
|
102
|
+
self._local.nesting = 0
|
|
103
|
+
if rollback:
|
|
104
|
+
try:
|
|
105
|
+
con.rollback()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
else:
|
|
109
|
+
try:
|
|
110
|
+
con.commit()
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
self._pool.put_nowait(con)
|
|
116
|
+
except Exception:
|
|
117
|
+
try:
|
|
118
|
+
con.close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def close_all(self) -> None:
|
|
123
|
+
"""Close and dispose all connections in the pool."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
con = self._pool.get_nowait()
|
|
128
|
+
try:
|
|
129
|
+
con.close()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
except queue.Empty:
|
|
133
|
+
break
|
|
134
|
+
self._created = 0
|
|
135
|
+
self._local.con = None
|
|
136
|
+
self._local.nesting = 0
|