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
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
"""C / C++ language plugin.
|
|
2
|
+
|
|
3
|
+
Exact mode: scip-clang over a compile database (compile_commands.json). Every reference is resolved by clang with
|
|
4
|
+
the project's real flags; definitions are matched to syntactic items by (file, line, name), and scip-clang's
|
|
5
|
+
relationships give virtual-dispatch edges (base method -> override) and class inheritance.
|
|
6
|
+
Heuristic mode (no compile database, or scip-clang not installed): name-based resolution over the tree-sitter
|
|
7
|
+
layer; every resolved reference is labelled `heuristic`.
|
|
8
|
+
|
|
9
|
+
The compile database is looked up at $CODEGRAPH_COMPDB, <root>/compile_commands.json, <root>/build*/,
|
|
10
|
+
<root>/out/, <root>/cmake-build-*/. Generate one with `cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` or
|
|
11
|
+
`bear -- make`. See docs/native.md.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
import time
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ... import presets
|
|
24
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
25
|
+
from ...core.fsutil import content_key, keep_file
|
|
26
|
+
from ...core.paths import rules as path_rules
|
|
27
|
+
from ...core.plugin import FrameworkPlugin, GraphBuilder, LanguagePlugin, Project
|
|
28
|
+
from ..native import gates as G
|
|
29
|
+
from ..native import runner, scipread
|
|
30
|
+
from ..native.ts import TreeSitterMissing
|
|
31
|
+
from .syntax import CFile, CItem, annotation_macros, annotation_regex, extract, generated_names
|
|
32
|
+
|
|
33
|
+
C_EXT = {".c"}
|
|
34
|
+
CPP_EXT = {".cc", ".cpp", ".cxx", ".c++", ".cp", ".C"}
|
|
35
|
+
HDR_EXT = {".h", ".hh", ".hpp", ".hxx", ".h++", ".ipp", ".inl", ".tcc", ".ixx", ".cuh"}
|
|
36
|
+
# vendored third-party trees, test frameworks, build output (codegraph/presets/c_cpp.yaml; .cg.yaml skip_dirs adjusts them)
|
|
37
|
+
SKIP_DIRS = presets.skip_dirs("c_cpp")
|
|
38
|
+
SKIP_PREFIXES = tuple(presets.values("c_cpp", "skip_dir_prefixes", default=[]))
|
|
39
|
+
CODE = {"function", "method"}
|
|
40
|
+
TYPES = {"class", "struct", "union", "enum", "typedef"}
|
|
41
|
+
VALUES = {"global", "enumerator", "macro"}
|
|
42
|
+
SCOPES = CODE | TYPES | {"global", "macro"}
|
|
43
|
+
TEST_DIR = re.compile(r"(^|/)(tests?|testing|unittests?|gtest|spec|check)(/|$)", re.I)
|
|
44
|
+
TEST_FILE = re.compile(r"(^|/)(test_[^/]*|[^/]*_(unit)?tests?\.[^/]+|[^/]*Tests?\.[^/]+|tests?\.[^/]+)$")
|
|
45
|
+
EXAMPLE_DIR = re.compile(r"(^|/)(examples?|samples?|demos?|tutorials?)(/|$)", re.I)
|
|
46
|
+
BENCH_DIR = re.compile(r"(^|/)(bench|benches|benchmarks?|perf)(/|$)", re.I)
|
|
47
|
+
INCLUDE_DIR = re.compile(r"(^|/)include/")
|
|
48
|
+
MAIN_NAMES = {"main", "wmain", "WinMain", "wWinMain", "_tmain"}
|
|
49
|
+
# C std / POSIX names: never resolved heuristically to a project function even if the project defines one
|
|
50
|
+
LIBC = set("""malloc calloc realloc free memcpy memmove memset memcmp strlen strcmp strncmp strcpy strncpy strcat strdup
|
|
51
|
+
printf fprintf sprintf snprintf vsnprintf vfprintf puts fputs fputc fopen fclose fread fwrite fflush exit abort assert
|
|
52
|
+
open close read write lseek getenv atoi atol strtol strtoul qsort bsearch time clock sleep usleep pthread_create""".split())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _skip_dirs() -> set:
|
|
56
|
+
extra = {d.strip() for d in os.environ.get("CODEGRAPH_EXCLUDE_DIRS", "").split(",") if d.strip()}
|
|
57
|
+
keep = {d.strip() for d in os.environ.get("CODEGRAPH_INCLUDE_DIRS", "").split(",") if d.strip()}
|
|
58
|
+
return (SKIP_DIRS | extra) - keep
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def source_files(root: Path, limit: int | None = None, project=None):
|
|
62
|
+
rules = path_rules(project, "c_cpp", base=_skip_dirs())
|
|
63
|
+
n = 0
|
|
64
|
+
for dp, dn, fn in os.walk(root):
|
|
65
|
+
rel_dir = Path(dp).relative_to(root).as_posix()
|
|
66
|
+
rd = "" if rel_dir == "." else rel_dir
|
|
67
|
+
dn[:] = [d for d in rules.prune(rd, dn, dot=True) if rules.on_include_path(f"{rd}/{d}" if rd else d)
|
|
68
|
+
or not (d.startswith(SKIP_PREFIXES) or (Path(dp) / d / "CMakeCache.txt").exists())]
|
|
69
|
+
for f in sorted(fn):
|
|
70
|
+
ext = os.path.splitext(f)[1]
|
|
71
|
+
rel = f if rel_dir == "." else f"{rel_dir}/{f}"
|
|
72
|
+
if (ext in C_EXT or ext in CPP_EXT or ext in HDR_EXT) and keep_file(os.path.join(dp, f)) and not rules.excluded(rel):
|
|
73
|
+
yield rel
|
|
74
|
+
n += 1
|
|
75
|
+
if limit and n >= limit:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_compdb(root: Path) -> Path | None:
|
|
80
|
+
v = os.environ.get("CODEGRAPH_COMPDB")
|
|
81
|
+
if v:
|
|
82
|
+
p = Path(v)
|
|
83
|
+
if p.is_dir():
|
|
84
|
+
p = p / "compile_commands.json"
|
|
85
|
+
return p.resolve() if p.exists() else None
|
|
86
|
+
cands = [root / "compile_commands.json"]
|
|
87
|
+
for d in sorted(root.iterdir()) if root.is_dir() else []:
|
|
88
|
+
if d.is_dir() and (d.name.startswith(("build", "cmake-build", "out")) or d.name in ("builddir", "_build")):
|
|
89
|
+
cands.append(d / "compile_commands.json")
|
|
90
|
+
cands += sorted(d.glob("*/compile_commands.json"))
|
|
91
|
+
return next((c.resolve() for c in cands if c.exists()), None)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _kind_ok(suffix: str, kind: str) -> bool:
|
|
95
|
+
"""SCIP descriptor suffix vs syntactic kind: a namespace (`/`) never matches an item, methods/functions are `(`,
|
|
96
|
+
types `#`, fields / globals / enumerators `.`, macros `!`."""
|
|
97
|
+
if suffix == "(":
|
|
98
|
+
return kind in CODE or kind == "macro"
|
|
99
|
+
if suffix == "#":
|
|
100
|
+
return kind in TYPES or kind in CODE # constructors of class templates sometimes surface as types
|
|
101
|
+
if suffix == ".":
|
|
102
|
+
return kind in ("field", "global", "enumerator", "typedef") or kind in CODE # `f(<hash>).` may parse as a term
|
|
103
|
+
if suffix == "!":
|
|
104
|
+
return kind == "macro"
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def scip_cache_key(root: Path, files: list[str], indexer_version: str, compdb: Path) -> str:
|
|
109
|
+
"""SCIP cache key: the sources' content plus the compile_commands.json content (flags and defines change the
|
|
110
|
+
index even when no source does)."""
|
|
111
|
+
return runner.fingerprint(root, files, f"{indexer_version}|{compdb}|{content_key(compdb)}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CFamilyPlugin(LanguagePlugin):
|
|
115
|
+
name = "c_cpp"
|
|
116
|
+
|
|
117
|
+
def detect(self, project: Project) -> bool:
|
|
118
|
+
if os.environ.get("CODEGRAPH_CFAMILY") == "0":
|
|
119
|
+
return False
|
|
120
|
+
root = project.root
|
|
121
|
+
if find_compdb(root):
|
|
122
|
+
return True
|
|
123
|
+
# another language owns the repo (incl. Python/Django and Dart/Flutter, whose desktop runners ship C++ sources)
|
|
124
|
+
other_lang = any(project.exists(m) for m in ("Cargo.toml", "package.json", "composer.json", "go.mod", "pyproject.toml",
|
|
125
|
+
"setup.py", "setup.cfg", "requirements.txt", "manage.py", "Pipfile",
|
|
126
|
+
"pubspec.yaml"))
|
|
127
|
+
if other_lang and os.environ.get("CODEGRAPH_CFAMILY") != "1":
|
|
128
|
+
return False
|
|
129
|
+
return next(source_files(root, limit=1, project=project), None) is not None
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------ main
|
|
132
|
+
def index(self, project: Project, builder: GraphBuilder, frameworks: list[FrameworkPlugin]) -> dict:
|
|
133
|
+
t0 = time.time()
|
|
134
|
+
self.root, self.b = project.root, builder
|
|
135
|
+
stats: dict = defaultdict(int)
|
|
136
|
+
try:
|
|
137
|
+
from ..native.ts import parser
|
|
138
|
+
parser("c")
|
|
139
|
+
parser("cpp")
|
|
140
|
+
except TreeSitterMissing as e:
|
|
141
|
+
return {"status": "error", "reason": str(e)}
|
|
142
|
+
files = list(source_files(self.root, project=project))
|
|
143
|
+
self.file_report = {"seen": files, "skipped_oversize": []}
|
|
144
|
+
is_cpp = any(os.path.splitext(f)[1] in CPP_EXT for f in files)
|
|
145
|
+
self.compdb_path = find_compdb(self.root)
|
|
146
|
+
self.compdb = self._read_compdb(self.compdb_path) if self.compdb_path else {}
|
|
147
|
+
stats["compile_database"] = str(self.compdb_path.relative_to(self.root) if self.compdb_path and self.compdb_path.is_relative_to(self.root)
|
|
148
|
+
else self.compdb_path) if self.compdb_path else None
|
|
149
|
+
stats["translation_units_in_compdb"] = len(self.compdb)
|
|
150
|
+
scen = (project.options.get("gates") or [None])[0]
|
|
151
|
+
self.scen = G.Scenario(scen)
|
|
152
|
+
self.files: dict[str, CFile] = {}
|
|
153
|
+
srcs = {}
|
|
154
|
+
limit = int(os.environ.get("CODEGRAPH_MAX_FILE_BYTES", str(30 * 1024 * 1024)))
|
|
155
|
+
for rel in files:
|
|
156
|
+
try:
|
|
157
|
+
src = (self.root / rel).read_bytes()
|
|
158
|
+
except OSError:
|
|
159
|
+
continue
|
|
160
|
+
if len(src) > limit:
|
|
161
|
+
stats["skipped_large_files"] += 1
|
|
162
|
+
self.file_report["skipped_oversize"].append(rel)
|
|
163
|
+
continue
|
|
164
|
+
srcs[rel] = src
|
|
165
|
+
annot = annotation_macros(srcs.values()) if os.environ.get("CODEGRAPH_C_MASK_ANNOTATIONS", "1") != "0" else set()
|
|
166
|
+
stats["annotation_macros_masked"] = len(annot)
|
|
167
|
+
blank = annotation_regex(annot)
|
|
168
|
+
for rel, src in srcs.items():
|
|
169
|
+
ext = os.path.splitext(rel)[1]
|
|
170
|
+
lang = "c" if ext in C_EXT or (ext == ".h" and not is_cpp) else "cpp"
|
|
171
|
+
mod = os.path.dirname(rel) or "."
|
|
172
|
+
self.files[rel] = extract(rel, src, lang, mod, blank)
|
|
173
|
+
self._macro_generated(srcs, blank, is_cpp, stats)
|
|
174
|
+
stats["files"] = len(self.files)
|
|
175
|
+
self.file_report["syntax_errors"] = {r: f.error_spans for r, f in self.files.items() if f.error_spans}
|
|
176
|
+
stats["recovered_definitions"] = sum(f.recovered for f in self.files.values())
|
|
177
|
+
stats["files_c"] = sum(1 for f in self.files.values() if f.lang == "c")
|
|
178
|
+
stats["files_cpp"] = sum(1 for f in self.files.values() if f.lang == "cpp")
|
|
179
|
+
self._finalize_items(stats)
|
|
180
|
+
self._emit_structure(stats)
|
|
181
|
+
mode = "heuristic"
|
|
182
|
+
scip_info = None
|
|
183
|
+
if os.environ.get("CODEGRAPH_C_SCIP", "1") != "0":
|
|
184
|
+
if self.compdb_path:
|
|
185
|
+
scip_path, scip_info = self._run_scip()
|
|
186
|
+
if scip_path:
|
|
187
|
+
self._import_scip(scip_path, stats)
|
|
188
|
+
mode = "scip"
|
|
189
|
+
if self._scip_warning:
|
|
190
|
+
scip_info["warning"] = self._scip_warning
|
|
191
|
+
else:
|
|
192
|
+
scip_info = {"status": "no compile_commands.json found; heuristic mode. Generate one with "
|
|
193
|
+
"`cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` or `bear -- make` (docs/native.md)"}
|
|
194
|
+
self._inc = self._resolve_includes(stats)
|
|
195
|
+
if mode == "heuristic":
|
|
196
|
+
self._heuristic_refs(stats)
|
|
197
|
+
self._heuristic_dispatch(stats)
|
|
198
|
+
self._includes(stats)
|
|
199
|
+
self._facts(stats)
|
|
200
|
+
self._entries(stats)
|
|
201
|
+
self._gating(stats)
|
|
202
|
+
out = dict(stats)
|
|
203
|
+
out.update({"mode": mode, "seconds": round(time.time() - t0, 2)})
|
|
204
|
+
if scip_info:
|
|
205
|
+
out["scip"] = scip_info
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
def _read_compdb(self, p: Path) -> dict:
|
|
209
|
+
try:
|
|
210
|
+
entries = json.loads(p.read_text())
|
|
211
|
+
except Exception:
|
|
212
|
+
return {}
|
|
213
|
+
out = {}
|
|
214
|
+
for e in entries:
|
|
215
|
+
d = Path(e.get("directory") or ".")
|
|
216
|
+
f = Path(e["file"])
|
|
217
|
+
f = (d / f) if not f.is_absolute() else f
|
|
218
|
+
try:
|
|
219
|
+
rel = f.resolve().relative_to(self.root).as_posix()
|
|
220
|
+
except ValueError:
|
|
221
|
+
continue
|
|
222
|
+
args = e.get("arguments") or shlex.split(e.get("command", ""))
|
|
223
|
+
incs, defs = [], []
|
|
224
|
+
i = 0
|
|
225
|
+
while i < len(args):
|
|
226
|
+
a = args[i]
|
|
227
|
+
if a in ("-I", "-isystem", "-iquote") and i + 1 < len(args):
|
|
228
|
+
incs.append(args[i + 1]); i += 2; continue
|
|
229
|
+
if a.startswith("-I") or a.startswith("-iquote"):
|
|
230
|
+
incs.append(a[2:] if a.startswith("-I") else a[7:])
|
|
231
|
+
elif a.startswith("-isystem"):
|
|
232
|
+
incs.append(a[8:])
|
|
233
|
+
elif a.startswith("-D"):
|
|
234
|
+
defs.append(a[2:])
|
|
235
|
+
i += 1
|
|
236
|
+
inc_rel = []
|
|
237
|
+
for x in incs:
|
|
238
|
+
xp = (d / x).resolve() if not Path(x).is_absolute() else Path(x)
|
|
239
|
+
try:
|
|
240
|
+
inc_rel.append(xp.relative_to(self.root).as_posix())
|
|
241
|
+
except ValueError:
|
|
242
|
+
pass
|
|
243
|
+
out[rel] = {"includes": inc_rel, "defines": defs}
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
def _macro_generated(self, srcs, blank, is_cpp, stats):
|
|
247
|
+
"""Functions defined by expanding a project macro at file level (`#define DEFINE_GETTER(n) int get_##n(void)
|
|
248
|
+
{...}` + `DEFINE_GETTER(width)` -> get_width): a node at the expansion line (attrs.macro_generated). The
|
|
249
|
+
expansion lines are blanked and the file parsed again, so tree-sitter does not read the definitions after
|
|
250
|
+
them as part of one bogus function."""
|
|
251
|
+
gens = {}
|
|
252
|
+
for f in self.files.values():
|
|
253
|
+
for k, v in f.generators.items():
|
|
254
|
+
gens.setdefault(k, v)
|
|
255
|
+
if not gens:
|
|
256
|
+
return
|
|
257
|
+
for rel, f in list(self.files.items()):
|
|
258
|
+
hits = [tc for tc in f.top_calls if tc[0] in gens]
|
|
259
|
+
if not hits:
|
|
260
|
+
continue
|
|
261
|
+
src = bytearray(srcs[rel])
|
|
262
|
+
starts = [0] + [i + 1 for i, ch in enumerate(src) if ch == 10]
|
|
263
|
+
for _name, _args, line, _col, end in hits:
|
|
264
|
+
for ln in range(line, end + 1):
|
|
265
|
+
a = starts[ln - 1]
|
|
266
|
+
z = starts[ln] - 1 if ln < len(starts) else len(src)
|
|
267
|
+
src[a:z] = b" " * (z - a)
|
|
268
|
+
lines = f.lines
|
|
269
|
+
f2 = extract(rel, bytes(src), f.lang, os.path.dirname(rel) or ".", blank)
|
|
270
|
+
f2.lines = lines
|
|
271
|
+
f2.generators = f.generators
|
|
272
|
+
mod = os.path.dirname(rel) or "."
|
|
273
|
+
for name, args, line, col, _end in hits:
|
|
274
|
+
params, value = gens[name]
|
|
275
|
+
for fname, static, sig in generated_names(params, value, _split_args(args)):
|
|
276
|
+
f2.items.append(CItem("function", fname, f"{rel}#{fname}" if static else fname, rel, line, col, line, _end,
|
|
277
|
+
mod, fname, static=static, attrs={"macro_generated": name}, sig=sig))
|
|
278
|
+
stats["macro_generated_functions"] += 1
|
|
279
|
+
self.files[rel] = f2
|
|
280
|
+
|
|
281
|
+
# ------------------------------------------------------------------ items
|
|
282
|
+
def _finalize_items(self, stats):
|
|
283
|
+
all_items = [it for f in self.files.values() for it in f.items]
|
|
284
|
+
class_quals = {it.qual for it in all_items if it.kind in ("class", "struct", "union")}
|
|
285
|
+
class_names = defaultdict(set)
|
|
286
|
+
for q in class_quals:
|
|
287
|
+
class_names[q.split("::")[-1]].add(q)
|
|
288
|
+
for it in all_items:
|
|
289
|
+
if it.kind == "method" and it.scope and not it.parent:
|
|
290
|
+
# out-of-line definition A::f: method if A names a known class (qualified or by short name)
|
|
291
|
+
owner = it.qual.rsplit("::", 1)[0]
|
|
292
|
+
if owner in class_quals:
|
|
293
|
+
it.parent = owner
|
|
294
|
+
elif class_names.get(it.scope[-1]):
|
|
295
|
+
it.parent = sorted(class_names[it.scope[-1]])[0]
|
|
296
|
+
else:
|
|
297
|
+
it.kind = "function"
|
|
298
|
+
# declarations: map to definitions by qualified name
|
|
299
|
+
self.decls_by_qual = defaultdict(list)
|
|
300
|
+
for f in self.files.values():
|
|
301
|
+
for d in f.decls:
|
|
302
|
+
self.decls_by_qual[d.qual].append(d)
|
|
303
|
+
# key collisions: overloads / same-named non-static C functions in different programs
|
|
304
|
+
by_key = defaultdict(list)
|
|
305
|
+
for it in all_items:
|
|
306
|
+
by_key[(it.kind in CODE and "code" or it.kind, it.key)].append(it)
|
|
307
|
+
for (k, key), lst in by_key.items():
|
|
308
|
+
if len(lst) < 2:
|
|
309
|
+
continue
|
|
310
|
+
if k == "code":
|
|
311
|
+
sigs = {x.sig for x in lst}
|
|
312
|
+
files = {x.file for x in lst}
|
|
313
|
+
for x in lst:
|
|
314
|
+
if len(sigs) == len(lst):
|
|
315
|
+
x.key = f"{x.key}({x.sig})"
|
|
316
|
+
elif len(files) == len(lst):
|
|
317
|
+
x.key = f"{x.file}#{x.key}"
|
|
318
|
+
else:
|
|
319
|
+
x.key = f"{x.key}@{x.file}:{x.line}"
|
|
320
|
+
stats["code_key_collisions"] += len(lst)
|
|
321
|
+
else:
|
|
322
|
+
for x in lst[1:]:
|
|
323
|
+
x.key = f"{x.key}@{x.file}:{x.line}"
|
|
324
|
+
self.items = all_items
|
|
325
|
+
self.by_key = {}
|
|
326
|
+
for it in all_items:
|
|
327
|
+
self.by_key.setdefault(it.key, it)
|
|
328
|
+
self.by_type_key = {}
|
|
329
|
+
for it in all_items:
|
|
330
|
+
if it.kind in ("class", "struct", "union", "enum"):
|
|
331
|
+
self.by_type_key.setdefault(it.key, it)
|
|
332
|
+
self.by_pos = {}
|
|
333
|
+
for it in all_items:
|
|
334
|
+
self.by_pos.setdefault((it.file, it.line, it.name), it)
|
|
335
|
+
if it.kind == "macro":
|
|
336
|
+
self.by_pos.setdefault((it.file, it.line, "!macro"), it)
|
|
337
|
+
if it.attrs.get("test_macro"):
|
|
338
|
+
self.by_pos.setdefault((it.file, it.line, "!test"), it)
|
|
339
|
+
self.by_line = defaultdict(list)
|
|
340
|
+
for it in all_items:
|
|
341
|
+
self.by_line[(it.file, it.line)].append(it)
|
|
342
|
+
self.by_name = defaultdict(list)
|
|
343
|
+
for it in all_items:
|
|
344
|
+
self.by_name[it.name].append(it)
|
|
345
|
+
|
|
346
|
+
def nid(self, it: CItem) -> str:
|
|
347
|
+
return f"{it.kind}:{it.key}"
|
|
348
|
+
|
|
349
|
+
def _lang(self, rel: str) -> str:
|
|
350
|
+
return self.files[rel].lang
|
|
351
|
+
|
|
352
|
+
def _emit_structure(self, stats):
|
|
353
|
+
b = self.b
|
|
354
|
+
for rel, f in self.files.items():
|
|
355
|
+
ext = os.path.splitext(rel)[1]
|
|
356
|
+
b.add_node("file", rel, name=os.path.basename(rel), file=rel, line=1, module=os.path.dirname(rel) or ".", lang=f.lang,
|
|
357
|
+
attrs={"header": ext in HDR_EXT, "translation_unit": rel in self.compdb})
|
|
358
|
+
for it in self.items:
|
|
359
|
+
attrs = dict(it.attrs)
|
|
360
|
+
if it.static:
|
|
361
|
+
attrs["static"] = True
|
|
362
|
+
if it.sig and it.kind in CODE:
|
|
363
|
+
attrs["signature"] = it.sig
|
|
364
|
+
decls = self.decls_by_qual.get(it.qual) if it.kind in CODE else None
|
|
365
|
+
if decls:
|
|
366
|
+
attrs["declared_in"] = sorted({f"{d.file}:{d.line}" for d in decls})[:4]
|
|
367
|
+
if any(d.export for d in decls):
|
|
368
|
+
attrs["export"] = True
|
|
369
|
+
for d in decls:
|
|
370
|
+
if d.virtual:
|
|
371
|
+
attrs["virtual"] = True
|
|
372
|
+
if d.override:
|
|
373
|
+
attrs["override"] = True
|
|
374
|
+
if d.pure:
|
|
375
|
+
attrs["pure"] = True
|
|
376
|
+
if d.access and "access" not in attrs:
|
|
377
|
+
attrs["access"] = d.access
|
|
378
|
+
if d.extern_c:
|
|
379
|
+
attrs["extern_c"] = True
|
|
380
|
+
n = b.add_node(it.kind, it.key, name=it.name, fqn=it.qual or it.key, file=it.file, line=it.line, end_line=it.end,
|
|
381
|
+
module=it.module, doc=it.doc, lang=self._lang(it.file), attrs=attrs)
|
|
382
|
+
par = (self.by_type_key.get(it.parent) or self.by_key.get(it.parent)) if it.parent else None
|
|
383
|
+
if par is not None:
|
|
384
|
+
b.add_edge(self.nid(par), n, "CONTAINS", it.file, it.line, EXACT)
|
|
385
|
+
else:
|
|
386
|
+
b.add_edge(f"file:{it.file}", n, "CONTAINS", it.file, it.line, EXACT)
|
|
387
|
+
stats[f"items_{it.kind}"] += 1
|
|
388
|
+
self.decl_items = []
|
|
389
|
+
# pure virtual declarations have no definition: give them a node so overrides can attach
|
|
390
|
+
for qual, decls in self.decls_by_qual.items():
|
|
391
|
+
for d in decls:
|
|
392
|
+
if d.kind == "method" and d.pure and qual not in self.by_key:
|
|
393
|
+
owner = qual.rsplit("::", 1)[0]
|
|
394
|
+
oit = self.by_type_key.get(owner) or self.by_key.get(owner)
|
|
395
|
+
n = b.add_node("method", qual, name=d.name, fqn=qual, file=d.file, line=d.line, module=os.path.dirname(d.file) or ".",
|
|
396
|
+
lang=self._lang(d.file), attrs={"pure": True, "virtual": True, "access": d.access, "declaration_only": True})
|
|
397
|
+
if oit is not None:
|
|
398
|
+
b.add_edge(self.nid(oit), n, "CONTAINS", d.file, d.line, EXACT)
|
|
399
|
+
self.decl_items.append(CItem("method", d.name, qual, d.file, d.line, d.col, d.line, d.line,
|
|
400
|
+
os.path.dirname(d.file) or ".", qual=qual, attrs={"pure": True}))
|
|
401
|
+
stats["pure_virtual_decls"] += 1
|
|
402
|
+
|
|
403
|
+
# ------------------------------------------------------------------ scip
|
|
404
|
+
def _run_scip(self):
|
|
405
|
+
tool = runner.find_tool("CODEGRAPH_SCIP_CLANG", ["scip-clang"], [Path.home() / ".local" / "bin"])
|
|
406
|
+
pre = os.environ.get("CODEGRAPH_C_SCIP_FILE")
|
|
407
|
+
if pre:
|
|
408
|
+
return (Path(pre) if Path(pre).exists() else None), {"source": "CODEGRAPH_C_SCIP_FILE", "path": pre}
|
|
409
|
+
if not tool:
|
|
410
|
+
return None, {"status": "scip-clang not installed; heuristic mode (docs/native.md)"}
|
|
411
|
+
ver = runner.tool_version(tool)
|
|
412
|
+
key = scip_cache_key(self.root, list(self.files), ver, self.compdb_path)
|
|
413
|
+
jobs = os.environ.get("CODEGRAPH_JOBS")
|
|
414
|
+
cmd = [tool, f"--compdb-path={self.compdb_path}"] + ([f"--jobs={jobs}"] if jobs else [])
|
|
415
|
+
timeout = int(os.environ.get("CODEGRAPH_INDEXER_TIMEOUT", "3600"))
|
|
416
|
+
path, info = runner.run_cached("cfamily", key, cmd, self.root, "--index-output-path", timeout, out_flag_style="eq")
|
|
417
|
+
info["indexer"] = ver
|
|
418
|
+
if info.get("stderr_tail"):
|
|
419
|
+
m = [l for l in info["stderr_tail"] if "num errored TUs" in l or "Finished indexing" in l]
|
|
420
|
+
if m:
|
|
421
|
+
info["summary"] = m[-1].strip()
|
|
422
|
+
return path, info
|
|
423
|
+
|
|
424
|
+
def _painter(self, rel: str):
|
|
425
|
+
f = self.files.get(rel)
|
|
426
|
+
if f is None:
|
|
427
|
+
return None
|
|
428
|
+
n = len(f.lines) + 2
|
|
429
|
+
paint = [None] * n
|
|
430
|
+
for it in sorted((x for x in f.items if x.kind in SCOPES), key=lambda x: -(x.end - x.start)):
|
|
431
|
+
for ln in range(max(0, it.start), min(n, it.end + 1)):
|
|
432
|
+
paint[ln] = it
|
|
433
|
+
return paint
|
|
434
|
+
|
|
435
|
+
def _import_scip(self, path: Path, stats):
|
|
436
|
+
idx = scipread.load(path)
|
|
437
|
+
stats["scip_documents"] = len(idx.docs)
|
|
438
|
+
sym_nodes: dict[str, list[tuple[str, str]]] = defaultdict(list)
|
|
439
|
+
for sym, defs in idx.defs.items():
|
|
440
|
+
d = scipread.descriptors(sym)
|
|
441
|
+
if not d or not d[1]:
|
|
442
|
+
continue
|
|
443
|
+
name, suf = d[1][-1]
|
|
444
|
+
for rel, o in defs:
|
|
445
|
+
it = self.by_pos.get((rel, o.line + 1, name))
|
|
446
|
+
if it is None and "<" in name:
|
|
447
|
+
it = self.by_pos.get((rel, o.line + 1, name.split("<")[0]))
|
|
448
|
+
if it is None and suf in ("(", "#", "."):
|
|
449
|
+
# operators / conversion functions / constructors of templates print differently: accept the single
|
|
450
|
+
# syntactic item of a compatible kind declared on that line
|
|
451
|
+
want = CODE if suf == "(" else (TYPES if suf == "#" else {"field", "global", "enumerator"})
|
|
452
|
+
cands = [x for x in self.by_line.get((rel, o.line + 1), []) if x.kind in want]
|
|
453
|
+
if len(cands) == 1:
|
|
454
|
+
it = cands[0]
|
|
455
|
+
if it is None and suf == "!":
|
|
456
|
+
it = self.by_pos.get((rel, o.line + 1, "!macro"))
|
|
457
|
+
if it is None:
|
|
458
|
+
it = self.by_pos.get((rel, o.line + 1, "!test"))
|
|
459
|
+
if it is not None and not _kind_ok(suf, it.kind):
|
|
460
|
+
stats[f"scip_kind_mismatch_{suf}_{it.kind}"] += 1
|
|
461
|
+
it = None
|
|
462
|
+
if it is None:
|
|
463
|
+
if suf == "!" or rel not in self.files:
|
|
464
|
+
continue
|
|
465
|
+
nid = self._synthetic(rel, o, sym, d)
|
|
466
|
+
if nid is None:
|
|
467
|
+
continue
|
|
468
|
+
stats["scip_defs_synthetic"] += 1
|
|
469
|
+
else:
|
|
470
|
+
nid = self.nid(it)
|
|
471
|
+
stats["scip_defs_matched"] += 1
|
|
472
|
+
# namespaces hidden behind macros (BEGIN_NAMESPACE-style) are only known to clang: adopt its
|
|
473
|
+
# qualified name for display / spec matching (the node id stays syntactic, stable across modes)
|
|
474
|
+
sq = "::".join(n for n, s in d[1] if s in ("/", "#", "(", ".") and not n.startswith(("<file>", "$")))
|
|
475
|
+
node = self.b.nodes.get(nid)
|
|
476
|
+
if node is not None and sq and len(sq) > len(node.fqn or "") and sq.endswith("::" + (node.fqn or "").split("<")[0]):
|
|
477
|
+
node.fqn = sq
|
|
478
|
+
stats["fqn_from_scip"] += 1
|
|
479
|
+
if (rel, nid) not in sym_nodes[sym]:
|
|
480
|
+
sym_nodes[sym].append((rel, nid))
|
|
481
|
+
self._scip_warning = scipread.native_warning(path, idx, stats)
|
|
482
|
+
# pure-virtual / declaration-only methods: the symbol is only ever declared
|
|
483
|
+
for rel, doc in idx.docs.items():
|
|
484
|
+
f = self.files.get(rel)
|
|
485
|
+
if f is None:
|
|
486
|
+
continue
|
|
487
|
+
for o in doc.occs:
|
|
488
|
+
if o.symbol in sym_nodes or (o.line + 1, o.col) not in f.decl_pos:
|
|
489
|
+
continue
|
|
490
|
+
d = scipread.descriptors(o.symbol)
|
|
491
|
+
if not d or not d[1]:
|
|
492
|
+
continue
|
|
493
|
+
qual = "::".join(n for n, s in d[1] if s in ("/", "#", "(") and not n.startswith("<file>") and not n.startswith("$"))
|
|
494
|
+
if f"method:{qual}" in self.b.nodes:
|
|
495
|
+
sym_nodes[o.symbol].append((rel, f"method:{qual}"))
|
|
496
|
+
self.sym_nodes = sym_nodes
|
|
497
|
+
# relationships: overrides and inheritance
|
|
498
|
+
virtual = set()
|
|
499
|
+
for rel, doc in idx.docs.items():
|
|
500
|
+
for sym, si in doc.symbols.items():
|
|
501
|
+
if sym not in sym_nodes:
|
|
502
|
+
continue
|
|
503
|
+
for r in si.relationships:
|
|
504
|
+
if not r.is_implementation or r.symbol not in sym_nodes:
|
|
505
|
+
continue
|
|
506
|
+
for _, child in sym_nodes[sym]:
|
|
507
|
+
for _, base in sym_nodes[r.symbol][:1]:
|
|
508
|
+
if child.startswith("method:") and base.startswith("method:"):
|
|
509
|
+
bn = self.b.nodes.get(base)
|
|
510
|
+
ek = "IMPLEMENTED_BY" if bn is not None and bn.attrs.get("pure") else "OVERRIDDEN_BY"
|
|
511
|
+
cn = self.b.nodes.get(child)
|
|
512
|
+
self.b.add_edge(base, child, ek, cn.file if cn else rel, cn.line if cn else None, RESOLVED)
|
|
513
|
+
virtual.add(base)
|
|
514
|
+
stats["virtual_dispatch_edges"] += 1
|
|
515
|
+
elif not child.startswith("method:") and not base.startswith("method:"):
|
|
516
|
+
cn = self.b.nodes.get(child)
|
|
517
|
+
self.b.add_edge(child, base, "EXTENDS", cn.file if cn else rel, cn.line if cn else None, EXACT)
|
|
518
|
+
stats["inheritance_edges"] += 1
|
|
519
|
+
self.virtual = virtual | {nid for nid, n in self.b.nodes.items() if n.kind == "method" and n.attrs.get("virtual")}
|
|
520
|
+
for rel, doc in idx.docs.items():
|
|
521
|
+
f = self.files.get(rel)
|
|
522
|
+
if f is None:
|
|
523
|
+
stats["scip_docs_outside_indexed_files"] += 1
|
|
524
|
+
continue
|
|
525
|
+
paint = self._painter(rel)
|
|
526
|
+
lines = f.lines
|
|
527
|
+
# references reported at a macro expansion site (the token under the range is the macro, not the target):
|
|
528
|
+
# real dependencies (TEST/REGISTER-style macros), but type-check / dispatch macros can expand to dozens of
|
|
529
|
+
# symbols per use. Keep small expansions (labelled via_macro, resolved), drop large ones.
|
|
530
|
+
exp_groups = defaultdict(set)
|
|
531
|
+
name_cache = {}
|
|
532
|
+
for o in doc.occs:
|
|
533
|
+
if o.roles & scipread.DEFINITION or o.symbol not in sym_nodes:
|
|
534
|
+
continue
|
|
535
|
+
tok = lines[o.line][o.col:o.end_col] if o.line < len(lines) and o.end_col > o.col else ""
|
|
536
|
+
nm = name_cache.get(o.symbol)
|
|
537
|
+
if nm is None:
|
|
538
|
+
d = scipread.descriptors(o.symbol)
|
|
539
|
+
nm = name_cache[o.symbol] = (d[1][-1][0].split("<")[0] if d and d[1] else "")
|
|
540
|
+
if tok and nm and tok != nm and not nm.startswith(("operator", "~")) and not tok.startswith(("operator", "~")) \
|
|
541
|
+
and re.fullmatch(r"[A-Za-z_]\w*", tok):
|
|
542
|
+
exp_groups[(o.line, o.col)].add(o.symbol)
|
|
543
|
+
max_exp = int(os.environ.get("CODEGRAPH_C_MAX_MACRO_REFS", "8"))
|
|
544
|
+
for o in doc.occs:
|
|
545
|
+
if o.roles & scipread.DEFINITION:
|
|
546
|
+
continue
|
|
547
|
+
if (o.line + 1, o.col) in f.decl_pos:
|
|
548
|
+
continue
|
|
549
|
+
cands = sym_nodes.get(o.symbol)
|
|
550
|
+
if not cands:
|
|
551
|
+
continue
|
|
552
|
+
ln = o.line + 1
|
|
553
|
+
owner = paint[ln] if ln < len(paint) else None
|
|
554
|
+
if owner is None:
|
|
555
|
+
stats["scip_refs_outside_items"] += 1
|
|
556
|
+
continue
|
|
557
|
+
if len(cands) > 1:
|
|
558
|
+
same = [c for c in cands if c[0] == rel]
|
|
559
|
+
cands = same[:1] if same else cands[:3]
|
|
560
|
+
line_txt = lines[o.line] if o.line < len(lines) else ""
|
|
561
|
+
before = line_txt[:o.col].rstrip()
|
|
562
|
+
after = line_txt[o.end_col:].lstrip()
|
|
563
|
+
grp = exp_groups.get((o.line, o.col))
|
|
564
|
+
via = {}
|
|
565
|
+
if grp and o.symbol in grp:
|
|
566
|
+
if len(grp) > max_exp:
|
|
567
|
+
stats["scip_macro_expansion_refs_dropped"] += 1
|
|
568
|
+
continue
|
|
569
|
+
via = {"via_macro": line_txt[o.col:o.end_col]}
|
|
570
|
+
stats["scip_macro_expansion_refs"] += 1
|
|
571
|
+
for _, dst in cands:
|
|
572
|
+
conf = HEURISTIC if len(cands) > 1 else (RESOLVED if via or before.endswith((".", "->")) else EXACT)
|
|
573
|
+
self._ref_edge(self.nid(owner), dst, rel, ln, conf, "(" if via and dst.split(":", 1)[0] in CODE else after, stats, **via)
|
|
574
|
+
|
|
575
|
+
def _synthetic(self, rel, o, sym, d) -> str | None:
|
|
576
|
+
names = [(n, s) for n, s in d[1] if not n.startswith("<file>") and not n.startswith("$")]
|
|
577
|
+
if not names:
|
|
578
|
+
return None
|
|
579
|
+
last, suf = names[-1]
|
|
580
|
+
if suf not in ("(", "#", "."):
|
|
581
|
+
return None
|
|
582
|
+
qual = "::".join(n for n, s in names if s in ("/", "#", "(", "."))
|
|
583
|
+
f = self.files[rel]
|
|
584
|
+
if suf == "(":
|
|
585
|
+
kind = "method" if any(s == "#" for _, s in names[:-1]) else "function"
|
|
586
|
+
elif suf == "#":
|
|
587
|
+
kind = "class"
|
|
588
|
+
else:
|
|
589
|
+
kind = "field" if any(s == "#" for _, s in names[:-1]) else "global"
|
|
590
|
+
key = qual if any(s == "/" for _, s in d[1][:1]) or kind in ("method", "class", "field") else qual
|
|
591
|
+
if any(n.startswith("<file>") or n.startswith("$anonymous") for n, _ in d[1]):
|
|
592
|
+
key = f"{rel}#{qual}"
|
|
593
|
+
if f"{kind}:{key}" in self.b.nodes:
|
|
594
|
+
return f"{kind}:{key}"
|
|
595
|
+
nid = self.b.add_node(kind, key, name=last, fqn=qual, file=rel, line=o.line + 1, module=os.path.dirname(rel) or ".",
|
|
596
|
+
lang=f.lang, attrs={"from": "scip"})
|
|
597
|
+
return nid
|
|
598
|
+
|
|
599
|
+
def _ref_edge(self, src: str, dst: str, rel: str, ln: int, conf: str, after: str, stats, **attrs):
|
|
600
|
+
if src == dst:
|
|
601
|
+
return
|
|
602
|
+
kind = dst.split(":", 1)[0]
|
|
603
|
+
node = self.b.nodes.get(dst)
|
|
604
|
+
if kind in CODE:
|
|
605
|
+
ek = "CALLS" if after.startswith(("(", "<")) else "REFERENCES_FN"
|
|
606
|
+
if dst in getattr(self, "virtual", ()) and ek == "CALLS":
|
|
607
|
+
attrs["dispatch"] = "virtual"
|
|
608
|
+
stats["calls_virtual_dispatch"] += 1
|
|
609
|
+
elif kind == "macro":
|
|
610
|
+
ek = "CALLS" if (node is not None and node.attrs.get("function_like")) else "USES_VALUE"
|
|
611
|
+
elif kind in TYPES:
|
|
612
|
+
ek = "USES_TYPE"
|
|
613
|
+
elif kind == "field":
|
|
614
|
+
ek = "ACCESSES_FIELD"
|
|
615
|
+
elif kind in ("global", "enumerator"):
|
|
616
|
+
ek = "USES_VALUE"
|
|
617
|
+
else:
|
|
618
|
+
return
|
|
619
|
+
self.b.add_edge(src, dst, ek, rel, ln, conf, **attrs)
|
|
620
|
+
stats[f"edges_{ek}"] += 1
|
|
621
|
+
|
|
622
|
+
# ------------------------------------------------------------------ heuristic
|
|
623
|
+
def _heuristic_refs(self, stats):
|
|
624
|
+
from ...platforms import c_relevant, path_convention
|
|
625
|
+
|
|
626
|
+
def _platform_scoped(it) -> bool:
|
|
627
|
+
if path_convention(it.file):
|
|
628
|
+
return True
|
|
629
|
+
f = self.files.get(it.file)
|
|
630
|
+
return bool(f) and any(a <= it.start and it.end <= z and c_relevant(c) for a, z, c, _l, _b in f.regions)
|
|
631
|
+
|
|
632
|
+
def _alternatives(cands):
|
|
633
|
+
# same symbol: keys equal up to the collision suffix (`@file:line`) or file prefix (`src/win/x.c#name`)
|
|
634
|
+
if (1 < len(cands) <= 8 and len({c.key.split("@", 1)[0].rsplit("#", 1)[-1] for c in cands}) == 1
|
|
635
|
+
and len({c.kind for c in cands}) == 1
|
|
636
|
+
and all(_platform_scoped(c) for c in cands)):
|
|
637
|
+
return list(cands)
|
|
638
|
+
return []
|
|
639
|
+
|
|
640
|
+
funcs = defaultdict(list)
|
|
641
|
+
methods = defaultdict(list)
|
|
642
|
+
for it in self.items + self.decl_items:
|
|
643
|
+
if it.kind == "function":
|
|
644
|
+
funcs[it.name].append(it)
|
|
645
|
+
elif it.kind == "method":
|
|
646
|
+
methods[it.name].append(it)
|
|
647
|
+
quals = {it.qual: it for it in self.items if it.kind in CODE}
|
|
648
|
+
macros = {it.name: it for it in self.items if it.kind == "macro"}
|
|
649
|
+
globals_ = defaultdict(list)
|
|
650
|
+
for it in self.items:
|
|
651
|
+
if it.kind in ("global", "enumerator"):
|
|
652
|
+
globals_[it.name].append(it)
|
|
653
|
+
types = defaultdict(list)
|
|
654
|
+
for it in self.items:
|
|
655
|
+
if it.kind in TYPES:
|
|
656
|
+
types[it.name].append(it)
|
|
657
|
+
|
|
658
|
+
def pick(cands, rel):
|
|
659
|
+
if not cands:
|
|
660
|
+
return []
|
|
661
|
+
same = [c for c in cands if c.file == rel]
|
|
662
|
+
if same:
|
|
663
|
+
return _alternatives(same) or same[:1]
|
|
664
|
+
# a static (inline) function defined in a header is compiled into every file that includes it (#92)
|
|
665
|
+
inc = self._included(rel) if any(c.static for c in cands) else ()
|
|
666
|
+
hdr = [c for c in cands if c.static and c.file in inc]
|
|
667
|
+
if hdr:
|
|
668
|
+
return _alternatives(hdr) or (hdr if len(hdr) == 1 else [])
|
|
669
|
+
nonstatic = [c for c in cands if not c.static]
|
|
670
|
+
if len(nonstatic) == 1:
|
|
671
|
+
return nonstatic
|
|
672
|
+
# one symbol defined once per build configuration (#ifdef _WIN32 / #else, src/win/x.c + src/unix/x.c):
|
|
673
|
+
# every definition is a target (each is tagged with its platforms)
|
|
674
|
+
return _alternatives(nonstatic)
|
|
675
|
+
|
|
676
|
+
for rel, f in self.files.items():
|
|
677
|
+
for owner_key, form, txt, name, line, col in f.calls:
|
|
678
|
+
owner = owner_key
|
|
679
|
+
if owner is None:
|
|
680
|
+
continue
|
|
681
|
+
targets = []
|
|
682
|
+
if form == "name":
|
|
683
|
+
if name in LIBC:
|
|
684
|
+
continue
|
|
685
|
+
if name in macros and macros[name].attrs.get("function_like"):
|
|
686
|
+
targets = [macros[name]]
|
|
687
|
+
else:
|
|
688
|
+
targets = pick(funcs.get(name), rel) or (pick(methods.get(name), rel) if owner.kind == "method" else [])
|
|
689
|
+
elif form == "qualified":
|
|
690
|
+
q = quals.get(txt) or next((v for k, v in quals.items() if k.endswith("::" + txt)), None)
|
|
691
|
+
targets = [q] if q else []
|
|
692
|
+
else:
|
|
693
|
+
c = methods.get(name, [])
|
|
694
|
+
if len(c) > 1:
|
|
695
|
+
# several classes define the method: if exactly one is the virtual root, dispatch through it
|
|
696
|
+
roots = [m for m in c if self._is_virtual(m) and not self._is_override(m)]
|
|
697
|
+
c = roots if len(roots) == 1 else c
|
|
698
|
+
targets = c if len(c) == 1 else []
|
|
699
|
+
if not targets:
|
|
700
|
+
stats["heuristic_unresolved_calls"] += 1
|
|
701
|
+
continue
|
|
702
|
+
for t in targets:
|
|
703
|
+
self._ref_edge(self.nid(owner), self.nid(t), rel, line, HEURISTIC, "(", stats,
|
|
704
|
+
**({"dispatch": "virtual"} if form == "member" and self._is_virtual(t) else {}))
|
|
705
|
+
for owner_key, name, line, col in f.idents:
|
|
706
|
+
owner = owner_key
|
|
707
|
+
if owner is None:
|
|
708
|
+
continue
|
|
709
|
+
if name in funcs:
|
|
710
|
+
t = pick(funcs[name], rel)
|
|
711
|
+
for x in t:
|
|
712
|
+
self._ref_edge(self.nid(owner), self.nid(x), rel, line, HEURISTIC, "", stats)
|
|
713
|
+
elif name in globals_:
|
|
714
|
+
for x in pick(globals_[name], rel):
|
|
715
|
+
self._ref_edge(self.nid(owner), self.nid(x), rel, line, HEURISTIC, "", stats)
|
|
716
|
+
elif name in macros and not macros[name].attrs.get("function_like"):
|
|
717
|
+
self._ref_edge(self.nid(owner), self.nid(macros[name]), rel, line, HEURISTIC, "", stats)
|
|
718
|
+
for owner_key, name, line, col in f.type_refs:
|
|
719
|
+
owner = owner_key
|
|
720
|
+
if owner is None:
|
|
721
|
+
continue
|
|
722
|
+
for x in pick(types.get(name), rel):
|
|
723
|
+
self._ref_edge(self.nid(owner), self.nid(x), rel, line, HEURISTIC, "", stats)
|
|
724
|
+
|
|
725
|
+
def _is_virtual(self, it) -> bool:
|
|
726
|
+
n = self.b.nodes.get(self.nid(it))
|
|
727
|
+
return bool(n is not None and n.attrs.get("virtual"))
|
|
728
|
+
|
|
729
|
+
def _is_override(self, it) -> bool:
|
|
730
|
+
n = self.b.nodes.get(self.nid(it))
|
|
731
|
+
return bool(n is not None and n.attrs.get("override"))
|
|
732
|
+
|
|
733
|
+
def _heuristic_dispatch(self, stats):
|
|
734
|
+
cls_by_name = defaultdict(list)
|
|
735
|
+
for it in self.items:
|
|
736
|
+
if it.kind in ("class", "struct"):
|
|
737
|
+
cls_by_name[it.name].append(it)
|
|
738
|
+
parents = defaultdict(list)
|
|
739
|
+
for f in self.files.values():
|
|
740
|
+
for ckey, base, line, access in f.bases:
|
|
741
|
+
cands = cls_by_name.get(base.split("::")[-1], [])
|
|
742
|
+
if len(cands) == 1 and ckey in self.by_key:
|
|
743
|
+
parents[ckey].append(cands[0].key)
|
|
744
|
+
self.b.add_edge(self.nid(self.by_key[ckey]), self.nid(cands[0]), "EXTENDS", f.path, line, HEURISTIC)
|
|
745
|
+
stats["inheritance_edges"] += 1
|
|
746
|
+
methods_of = defaultdict(dict)
|
|
747
|
+
for nid, n in self.b.nodes.items():
|
|
748
|
+
if n.kind == "method" and n.fqn and "::" in n.fqn:
|
|
749
|
+
methods_of[n.fqn.rsplit("::", 1)[0]][n.name] = nid
|
|
750
|
+
for ckey, ps in parents.items():
|
|
751
|
+
cq = self.by_key[ckey].qual
|
|
752
|
+
for pkey in ps:
|
|
753
|
+
pq = self.by_key[pkey].qual
|
|
754
|
+
for mname, child in methods_of.get(cq, {}).items():
|
|
755
|
+
base = methods_of.get(pq, {}).get(mname)
|
|
756
|
+
if base:
|
|
757
|
+
bn = self.b.nodes[base]
|
|
758
|
+
if bn.attrs.get("virtual") or self.b.nodes[child].attrs.get("override"):
|
|
759
|
+
ek = "IMPLEMENTED_BY" if bn.attrs.get("pure") else "OVERRIDDEN_BY"
|
|
760
|
+
cn = self.b.nodes[child]
|
|
761
|
+
self.b.add_edge(base, child, ek, cn.file, cn.line, HEURISTIC)
|
|
762
|
+
stats["virtual_dispatch_edges"] += 1
|
|
763
|
+
|
|
764
|
+
# ------------------------------------------------------------------ includes, env, gates
|
|
765
|
+
def _includes(self, stats):
|
|
766
|
+
for rel, hits in self._inc.items():
|
|
767
|
+
for hit, line, conf in hits:
|
|
768
|
+
self.b.add_edge(f"file:{rel}", f"file:{hit}", "INCLUDES", rel, line, conf)
|
|
769
|
+
|
|
770
|
+
def _included(self, rel: str) -> set:
|
|
771
|
+
"""Files `rel` includes, directly or through other project headers."""
|
|
772
|
+
memo = self.__dict__.setdefault("_inc_closure", {})
|
|
773
|
+
if rel not in memo:
|
|
774
|
+
seen, todo = set(), [rel]
|
|
775
|
+
while todo and len(seen) < 2000:
|
|
776
|
+
for hit, _l, _c in self._inc.get(todo.pop(), ()):
|
|
777
|
+
if hit not in seen and hit != rel:
|
|
778
|
+
seen.add(hit)
|
|
779
|
+
todo.append(hit)
|
|
780
|
+
memo[rel] = seen
|
|
781
|
+
return memo[rel]
|
|
782
|
+
|
|
783
|
+
def _resolve_includes(self, stats) -> dict:
|
|
784
|
+
out = defaultdict(list)
|
|
785
|
+
by_base = defaultdict(list)
|
|
786
|
+
for rel in self.files:
|
|
787
|
+
by_base[os.path.basename(rel)].append(rel)
|
|
788
|
+
global_incs = set()
|
|
789
|
+
for e in self.compdb.values():
|
|
790
|
+
global_incs.update(e["includes"])
|
|
791
|
+
default_incs = [d for d in ("include", "src", "inc", "") if d == "" or (self.root / d).is_dir()]
|
|
792
|
+
for rel, f in self.files.items():
|
|
793
|
+
dirs = (self.compdb.get(rel) or {}).get("includes") or []
|
|
794
|
+
for path, line, system in f.includes:
|
|
795
|
+
hit, conf = None, EXACT
|
|
796
|
+
cand = [os.path.normpath(os.path.join(os.path.dirname(rel), path))] if not system else []
|
|
797
|
+
cand += [os.path.normpath(os.path.join(d, path)) for d in dirs]
|
|
798
|
+
cand += [os.path.normpath(os.path.join(d, path)) for d in sorted(global_incs) + default_incs]
|
|
799
|
+
for c in cand:
|
|
800
|
+
c = c.replace("\\", "/").lstrip("./") if not c.startswith("../") else c
|
|
801
|
+
if c in self.files:
|
|
802
|
+
hit = c
|
|
803
|
+
conf = EXACT if c == cand[0] or dirs else RESOLVED
|
|
804
|
+
break
|
|
805
|
+
if hit is None:
|
|
806
|
+
bb = [x for x in by_base.get(os.path.basename(path), []) if x.endswith("/" + path) or x == path]
|
|
807
|
+
if len(bb) == 1:
|
|
808
|
+
hit, conf = bb[0], HEURISTIC
|
|
809
|
+
if hit is None:
|
|
810
|
+
if not system:
|
|
811
|
+
stats["includes_unresolved"] += 1
|
|
812
|
+
continue
|
|
813
|
+
out[rel].append((hit, line, conf))
|
|
814
|
+
stats["includes_resolved"] += 1
|
|
815
|
+
return out
|
|
816
|
+
|
|
817
|
+
def _facts(self, stats):
|
|
818
|
+
b = self.b
|
|
819
|
+
for rel, f in self.files.items():
|
|
820
|
+
for key, line, owner_key in f.env:
|
|
821
|
+
owner = owner_key
|
|
822
|
+
if owner is None:
|
|
823
|
+
continue
|
|
824
|
+
b.add_node("env", key, name=key, lang=f.lang)
|
|
825
|
+
b.add_edge(self.nid(owner), f"env:{key}", "READS_ENV", rel, line, EXACT, how="getenv")
|
|
826
|
+
stats["env_reads"] += 1
|
|
827
|
+
# preprocessor gates: items overlapping each conditional region
|
|
828
|
+
if not f.regions:
|
|
829
|
+
continue
|
|
830
|
+
self._platform_marks(rel, f)
|
|
831
|
+
scoped = [it for it in f.items if it.kind in CODE | TYPES | {"global", "field", "macro"}]
|
|
832
|
+
for start, end, cond, dline, branch in f.regions:
|
|
833
|
+
macros = [m for m in G.pp_macros(re.sub(r"__has_\w+\s*\([^)]*\)", "", cond)) if m != "__cplusplus" and m not in f.guards]
|
|
834
|
+
if not macros:
|
|
835
|
+
continue
|
|
836
|
+
for it in scoped:
|
|
837
|
+
if it.start > end or it.end < start:
|
|
838
|
+
continue
|
|
839
|
+
inside = start <= it.start and it.end <= end
|
|
840
|
+
if not inside and it.kind not in CODE:
|
|
841
|
+
continue
|
|
842
|
+
for m in macros:
|
|
843
|
+
b.add_node("define", m, name=m, lang=f.lang)
|
|
844
|
+
b.add_edge(self.nid(it), f"define:{m}", "GATED_BY", rel, dline, EXACT, cond=cond,
|
|
845
|
+
scope="item" if inside else "block")
|
|
846
|
+
stats["gated_by_edges"] += 1
|
|
847
|
+
|
|
848
|
+
def _platform_marks(self, rel: str, f) -> None:
|
|
849
|
+
"""Platform conditions (codegraph/platforms.py): #if regions over platform macros (_WIN32, __APPLE__, ...)."""
|
|
850
|
+
from ...platforms import Cond, c_relevant, mark
|
|
851
|
+
for start, end, cond, dline, branch in f.regions:
|
|
852
|
+
if c_relevant(cond):
|
|
853
|
+
txt = f"#{'if' if branch == 'if' else branch} {cond}" if branch in ("if", "elif") else f"#{branch} ({cond})"
|
|
854
|
+
mark(self.b, rel, start, end, Cond("c", cond, txt[:200]), line=dline)
|
|
855
|
+
|
|
856
|
+
def _entries(self, stats):
|
|
857
|
+
b = self.b
|
|
858
|
+
for it in self.items:
|
|
859
|
+
if it.kind not in CODE:
|
|
860
|
+
continue
|
|
861
|
+
n = b.nodes.get(self.nid(it))
|
|
862
|
+
if n is None:
|
|
863
|
+
continue
|
|
864
|
+
path = it.file
|
|
865
|
+
kinds = []
|
|
866
|
+
is_test_file = bool(TEST_DIR.search(path) or TEST_FILE.search(path))
|
|
867
|
+
if it.attrs.get("test_macro"):
|
|
868
|
+
kinds.append("bench" if it.attrs["test_macro"] == "BENCHMARK" else "test")
|
|
869
|
+
if it.kind == "function" and it.name in MAIN_NAMES and not it.qual.count("::"):
|
|
870
|
+
if is_test_file:
|
|
871
|
+
kinds.append("test")
|
|
872
|
+
elif EXAMPLE_DIR.search(path):
|
|
873
|
+
kinds.append("example")
|
|
874
|
+
elif BENCH_DIR.search(path):
|
|
875
|
+
kinds.append("bench")
|
|
876
|
+
else:
|
|
877
|
+
kinds.append("main")
|
|
878
|
+
elif is_test_file and re.match(r"(?i)test", it.name) and it.kind == "function":
|
|
879
|
+
kinds.append("test")
|
|
880
|
+
if not it.static and not is_test_file and not EXAMPLE_DIR.search(path) and "#" not in it.key.split("(")[0]:
|
|
881
|
+
decls = self.decls_by_qual.get(it.qual, [])
|
|
882
|
+
public = n.attrs.get("export") or any(d.export for d in decls)
|
|
883
|
+
if not public:
|
|
884
|
+
hdr = [d for d in decls if INCLUDE_DIR.search(d.file)]
|
|
885
|
+
public = bool(hdr) and all(d.access in (None, "public") for d in hdr)
|
|
886
|
+
if not public and it.kind == "method" and INCLUDE_DIR.search(it.file) and it.attrs.get("access", "public") == "public":
|
|
887
|
+
public = True
|
|
888
|
+
if public and n.attrs.get("access") in ("private",):
|
|
889
|
+
public = False
|
|
890
|
+
if public:
|
|
891
|
+
kinds.append("public_api")
|
|
892
|
+
if not kinds:
|
|
893
|
+
continue
|
|
894
|
+
order = ["main", "test", "bench", "example", "public_api"]
|
|
895
|
+
n.entry_kind = min(kinds, key=order.index)
|
|
896
|
+
stats[f"entry_{n.entry_kind}"] += 1
|
|
897
|
+
|
|
898
|
+
def _gating(self, stats):
|
|
899
|
+
sc = self.scen
|
|
900
|
+
if not sc.active or not (sc.defines or sc.undefined):
|
|
901
|
+
return
|
|
902
|
+
dead = defaultdict(list)
|
|
903
|
+
for rel, f in self.files.items():
|
|
904
|
+
for start, end, cond, dline, branch in f.regions:
|
|
905
|
+
if sc.eval_pp(cond) is False:
|
|
906
|
+
dead[rel].append((start, end, cond, dline))
|
|
907
|
+
n = 0
|
|
908
|
+
for e in self.b.edges.values():
|
|
909
|
+
if e.file in dead and e.line is not None and e.gate is None and e.kind != "GATED_BY":
|
|
910
|
+
for a, z, cond, dline in dead[e.file]:
|
|
911
|
+
if a <= e.line <= z:
|
|
912
|
+
e.gate = sc.name
|
|
913
|
+
e.attrs = {**e.attrs, "guard": f"{e.file}:{dline}", "guard_expr": f"#if {cond}"}
|
|
914
|
+
n += 1
|
|
915
|
+
break
|
|
916
|
+
stats["gated_edges"] = n
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _split_args(args: str) -> list[str]:
|
|
920
|
+
out, depth, cur = [], 0, ""
|
|
921
|
+
for ch in args:
|
|
922
|
+
if ch == "," and depth == 0:
|
|
923
|
+
out.append(cur)
|
|
924
|
+
cur = ""
|
|
925
|
+
continue
|
|
926
|
+
depth += ch in "([{"
|
|
927
|
+
depth -= ch in ")]}"
|
|
928
|
+
cur += ch
|
|
929
|
+
out.append(cur)
|
|
930
|
+
return [x.strip() for x in out]
|