cg-code-graph 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
codegraph/core/cache.py
ADDED
|
@@ -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"
|
codegraph/core/detect.py
ADDED
|
@@ -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:]}")
|