arcgraph 0.1.0__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.
- arcgraph/__init__.py +41 -0
- arcgraph/_build_provenance.json +10 -0
- arcgraph/adapters/__init__.py +6 -0
- arcgraph/adapters/arq_adapter.py +369 -0
- arcgraph/adapters/celery_adapter.py +440 -0
- arcgraph/adapters/common.py +436 -0
- arcgraph/adapters/django_adapter.py +765 -0
- arcgraph/adapters/fastapi_adapter.py +415 -0
- arcgraph/adapters/logging_adapter.py +335 -0
- arcgraph/adapters/mcp_adapter.py +100 -0
- arcgraph/adapters/pydantic_adapter.py +290 -0
- arcgraph/adapters/pytest_adapter.py +221 -0
- arcgraph/adapters/pytest_collection.py +96 -0
- arcgraph/adapters/pytest_fixtures.py +371 -0
- arcgraph/adapters/pytest_types.py +251 -0
- arcgraph/adapters/registry.py +34 -0
- arcgraph/adapters/service_container_adapter.py +548 -0
- arcgraph/adapters/sqlalchemy_adapter.py +446 -0
- arcgraph/adapters/typer_click_adapter.py +292 -0
- arcgraph/analyzers/__init__.py +1 -0
- arcgraph/analyzers/bindings.py +782 -0
- arcgraph/analyzers/calls/__init__.py +6 -0
- arcgraph/analyzers/calls/analyzer.py +2921 -0
- arcgraph/analyzers/calls/constants.py +702 -0
- arcgraph/analyzers/calls/context.py +298 -0
- arcgraph/analyzers/calls/lexical.py +237 -0
- arcgraph/analyzers/coverage.py +826 -0
- arcgraph/analyzers/exports.py +46 -0
- arcgraph/analyzers/har_trace.py +323 -0
- arcgraph/analyzers/imports.py +190 -0
- arcgraph/analyzers/otel_trace.py +352 -0
- arcgraph/analyzers/precise_references.py +1199 -0
- arcgraph/analyzers/python_scopes.py +56 -0
- arcgraph/analyzers/runtime_trace.py +516 -0
- arcgraph/analyzers/similarity.py +430 -0
- arcgraph/analyzers/symbols.py +363 -0
- arcgraph/analyzers/type_unions.py +89 -0
- arcgraph/analyzers/types.py +2293 -0
- arcgraph/assets/__init__.py +1 -0
- arcgraph/assets/workbench/__init__.py +1 -0
- arcgraph/assets/workbench/index.html +2398 -0
- arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
- arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
- arcgraph/change/__init__.py +10 -0
- arcgraph/change/baseline.py +145 -0
- arcgraph/change/changed_regions.py +728 -0
- arcgraph/change/contracts.py +927 -0
- arcgraph/change/errors.py +88 -0
- arcgraph/change/evidence.py +289 -0
- arcgraph/change/graph_delta.py +1422 -0
- arcgraph/change/identities.py +1277 -0
- arcgraph/change/paths.py +234 -0
- arcgraph/change/pins.py +426 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +1030 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1138 -0
- arcgraph/change/trusted_runners.py +154 -0
- arcgraph/change/verdicts.py +141 -0
- arcgraph/change/verification_plan.py +113 -0
- arcgraph/change/verifier.py +579 -0
- arcgraph/core/__init__.py +1 -0
- arcgraph/core/assurance.py +343 -0
- arcgraph/core/binding_metrics.py +75 -0
- arcgraph/core/cleanup.py +701 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +862 -0
- arcgraph/core/expression_kinds.py +11 -0
- arcgraph/core/force_graph_export.py +1125 -0
- arcgraph/core/freshness.py +94 -0
- arcgraph/core/graph_store.py +1099 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +30 -0
- arcgraph/core/operation_lock.py +218 -0
- arcgraph/core/passes.py +142 -0
- arcgraph/core/payload_policy.py +708 -0
- arcgraph/core/precise_references.py +124 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1454 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -0
- arcgraph/core/sharing_retry.py +84 -0
- arcgraph/core/structural.py +260 -0
- arcgraph/core/target_resolver.py +486 -0
- arcgraph/core/type_metrics.py +67 -0
- arcgraph/core/unresolved_classification.py +471 -0
- arcgraph/core/utils.py +100 -0
- arcgraph/core/visual_contract.py +240 -0
- arcgraph/core/visual_slice_engine.py +933 -0
- arcgraph/interfaces/__init__.py +1 -0
- arcgraph/interfaces/agent_capabilities.py +493 -0
- arcgraph/interfaces/agent_help.py +313 -0
- arcgraph/interfaces/benchmark.py +548 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2799 -0
- arcgraph/interfaces/cli_change.py +690 -0
- arcgraph/interfaces/cli_feedback.py +176 -0
- arcgraph/interfaces/cli_ops.py +69 -0
- arcgraph/interfaces/cli_support.py +113 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +601 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +481 -0
- arcgraph/interfaces/docs.py +1255 -0
- arcgraph/interfaces/human_output.py +409 -0
- arcgraph/interfaces/local_state.py +577 -0
- arcgraph/interfaces/mcp_server.py +457 -0
- arcgraph/interfaces/mcp_tools.py +1209 -0
- arcgraph/interfaces/metrics.py +571 -0
- arcgraph/interfaces/ops.py +330 -0
- arcgraph/interfaces/precision.py +1670 -0
- arcgraph/interfaces/project_probe.py +75 -0
- arcgraph/interfaces/reports.py +1121 -0
- arcgraph/interfaces/stdio_encoding.py +29 -0
- arcgraph/interfaces/trace.py +592 -0
- arcgraph/interfaces/trial_feedback.py +519 -0
- arcgraph/interfaces/trial_setup.py +535 -0
- arcgraph/interfaces/visual_server.py +354 -0
- arcgraph/interfaces/visual_slice.py +168 -0
- arcgraph/interfaces/visual_smoke.py +666 -0
- arcgraph/interfaces/workbench.py +576 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +451 -0
- arcgraph/pipeline/external_l3_frontends.py +368 -0
- arcgraph/pipeline/external_protocol.py +358 -0
- arcgraph/pipeline/frontends.py +437 -0
- arcgraph/pipeline/indexer.py +661 -0
- arcgraph/pipeline/openapi_frontend.py +708 -0
- arcgraph/pipeline/python_frontend.py +298 -0
- arcgraph/pipeline/reindexer.py +1333 -0
- arcgraph/pipeline/scip_frontend.py +726 -0
- arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
- arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
- arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
- arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
- arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
- arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
- arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
- arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
- arcgraph/pipeline/typescript_extractor.mjs +177 -0
- arcgraph/pipeline/typescript_frameworks.py +947 -0
- arcgraph/pipeline/typescript_frontend.py +656 -0
- arcgraph/pipeline/typescript_references.mjs +167 -0
- arcgraph/providers/__init__.py +27 -0
- arcgraph/providers/context_provider.py +1258 -0
- arcgraph/providers/flow_payload.py +117 -0
- arcgraph/providers/memory_connector.py +216 -0
- arcgraph/providers/orchestrator.py +475 -0
- arcgraph/providers/relationship_payload.py +102 -0
- arcgraph/providers/risk_provider.py +843 -0
- arcgraph/providers/why_provider.py +349 -0
- arcgraph/version_info.py +246 -0
- arcgraph-0.1.0.dist-info/METADATA +676 -0
- arcgraph-0.1.0.dist-info/RECORD +165 -0
- arcgraph-0.1.0.dist-info/WHEEL +4 -0
- arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Shared helpers for framework adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
from arcgraph.core.ids import class_id, function_id, method_id, module_id
|
|
11
|
+
from arcgraph.core.schemas import (
|
|
12
|
+
BuildWarning,
|
|
13
|
+
Confidence,
|
|
14
|
+
Edge,
|
|
15
|
+
Evidence,
|
|
16
|
+
FileRecord,
|
|
17
|
+
Node,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AdapterAnalysis:
|
|
23
|
+
nodes: list[Node] = field(default_factory=list)
|
|
24
|
+
edges: list[Edge] = field(default_factory=list)
|
|
25
|
+
warnings: list[BuildWarning] = field(default_factory=list)
|
|
26
|
+
diagnostics: list[BuildWarning] = field(default_factory=list)
|
|
27
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SemanticAdapter:
|
|
31
|
+
"""Stable extension contract for framework and DSL-specific extraction."""
|
|
32
|
+
|
|
33
|
+
name = "semantic_adapter"
|
|
34
|
+
capabilities: tuple[str, ...] = ()
|
|
35
|
+
required_facts: tuple[str, ...] = ()
|
|
36
|
+
|
|
37
|
+
def detect(
|
|
38
|
+
self,
|
|
39
|
+
files: list[FileRecord],
|
|
40
|
+
parsed_files: dict[str, ast.Module],
|
|
41
|
+
nodes: list[Node],
|
|
42
|
+
module_names: set[str],
|
|
43
|
+
) -> bool:
|
|
44
|
+
del files, parsed_files, nodes, module_names
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def analyze(
|
|
48
|
+
self,
|
|
49
|
+
files: list[FileRecord],
|
|
50
|
+
parsed_files: dict[str, ast.Module],
|
|
51
|
+
nodes: list[Node],
|
|
52
|
+
module_names: set[str],
|
|
53
|
+
) -> AdapterAnalysis:
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AdapterRegistry:
|
|
58
|
+
"""Run semantic adapters with incremental graph context and failure isolation."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
adapters: Iterable[SemanticAdapter],
|
|
63
|
+
*,
|
|
64
|
+
dedupe_nodes: Callable[[list[Node]], list[Node]] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._adapters = tuple(adapters)
|
|
67
|
+
self._dedupe_nodes = dedupe_nodes or self._stable_dedupe_nodes
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def adapters(self) -> tuple[SemanticAdapter, ...]:
|
|
71
|
+
return self._adapters
|
|
72
|
+
|
|
73
|
+
def analyze(
|
|
74
|
+
self,
|
|
75
|
+
files: list[FileRecord],
|
|
76
|
+
parsed_files: dict[str, ast.Module],
|
|
77
|
+
nodes: list[Node],
|
|
78
|
+
module_names: set[str],
|
|
79
|
+
) -> AdapterAnalysis:
|
|
80
|
+
aggregate = AdapterAnalysis(metrics={"adapters": {}})
|
|
81
|
+
context = self._dedupe_nodes(list(nodes))
|
|
82
|
+
adapter_metrics: dict[str, dict[str, Any]] = aggregate.metrics["adapters"]
|
|
83
|
+
|
|
84
|
+
for adapter in self._adapters:
|
|
85
|
+
name = adapter.name
|
|
86
|
+
try:
|
|
87
|
+
if not adapter.detect(files, parsed_files, context, module_names):
|
|
88
|
+
adapter_metrics[name] = {
|
|
89
|
+
"status": "skipped",
|
|
90
|
+
"nodes": 0,
|
|
91
|
+
"edges": 0,
|
|
92
|
+
"warnings": 0,
|
|
93
|
+
}
|
|
94
|
+
continue
|
|
95
|
+
analysis = adapter.analyze(files, parsed_files, context, module_names)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
aggregate.warnings.append(
|
|
98
|
+
BuildWarning(
|
|
99
|
+
kind="adapter_error",
|
|
100
|
+
message=f"{name}: {exc.__class__.__name__}: {exc}",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
adapter_metrics[name] = {
|
|
104
|
+
"status": "error",
|
|
105
|
+
"nodes": 0,
|
|
106
|
+
"edges": 0,
|
|
107
|
+
"warnings": 1,
|
|
108
|
+
}
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
aggregate.nodes.extend(analysis.nodes)
|
|
112
|
+
aggregate.edges.extend(analysis.edges)
|
|
113
|
+
aggregate.warnings.extend(analysis.warnings)
|
|
114
|
+
aggregate.warnings.extend(analysis.diagnostics)
|
|
115
|
+
unresolved_registrations = sum(
|
|
116
|
+
1
|
|
117
|
+
for node in analysis.nodes
|
|
118
|
+
if node.properties.get("resolution_status") == "unresolved"
|
|
119
|
+
)
|
|
120
|
+
metrics = {
|
|
121
|
+
"discovered_registrations": (
|
|
122
|
+
len(analysis.nodes) - unresolved_registrations
|
|
123
|
+
),
|
|
124
|
+
"resolved_handlers": self._resolved_handler_count(analysis.edges),
|
|
125
|
+
"unresolved_registrations": unresolved_registrations,
|
|
126
|
+
**analysis.metrics,
|
|
127
|
+
}
|
|
128
|
+
adapter_metrics[name] = {
|
|
129
|
+
"status": "available",
|
|
130
|
+
"nodes": len(analysis.nodes),
|
|
131
|
+
"edges": len(analysis.edges),
|
|
132
|
+
"warnings": len(analysis.warnings) + len(analysis.diagnostics),
|
|
133
|
+
**metrics,
|
|
134
|
+
}
|
|
135
|
+
context = self._dedupe_nodes([*context, *analysis.nodes])
|
|
136
|
+
|
|
137
|
+
return aggregate
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _resolved_handler_count(edges: list[Edge]) -> int:
|
|
141
|
+
return sum(
|
|
142
|
+
1
|
|
143
|
+
for edge in edges
|
|
144
|
+
if edge.kind
|
|
145
|
+
in {
|
|
146
|
+
"invokes",
|
|
147
|
+
"provides",
|
|
148
|
+
"injects",
|
|
149
|
+
"calls",
|
|
150
|
+
"maps_to",
|
|
151
|
+
"reads",
|
|
152
|
+
"writes",
|
|
153
|
+
"enqueues",
|
|
154
|
+
"consumes",
|
|
155
|
+
"declares",
|
|
156
|
+
"configures",
|
|
157
|
+
"logs",
|
|
158
|
+
}
|
|
159
|
+
and edge.resolution.status == "resolved"
|
|
160
|
+
and edge.confidence != "unresolved"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _stable_dedupe_nodes(nodes: list[Node]) -> list[Node]:
|
|
165
|
+
deduped: dict[str, Node] = {}
|
|
166
|
+
for node in nodes:
|
|
167
|
+
deduped.setdefault(node.id, node)
|
|
168
|
+
return list(deduped.values())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class ImportAliases:
|
|
173
|
+
aliases: dict[str, str] = field(default_factory=dict)
|
|
174
|
+
modules: dict[str, str] = field(default_factory=dict)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class NodeIndex:
|
|
178
|
+
def __init__(self, nodes: Iterable[Node]) -> None:
|
|
179
|
+
self.by_id = {node.id: node for node in nodes}
|
|
180
|
+
self.classes_by_name: dict[str, list[Node]] = {}
|
|
181
|
+
self.functions_by_name: dict[str, list[Node]] = {}
|
|
182
|
+
self.methods_by_class_and_name: dict[tuple[str, str], Node] = {}
|
|
183
|
+
for node in nodes:
|
|
184
|
+
if node.kind == "class":
|
|
185
|
+
self.classes_by_name.setdefault(node.name, []).append(node)
|
|
186
|
+
elif node.kind == "function":
|
|
187
|
+
self.functions_by_name.setdefault(node.name, []).append(node)
|
|
188
|
+
elif node.kind == "method" and node.qualname:
|
|
189
|
+
class_qualname = node.qualname.rsplit(".", 1)[0]
|
|
190
|
+
self.methods_by_class_and_name[(class_qualname, node.name)] = node
|
|
191
|
+
|
|
192
|
+
def function_id_for(
|
|
193
|
+
self, name: str, file_record: FileRecord, imports: ImportAliases
|
|
194
|
+
) -> str | None:
|
|
195
|
+
qualname = resolve_name(name, file_record, imports)
|
|
196
|
+
candidates = [function_id(qualname)]
|
|
197
|
+
if "." not in name:
|
|
198
|
+
candidates.append(function_id(f"{file_record.module}.{name}"))
|
|
199
|
+
for candidate in candidates:
|
|
200
|
+
if candidate in self.by_id:
|
|
201
|
+
return candidate
|
|
202
|
+
same_name = self.functions_by_name.get(name.rsplit(".", 1)[-1], [])
|
|
203
|
+
if len(same_name) == 1:
|
|
204
|
+
return same_name[0].id
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def class_for_annotation(
|
|
208
|
+
self, annotation: str | None, file_record: FileRecord, imports: ImportAliases
|
|
209
|
+
) -> Node | None:
|
|
210
|
+
if not annotation:
|
|
211
|
+
return None
|
|
212
|
+
name = clean_annotation_name(annotation)
|
|
213
|
+
if not name:
|
|
214
|
+
return None
|
|
215
|
+
qualname = resolve_name(name, file_record, imports)
|
|
216
|
+
candidate = self.by_id.get(class_id(qualname))
|
|
217
|
+
if candidate:
|
|
218
|
+
return candidate
|
|
219
|
+
matches = self.classes_by_name.get(name.rsplit(".", 1)[-1], [])
|
|
220
|
+
if len(matches) == 1:
|
|
221
|
+
return matches[0]
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def method_for_class(self, class_node: Node, method_name: str) -> Node | None:
|
|
225
|
+
if not class_node.qualname:
|
|
226
|
+
return None
|
|
227
|
+
return self.methods_by_class_and_name.get((class_node.qualname, method_name))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def import_aliases(
|
|
231
|
+
file_record: FileRecord, tree: ast.Module, module_names: set[str]
|
|
232
|
+
) -> ImportAliases:
|
|
233
|
+
aliases: dict[str, str] = {}
|
|
234
|
+
modules: dict[str, str] = {}
|
|
235
|
+
for node in tree.body:
|
|
236
|
+
if isinstance(node, ast.Import):
|
|
237
|
+
for alias in node.names:
|
|
238
|
+
local = alias.asname or alias.name.split(".", 1)[0]
|
|
239
|
+
aliases[local] = alias.name
|
|
240
|
+
modules[local] = alias.name
|
|
241
|
+
elif isinstance(node, ast.ImportFrom):
|
|
242
|
+
base = resolve_import_from_base(file_record, node)
|
|
243
|
+
if not base:
|
|
244
|
+
continue
|
|
245
|
+
for alias in node.names:
|
|
246
|
+
if alias.name == "*":
|
|
247
|
+
continue
|
|
248
|
+
local = alias.asname or alias.name
|
|
249
|
+
candidate = f"{base}.{alias.name}"
|
|
250
|
+
aliases[local] = candidate
|
|
251
|
+
modules[local] = candidate if candidate in module_names else base
|
|
252
|
+
return ImportAliases(aliases=aliases, modules=modules)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def resolve_name(name: str, file_record: FileRecord, imports: ImportAliases) -> str:
|
|
256
|
+
if not name:
|
|
257
|
+
return name
|
|
258
|
+
parts = name.split(".")
|
|
259
|
+
if parts[0] in imports.aliases:
|
|
260
|
+
return ".".join([imports.aliases[parts[0]], *parts[1:]])
|
|
261
|
+
if "." in name:
|
|
262
|
+
return name
|
|
263
|
+
return f"{file_record.module}.{name}"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def resolve_alias(name: str | None, imports: ImportAliases) -> str | None:
|
|
267
|
+
if not name:
|
|
268
|
+
return None
|
|
269
|
+
parts = name.split(".")
|
|
270
|
+
if parts[0] in imports.aliases:
|
|
271
|
+
return ".".join([imports.aliases[parts[0]], *parts[1:]])
|
|
272
|
+
return name
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def resolve_module_name(name: str, imports: ImportAliases) -> str | None:
|
|
276
|
+
parts = name.split(".")
|
|
277
|
+
if parts[0] in imports.modules:
|
|
278
|
+
resolved = imports.modules[parts[0]]
|
|
279
|
+
return ".".join([resolved, *parts[1:-1]]) if len(parts) > 1 else resolved
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def resolve_import_from_base(
|
|
284
|
+
file_record: FileRecord, node: ast.ImportFrom
|
|
285
|
+
) -> str | None:
|
|
286
|
+
if node.level == 0:
|
|
287
|
+
return node.module
|
|
288
|
+
|
|
289
|
+
module_parts = file_record.module.split(".")
|
|
290
|
+
package_parts = module_parts if file_record.is_package else module_parts[:-1]
|
|
291
|
+
keep_count = len(package_parts) - node.level + 1
|
|
292
|
+
if keep_count < 0:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
base_parts = package_parts[:keep_count]
|
|
296
|
+
if node.module:
|
|
297
|
+
base_parts.extend(node.module.split("."))
|
|
298
|
+
return ".".join(part for part in base_parts if part)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def iter_symbol_defs(
|
|
302
|
+
tree: ast.Module,
|
|
303
|
+
) -> Iterable[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str, Callable[[str], str]]]:
|
|
304
|
+
for stmt in tree.body:
|
|
305
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
306
|
+
yield stmt, stmt.name, function_id
|
|
307
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
308
|
+
class_qualname = stmt.name
|
|
309
|
+
for child in stmt.body:
|
|
310
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
311
|
+
yield child, f"{class_qualname}.{child.name}", method_id
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def symbol_id_for_def(
|
|
315
|
+
file_record: FileRecord, qualified_name: str, id_factory: Callable[[str], str]
|
|
316
|
+
) -> str:
|
|
317
|
+
return id_factory(f"{file_record.module}.{qualified_name}")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def call_name(node: ast.AST) -> str | None:
|
|
321
|
+
if isinstance(node, ast.Name):
|
|
322
|
+
return node.id
|
|
323
|
+
if isinstance(node, ast.Attribute):
|
|
324
|
+
base = call_name(node.value)
|
|
325
|
+
return f"{base}.{node.attr}" if base else node.attr
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def literal_str(node: ast.AST | None) -> str | None:
|
|
330
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
331
|
+
return node.value
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def literal_bool(node: ast.AST | None) -> bool | None:
|
|
336
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, bool):
|
|
337
|
+
return node.value
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def literal_str_list(node: ast.AST | None) -> list[str]:
|
|
342
|
+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
343
|
+
return [value for item in node.elts if (value := literal_str(item))]
|
|
344
|
+
value = literal_str(node)
|
|
345
|
+
return [value] if value else []
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def keyword(call: ast.Call, name: str) -> ast.AST | None:
|
|
349
|
+
for item in call.keywords:
|
|
350
|
+
if item.arg == name:
|
|
351
|
+
return item.value
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def make_adapter_edge(
|
|
356
|
+
source: str,
|
|
357
|
+
target: str,
|
|
358
|
+
kind: str,
|
|
359
|
+
confidence: Confidence,
|
|
360
|
+
evidence_kind: str,
|
|
361
|
+
file_record: FileRecord,
|
|
362
|
+
node: ast.AST,
|
|
363
|
+
detail: str,
|
|
364
|
+
*,
|
|
365
|
+
semantic_role: str | None = None,
|
|
366
|
+
) -> Edge:
|
|
367
|
+
return Edge(
|
|
368
|
+
source=source,
|
|
369
|
+
target=target,
|
|
370
|
+
kind=kind,
|
|
371
|
+
confidence=confidence,
|
|
372
|
+
semantic_role=semantic_role or f"{kind}:{evidence_kind}",
|
|
373
|
+
evidence=[
|
|
374
|
+
Evidence(
|
|
375
|
+
kind=evidence_kind,
|
|
376
|
+
path=file_record.path,
|
|
377
|
+
start_line=getattr(node, "lineno", None),
|
|
378
|
+
end_line=getattr(node, "end_lineno", getattr(node, "lineno", None)),
|
|
379
|
+
column=getattr(node, "col_offset", None),
|
|
380
|
+
detail=detail,
|
|
381
|
+
)
|
|
382
|
+
],
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def annotation_name(node: ast.AST | None) -> str | None:
|
|
387
|
+
if node is None:
|
|
388
|
+
return None
|
|
389
|
+
if isinstance(node, ast.Name):
|
|
390
|
+
return node.id
|
|
391
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
392
|
+
return node.value
|
|
393
|
+
if isinstance(node, ast.Attribute):
|
|
394
|
+
base = annotation_name(node.value)
|
|
395
|
+
return f"{base}.{node.attr}" if base else node.attr
|
|
396
|
+
if isinstance(node, ast.Subscript):
|
|
397
|
+
root = annotation_name(node.value)
|
|
398
|
+
if root and root.rsplit(".", 1)[-1] == "Annotated":
|
|
399
|
+
if isinstance(node.slice, ast.Tuple) and node.slice.elts:
|
|
400
|
+
return annotation_name(node.slice.elts[0])
|
|
401
|
+
return annotation_name(node.value)
|
|
402
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
403
|
+
return annotation_name(node.left) or annotation_name(node.right)
|
|
404
|
+
return unparse(node)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def clean_annotation_name(name: str) -> str:
|
|
408
|
+
cleaned = name.strip("\"'")
|
|
409
|
+
if "|" in cleaned:
|
|
410
|
+
cleaned = cleaned.split("|", 1)[0].strip()
|
|
411
|
+
if cleaned.startswith("Optional[") and cleaned.endswith("]"):
|
|
412
|
+
cleaned = cleaned.removeprefix("Optional[").removesuffix("]")
|
|
413
|
+
return cleaned.strip("\"'")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def unparse(node: ast.AST) -> str:
|
|
417
|
+
try:
|
|
418
|
+
return ast.unparse(node)
|
|
419
|
+
except Exception:
|
|
420
|
+
return node.__class__.__name__
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def combine_paths(prefix: str, path: str) -> str:
|
|
424
|
+
if not prefix:
|
|
425
|
+
combined = path or "/"
|
|
426
|
+
elif not path:
|
|
427
|
+
combined = prefix
|
|
428
|
+
else:
|
|
429
|
+
combined = f"{prefix.rstrip('/')}/{path.lstrip('/')}"
|
|
430
|
+
if not combined.startswith("/"):
|
|
431
|
+
combined = f"/{combined}"
|
|
432
|
+
return combined.replace("//", "/")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def module_node_id(file_record: FileRecord) -> str:
|
|
436
|
+
return module_id(file_record.module)
|