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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -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}"
@@ -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, "/")
@@ -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."""
@@ -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()