cg-code-graph 0.10.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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
codegraph/core/model.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Graph data model: node/edge kinds, confidence levels, stable ids."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
# Confidence of an edge, strongest first.
|
|
8
|
+
EXACT = "exact" # syntactically certain (static call, `new X`, $this->m(), literal key)
|
|
9
|
+
RESOLVED = "resolved" # needed type/name resolution (typed property/param, inferred var type, model->table)
|
|
10
|
+
HEURISTIC = "heuristic" # best effort (unique-method-name fallback, unique column-name literal)
|
|
11
|
+
CONFIDENCE_RANK = {EXACT: 3, RESOLVED: 2, HEURISTIC: 1}
|
|
12
|
+
|
|
13
|
+
# Edge kinds. propagates=True means "src depends on dst": reverse traversal from a
|
|
14
|
+
# target follows these edges to find everything that depends on it.
|
|
15
|
+
EDGE_KINDS: dict[str, tuple[bool, str]] = {
|
|
16
|
+
"CALLS": (True, "function/method calls function/method"),
|
|
17
|
+
"IMPLEMENTED_BY": (True, "interface/abstract method may dispatch to implementation"),
|
|
18
|
+
"OVERRIDDEN_BY": (True, "parent method may dispatch to child override"),
|
|
19
|
+
"BOUND_TO": (True, "container binding abstract -> concrete"),
|
|
20
|
+
"ROUTES_TO": (True, "HTTP route -> controller action"),
|
|
21
|
+
"USES_MIDDLEWARE": (True, "route -> middleware handle()"),
|
|
22
|
+
"HANDLED_BY": (True, "artisan command name -> handler method"),
|
|
23
|
+
"SCHEDULES": (True, "scheduler entry -> command/job"),
|
|
24
|
+
"DISPATCHES": (True, "code dispatches job/event"),
|
|
25
|
+
"LISTENED_BY": (True, "event -> listener handler"),
|
|
26
|
+
"READS_COLUMN": (True, "code reads column"),
|
|
27
|
+
"WRITES_COLUMN": (True, "code writes column"),
|
|
28
|
+
"MENTIONS_COLUMN": (True, "string literal equal to a distinctive column name"),
|
|
29
|
+
"READS_TABLE": (True, "code reads table"),
|
|
30
|
+
"WRITES_TABLE": (True, "code writes table"),
|
|
31
|
+
"USES_CONNECTION": (True, "code/model uses DB connection"),
|
|
32
|
+
"REGISTERS_CONNECTION": (True, "code registers a (dynamic) DB connection"),
|
|
33
|
+
"READS_CONFIG": (True, "code reads config key"),
|
|
34
|
+
"WRITES_CONFIG": (True, "code sets config key at runtime"),
|
|
35
|
+
"READS_ENV": (True, "code/config reads env key"),
|
|
36
|
+
"REFERS_TO": (True, "config value names a connection/other entity"),
|
|
37
|
+
"CONFIGURED_BY": (True, "connection or external system defined by a config / env key"),
|
|
38
|
+
"CONFIG_CONTAINS": (True, "config parent key contains child key"),
|
|
39
|
+
"MAPS_TO_TABLE": (False, "model class -> table"),
|
|
40
|
+
"HAS_RELATION": (False, "model -> related model (hasMany, belongsTo, ...)"),
|
|
41
|
+
"CONTAINS": (False, "class -> member, table -> column"),
|
|
42
|
+
"EXTENDS": (False, "class extends class"),
|
|
43
|
+
"IMPLEMENTS": (False, "class implements interface"),
|
|
44
|
+
"USES_TRAIT": (False, "class uses trait"),
|
|
45
|
+
"INSTANTIATES": (False, "code instantiates class (constructor call is a separate CALLS edge)"),
|
|
46
|
+
"INJECTS": (False, "constructor-injected dependency type"),
|
|
47
|
+
"REFERENCES": (False, "X::class reference"),
|
|
48
|
+
"OBSERVED_BY": (False, "model observer registration"),
|
|
49
|
+
"DEFINES": (False, "file/migration defines table/column"),
|
|
50
|
+
"BINDS": (False, "service provider registers binding"),
|
|
51
|
+
# TypeScript / Vue / Nuxt
|
|
52
|
+
"IMPORTS": (False, "module/component imports module/component"),
|
|
53
|
+
"RENDERS": (True, "component template renders child component"),
|
|
54
|
+
"USES_COMPOSABLE": (True, "code calls a composable (useX())"),
|
|
55
|
+
"USES_STORE": (True, "code calls a Pinia store (useXStore())"),
|
|
56
|
+
"HTTP_CALLS": (True, "client code calls an HTTP endpoint (method + path template)"),
|
|
57
|
+
"MATCHES_ROUTE": (True, "client endpoint matched to a backend route (cross-repo link)"),
|
|
58
|
+
"USES_LAYOUT": (False, "page uses layout"),
|
|
59
|
+
"USES_I18N": (False, "code/template uses i18n key"),
|
|
60
|
+
"REFERENCES_TYPE": (False, "code references a type/interface"),
|
|
61
|
+
# value facts
|
|
62
|
+
"READS_SETTING": (True, "code reads a JSON settings key (getSetting('a.b', default))"),
|
|
63
|
+
"WRITES_SETTING": (True, "code writes a JSON settings key (setSetting)"),
|
|
64
|
+
"READS_INPUT": (True, "code reads an HTTP request key ($request->input('k'), $validated['k'], $filters['k'] via arg flow)"),
|
|
65
|
+
"VALIDATES": (True, "FormRequest::rules() declares a request key"),
|
|
66
|
+
"VALIDATED_BY": (True, "controller action is validated by a FormRequest (rules())"),
|
|
67
|
+
"HAS_RESOLUTION": (True, "code resolves a value through a fallback chain (resolution node)"),
|
|
68
|
+
"FALLS_BACK_TO": (True, "resolution chain step -> source node (setting, request key, column, config, env); attrs.order"),
|
|
69
|
+
# native code (Rust, C, C++)
|
|
70
|
+
"USES_TYPE": (True, "code/type refers to a type (struct, enum, union, trait, class, typedef)"),
|
|
71
|
+
"ACCESSES_FIELD": (True, "code reads/writes a struct/class field or enum variant"),
|
|
72
|
+
"READS_PROP": (True, "code reads a stored property (field node: Swift `var x`, `@State`, `@Published`)"),
|
|
73
|
+
"WRITES_PROP": (True, "code writes a stored property (assignment, compound assignment)"),
|
|
74
|
+
"USES_VALUE": (True, "code refers to an enum case, a constant, a static/global variable or an object-like macro"),
|
|
75
|
+
"REFERENCES_FN": (True, "code/data takes a function as a value (callback, dispatch table, handler registration)"),
|
|
76
|
+
"USES_UNSAFE": (True, "code contains an unsafe block or is an unsafe fn (sink node unsafe:<crate>)"),
|
|
77
|
+
"GATED_BY": (True, "code compiled only under a condition: Cargo feature, cfg predicate or preprocessor macro"),
|
|
78
|
+
"INCLUDES": (True, "source/header file #includes a header"),
|
|
79
|
+
# Python / Django
|
|
80
|
+
"USES_SCHEMA": (False, "handler/view uses a wire schema (ninja/pydantic Schema, DRF serializer) for request/response; attrs.role"),
|
|
81
|
+
# Dart / Flutter
|
|
82
|
+
"EMITS_STATE": (False, "bloc/cubit code emits a state class"),
|
|
83
|
+
"HANDLES_STATE": (False, "UI code checks for a state class (is / switch pattern / BlocListener)"),
|
|
84
|
+
"NAVIGATES_TO": (False, "UI code navigates to a page/route (Navigator.push, context.go, named route)"),
|
|
85
|
+
"PARSES_JSON": (False, "code parses an HTTP response into a model (X.fromJson) or serialises a request body (toJson)"),
|
|
86
|
+
# realtime broadcasting (Laravel channels, Echo / pusher-js subscriptions)
|
|
87
|
+
"AUTHORIZES_CHANNEL": (True, "broadcasting auth route -> channel whose authorization callback it runs"),
|
|
88
|
+
"BROADCASTS_ON": (False, "broadcast event -> channel it publishes on (broadcastOn(); attrs.name = evaluated channel name)"),
|
|
89
|
+
"SUBSCRIBES_CHANNEL": (True, "client code subscribes to a channel (Echo.private / channel / join, pusher.subscribe, useEcho)"),
|
|
90
|
+
"MATCHES_CHANNEL": (True, "client channel subscription matched to a backend channel pattern (cross-repo link)"),
|
|
91
|
+
"LISTENS_FOR": (False, "client channel subscription listens for a backend broadcast event (.listen('Name'))"),
|
|
92
|
+
# protocol endpoints (#31 model, codegraph/protocols/; web / native bridges in codegraph/bridges.py):
|
|
93
|
+
# code -> endpoint:<protocol>:<name> -> handler
|
|
94
|
+
"SENDS_TO": (True, "code sends to a protocol endpoint (publish, emit, request, invoke, enqueue; bridge call: Capacitor "
|
|
95
|
+
"plugin / React Native module method, Flutter MethodChannel.invokeMethod); attrs.role, via"),
|
|
96
|
+
"RECEIVED_BY": (True, "protocol endpoint -> the handler receiving it (subscriber, event handler; native @PluginMethod / "
|
|
97
|
+
"@ReactMethod / channel handler); attrs.platform"),
|
|
98
|
+
"MATCHES_ENDPOINT": (True, "send-side endpoint -> receive-side endpoint of the same protocol whose name matches by the "
|
|
99
|
+
"protocol's rules (wildcards, {param} templates); attrs.pattern, segments"),
|
|
100
|
+
# external systems (#40, codegraph/external.py): external:<protocol>:<target>
|
|
101
|
+
"CONNECTS_TO": (True, "code or a logical connection -> external system it connects to (database, cache, broker, mail relay, "
|
|
102
|
+
"directory, file-transfer host, object store); attrs.op, via"),
|
|
103
|
+
"CREDENTIAL_FROM": (False, "external system -> env / config node holding its password / token (never the value); attrs.kind"),
|
|
104
|
+
# AI harnesses (#66): agents and the tools they expose (tools themselves are endpoint:llm_tool / mcp_tool nodes)
|
|
105
|
+
"OFFERS_TOOL": (True, "agent -> a tool endpoint it gives the model (Agents SDK Agent(tools=), create_react_agent)"),
|
|
106
|
+
"HANDS_OFF_TO": (True, "agent -> agent it can hand the conversation to (Agents SDK handoffs=[...])"),
|
|
107
|
+
# generated / copied files (codegraph/core/generated.py), only with --include-generated
|
|
108
|
+
"COPY_OF": (False, "copied file (Capacitor / Cordova web assets in a native project) -> the source file it is copied from"),
|
|
109
|
+
# test code (tests/, *.spec.ts, ...). Never propagating: tests do not change blast radius, caller counts or entry
|
|
110
|
+
# tagging; the `tests` query walks these on purpose
|
|
111
|
+
"TEST_CALLS": (False, "test code calls / dispatches to code (attrs.orig = the original edge kind)"),
|
|
112
|
+
"TEST_USES": (False, "test code touches a table, config key, class... (attrs.orig = the original edge kind)"),
|
|
113
|
+
"TEST_HTTP": (False, "test sends an HTTP request to a route ($this->getJson('/x'), Pest get(), Playwright request)"),
|
|
114
|
+
"TEST_VISITS": (False, "browser test opens a frontend page (Playwright / Cypress page.goto('/x'))"),
|
|
115
|
+
}
|
|
116
|
+
TEST_EDGE_KINDS = ("TEST_CALLS", "TEST_USES", "TEST_HTTP", "TEST_VISITS")
|
|
117
|
+
PROPAGATING = sorted(k for k, (p, _) in EDGE_KINDS.items() if p)
|
|
118
|
+
|
|
119
|
+
ENTRY_KINDS = ("http_route", "websocket", "artisan_command", "management_command", "scheduled", "queue_job", "listener",
|
|
120
|
+
"admin_panel", "observer", "ui_page", "ui_global",
|
|
121
|
+
"main", "ffi_export", "public_api", "test", "bench", "example", "build_script")
|
|
122
|
+
UI_ENTRY_KINDS = ("ui_page", "ui_global")
|
|
123
|
+
# websocket = Channels consumer routes; management_command = Django `manage.py <name>` (operator, like artisan)
|
|
124
|
+
RUNTIME_ENTRY_KINDS = ("http_route", "websocket", "scheduled", "queue_job", "listener", "main", "ffi_export")
|
|
125
|
+
OPERATOR_ENTRY_KINDS = ("artisan_command", "management_command", "admin_panel")
|
|
126
|
+
# native code: library API surface (pub items of a lib crate, exported C/C++ API) and dev/build-time entries
|
|
127
|
+
LIBRARY_ENTRY_KINDS = ("public_api",)
|
|
128
|
+
DEV_ENTRY_KINDS = ("test", "bench", "example", "build_script")
|
|
129
|
+
# TS server frameworks: microservice / WebSocket message handlers run at runtime; CLI commands (nest-commander) are
|
|
130
|
+
# operator-only, like artisan commands.
|
|
131
|
+
ENTRY_KINDS += ("message_handler", "cli_command")
|
|
132
|
+
RUNTIME_ENTRY_KINDS += ("message_handler",)
|
|
133
|
+
# realtime: a channel authorization callback (Laravel Broadcast::channel) runs on every private/presence subscription
|
|
134
|
+
ENTRY_KINDS += ("channel_auth",)
|
|
135
|
+
RUNTIME_ENTRY_KINDS += ("channel_auth",)
|
|
136
|
+
OPERATOR_ENTRY_KINDS += ("cli_command",)
|
|
137
|
+
# AI harnesses: a tool endpoint the model (agent runner) or an MCP client calls by name
|
|
138
|
+
ENTRY_KINDS += ("llm_tool",)
|
|
139
|
+
RUNTIME_ENTRY_KINDS += ("llm_tool",)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class Node:
|
|
144
|
+
id: str
|
|
145
|
+
kind: str
|
|
146
|
+
name: str
|
|
147
|
+
fqn: str | None = None
|
|
148
|
+
file: str | None = None
|
|
149
|
+
line: int | None = None
|
|
150
|
+
end_line: int | None = None
|
|
151
|
+
module: str | None = None
|
|
152
|
+
doc: str | None = None
|
|
153
|
+
lang: str | None = None
|
|
154
|
+
attrs: dict[str, Any] = field(default_factory=dict)
|
|
155
|
+
entry_kind: str | None = None # set when the node itself is an entry point
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class Edge:
|
|
160
|
+
src: str
|
|
161
|
+
dst: str
|
|
162
|
+
kind: str
|
|
163
|
+
file: str | None = None
|
|
164
|
+
line: int | None = None
|
|
165
|
+
confidence: str = EXACT
|
|
166
|
+
attrs: dict[str, Any] = field(default_factory=dict)
|
|
167
|
+
# gate scenario under which this reference is dead (e.g. "new_inventory"), None = live.
|
|
168
|
+
# Set deterministically by the guard evaluator (plugins/php/gating.py); evidence in attrs["guard"].
|
|
169
|
+
gate: str | None = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def node_id(kind: str, key: str) -> str:
|
|
173
|
+
"""Stable id = kind + ':' + canonical key (FQN, table.column, route signature...)."""
|
|
174
|
+
return f"{kind}:{key}"
|
codegraph/core/paths.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Shared skip rules for every directory walk: the preset skip lists plus the project's `.cg.yaml`.
|
|
2
|
+
|
|
3
|
+
exclude: ["legacy/**", "*.generated.ts", "/tools"] paths never indexed (gitignore-style globs)
|
|
4
|
+
skip_dirs: {add: [fixtures_big], keep: [static]} directory names skipped / not skipped anywhere
|
|
5
|
+
include: [src/generated, node_modules/@acme/sdk] directories indexed although a built-in skip
|
|
6
|
+
(a directory name, generated files) leaves them out
|
|
7
|
+
|
|
8
|
+
`rules(project, preset, *keys)` gives the PathRules of one walk; every language plugin, the extractors' configs and
|
|
9
|
+
the coverage scan use it, so one config entry applies to the whole index."""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
from .. import presets
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def glob_regex(pattern: str, below: bool = True) -> str:
|
|
19
|
+
"""gitignore-style glob -> regex over repo-relative POSIX paths (Python and JavaScript compatible).
|
|
20
|
+
`*` and `?` stay inside one path segment, `**` spans segments, a leading `/` anchors at the root, a pattern
|
|
21
|
+
without `/` matches at any depth, and a match on a directory covers everything below it (below=False: only the
|
|
22
|
+
path itself matches, as in .gitattributes)."""
|
|
23
|
+
p = pattern.strip().replace("\\", "/")
|
|
24
|
+
anchored = p.startswith("/") or "/" in p.rstrip("/")
|
|
25
|
+
p = p.strip("/")
|
|
26
|
+
out, i = [], 0
|
|
27
|
+
while i < len(p):
|
|
28
|
+
c = p[i]
|
|
29
|
+
if p.startswith("**/", i):
|
|
30
|
+
out.append("(?:.*/)?")
|
|
31
|
+
i += 3
|
|
32
|
+
elif p.startswith("**", i):
|
|
33
|
+
out.append(".*")
|
|
34
|
+
i += 2
|
|
35
|
+
elif c == "*":
|
|
36
|
+
out.append("[^/]*")
|
|
37
|
+
i += 1
|
|
38
|
+
elif c == "?":
|
|
39
|
+
out.append("[^/]")
|
|
40
|
+
i += 1
|
|
41
|
+
else:
|
|
42
|
+
out.append(re.escape(c) if c not in "/-_" else c)
|
|
43
|
+
i += 1
|
|
44
|
+
return ("^" if anchored else "^(?:.*/)?") + "".join(out) + ("(?:/.*)?$" if below else "$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def names_regex(names: Iterable[str]) -> str | None:
|
|
48
|
+
"""Directory names anywhere in a relative path, as one regex (for the TS extractor's skip config)."""
|
|
49
|
+
names = sorted(set(names))
|
|
50
|
+
return r"(^|/)(" + "|".join(re.escape(n) for n in names) + r")(/|$)" if names else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PathRules:
|
|
54
|
+
"""skip(rel_dir, name) for directories, excluded(rel_path) for files; rel paths are POSIX, relative to the root.
|
|
55
|
+
`generated`: the project's generated / copied / vendored file classifier (codegraph/core/generated.py); its files
|
|
56
|
+
are excluded too unless the index includes them (`--include-generated`)."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, names: Iterable[str] = (), exclude: Iterable[str] = (), paths: Iterable[str] = (), generated=None,
|
|
59
|
+
include: Iterable[str] = (), keep: Iterable[str] = ()):
|
|
60
|
+
self.names = frozenset(names)
|
|
61
|
+
self.include = tuple(sorted({p.strip("/") for p in include if p.strip("/")}))
|
|
62
|
+
self.keep = frozenset(keep) # names kept by .cg.yaml skip_dirs.keep (also hidden directories)
|
|
63
|
+
self._forced: dict[str, bool] = {}
|
|
64
|
+
self.exclude = tuple(exclude)
|
|
65
|
+
self.paths = frozenset(p.strip("/") for p in paths) # root-relative directories (PHP vendor/, storage/ ...)
|
|
66
|
+
self._rx = re.compile("|".join(f"(?:{glob_regex(g)})" for g in self.exclude)) if self.exclude else None
|
|
67
|
+
self.generated = generated if generated is not None and not generated.include else None
|
|
68
|
+
|
|
69
|
+
def included(self, rel: str) -> bool:
|
|
70
|
+
"""`rel` is an `include` directory or inside one."""
|
|
71
|
+
rel = rel.rstrip("/")
|
|
72
|
+
return any(rel == i or rel.startswith(i + "/") for i in self.include)
|
|
73
|
+
|
|
74
|
+
def on_include_path(self, rel: str) -> bool:
|
|
75
|
+
"""`rel` is an `include` directory, inside one, or a directory on the way to one."""
|
|
76
|
+
rel = rel.rstrip("/")
|
|
77
|
+
return self.included(rel) or any(i.startswith(rel + "/") for i in self.include)
|
|
78
|
+
|
|
79
|
+
def excluded(self, rel: str, generated: bool = True) -> bool:
|
|
80
|
+
"""`rel` is never indexed: a .cg.yaml exclude glob, or (generated=True) a generated / copied / vendored file
|
|
81
|
+
outside the `include` directories."""
|
|
82
|
+
if self._rx and self._rx.match(rel):
|
|
83
|
+
return True
|
|
84
|
+
if self.include and self.included(rel):
|
|
85
|
+
return False
|
|
86
|
+
return bool(generated and self.generated is not None and not rel.endswith("/") and self.generated.excludes(rel))
|
|
87
|
+
|
|
88
|
+
def _skip_own(self, rel_dir: str, name: str, dot: bool = False) -> bool:
|
|
89
|
+
if name in self.names or (dot and name.startswith(".") and name not in self.keep):
|
|
90
|
+
return True
|
|
91
|
+
rel = f"{rel_dir}/{name}" if rel_dir else name
|
|
92
|
+
return rel in self.paths or self.excluded(rel + "/") or (self.generated is not None and self.generated.dir_excluded(rel))
|
|
93
|
+
|
|
94
|
+
def _forced_dir(self, rel_dir: str, dot: bool) -> bool:
|
|
95
|
+
"""`rel_dir` (or a directory above it) is skipped by itself and only walked to reach an `include` directory."""
|
|
96
|
+
if not rel_dir or self.included(rel_dir):
|
|
97
|
+
return False
|
|
98
|
+
k = f"{int(dot)}{rel_dir}"
|
|
99
|
+
if k not in self._forced:
|
|
100
|
+
parent, _, name = rel_dir.rpartition("/")
|
|
101
|
+
self._forced[k] = self._skip_own(parent, name, dot) or self._forced_dir(parent, dot)
|
|
102
|
+
return self._forced[k]
|
|
103
|
+
|
|
104
|
+
def skip(self, rel_dir: str, name: str, dot: bool = False) -> bool:
|
|
105
|
+
if not self.include:
|
|
106
|
+
return self._skip_own(rel_dir, name, dot)
|
|
107
|
+
rel = f"{rel_dir}/{name}" if rel_dir else name
|
|
108
|
+
if self._rx and self._rx.match(rel + "/"):
|
|
109
|
+
return True
|
|
110
|
+
if self.on_include_path(rel):
|
|
111
|
+
return False
|
|
112
|
+
return self._forced_dir(rel_dir, dot) or self._skip_own(rel_dir, name, dot)
|
|
113
|
+
|
|
114
|
+
def prune(self, rel_dir: str, dns: list[str], dot: bool = False) -> list[str]:
|
|
115
|
+
"""Directory names of one os.walk step to descend into (sorted); dot=True also drops hidden directories
|
|
116
|
+
(except names in skip_dirs.keep)."""
|
|
117
|
+
return sorted(d for d in dns if not self.skip(rel_dir, d, dot))
|
|
118
|
+
|
|
119
|
+
def extractor_cfg(self, extra: Iterable[str] = ()) -> dict:
|
|
120
|
+
"""The skip rules for an extractor that walks on its own (TS, Dart): directory names (`extra`: more names of
|
|
121
|
+
that walk, minus skip_dirs.keep), the names kept (hidden directories included) and the include paths."""
|
|
122
|
+
return {"skip_names": sorted(self.names | (set(extra) - self.keep)), "keep_names": sorted(self.keep),
|
|
123
|
+
"include": list(self.include)}
|
|
124
|
+
|
|
125
|
+
def user_exclude_regex(self) -> str | None:
|
|
126
|
+
"""The .cg.yaml exclude globs as one JavaScript-compatible regex (they apply inside `include` too)."""
|
|
127
|
+
return "|".join(f"(?:{glob_regex(g)})" for g in self.exclude) or None
|
|
128
|
+
|
|
129
|
+
def generated_regex(self) -> str | None:
|
|
130
|
+
"""The directories the generated-file classifier excludes as a whole, as one regex."""
|
|
131
|
+
return "|".join(self.generated.dir_regexes()) if self.generated is not None else None
|
|
132
|
+
|
|
133
|
+
def exclude_regex(self) -> str | None:
|
|
134
|
+
"""The exclude globs (and the directories the generated-file classifier excludes as a whole) as one
|
|
135
|
+
JavaScript-compatible regex (None without either)."""
|
|
136
|
+
parts = [f"(?:{glob_regex(g)})" for g in self.exclude]
|
|
137
|
+
if self.generated is not None:
|
|
138
|
+
parts += self.generated.dir_regexes()
|
|
139
|
+
return "|".join(parts) or None
|
|
140
|
+
|
|
141
|
+
def excluded_files(self, exts: Iterable[str]) -> list[str]:
|
|
142
|
+
"""Classified generated / copied / vendored files with one of `exts` that the scan found (for an extractor
|
|
143
|
+
that walks on its own)."""
|
|
144
|
+
if self.generated is None:
|
|
145
|
+
return []
|
|
146
|
+
exts = tuple(exts)
|
|
147
|
+
build = self.generated.build_dirs
|
|
148
|
+
# declaration files and framework build directories (Nuxt's .nuxt/) stay readable: they are resolution input
|
|
149
|
+
return sorted(f for f in self.generated.files if f.lower().endswith(exts) and not f.endswith(".d.ts")
|
|
150
|
+
and not any(p in build for p in f.split("/")[:-1]) and not (self.include and self.included(f)))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def project_settings(project) -> dict:
|
|
154
|
+
cfg = (getattr(project, "options", None) or {}).get("config") or {}
|
|
155
|
+
sd = cfg.get("skip_dirs") or {}
|
|
156
|
+
return {"exclude": list(cfg.get("exclude") or []), "add": list(sd.get("add") or []), "keep": list(sd.get("keep") or []),
|
|
157
|
+
"include": list(cfg.get("include") or [])}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def rules(project, preset: str = "common", *keys: str, base: Iterable[str] | None = None, generated: bool = True) -> PathRules:
|
|
161
|
+
"""PathRules of one walk: preset skip dirs (common.skip_dirs + `keys` of `preset`, or `base`) adjusted by the
|
|
162
|
+
project's .cg.yaml (skip_dirs.add / keep, exclude), plus the project's generated-file classifier (generated=False:
|
|
163
|
+
without it, for the scan that runs the classifier)."""
|
|
164
|
+
s = project_settings(project) if project is not None else {"exclude": [], "add": [], "keep": [], "include": []}
|
|
165
|
+
names = set(base if base is not None else presets.skip_dirs(preset, *keys))
|
|
166
|
+
names = (names | set(s["add"])) - set(s["keep"])
|
|
167
|
+
paths = [p for p in (presets.values(preset, "skip_paths", default=[]) or []) if p not in s["keep"]] if base is None else []
|
|
168
|
+
gen = (getattr(project, "options", None) or {}).get("generated") if generated and project is not None else None
|
|
169
|
+
return PathRules(names, s["exclude"], paths, generated=gen, include=s["include"], keep=s["keep"])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def rel_dir(root: str, dp: str) -> str:
|
|
173
|
+
import os
|
|
174
|
+
r = os.path.relpath(dp, root)
|
|
175
|
+
return "" if r == "." else r.replace(os.sep, "/")
|
codegraph/core/plugin.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Plugin interfaces.
|
|
2
|
+
|
|
3
|
+
LanguagePlugin detect(project) -> bool; index(project, builder, frameworks) adds nodes/edges.
|
|
4
|
+
FrameworkPlugin sits on top of one language plugin (e.g. Laravel on PHP). It gets the
|
|
5
|
+
language plugin's analysis context (symbol tables + resolution hooks)
|
|
6
|
+
and contributes framework nodes/edges (routes, tables, commands...).
|
|
7
|
+
ScipPlugin a LanguagePlugin backed by any SCIP-producing indexer (scip-php,
|
|
8
|
+
scip-typescript, scip-clang, rust-analyzer scip, scip-go...). Imports
|
|
9
|
+
the SCIP index into the same graph (see plugins/scip/importer.py).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import gc
|
|
14
|
+
import json
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .model import Edge, Node, node_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Project:
|
|
26
|
+
root: Path
|
|
27
|
+
name: str
|
|
28
|
+
# filled by detection: {"languages": {...}, "frameworks": {...}}
|
|
29
|
+
detected: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
# user options, e.g. {"gates": [scenario, ...]} loaded from config/<project>.gates.json
|
|
31
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def exists(self, rel: str) -> bool:
|
|
34
|
+
return (self.root / rel).exists()
|
|
35
|
+
|
|
36
|
+
def read_json(self, rel: str) -> dict | None:
|
|
37
|
+
p = self.root / rel
|
|
38
|
+
if not p.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(p.read_text())
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GraphBuilder:
|
|
47
|
+
"""In-memory accumulator; de-duplicates nodes by id and edges by (src,dst,kind,line)."""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.nodes: dict[str, Node] = {}
|
|
51
|
+
self.edges: dict[tuple, Edge] = {}
|
|
52
|
+
# set by a language plugin while emitting edges for a reference that is dead under a gate
|
|
53
|
+
# scenario: {"gate": name, "guard": "file:line", "guard_call": "..."}
|
|
54
|
+
self.current_gate: dict | None = None
|
|
55
|
+
# platform conditions recorded by the language plugins (codegraph/platforms.py mark()) and Dart conditional
|
|
56
|
+
# imports / exports ({file, line, default, configs: [(name, value, file)]}); applied after all plugins ran
|
|
57
|
+
self.platform_marks: list[dict] = []
|
|
58
|
+
self.platform_imports: list[dict] = []
|
|
59
|
+
# Pigeon API definitions found by the Dart plugin ({Api: {kind: host | flutter, methods, file}}): bridges.py
|
|
60
|
+
# links their native implementations / native callers
|
|
61
|
+
self.pigeon_apis: dict = {}
|
|
62
|
+
|
|
63
|
+
def add_node(self, kind: str, key: str, name: str | None = None, **kw) -> str:
|
|
64
|
+
nid = node_id(kind, key)
|
|
65
|
+
n = self.nodes.get(nid)
|
|
66
|
+
if n is None:
|
|
67
|
+
self.nodes[nid] = Node(id=nid, kind=kind, name=name or key, **kw)
|
|
68
|
+
else:
|
|
69
|
+
for k, v in kw.items(): # fill blanks, merge attrs
|
|
70
|
+
if k == "attrs" and v:
|
|
71
|
+
n.attrs.update(v)
|
|
72
|
+
elif v is not None and getattr(n, k, None) in (None, ""):
|
|
73
|
+
setattr(n, k, v)
|
|
74
|
+
return nid
|
|
75
|
+
|
|
76
|
+
def has(self, nid: str) -> bool:
|
|
77
|
+
return nid in self.nodes
|
|
78
|
+
|
|
79
|
+
def add_edge(self, src: str, dst: str, kind: str, file=None, line=None, confidence="exact", **attrs) -> None:
|
|
80
|
+
if src == dst and kind == "CALLS":
|
|
81
|
+
return
|
|
82
|
+
g = self.current_gate
|
|
83
|
+
if g and g.get("src") != src:
|
|
84
|
+
g = None # only references made by the gated function itself carry the gate
|
|
85
|
+
key = (src, dst, kind, file, line, g["gate"] if g else None)
|
|
86
|
+
if key in self.edges:
|
|
87
|
+
return
|
|
88
|
+
if g:
|
|
89
|
+
attrs = {**attrs, "guard": g["guard"], "guard_expr": g.get("expr")}
|
|
90
|
+
self.edges[key] = Edge(src=src, dst=dst, kind=kind, file=file, line=line, confidence=confidence, attrs=attrs,
|
|
91
|
+
gate=g["gate"] if g else None)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def move_edges(self, moves: dict[str, list[tuple[int, int, str]]]) -> int:
|
|
95
|
+
"""Re-attribute edges to a finer-grained source: moves = {src: [(lo, hi, new_src), ...]}; an edge from src
|
|
96
|
+
whose line lies in [lo, hi] now starts at new_src (the innermost range wins). Used for closures that are
|
|
97
|
+
their own graph nodes (channel callbacks, Pest tests) while the PHP plugin emits their facts from the file."""
|
|
98
|
+
n = 0
|
|
99
|
+
for key, e in list(self.edges.items()):
|
|
100
|
+
rs = moves.get(e.src)
|
|
101
|
+
if not rs or e.line is None:
|
|
102
|
+
continue
|
|
103
|
+
hit = [r for r in rs if r[0] <= e.line <= r[1]]
|
|
104
|
+
if not hit:
|
|
105
|
+
continue
|
|
106
|
+
new_src = min(hit, key=lambda r: r[1] - r[0])[2]
|
|
107
|
+
del self.edges[key]
|
|
108
|
+
nk = (new_src,) + key[1:]
|
|
109
|
+
if nk not in self.edges and not (new_src == e.dst and e.kind == "CALLS"):
|
|
110
|
+
e.src = new_src
|
|
111
|
+
self.edges[nk] = e
|
|
112
|
+
n += 1
|
|
113
|
+
return n
|
|
114
|
+
|
|
115
|
+
def retype_edge(self, key: tuple, kind: str, **attrs) -> None:
|
|
116
|
+
e = self.edges.pop(key)
|
|
117
|
+
e.kind = kind
|
|
118
|
+
e.attrs = {**e.attrs, **attrs}
|
|
119
|
+
nk = (key[0], key[1], kind) + key[3:]
|
|
120
|
+
if nk not in self.edges:
|
|
121
|
+
self.edges[nk] = e
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@contextmanager
|
|
125
|
+
def gc_paused():
|
|
126
|
+
"""Pause the cyclic garbage collector for a pass over the whole graph that allocates many small, acyclic objects
|
|
127
|
+
(sets, dicts, deques): on a large graph the collections that allocation triggers cost far more than the pass."""
|
|
128
|
+
was = gc.isenabled()
|
|
129
|
+
gc.disable()
|
|
130
|
+
try:
|
|
131
|
+
yield
|
|
132
|
+
finally:
|
|
133
|
+
if was:
|
|
134
|
+
gc.enable()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LanguagePlugin(ABC):
|
|
138
|
+
name: str = "?"
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def detect(self, project: Project) -> bool: ...
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def index(self, project: Project, builder: GraphBuilder, frameworks: list["FrameworkPlugin"]) -> dict:
|
|
145
|
+
"""Add nodes/edges; return stats."""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class FrameworkPlugin(ABC):
|
|
149
|
+
name: str = "?"
|
|
150
|
+
language: str = "?"
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def detect(self, project: Project) -> bool: ...
|
|
154
|
+
|
|
155
|
+
def register_hooks(self, lang_ctx: Any) -> None:
|
|
156
|
+
"""Called before the language plugin resolves references (type rules, fact handlers)."""
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
def contribute(self, project: Project, builder: GraphBuilder, lang_ctx: Any) -> dict:
|
|
160
|
+
"""Called after language indexing; add framework nodes/edges; return stats."""
|
codegraph/core/store.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""SQLite graph store. One DB per project. Traversals use recursive CTEs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from .model import EDGE_KINDS, Edge, Node, CONFIDENCE_RANK
|
|
10
|
+
|
|
11
|
+
SCHEMA = """
|
|
12
|
+
PRAGMA journal_mode=WAL;
|
|
13
|
+
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS nodes(
|
|
15
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, name TEXT NOT NULL, fqn TEXT,
|
|
16
|
+
file TEXT, line INTEGER, end_line INTEGER, module TEXT, doc TEXT, lang TEXT,
|
|
17
|
+
entry_kind TEXT, attrs TEXT);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS edges(
|
|
19
|
+
id INTEGER PRIMARY KEY, src TEXT NOT NULL, dst TEXT NOT NULL, kind TEXT NOT NULL,
|
|
20
|
+
file TEXT, line INTEGER, confidence TEXT NOT NULL, conf_rank INTEGER NOT NULL, attrs TEXT, gate TEXT);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS edge_kinds(kind TEXT PRIMARY KEY, propagates INTEGER, description TEXT);
|
|
22
|
+
-- which entry-point kinds reach a node (forward closure from entry points)
|
|
23
|
+
CREATE TABLE IF NOT EXISTS node_entry(node_id TEXT, entry_kind TEXT, entry_count INTEGER, sample_entry TEXT,
|
|
24
|
+
PRIMARY KEY(node_id, entry_kind));
|
|
25
|
+
-- same closure, but only over edges that are live under a gate scenario (gate column NULL or != scenario)
|
|
26
|
+
CREATE TABLE IF NOT EXISTS node_entry_live(scenario TEXT, node_id TEXT, entry_kind TEXT, entry_count INTEGER, sample_entry TEXT,
|
|
27
|
+
PRIMARY KEY(scenario, node_id, entry_kind));
|
|
28
|
+
-- guard predicates found by the evaluator: method -> constant value under a scenario
|
|
29
|
+
CREATE TABLE IF NOT EXISTS gate_predicates(scenario TEXT, method TEXT, value TEXT, how TEXT, PRIMARY KEY(scenario, method));
|
|
30
|
+
CREATE INDEX IF NOT EXISTS ix_edges_src ON edges(src, kind);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS ix_edges_dst ON edges(dst, kind);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS ix_nodes_kind ON nodes(kind);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS ix_nodes_fqn ON nodes(fqn);
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GraphStore:
|
|
38
|
+
def __init__(self, path: str | Path):
|
|
39
|
+
self.path = str(path)
|
|
40
|
+
self.db = sqlite3.connect(self.path)
|
|
41
|
+
self.db.row_factory = sqlite3.Row
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def create(cls, path: str | Path) -> "GraphStore":
|
|
45
|
+
p = Path(path)
|
|
46
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
for suffix in ("", "-wal", "-shm"):
|
|
48
|
+
q = Path(str(p) + suffix)
|
|
49
|
+
if q.exists():
|
|
50
|
+
q.unlink()
|
|
51
|
+
s = cls(p)
|
|
52
|
+
s.db.executescript(SCHEMA)
|
|
53
|
+
s.db.executemany("INSERT INTO edge_kinds VALUES (?,?,?)",
|
|
54
|
+
[(k, int(v[0]), v[1]) for k, v in EDGE_KINDS.items()])
|
|
55
|
+
return s
|
|
56
|
+
|
|
57
|
+
def write(self, nodes: Iterable[Node], edges: Iterable[Edge]) -> None:
|
|
58
|
+
self.db.executemany(
|
|
59
|
+
"INSERT OR REPLACE INTO nodes VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
60
|
+
[(n.id, n.kind, n.name, n.fqn, n.file, n.line, n.end_line, n.module, n.doc, n.lang,
|
|
61
|
+
n.entry_kind, json.dumps(n.attrs, default=str) if n.attrs else None) for n in nodes])
|
|
62
|
+
self.db.executemany(
|
|
63
|
+
"INSERT INTO edges(src,dst,kind,file,line,confidence,conf_rank,attrs,gate) VALUES (?,?,?,?,?,?,?,?,?)",
|
|
64
|
+
[(e.src, e.dst, e.kind, e.file, e.line, e.confidence, CONFIDENCE_RANK[e.confidence],
|
|
65
|
+
json.dumps(e.attrs, default=str) if e.attrs else None, e.gate) for e in edges])
|
|
66
|
+
self.db.commit()
|
|
67
|
+
|
|
68
|
+
def set_meta(self, **kv) -> None:
|
|
69
|
+
self.db.executemany("INSERT OR REPLACE INTO meta VALUES (?,?)",
|
|
70
|
+
[(k, json.dumps(v, default=str)) for k, v in kv.items()])
|
|
71
|
+
self.db.commit()
|
|
72
|
+
|
|
73
|
+
def meta(self) -> dict:
|
|
74
|
+
return {r["key"]: json.loads(r["value"]) for r in self.db.execute("SELECT * FROM meta")}
|
|
75
|
+
|
|
76
|
+
def node(self, nid: str):
|
|
77
|
+
return self.db.execute("SELECT * FROM nodes WHERE id=?", (nid,)).fetchone()
|
|
78
|
+
|
|
79
|
+
def q(self, sql: str, params=()):
|
|
80
|
+
return self.db.execute(sql, params).fetchall()
|