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,415 @@
1
+ """Swift exact layer: compiler-resolved call edges from the Swift index store.
2
+
3
+ The index store is what `swift build --enable-index-store` (SwiftPM) or Xcode (DerivedData `Index.noindex/DataStore`)
4
+ writes; it is read through the toolchain's libIndexStore (indexstore.py). Index-store definitions are matched to the
5
+ syntax layer's declarations by file, line and name, so node ids stay those of the heuristic layer; the heuristic
6
+ CALLS / INSTANTIATES edges of the covered files are replaced by the compiler's call occurrences (the caller comes from
7
+ the `calledBy` / `containedBy` relation).
8
+
9
+ Where the store comes from, in order:
10
+ CODEGRAPH_SWIFT_INDEX_STORE=/path/to/store an existing store (CI, Xcode DerivedData, a previous build)
11
+ CODEGRAPH_SWIFT_INDEX=1 run `swift build --enable-index-store` for a SwiftPM package (the build
12
+ runs the package manifest and plugins, hence opt-in), with the build
13
+ directory under the codegraph cache and a source fingerprint so an
14
+ unchanged package is not rebuilt
15
+ Without one of those (or without a toolchain / libIndexStore) the heuristic layer stays and coverage says why.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import glob
20
+ import json
21
+ import os
22
+ import re
23
+ import shutil
24
+ import subprocess
25
+ import time
26
+ from collections import defaultdict
27
+ from pathlib import Path
28
+
29
+ from ...core.model import EXACT, HEURISTIC
30
+ from ...core import cache, fsutil
31
+ from ..native import runner
32
+ from . import indexstore as ix
33
+
34
+ # index-store names of accessor definitions (`getter:label`, `didSet:items`): owned by their property
35
+ ACCESSORS = ("getter:", "setter:", "_modify:", "modify:", "read:", "_read:", "willSet:", "didSet:")
36
+ ACCESSOR_ATTR = {"getter": "get", "setter": "set", "willSet": "willSet", "didSet": "didSet", "modify": "modify",
37
+ "_modify": "modify"}
38
+
39
+ CALL_KINDS = ("CALLS", "INSTANTIATES")
40
+
41
+
42
+ def find_swift() -> str | None:
43
+ env = os.environ.get("CODEGRAPH_SWIFT")
44
+ if env:
45
+ return env if (Path(env).exists() or shutil.which(env)) else None
46
+ w = shutil.which("swift")
47
+ if w:
48
+ return w
49
+ for pat in (str(Path.home() / "tools" / "swift-*" / "usr" / "bin" / "swift"), "/usr/share/swift/usr/bin/swift",
50
+ "/opt/swift/usr/bin/swift"):
51
+ hits = sorted(glob.glob(pat))
52
+ if hits:
53
+ return hits[-1]
54
+ return None
55
+
56
+
57
+ def _store_dirs(build: Path) -> list[Path]:
58
+ return [Path(p) for p in sorted(glob.glob(str(build / "*" / "debug" / "index" / "store")))
59
+ + sorted(glob.glob(str(build / "debug" / "index" / "store")))]
60
+
61
+
62
+ def find_store(project, files: list[str]) -> tuple[Path | None, dict]:
63
+ """(index store directory or None, info); info["status"] explains a missing store."""
64
+ pre = os.environ.get("CODEGRAPH_SWIFT_INDEX_STORE")
65
+ swift = find_swift()
66
+ lib = ix.find_lib(swift)
67
+ if pre:
68
+ if not Path(pre).is_dir():
69
+ return None, {"status": f"CODEGRAPH_SWIFT_INDEX_STORE={pre} is not a directory"}
70
+ if not lib:
71
+ return None, {"status": "libIndexStore not found (a Swift toolchain or CODEGRAPH_LIBINDEXSTORE is needed "
72
+ "to read the index store)"}
73
+ return Path(pre), {"source": "CODEGRAPH_SWIFT_INDEX_STORE", "path": pre, "lib": lib}
74
+ root = Path(project.root)
75
+ if not (root / "Package.swift").is_file():
76
+ return None, {"status": "no Package.swift at the project root (Xcode projects: set CODEGRAPH_SWIFT_INDEX_STORE "
77
+ "to DerivedData/<app>/Index.noindex/DataStore)"}
78
+ if not swift:
79
+ return None, {"status": "no Swift toolchain (swift not on PATH; CODEGRAPH_SWIFT)"}
80
+ if not lib:
81
+ return None, {"status": f"libIndexStore not found next to {swift} (CODEGRAPH_LIBINDEXSTORE)"}
82
+ if os.environ.get("CODEGRAPH_SWIFT_INDEX") != "1":
83
+ return None, {"status": "Swift toolchain found but the package was not built: `swift build` runs the package "
84
+ "manifest and plugins; set CODEGRAPH_SWIFT_INDEX=1, or CODEGRAPH_SWIFT_INDEX_STORE to an "
85
+ "existing store (Xcode DerivedData)",
86
+ "toolchain": swift}
87
+ return _build(root, files, swift, lib)
88
+
89
+
90
+ def _build(root: Path, files: list[str], swift: str, lib: str) -> tuple[Path | None, dict]:
91
+ """`swift build --enable-index-store` into a per-project build directory under the cache; skipped when the sources
92
+ and manifests are unchanged since the last successful build."""
93
+ srcs = list(files) + [f for f in ("Package.swift", "Package.resolved") if (root / f).exists()]
94
+ srcs += [p.name for p in root.glob("Package@swift-*.swift")]
95
+ key = runner.fingerprint(root, srcs, f"swift-index|{runner.tool_version(swift, ('--version',))}")
96
+ build = cache.subdir("swift-build") / cache.swift_build_key(root)
97
+ stamp = build / "codegraph-stamp.json"
98
+ info = {"source": "swift build", "toolchain": swift, "lib": lib, "build_path": str(build)}
99
+ timeout = int(os.environ.get("CODEGRAPH_INDEXER_TIMEOUT", "3600"))
100
+ use_cache = os.environ.get("CODEGRAPH_NO_CACHE") != "1"
101
+ with runner.key_lock("swift", build.name, timeout) as waited: # one build per build directory at a time
102
+ if waited:
103
+ info["lock_wait_seconds"] = waited
104
+ try:
105
+ done = json.loads(stamp.read_text()) if stamp.exists() else {}
106
+ except (OSError, ValueError):
107
+ done = {}
108
+ stores = _store_dirs(build)
109
+ if use_cache and done.get("key") == key and stores:
110
+ info["cache"] = "hit"
111
+ return stores[0], info
112
+ info["cache"] = "miss"
113
+ build.mkdir(parents=True, exist_ok=True)
114
+ cache.note_project(root)
115
+ cmd = [swift, "build", "--enable-index-store", "--build-path", str(build)]
116
+ info["command"] = " ".join(cmd[1:])
117
+ t0 = time.time()
118
+ try:
119
+ r = subprocess.run(cmd, cwd=root, capture_output=True, text=True, timeout=timeout)
120
+ except subprocess.TimeoutExpired:
121
+ info["error"] = f"timeout after {timeout}s"
122
+ info["status"] = f"swift build failed ({info['error']})"
123
+ return None, info
124
+ except OSError as e:
125
+ info["error"] = str(e)
126
+ info["status"] = f"swift build failed ({e})"
127
+ return None, info
128
+ info["seconds"] = round(time.time() - t0, 2)
129
+ out = (r.stdout or "") + "\n" + (r.stderr or "")
130
+ info["stderr_tail"] = [x for x in out.strip().splitlines()[-8:]]
131
+ stores = _store_dirs(build)
132
+ if r.returncode != 0:
133
+ errs = [x.strip() for x in out.splitlines() if re.search(r"\berror:", x)]
134
+ info["error"] = f"exit {r.returncode}"
135
+ info["notes"] = list(dict.fromkeys(errs))[:6]
136
+ first = re.sub(r"^/\S*/", "", errs[0].replace(str(root) + os.sep, ""))[:160] if errs else ""
137
+ if not stores:
138
+ info["status"] = "swift build failed (" + info["error"] + (f": {first}" if first else "") + ")"
139
+ return None, info
140
+ # a partial build still indexed the modules that compiled
141
+ info["partial"] = True
142
+ info["build_error"] = first
143
+ else:
144
+ stamp.write_text(json.dumps({"key": key, "cache_version": fsutil.CACHE_VERSION}))
145
+ return (stores[0] if stores else None), info
146
+
147
+
148
+ class ExactLayer:
149
+ def __init__(self, plugin):
150
+ self.p = plugin
151
+ self.b = plugin.b
152
+
153
+ def _all_decls(self):
154
+ return list(self.p.decls.values())
155
+
156
+ def apply(self, store: Path, lib: str, root: Path, files: list[str], st: dict) -> bool:
157
+ occ_by_path = ix.load(store, None, lib)
158
+ rels = self._map_paths(occ_by_path, root, files)
159
+ st["index_files"] = len(rels)
160
+ if not rels:
161
+ st["index_units_outside_project"] = len(occ_by_path)
162
+ return False
163
+ by_line = defaultdict(list)
164
+ for d in self._all_decls():
165
+ by_line[(d.file, d.line)].append(d)
166
+ usr_node: dict[str, str] = {}
167
+ ctor_type: dict[str, str] = {}
168
+ matched = unmatched = 0
169
+ accessor_of: dict[str, str] = {}
170
+ accessor_kind: dict[str, str] = {} # accessor usr -> "getter" | "setter" | "didSet" | ...
171
+ prop_def: dict[str, str] = {}
172
+ prop_how: dict[str, str] = {} # property usr -> "computed" | "observed" | "lazy" (property nodes)
173
+ parent_of: dict[str, str] = {} # definition usr -> enclosing type / extension usr (childOf)
174
+ ext_of: dict[str, str] = {} # extension usr -> extended type usr
175
+ for rel, occs in rels.items():
176
+ for o in occs:
177
+ if not o.roles & ix.DEFINITION:
178
+ for rr, ru, _rn, _rk in o.rels:
179
+ if rr & ix.REL_EXTENDEDBY and o.kind in ix.TYPES: # `extension DispatchQueue { }`
180
+ ext_of[ru] = o.usr
181
+ if o.usr not in usr_node:
182
+ nid = self._decl(rel, o.line, o.name, ("class",), by_line) or \
183
+ (f"class:{o.name}" if f"class:{o.name}" in self.b.nodes else None)
184
+ if nid:
185
+ usr_node[o.usr] = nid
186
+ continue
187
+ for rr, ru, _rn, _rk in o.rels:
188
+ if rr & ix.REL_CHILDOF:
189
+ parent_of.setdefault(o.usr, ru)
190
+ if o.kind in ("instancemethod", "function", "staticmethod", "classmethod") and \
191
+ o.name.startswith(ACCESSORS):
192
+ for rr, ru, _rn, _rk in o.rels:
193
+ if rr & ix.REL_ACCESSOROF:
194
+ accessor_of[o.usr] = ru
195
+ accessor_kind[o.usr] = o.name.split(":", 1)[0]
196
+ continue
197
+ if o.kind in ("instanceproperty", "staticproperty", "classproperty", "variable"):
198
+ if o.name == "body":
199
+ prop_def[o.usr] = self._decl(rel, o.line, "body", ("method",), by_line) or ""
200
+ else:
201
+ d = self._prop_decl(rel, o.line, o.name, by_line)
202
+ if d is not None: # a computed / observed / lazy property node (#72)
203
+ prop_def[o.usr] = d.id
204
+ prop_how[o.usr] = next(m for m in ("computed", "observed", "lazy") if m in d.modifiers)
205
+ continue
206
+ if o.kind not in ix.CALLABLE and o.kind not in ix.TYPES:
207
+ continue
208
+ base = "init" if o.kind == "constructor" else o.name.split("(")[0]
209
+ kinds = ("class",) if o.kind in ix.TYPES else ("function", "method")
210
+ nid = self._decl(rel, o.line, base, kinds, by_line)
211
+ if nid is None and o.kind != "constructor":
212
+ nid = self._decl_by_name(rel, base, kinds, o)
213
+ if o.kind == "constructor":
214
+ parent = next((ru for rr, ru, _rn, rk in o.rels if rr & ix.REL_CHILDOF), None)
215
+ if parent:
216
+ ctor_type[o.usr] = parent
217
+ if nid is None:
218
+ if o.kind != "constructor":
219
+ unmatched += 1
220
+ continue
221
+ usr_node[o.usr] = nid
222
+ matched += 1
223
+ for acc, prop in accessor_of.items(): # SwiftUI `body` getter -> method:<View>.body
224
+ if prop_def.get(prop):
225
+ usr_node[acc] = prop_def[prop]
226
+ st["index_defs_matched"] = matched
227
+ st["index_defs_unmatched"] = unmatched
228
+ def owner(usr: str, depth: int = 0) -> str | None:
229
+ """Node of a caller symbol: itself, the `body` property of an accessor, else its enclosing type
230
+ (accessors -> property -> type; extension -> extended type)."""
231
+ if not usr or depth > 6:
232
+ return None
233
+ if usr in usr_node:
234
+ return usr_node[usr]
235
+ prop = accessor_of.get(usr)
236
+ if prop:
237
+ if prop_def.get(prop):
238
+ return prop_def[prop]
239
+ return owner(prop, depth + 1)
240
+ if usr in ext_of:
241
+ return owner(ext_of[usr], depth + 1)
242
+ return owner(parent_of.get(usr, ""), depth + 1)
243
+
244
+ covered = set(rels)
245
+ lambdas = defaultdict(list) # Vapor route handlers / BGTask closures own the calls inside them
246
+ for nid, n in self.b.nodes.items():
247
+ if n.lang == "swift" and n.kind == "function" and n.attrs.get("lambda") and n.file in covered and n.line:
248
+ lambdas[n.file].append((n.line, n.end_line or n.line, nid))
249
+ seen_lines = {(rel, o.line) for rel, occs in rels.items() for o in occs}
250
+ heur, kept = self._remove_heuristic(covered, seen_lines)
251
+ st["heuristic_kept_not_compiled"] = kept
252
+ exact = set()
253
+ refs = ext = 0
254
+ for rel, occs in rels.items():
255
+ fid = f"file:swift:{rel}"
256
+ for o in occs:
257
+ if o.roles & ix.DEFINITION or not (o.roles & ix.CALL or o.kind == "constructor"):
258
+ continue
259
+ if o.kind not in ix.CALLABLE:
260
+ continue
261
+ prop = accessor_of.get(o.usr) if o.name.startswith(ACCESSORS) else None
262
+ if o.name.startswith(ACCESSORS) and not (prop in prop_how and self._runs(accessor_kind.get(o.usr, ""),
263
+ prop_how[prop])):
264
+ continue
265
+ if prop is not None:
266
+ # a read (getter) or write (setter / modify) of a property node: a call of it (#72)
267
+ targets = [(prop_def[prop], "CALLS")]
268
+ elif o.kind == "constructor":
269
+ tt = ctor_type.get(o.usr, "")
270
+ # an initializer declared in an extension: its parent is the extension, whose node is the type's
271
+ tnode = usr_node.get(tt) or usr_node.get(ext_of.get(tt, ""))
272
+ if tnode is None:
273
+ ext += 1
274
+ continue
275
+ targets = [(tnode, "INSTANTIATES")]
276
+ if o.usr in usr_node:
277
+ targets.append((usr_node[o.usr], "CALLS"))
278
+ else:
279
+ t = usr_node.get(o.usr)
280
+ if t is None:
281
+ ext += 1
282
+ continue
283
+ targets = [(t, "CALLS")]
284
+ src = None
285
+ acc = {}
286
+ for rr, ru, _rn, _rk in o.rels:
287
+ if rr & (ix.REL_CALLEDBY | ix.REL_CONTAINEDBY):
288
+ src = owner(ru)
289
+ if src is not None:
290
+ acc = self._accessor_attr(src, accessor_kind.get(ru))
291
+ break
292
+ if src is None:
293
+ src = self._owner_type(rel, o.line) or fid
294
+ if src.startswith("class:"):
295
+ # the initializer of a `lazy var` belongs to the type in the store; here to the property node
296
+ pd = next((d for d in self.p.props_in.get(rel, ()) if d.line <= o.line <= d.end
297
+ and f"class:{d.cls}" == src), None)
298
+ src = pd.id if pd is not None else src
299
+ lam = min((x for x in lambdas.get(rel, ()) if x[0] <= o.line <= x[1]),
300
+ key=lambda x: x[1] - x[0], default=None)
301
+ if lam is not None:
302
+ src = lam[2]
303
+ for dst, kind in targets:
304
+ if (src == dst and kind == "CALLS") or src not in self.b.nodes or dst not in self.b.nodes:
305
+ continue
306
+ if prop is not None:
307
+ acc = {**acc, "property": "read" if accessor_kind.get(o.usr) == "getter" else "write"}
308
+ br = self.p.branch_at.get((rel, o.line, self.b.nodes[dst].name), {}) \
309
+ if kind == "INSTANTIATES" else {}
310
+ self.b.add_edge(src, dst, kind, rel, o.line, EXACT, source="indexstore",
311
+ **({"dynamic": True} if o.roles & ix.DYNAMIC else {}), **acc, **br)
312
+ exact.add((src, dst, kind))
313
+ refs += 1
314
+ st["index_references"] = refs
315
+ st["index_refs_external"] = ext
316
+ agree = len(heur & exact)
317
+ st["exact_vs_heuristic"] = {
318
+ "heuristic_edges": len(heur), "exact_edges": len(exact), "agree": agree,
319
+ "precision": round(agree / len(heur), 3) if heur else None,
320
+ "recall": round(agree / len(exact), 3) if exact else None,
321
+ # candidate edges (receiver type unknown, one per same-name method, #83) are compared on their own
322
+ "candidate_edges": len(self.candidates), "candidate_agree": len(self.candidates & exact)}
323
+ return True
324
+
325
+ def _map_paths(self, occ_by_path: dict, root: Path, files: list[str]) -> dict:
326
+ """Index-store source paths -> project-relative paths: under the project root, else by the longest
327
+ project path suffix (a store built in another checkout or on another machine)."""
328
+ rootr = str(Path(root).resolve()) + os.sep
329
+ fset = set(files)
330
+ by_name = defaultdict(list)
331
+ for f in files:
332
+ by_name[f.rsplit("/", 1)[-1]].append(f)
333
+ out = {}
334
+ for p, occs in occ_by_path.items():
335
+ rel = None
336
+ if p.startswith(rootr) and p[len(rootr):] in fset:
337
+ rel = p[len(rootr):]
338
+ else:
339
+ cands = [f for f in by_name.get(p.rsplit("/", 1)[-1], []) if p.endswith("/" + f)]
340
+ rel = max(cands, key=len) if cands else None
341
+ if rel is not None:
342
+ out[rel] = occs
343
+ return out
344
+
345
+ @staticmethod
346
+ def _runs(accessor: str, how: str) -> bool:
347
+ """Does a use of this accessor run code of a property node? A read of a computed or lazy property, a write of
348
+ a computed or observed one (the getter of a stored property with observers is the plain storage)."""
349
+ if accessor in ("getter", "read", "_read"):
350
+ return how in ("computed", "lazy")
351
+ if accessor in ("setter", "modify", "_modify"):
352
+ return how in ("computed", "observed")
353
+ return False
354
+
355
+ def _accessor_attr(self, src: str, accessor: str | None) -> dict:
356
+ """{"accessor": "didSet"} for a call inside an explicit accessor of a property node (as the heuristic pass)."""
357
+ name = ACCESSOR_ATTR.get(accessor or "")
358
+ n = self.b.nodes.get(src)
359
+ return {"accessor": name} if name and n is not None and name in (n.attrs.get("accessors") or ()) else {}
360
+
361
+ def _prop_decl(self, rel, line, name, by_line):
362
+ """The property node of a property definition: by its line, else (attributes on the lines above the name)
363
+ the one of that name whose range holds the line."""
364
+ for d in by_line.get((rel, line), ()):
365
+ if d.name == name and "property" in d.modifiers:
366
+ return d
367
+ for d in self.p.props_in.get(rel, ()):
368
+ if d.name == name and d.line <= line <= d.end:
369
+ return d
370
+ return None
371
+
372
+ def _decl(self, rel, line, name, kinds, by_line):
373
+ for d in by_line.get((rel, line), ()):
374
+ if d.name == name and d.kind in kinds:
375
+ return d.id
376
+ return None
377
+
378
+ def _decl_by_name(self, rel, name, kinds, o):
379
+ """An overload (one node per name) or a declaration whose name line differs from the node's line."""
380
+ parent = next((rn for rr, _ru, rn, _rk in o.rels if rr & ix.REL_CHILDOF), None)
381
+ for d in self._all_decls():
382
+ if d.file == rel and d.name == name and d.kind in kinds and "property" not in d.modifiers and \
383
+ (parent is None or (d.cls or "").split(".")[-1] == parent or d.kind == "class"):
384
+ if d.line <= o.line <= d.end or d.kind != "class":
385
+ return d.id
386
+ return None
387
+
388
+ def _owner_type(self, rel, line):
389
+ best = None
390
+ for d in self._all_decls():
391
+ if d.file == rel and d.kind == "class" and d.line <= line <= d.end:
392
+ if best is None or d.end - d.line < best.end - best.line:
393
+ best = d
394
+ return best.id if best else None
395
+
396
+ def _remove_heuristic(self, covered: set, seen_lines: set) -> tuple[set, int]:
397
+ """Drop the heuristic call edges of the covered files; returns their (src, dst, kind) set. An edge on a line
398
+ without any index occurrence is code the compiler did not see (an inactive `#if` branch on this platform):
399
+ it is kept, marked `via: not-compiled`, and left out of the comparison. Candidate edges (#83) go to
400
+ self.candidates, compared on their own."""
401
+ out, cand, kept = set(), set(), 0
402
+ for key, e in list(self.b.edges.items()):
403
+ if e.confidence != HEURISTIC or e.file not in covered or e.kind not in CALL_KINDS:
404
+ continue
405
+ n = self.b.nodes.get(e.src)
406
+ if n is None or n.lang != "swift":
407
+ continue
408
+ if (e.file, e.line) not in seen_lines:
409
+ e.attrs["via"] = "not-compiled"
410
+ kept += 1
411
+ continue
412
+ (cand if e.attrs.get("binding") == "candidate" else out).add((e.src, e.dst, e.kind))
413
+ del self.b.edges[key]
414
+ self.candidates = cand
415
+ return out, kept
@@ -0,0 +1,209 @@
1
+ """Read a Swift compiler index store (`swift build --enable-index-store`, Xcode DerivedData `Index.noindex/DataStore`)
2
+ through the toolchain's libIndexStore C API (ctypes, the function-pointer `_apply_f` variants).
3
+
4
+ load(store, roots) -> {source path: [Occurrence]} for the units whose main file lies under one of `roots`.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import ctypes
9
+ import os
10
+ import shutil
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+
14
+ DECLARATION, DEFINITION, REFERENCE, READ, WRITE, CALL, DYNAMIC, ADDRESSOF, IMPLICIT = (1 << i for i in range(9))
15
+ REL_CHILDOF, REL_BASEOF, REL_OVERRIDEOF, REL_RECEIVEDBY, REL_CALLEDBY, REL_EXTENDEDBY, REL_ACCESSOROF, \
16
+ REL_CONTAINEDBY = (1 << i for i in range(9, 17))
17
+
18
+ KIND = {1: "module", 5: "enum", 6: "struct", 7: "class", 8: "protocol", 9: "extension", 11: "typealias",
19
+ 12: "function", 13: "variable", 14: "field", 15: "enumconstant", 16: "instancemethod", 17: "classmethod",
20
+ 18: "staticmethod", 19: "instanceproperty", 20: "classproperty", 21: "staticproperty", 22: "constructor",
21
+ 23: "destructor", 25: "parameter"}
22
+ CALLABLE = {"function", "instancemethod", "classmethod", "staticmethod", "constructor"}
23
+ TYPES = {"enum", "struct", "class", "protocol"}
24
+
25
+
26
+ class StrRef(ctypes.Structure):
27
+ _fields_ = [("data", ctypes.c_char_p), ("length", ctypes.c_size_t)]
28
+
29
+
30
+ def _s(r: StrRef) -> str:
31
+ if not r.data or not r.length:
32
+ return ""
33
+ return ctypes.string_at(r.data, r.length).decode("utf-8", "replace")
34
+
35
+
36
+ @dataclass
37
+ class Occurrence:
38
+ line: int
39
+ col: int
40
+ roles: int
41
+ usr: str
42
+ name: str
43
+ kind: str
44
+ subkind: int
45
+ rels: list = field(default_factory=list) # (roles, usr, name, kind)
46
+
47
+
48
+ def find_lib(swift: str | None = None) -> str | None:
49
+ """libIndexStore.so: CODEGRAPH_LIBINDEXSTORE, else next to the `swift` toolchain (usr/lib)."""
50
+ env = os.environ.get("CODEGRAPH_LIBINDEXSTORE")
51
+ if env:
52
+ return env if Path(env).exists() else None
53
+ sw = swift or shutil.which("swift")
54
+ if sw:
55
+ base = Path(sw).resolve().parent.parent
56
+ for name in ("libIndexStore.so", "libIndexStore.dylib"):
57
+ for d in (base / "lib", base / "lib" / "swift" / "linux"):
58
+ if (d / name).exists():
59
+ return str(d / name)
60
+ # macOS: /usr/bin/swift is a shim; the library lives in the selected Xcode's default toolchain
61
+ for dev in (os.environ.get("DEVELOPER_DIR"), "/Applications/Xcode.app/Contents/Developer"):
62
+ if dev:
63
+ p = Path(dev) / "Toolchains" / "XcodeDefault.xctoolchain" / "usr" / "lib" / "libIndexStore.dylib"
64
+ if p.exists():
65
+ return str(p)
66
+ return None
67
+
68
+
69
+ _LIB = {}
70
+
71
+
72
+ def _lib(path: str):
73
+ if path in _LIB:
74
+ return _LIB[path]
75
+ L = ctypes.CDLL(path)
76
+ vp, u64, cb = ctypes.c_void_p, ctypes.c_uint64, ctypes.CFUNCTYPE
77
+ L.indexstore_store_create.restype = vp
78
+ L.indexstore_store_create.argtypes = [ctypes.c_char_p, ctypes.POINTER(vp)]
79
+ L.indexstore_error_get_description.restype = ctypes.c_char_p
80
+ L.indexstore_error_get_description.argtypes = [vp]
81
+ L.indexstore_store_dispose.argtypes = [vp]
82
+ L.UNIT_CB = cb(ctypes.c_bool, vp, StrRef)
83
+ L.indexstore_store_units_apply_f.argtypes = [vp, ctypes.c_uint, vp, L.UNIT_CB]
84
+ L.indexstore_unit_reader_create.restype = vp
85
+ L.indexstore_unit_reader_create.argtypes = [vp, ctypes.c_char_p, ctypes.POINTER(vp)]
86
+ L.indexstore_unit_reader_dispose.argtypes = [vp]
87
+ for f in ("main_file", "module_name", "working_dir"):
88
+ fn = getattr(L, f"indexstore_unit_reader_get_{f}")
89
+ fn.restype, fn.argtypes = StrRef, [vp]
90
+ L.indexstore_unit_reader_is_system_unit.restype = ctypes.c_bool
91
+ L.indexstore_unit_reader_is_system_unit.argtypes = [vp]
92
+ L.DEP_CB = cb(ctypes.c_bool, vp, vp)
93
+ L.indexstore_unit_reader_dependencies_apply_f.argtypes = [vp, vp, L.DEP_CB]
94
+ L.indexstore_unit_dependency_get_kind.restype = ctypes.c_int
95
+ L.indexstore_unit_dependency_get_kind.argtypes = [vp]
96
+ for f in ("filepath", "name"):
97
+ fn = getattr(L, f"indexstore_unit_dependency_get_{f}")
98
+ fn.restype, fn.argtypes = StrRef, [vp]
99
+ L.indexstore_record_reader_create.restype = vp
100
+ L.indexstore_record_reader_create.argtypes = [vp, ctypes.c_char_p, ctypes.POINTER(vp)]
101
+ L.indexstore_record_reader_dispose.argtypes = [vp]
102
+ L.OCC_CB = cb(ctypes.c_bool, vp, vp)
103
+ L.indexstore_record_reader_occurrences_apply_f.argtypes = [vp, vp, L.OCC_CB]
104
+ L.indexstore_occurrence_get_symbol.restype = vp
105
+ L.indexstore_occurrence_get_symbol.argtypes = [vp]
106
+ L.indexstore_occurrence_get_roles.restype = u64
107
+ L.indexstore_occurrence_get_roles.argtypes = [vp]
108
+ L.indexstore_occurrence_get_line_col.argtypes = [vp, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint)]
109
+ L.REL_CB = cb(ctypes.c_bool, vp, vp)
110
+ L.indexstore_occurrence_relations_apply_f.argtypes = [vp, vp, L.REL_CB]
111
+ L.indexstore_symbol_relation_get_roles.restype = u64
112
+ L.indexstore_symbol_relation_get_roles.argtypes = [vp]
113
+ L.indexstore_symbol_relation_get_symbol.restype = vp
114
+ L.indexstore_symbol_relation_get_symbol.argtypes = [vp]
115
+ for f in ("name", "usr"):
116
+ fn = getattr(L, f"indexstore_symbol_get_{f}")
117
+ fn.restype, fn.argtypes = StrRef, [vp]
118
+ for f in ("kind", "subkind", "language"):
119
+ fn = getattr(L, f"indexstore_symbol_get_{f}")
120
+ fn.restype, fn.argtypes = ctypes.c_int, [vp]
121
+ _LIB[path] = L
122
+ return L
123
+
124
+
125
+ class IndexStoreError(RuntimeError):
126
+ pass
127
+
128
+
129
+ def load(store: str | Path, roots: list[str | Path] | None, lib_path: str) -> dict[str, list[Occurrence]]:
130
+ """Occurrences per absolute source path, for non-system units whose main file is under `roots`."""
131
+ L = _lib(lib_path)
132
+ err = ctypes.c_void_p()
133
+ st = L.indexstore_store_create(str(store).encode(), ctypes.byref(err))
134
+ if not st:
135
+ msg = L.indexstore_error_get_description(err) if err else b"?"
136
+ raise IndexStoreError(f"cannot open index store {store}: {msg.decode(errors='replace')}")
137
+ roots_s = [str(Path(r).resolve()) + os.sep for r in roots] if roots else None
138
+ units: list[str] = []
139
+
140
+ @L.UNIT_CB
141
+ def on_unit(_ctx, name):
142
+ units.append(_s(name))
143
+ return True
144
+
145
+ L.indexstore_store_units_apply_f(st, 0, None, on_unit)
146
+ records: dict[str, str] = {} # source path -> record name (the newest unit wins)
147
+ for u in units:
148
+ ur = L.indexstore_unit_reader_create(st, u.encode(), ctypes.byref(err))
149
+ if not ur:
150
+ continue
151
+ try:
152
+ if L.indexstore_unit_reader_is_system_unit(ur):
153
+ continue
154
+ main = _s(L.indexstore_unit_reader_get_main_file(ur))
155
+ if not main or (roots_s is not None and not any(main.startswith(r) for r in roots_s)):
156
+ continue
157
+
158
+ @L.DEP_CB
159
+ def on_dep(_ctx, dep):
160
+ if L.indexstore_unit_dependency_get_kind(dep) == 2: # record
161
+ fp = _s(L.indexstore_unit_dependency_get_filepath(dep))
162
+ if fp == main:
163
+ records[fp] = _s(L.indexstore_unit_dependency_get_name(dep))
164
+ return True
165
+
166
+ L.indexstore_unit_reader_dependencies_apply_f(ur, None, on_dep)
167
+ finally:
168
+ L.indexstore_unit_reader_dispose(ur)
169
+ out: dict[str, list[Occurrence]] = {}
170
+ sym_cache: dict[int, tuple] = {}
171
+
172
+ def sym(p):
173
+ r = sym_cache.get(p)
174
+ if r is None:
175
+ r = (_s(L.indexstore_symbol_get_usr(p)), _s(L.indexstore_symbol_get_name(p)),
176
+ KIND.get(L.indexstore_symbol_get_kind(p), "other"), L.indexstore_symbol_get_subkind(p))
177
+ sym_cache[p] = r
178
+ return r
179
+
180
+ for path, rec in records.items():
181
+ rr = L.indexstore_record_reader_create(st, rec.encode(), ctypes.byref(err))
182
+ if not rr:
183
+ continue
184
+ occs: list[Occurrence] = []
185
+ sym_cache.clear()
186
+ try:
187
+ @L.OCC_CB
188
+ def on_occ(_ctx, o):
189
+ line, col = ctypes.c_uint(), ctypes.c_uint()
190
+ L.indexstore_occurrence_get_line_col(o, ctypes.byref(line), ctypes.byref(col))
191
+ usr, name, kind, sub = sym(L.indexstore_occurrence_get_symbol(o))
192
+ oc = Occurrence(line.value, col.value, L.indexstore_occurrence_get_roles(o), usr, name, kind, sub)
193
+
194
+ @L.REL_CB
195
+ def on_rel(_c, rel):
196
+ rs = sym(L.indexstore_symbol_relation_get_symbol(rel))
197
+ oc.rels.append((L.indexstore_symbol_relation_get_roles(rel), rs[0], rs[1], rs[2]))
198
+ return True
199
+
200
+ L.indexstore_occurrence_relations_apply_f(o, None, on_rel)
201
+ occs.append(oc)
202
+ return True
203
+
204
+ L.indexstore_record_reader_occurrences_apply_f(rr, None, on_occ)
205
+ finally:
206
+ L.indexstore_record_reader_dispose(rr)
207
+ out[path] = occs
208
+ L.indexstore_store_dispose(st)
209
+ return out