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,375 @@
1
+ """The per-user cache: where it lives, what is in it, and `cg clean`.
2
+
3
+ Root (one lookup for every user: extractors, SCIP outputs, parse caches, the Swift build directory):
4
+ $CODEGRAPH_CACHE, else $CODEGRAPH_CACHE_DIR (older name, still read), else %LOCALAPPDATA%\\codegraph on Windows, else
5
+ $XDG_CACHE_HOME/codegraph, else ~/.cache/codegraph.
6
+
7
+ Layout below the root (`v<N>` is fsutil.CACHE_VERSION, `<rkey>` the first 12 hex digits of sha256 of the resolved
8
+ project root):
9
+
10
+ extractors/<lang>-<lock hash>/ extractor dependency installs (npm, Composer, dart pub)
11
+ scip/<name>-v<N>-<rkey>-<key>.scip SCIP outputs of the exact layers (rust, cfamily, kotlin), and .lock files
12
+ scip/ra-config-<rkey>-<hash>.json rust-analyzer configs
13
+ scip/swift-v<N>-<build key>.lock lock of one Swift build directory
14
+ swift-build/<build key>/ `swift build --enable-index-store` output per project
15
+ ts/<rkey>-v<N>-<fingerprint>.json TypeScript extractor facts
16
+ dart/<rkey>-v<N>-<fingerprint>.json Dart extractor facts
17
+ projects/<rkey> the project root path behind <rkey> (so `cg clean <dir>` finds the
18
+ entries of every project indexed at or below <dir>)
19
+
20
+ An entry whose name does not follow this layout for the current cache version can never be read again: `--stale`
21
+ removes those (older cache versions, names from before this layout) and orphaned temporary and lock files.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import os
27
+ import re
28
+ import shutil
29
+ import time
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+
33
+ from . import fsutil
34
+
35
+ try:
36
+ import fcntl
37
+ except ImportError: # Windows
38
+ fcntl = None
39
+
40
+ KINDS = ("extractors", "scip", "rust-analyzer", "swift-build", "ts", "dart", "projects", "other")
41
+ PROJECT_KINDS = ("scip", "rust-analyzer", "swift-build", "ts", "dart", "projects")
42
+
43
+
44
+ class CacheError(Exception):
45
+ pass
46
+
47
+
48
+ def root() -> Path:
49
+ """The cache root (not created)."""
50
+ env = os.environ.get("CODEGRAPH_CACHE") or os.environ.get("CODEGRAPH_CACHE_DIR")
51
+ if env:
52
+ return Path(env).expanduser()
53
+ if os.name == "nt" and os.environ.get("LOCALAPPDATA"):
54
+ return Path(os.environ["LOCALAPPDATA"]) / "codegraph"
55
+ return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "codegraph"
56
+
57
+
58
+ def subdir(name: str, create: bool = False) -> Path:
59
+ d = root() / name
60
+ if create:
61
+ d.mkdir(parents=True, exist_ok=True)
62
+ return d
63
+
64
+
65
+ def root_key(project_root) -> str:
66
+ return hashlib.sha256(str(Path(project_root).resolve()).encode()).hexdigest()[:12]
67
+
68
+
69
+ def note_project(project_root) -> None:
70
+ """Remember which project root a <rkey> stands for (best effort; a read-only cache only loses `cg clean <dir>`
71
+ matching of sub-projects)."""
72
+ try:
73
+ p = str(Path(project_root).resolve())
74
+ f = subdir("projects", create=True) / root_key(p)
75
+ if not f.exists() or f.read_text(errors="replace") != p:
76
+ tmp = f.with_name(f"{f.name}.{os.getpid()}.tmp")
77
+ tmp.write_text(p)
78
+ os.replace(tmp, f)
79
+ except OSError:
80
+ pass
81
+
82
+
83
+ def swift_build_key(project_root) -> str:
84
+ """Name of a project's Swift build directory (the same key plugins/swift/exact.py builds into)."""
85
+ from ..plugins.native import runner
86
+ return runner.fingerprint(Path(project_root), [], "swift-build-dir")[:16]
87
+
88
+
89
+ def check_root(r: Path | None = None) -> Path:
90
+ """The cache root, refused when deleting below it could hit unrelated files: `/`, a drive root, $HOME or one of
91
+ its ancestors."""
92
+ r = Path(r or root())
93
+ try:
94
+ rr = r.resolve()
95
+ except OSError as e:
96
+ raise CacheError(f"cannot resolve the cache root {r}: {e}")
97
+ home = Path.home().resolve()
98
+ if rr == Path(rr.anchor) or rr == home or rr in home.parents:
99
+ raise CacheError(f"refusing to clean: the cache root resolves to {rr} (set CODEGRAPH_CACHE to a directory of "
100
+ "its own, e.g. ~/.cache/codegraph)")
101
+ return rr
102
+
103
+
104
+ # ------------------------------------------------------------------ classification
105
+ V = r"v(\d+)"
106
+ SCIP_RE = re.compile(rf"^(?P<name>[a-z]+)-{V}-(?P<rkey>[0-9a-f]{{12}})-(?P<key>[0-9a-z]+)\.(?:scip|lock)$")
107
+ SWIFT_LOCK_RE = re.compile(rf"^swift-{V}-(?P<key>[0-9a-f]{{16}})\.lock$")
108
+ RA_RE = re.compile(r"^ra-config-(?P<rkey>[0-9a-f]{12})-[0-9a-f]{12}\.json$")
109
+ FACTS_RE = re.compile(rf"^(?P<rkey>[0-9a-f]{{12}})-{V}-[0-9a-f]+\.json$")
110
+ TMP_RE = re.compile(r"\.tmp(\.scip)?$")
111
+
112
+
113
+ @dataclass
114
+ class Entry:
115
+ path: Path
116
+ kind: str
117
+ size: int
118
+ rkey: str | None = None # project the entry belongs to, when its name says so
119
+ swift_key: str | None = None
120
+ stale: str | None = None # why it can never be read again
121
+ lock: bool = False
122
+ tmp: bool = False
123
+
124
+
125
+ def _size(p: Path) -> int:
126
+ try:
127
+ if p.is_symlink() or not p.is_dir():
128
+ return p.lstat().st_size
129
+ n = 0
130
+ for dp, _dns, fns in os.walk(p):
131
+ for f in fns:
132
+ try:
133
+ n += os.lstat(os.path.join(dp, f)).st_size
134
+ except OSError:
135
+ pass
136
+ return n
137
+ except OSError:
138
+ return 0
139
+
140
+
141
+ def _tmp_age_limit() -> float:
142
+ return float(os.environ.get("CODEGRAPH_INDEXER_TIMEOUT", "3600")) + 600
143
+
144
+
145
+ def entries(r: Path | None = None) -> list[Entry]:
146
+ """Every top-level entry of the cache (files in scip/ ts/ dart/ projects/, directories in extractors/ and
147
+ swift-build/, anything else as `other`)."""
148
+ r = Path(r or root())
149
+ out: list[Entry] = []
150
+ if not r.is_dir():
151
+ return out
152
+ cur = fsutil.CACHE_VERSION
153
+ now = time.time()
154
+ for top in sorted(r.iterdir()):
155
+ name = top.name
156
+ if not top.is_dir() or top.is_symlink():
157
+ out.append(Entry(top, "other", _size(top)))
158
+ continue
159
+ for p in sorted(top.iterdir()):
160
+ e = Entry(p, name if name in KINDS else "other", 0)
161
+ n = p.name
162
+ if TMP_RE.search(n):
163
+ e.tmp = True
164
+ try:
165
+ if now - p.lstat().st_mtime > _tmp_age_limit():
166
+ e.stale = "orphaned temporary file"
167
+ except OSError:
168
+ pass
169
+ elif name == "scip":
170
+ m = SCIP_RE.match(n)
171
+ ms = SWIFT_LOCK_RE.match(n)
172
+ mr = RA_RE.match(n)
173
+ if mr:
174
+ e.kind, e.rkey = "rust-analyzer", mr.group("rkey")
175
+ elif n.startswith("ra-config-"):
176
+ e.kind, e.stale = "rust-analyzer", "name from before cache layout v2 (no project key)"
177
+ elif ms:
178
+ e.swift_key, e.lock = ms.group("key"), True
179
+ if int(ms.group(1)) != cur:
180
+ e.stale = f"cache version {ms.group(1)} (current {cur})"
181
+ elif m:
182
+ e.rkey, e.lock = m.group("rkey"), n.endswith(".lock")
183
+ if int(m.group(2)) != cur:
184
+ e.stale = f"cache version {m.group(2)} (current {cur})"
185
+ else:
186
+ e.lock = n.endswith(".lock")
187
+ e.stale = "name from an older cache layout"
188
+ elif name in ("ts", "dart"):
189
+ m = FACTS_RE.match(n)
190
+ if m:
191
+ e.rkey = m.group("rkey")
192
+ if int(m.group(2)) != cur:
193
+ e.stale = f"cache version {m.group(2)} (current {cur})"
194
+ elif re.match(r"^[0-9a-f]{12}-", n):
195
+ e.rkey, e.stale = n[:12], "name from an older cache layout (no cache version)"
196
+ elif name == "swift-build":
197
+ e.swift_key = n
198
+ try:
199
+ import json
200
+ v = json.loads((p / "codegraph-stamp.json").read_text()).get("cache_version")
201
+ except (OSError, ValueError, AttributeError):
202
+ v = None
203
+ if v is not None and v != cur:
204
+ e.stale = f"cache version {v} (current {cur})"
205
+ elif name == "projects":
206
+ e.rkey = n
207
+ e.size = _size(p)
208
+ out.append(e)
209
+ return out
210
+
211
+
212
+ def _held(p: Path) -> bool:
213
+ """A lock file some process holds right now."""
214
+ if fcntl is None:
215
+ return False
216
+ try:
217
+ fd = os.open(p, os.O_RDWR)
218
+ except OSError:
219
+ return False
220
+ try:
221
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
222
+ fcntl.flock(fd, fcntl.LOCK_UN)
223
+ return False
224
+ except OSError:
225
+ return True
226
+ finally:
227
+ os.close(fd)
228
+
229
+
230
+ def _projects(r: Path) -> dict[str, str]:
231
+ out = {}
232
+ d = r / "projects"
233
+ if d.is_dir():
234
+ for f in d.iterdir():
235
+ if re.fullmatch(r"[0-9a-f]{12}", f.name):
236
+ try:
237
+ out[f.name] = f.read_text(errors="replace").strip()
238
+ except OSError:
239
+ pass
240
+ return out
241
+
242
+
243
+ def _under(path: str, top: Path) -> bool:
244
+ try:
245
+ Path(path).relative_to(top)
246
+ return True
247
+ except ValueError:
248
+ return False
249
+
250
+
251
+ # ------------------------------------------------------------------ cleaning
252
+ @dataclass
253
+ class Plan:
254
+ root: Path
255
+ remove: list[Entry] = field(default_factory=list)
256
+ skipped: list[tuple[Path, str]] = field(default_factory=list)
257
+ db_files: list[Path] = field(default_factory=list)
258
+ projects: list[str] = field(default_factory=list) # project roots matched by `cg clean <dir>`
259
+
260
+ @property
261
+ def bytes(self) -> int:
262
+ return sum(e.size for e in self.remove) + sum(_size(p) for p in self.db_files)
263
+
264
+
265
+ def plan(project: str | os.PathLike | None = None, all_: bool = False, extractors: bool = False,
266
+ stale: bool = False, db: str | os.PathLike | None = None) -> Plan:
267
+ if not (project or all_ or stale or db):
268
+ raise CacheError("nothing to clean: pass a project root, --all, --stale or --db")
269
+ if extractors and not all_:
270
+ raise CacheError("--extractors goes with --all")
271
+ if not (project or all_ or stale): # only --db: the cache is not touched
272
+ pl = Plan(root())
273
+ pl.db_files = _db_files(Path(db))
274
+ return pl
275
+ r = check_root()
276
+ pl = Plan(r)
277
+ es = entries(r)
278
+ pick: dict[Path, Entry] = {}
279
+ if all_:
280
+ for e in es:
281
+ if e.kind == "extractors" and not extractors:
282
+ continue
283
+ pick[e.path] = e
284
+ if project:
285
+ top = Path(project).expanduser().resolve()
286
+ known = _projects(r)
287
+ keys = {root_key(top)} | {k for k, p in known.items() if _under(p, top)}
288
+ pl.projects = sorted(known[k] for k in keys if k in known)
289
+ skeys = {swift_build_key(p) for p in {*pl.projects, str(top)}}
290
+ for e in es:
291
+ if (e.rkey and e.rkey in keys) or (e.swift_key and e.swift_key in skeys):
292
+ pick[e.path] = e
293
+ if stale:
294
+ for e in es:
295
+ if e.stale:
296
+ pick[e.path] = e
297
+ elif e.lock and e.kind == "scip" and not _lock_has_output(e, es):
298
+ e.stale = "lock without a cache entry"
299
+ pick[e.path] = e
300
+ for p, e in sorted(pick.items()):
301
+ if e.lock and _held(p):
302
+ pl.skipped.append((p, "lock held by a running cg"))
303
+ continue
304
+ pl.remove.append(e)
305
+ if db:
306
+ pl.db_files = _db_files(Path(db))
307
+ return pl
308
+
309
+
310
+ def _lock_has_output(e: Entry, es: list[Entry]) -> bool:
311
+ if e.swift_key:
312
+ return any(x.kind == "swift-build" and x.swift_key == e.swift_key for x in es)
313
+ return e.path.with_suffix(".scip").exists()
314
+
315
+
316
+ def _db_files(db: Path) -> list[Path]:
317
+ db = db.expanduser()
318
+ if not db.exists():
319
+ raise CacheError(f"--db {db}: no such file")
320
+ if db.is_dir() or db.is_symlink():
321
+ raise CacheError(f"--db {db}: not a regular file")
322
+ with open(db, "rb") as f:
323
+ head = f.read(16)
324
+ if head != b"SQLite format 3\0" and db.stat().st_size:
325
+ raise CacheError(f"--db {db}: not an SQLite database; nothing deleted")
326
+ return [p for p in (db, Path(f"{db}-wal"), Path(f"{db}-shm"), Path(f"{db}-journal")) if p.exists()]
327
+
328
+
329
+ def execute(pl: Plan) -> list[tuple[Path, str]]:
330
+ """Delete what the plan lists; returns the paths that could not be removed. Every cache path is re-checked to sit
331
+ inside the cache root; symlinks are unlinked, never followed."""
332
+ errors = []
333
+ for e in pl.remove:
334
+ p = e.path
335
+ try:
336
+ parent = p.parent.resolve()
337
+ if parent != pl.root and pl.root not in parent.parents:
338
+ errors.append((p, "outside the cache root"))
339
+ continue
340
+ if p.is_symlink() or not p.is_dir():
341
+ p.unlink(missing_ok=True)
342
+ else:
343
+ shutil.rmtree(p)
344
+ except OSError as ex:
345
+ errors.append((p, str(ex)))
346
+ for p in pl.db_files:
347
+ try:
348
+ p.unlink(missing_ok=True)
349
+ except OSError as ex:
350
+ errors.append((p, str(ex)))
351
+ return errors
352
+
353
+
354
+ def usage(r: Path | None = None) -> dict:
355
+ """Total cache size and the size per kind (for `cg doctor`)."""
356
+ r = Path(r or root())
357
+ by: dict[str, dict] = {}
358
+ stale = 0
359
+ for e in entries(r):
360
+ k = by.setdefault(e.kind, {"bytes": 0, "entries": 0})
361
+ k["bytes"] += e.size
362
+ k["entries"] += 1
363
+ if e.stale:
364
+ stale += e.size
365
+ return {"root": str(r), "bytes": sum(v["bytes"] for v in by.values()), "stale_bytes": stale,
366
+ "kinds": {k: by[k] for k in KINDS if k in by}}
367
+
368
+
369
+ def human(n: int) -> str:
370
+ x = float(n)
371
+ for u in ("B", "KB", "MB", "GB"):
372
+ if x < 1024 or u == "GB":
373
+ return f"{x:.0f} {u}" if u == "B" else f"{x:.1f} {u}"
374
+ x /= 1024
375
+ return f"{n} B"
@@ -0,0 +1,80 @@
1
+ """Language/framework detection from project files (no code execution)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+
8
+ MARKERS = {
9
+ # language: files/globs that indicate it
10
+ "php": ["composer.json", "artisan"],
11
+ "typescript": ["tsconfig.json"],
12
+ "javascript": ["package.json"],
13
+ "go": ["go.mod"],
14
+ "rust": ["Cargo.toml"],
15
+ "c_cpp": ["CMakeLists.txt", "compile_commands.json", "meson.build", "Makefile"],
16
+ "python": ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "manage.py", "Pipfile"],
17
+ "dart": ["pubspec.yaml"],
18
+ "java": ["pom.xml", "build.gradle", "build.gradle.kts"],
19
+ "kotlin": ["settings.gradle.kts", "build.gradle.kts"],
20
+ "swift": ["Package.swift"],
21
+ }
22
+
23
+
24
+ def _pkg_deps(root: Path) -> dict:
25
+ p = root / "package.json"
26
+ if not p.exists():
27
+ return {}
28
+ try:
29
+ d = json.loads(p.read_text())
30
+ except Exception:
31
+ return {}
32
+ return {**(d.get("dependencies") or {}), **(d.get("devDependencies") or {})}
33
+
34
+
35
+ def detect(root: Path) -> dict:
36
+ root = Path(root)
37
+ langs = {lang: [m for m in marks if (root / m).exists()] for lang, marks in MARKERS.items()}
38
+ langs = {k: v for k, v in langs.items() if v}
39
+ fw = {}
40
+ cj = {}
41
+ if (root / "composer.json").exists():
42
+ try:
43
+ cj = json.loads((root / "composer.json").read_text())
44
+ except Exception:
45
+ cj = {}
46
+ req = {**(cj.get("require") or {}), **(cj.get("require-dev") or {})}
47
+ if "laravel/framework" in req or (root / "artisan").exists():
48
+ fw["laravel"] = {"version": req.get("laravel/framework")}
49
+ if "filament/filament" in req:
50
+ fw["filament"] = {"version": req.get("filament/filament")}
51
+ deps = _pkg_deps(root)
52
+ if "nuxt" in deps or any((root / n).exists() for n in ("nuxt.config.ts", "nuxt.config.js")):
53
+ fw["nuxt"] = {"version": deps.get("nuxt")}
54
+ if "vue" in deps:
55
+ fw["vue"] = {"version": deps.get("vue")}
56
+ from ..plugins.tsweb.common import SERVER_DEPS # TS / JS servers (NestJS, Next.js, Express / Koa / Fastify / Hono ...)
57
+ for name, pkgs in SERVER_DEPS.items():
58
+ hit = [d for d in pkgs if d in deps]
59
+ if hit:
60
+ key = {"next": "nextjs"}.get(name, name) # framework plugin names
61
+ fw[key] = {"version": deps.get(hit[0])} if name != "express" else {"via": hit, "version": deps.get(hit[0])}
62
+ py_req = ""
63
+ for f in ("requirements.txt", "pyproject.toml", "setup.cfg", "Pipfile", "requirements/base.txt"):
64
+ if (root / f).exists():
65
+ py_req += (root / f).read_text(errors="replace").lower()
66
+ if "django" in py_req or (root / "manage.py").exists():
67
+ fw["django"] = {}
68
+ for extra in ("django-ninja", "djangorestframework", "channels", "celery"):
69
+ if extra in py_req:
70
+ fw[extra] = {}
71
+ if (root / "pubspec.yaml").exists():
72
+ ps = (root / "pubspec.yaml").read_text(errors="replace")
73
+ if "flutter:" in ps:
74
+ fw["flutter"] = {}
75
+ for extra in ("flutter_bloc", "dio", "go_router", "riverpod", "provider", "json_serializable", "freezed", "retrofit", "chopper"):
76
+ if re.search(rf"^\s+{extra}:", ps, re.M):
77
+ fw[extra] = {}
78
+ if "typescript" in deps:
79
+ langs.setdefault("typescript", ["package.json:typescript"])
80
+ return {"languages": langs, "frameworks": fw}
@@ -0,0 +1,187 @@
1
+ """Where the Node / PHP / Dart extractors run from, and their dependency installs.
2
+
3
+ The extractor sources ship inside the package (codegraph/plugins/{ts,php,dart}/extractor). Their dependencies
4
+ (npm `node_modules`, Composer `vendor/`, `dart pub get` + the compiled Dart extractor) are not part of the package:
5
+
6
+ - a checkout whose extractor directory already has them (`npm ci` run there) keeps using that directory;
7
+ - otherwise the sources are copied into a per-user cache directory and the dependencies installed there, on first
8
+ use or by `cg setup`. The directory is keyed by the lock file, so an update that only changes the extractor source
9
+ reuses the installed dependencies, and one that changes the lock file installs into a fresh directory.
10
+
11
+ Cache root (core/cache.py): $CODEGRAPH_CACHE, else $CODEGRAPH_CACHE_DIR, else %LOCALAPPDATA%\\codegraph on Windows,
12
+ else $XDG_CACHE_HOME/codegraph or ~/.cache/codegraph; the extractors live in <root>/extractors.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import contextlib
17
+ import hashlib
18
+ import os
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import tempfile
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ from . import cache
27
+
28
+ try:
29
+ import fcntl
30
+ except ImportError: # Windows: msvcrt byte-range locks instead
31
+ fcntl = None
32
+
33
+ PLUGINS = Path(__file__).resolve().parent.parent / "plugins"
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Spec:
38
+ lang: str
39
+ pkg: Path # extractor directory inside the package
40
+ sources: tuple[str, ...] # files copied into the cache directory
41
+ lock: str # lock file: keys the cache directory
42
+ markers: tuple[str, ...] # any of these present: dependencies installed
43
+ tool: str # program the install needs
44
+ label: str
45
+
46
+
47
+ SPECS = {
48
+ "typescript": Spec("typescript", PLUGINS / "ts" / "extractor", ("extract.mjs", "fw.mjs", "package.json", "package-lock.json"),
49
+ "package-lock.json", ("node_modules/typescript/package.json",), "npm", "TypeScript / JavaScript"),
50
+ "php": Spec("php", PLUGINS / "php" / "extractor", ("extract.php", "composer.json", "composer.lock"), "composer.lock",
51
+ ("vendor/autoload.php",), "composer", "PHP"),
52
+ "dart": Spec("dart", PLUGINS / "dart" / "extractor", ("bin/extract.dart", "pubspec.yaml", "pubspec.lock"), "pubspec.lock",
53
+ (".dart_tool/package_config.json", ".bin/extract"), "dart", "Dart"),
54
+ }
55
+
56
+
57
+ def cache_root() -> Path:
58
+ return cache.root() / "extractors"
59
+
60
+
61
+ def _installed(d: Path, spec: Spec) -> bool:
62
+ return any((d / m).exists() for m in spec.markers)
63
+
64
+
65
+ def cache_dir(lang: str) -> Path:
66
+ spec = SPECS[lang]
67
+ lock = spec.pkg / spec.lock
68
+ key = hashlib.sha256(lock.read_bytes() if lock.exists() else b"").hexdigest()[:12]
69
+ return cache_root() / f"{lang}-{key}"
70
+
71
+
72
+ def workdir(lang: str) -> Path:
73
+ """The directory the extractor runs from: the package's own one when its dependencies are installed there (a
74
+ development checkout), else the cache directory (sources synced into it; dependencies maybe not yet installed)."""
75
+ spec = SPECS[lang]
76
+ if _installed(spec.pkg, spec):
77
+ return spec.pkg
78
+ d = cache_dir(lang)
79
+ _sync(spec, d)
80
+ return d
81
+
82
+
83
+ def _sync(spec: Spec, d: Path) -> None:
84
+ for rel in spec.sources:
85
+ src, dst = spec.pkg / rel, d / rel
86
+ if not src.exists():
87
+ continue
88
+ data = src.read_bytes()
89
+ if dst.exists() and dst.read_bytes() == data:
90
+ continue
91
+ dst.parent.mkdir(parents=True, exist_ok=True)
92
+ fd, tmp = tempfile.mkstemp(dir=dst.parent, prefix=".sync-")
93
+ with os.fdopen(fd, "wb") as fh:
94
+ fh.write(data)
95
+ os.replace(tmp, dst) # atomic: a concurrent index sees the old or the new file
96
+
97
+
98
+ def status(lang: str) -> dict:
99
+ spec = SPECS[lang]
100
+ if _installed(spec.pkg, spec):
101
+ return {"installed": True, "dir": str(spec.pkg), "where": "package"}
102
+ d = cache_dir(lang)
103
+ return {"installed": _installed(d, spec), "dir": str(d), "where": "cache"}
104
+
105
+
106
+ def install_command(lang: str, dart: str | None = None) -> list[str]:
107
+ if lang == "typescript":
108
+ npm = shutil.which("npm") or "npm"
109
+ return [npm, "ci", "--no-audit", "--no-fund", "--loglevel=error"]
110
+ if lang == "php":
111
+ return [shutil.which("composer") or "composer", "install", "--no-dev", "--no-interaction", "--no-progress", "--quiet"]
112
+ return [dart or shutil.which("dart") or "dart", "pub", "get"]
113
+
114
+
115
+ @contextlib.contextmanager
116
+ def install_lock(d: Path):
117
+ """Hold `<d>/.install.lock` while installing into d: two first-time installs into the same directory at once
118
+ (two `cg index` runs, or `cg setup` next to one) wait for each other instead of running npm / composer / dart pub
119
+ in one directory twice (#65)."""
120
+ d.mkdir(parents=True, exist_ok=True)
121
+ fd = os.open(d / ".install.lock", os.O_RDWR | os.O_CREAT, 0o644)
122
+ try:
123
+ if fcntl is not None:
124
+ fcntl.flock(fd, fcntl.LOCK_EX)
125
+ else:
126
+ try:
127
+ import msvcrt
128
+ msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
129
+ except (ImportError, OSError):
130
+ pass
131
+ yield
132
+ finally:
133
+ try:
134
+ if fcntl is not None:
135
+ fcntl.flock(fd, fcntl.LOCK_UN)
136
+ finally:
137
+ os.close(fd)
138
+
139
+
140
+ def ensure(lang: str, dart: str | None = None) -> Path:
141
+ """Install the extractor's dependencies if missing; returns the directory to run it from."""
142
+ spec = SPECS[lang]
143
+ d = workdir(lang)
144
+ if lang == "dart":
145
+ done = lambda: (d / ".dart_tool" / "package_config.json").exists()
146
+ else:
147
+ done = lambda: _installed(d, spec)
148
+ if done():
149
+ return d
150
+ with install_lock(d):
151
+ if not done(): # another process may have finished the install while this one waited
152
+ _run(install_command(lang, dart), d, spec)
153
+ return d
154
+
155
+
156
+ CACHE_DIR_RE = re.compile(r"^(?P<lang>[a-z]+)-[0-9a-f]{12}$")
157
+
158
+
159
+ def prune(dry_run: bool = False) -> list[tuple[Path, int]]:
160
+ """`cg setup --prune`: remove extractor installs (`<language>-<lock hash>` directories) this version of cg does
161
+ not use: left behind by updates that changed a lock file. Another installed cg version that still uses one
162
+ reinstalls it on its next index. A directory whose install lock is held is kept."""
163
+ root = cache_root()
164
+ keep = {cache_dir(lang).name for lang in SPECS}
165
+ out = []
166
+ if not root.is_dir():
167
+ return out
168
+ for p in sorted(root.iterdir()):
169
+ m = CACHE_DIR_RE.match(p.name)
170
+ if not p.is_dir() or p.is_symlink() or not m or m.group("lang") not in SPECS or p.name in keep:
171
+ continue
172
+ if cache._held(p / ".install.lock"):
173
+ continue
174
+ size = cache._size(p)
175
+ if not dry_run:
176
+ shutil.rmtree(p, ignore_errors=True)
177
+ out.append((p, size))
178
+ return out
179
+
180
+
181
+ def _run(cmd: list[str], d: Path, spec: Spec) -> None:
182
+ if not shutil.which(cmd[0]) and not Path(cmd[0]).exists():
183
+ raise RuntimeError(f"{spec.label} extractor dependencies missing and `{spec.tool}` is not installed")
184
+ r = subprocess.run(cmd, cwd=d, capture_output=True, text=True)
185
+ if r.returncode != 0:
186
+ raise RuntimeError(f"{spec.label} extractor dependency install failed ({' '.join(cmd[:2])} in {d}): "
187
+ f"{(r.stderr or r.stdout)[-1500:]}")