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,881 @@
|
|
|
1
|
+
"""Syntactic layer for C and C++ (tree-sitter-c / tree-sitter-cpp, no preprocessing).
|
|
2
|
+
|
|
3
|
+
Per file: function definitions (with namespace/class qualification), prototypes and in-class method declarations
|
|
4
|
+
(virtual / override / pure, access), classes/structs/unions/enums (+ bases, fields, enumerators), typedefs, globals
|
|
5
|
+
(with initializer ranges: dispatch tables), macros, #includes, getenv() keys, call sites and identifier uses (for the
|
|
6
|
+
heuristic mode), and preprocessor conditional regions (line-based scan, so it works even where tree-sitter cannot
|
|
7
|
+
structure the #if).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from ..native.ts import parser, string_value, text
|
|
15
|
+
|
|
16
|
+
TEST_MACROS = {"TEST", "TEST_F", "TEST_P", "TYPED_TEST", "TYPED_TEST_P", "TEST_CASE", "TEST_CASE_METHOD", "SCENARIO",
|
|
17
|
+
"BOOST_AUTO_TEST_CASE", "BOOST_FIXTURE_TEST_CASE", "BOOST_DATA_TEST_CASE", "CATCH_TEST_CASE",
|
|
18
|
+
"DOCTEST_TEST_CASE", "TEST_CASE_TEMPLATE", "UTEST", "UTEST_F", "CTEST", "CTEST2", "START_TEST", "BENCHMARK"}
|
|
19
|
+
GETENV = {"getenv", "secure_getenv", "_wgetenv", "std::getenv", "g_getenv", "qgetenv", "qEnvironmentVariable",
|
|
20
|
+
"__secure_getenv", "_dupenv_s", "getenv_s"}
|
|
21
|
+
EXPORT_RE = re.compile(r"\b(__declspec\s*\(\s*dllexport\s*\)|__attribute__\s*\(\(\s*visibility\s*\(\s*\"default\"\s*\)\s*\)\)|"
|
|
22
|
+
r"[A-Z][A-Z0-9_]*(?:_API|_EXPORT|_EXTERN|_PUBLIC|_DLL|_VISIBLE|API|EXPORT|EXTERN|PUBLIC)(?:_[A-Z0-9_]+)?)\b")
|
|
23
|
+
NOT_EXPORT = {"EXTERN", "API"} # too generic on their own
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CItem:
|
|
28
|
+
kind: str # function method class struct union enum enumerator typedef global field macro
|
|
29
|
+
name: str
|
|
30
|
+
key: str
|
|
31
|
+
file: str
|
|
32
|
+
line: int
|
|
33
|
+
col: int
|
|
34
|
+
start: int
|
|
35
|
+
end: int
|
|
36
|
+
module: str
|
|
37
|
+
qual: str = "" # qualified name without file prefix / signature
|
|
38
|
+
static: bool = False
|
|
39
|
+
attrs: dict = field(default_factory=dict)
|
|
40
|
+
parent: str | None = None
|
|
41
|
+
scope: list = field(default_factory=list) # raw qualifier of out-of-line definitions (A::B::f -> [A, B])
|
|
42
|
+
ns: list = field(default_factory=list)
|
|
43
|
+
sig: str = ""
|
|
44
|
+
doc: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CDecl:
|
|
49
|
+
name: str
|
|
50
|
+
qual: str
|
|
51
|
+
file: str
|
|
52
|
+
line: int
|
|
53
|
+
col: int
|
|
54
|
+
kind: str # function | method
|
|
55
|
+
export: bool = False
|
|
56
|
+
access: str | None = None
|
|
57
|
+
virtual: bool = False
|
|
58
|
+
pure: bool = False
|
|
59
|
+
override: bool = False
|
|
60
|
+
static: bool = False
|
|
61
|
+
extern_c: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class CFile:
|
|
66
|
+
path: str
|
|
67
|
+
lang: str # c | cpp
|
|
68
|
+
items: list = field(default_factory=list)
|
|
69
|
+
decls: list = field(default_factory=list)
|
|
70
|
+
includes: list = field(default_factory=list) # (path, line, system)
|
|
71
|
+
regions: list = field(default_factory=list) # (start, end, cond, directive line, branch)
|
|
72
|
+
guards: set = field(default_factory=set) # include-guard macro names
|
|
73
|
+
env: list = field(default_factory=list) # (key, line, owner CItem)
|
|
74
|
+
calls: list = field(default_factory=list) # (owner CItem, form, text, name, line, col)
|
|
75
|
+
idents: list = field(default_factory=list) # (owner CItem, name, line, col, followed_by_paren)
|
|
76
|
+
type_refs: list = field(default_factory=list) # (owner CItem, name, line, col)
|
|
77
|
+
bases: list = field(default_factory=list) # (class key, base name text, line, access)
|
|
78
|
+
decl_pos: set = field(default_factory=set) # (line, col) of declaration names (not references)
|
|
79
|
+
lines: list = field(default_factory=list)
|
|
80
|
+
generators: dict = field(default_factory=dict) # function-like macro -> (params, value) when it defines functions
|
|
81
|
+
error_spans: list = field(default_factory=list) # [first, last] lines tree-sitter could not parse (#73)
|
|
82
|
+
top_calls: list = field(default_factory=list) # (name, args, line, col, end line): `NAME(args)` at column 0
|
|
83
|
+
recovered: int = 0 # definitions recovered after a region tree-sitter could not parse
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _first(n, *types):
|
|
87
|
+
for c in n.children:
|
|
88
|
+
if c.type in types:
|
|
89
|
+
return c
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _declarator_name(src, d):
|
|
94
|
+
"""Unwrap pointer/reference/function/array declarators -> (name node, function_declarator or None)."""
|
|
95
|
+
fd = None
|
|
96
|
+
while d is not None:
|
|
97
|
+
t = d.type
|
|
98
|
+
if t == "function_declarator":
|
|
99
|
+
fd = fd or d
|
|
100
|
+
d = d.child_by_field_name("declarator")
|
|
101
|
+
elif t in ("pointer_declarator", "reference_declarator", "array_declarator", "init_declarator",
|
|
102
|
+
"parenthesized_declarator", "attributed_declarator", "abstract_function_declarator"):
|
|
103
|
+
nxt = d.child_by_field_name("declarator")
|
|
104
|
+
if nxt is None:
|
|
105
|
+
nxt = next((c for c in d.named_children if c.type.endswith("declarator") or c.type in
|
|
106
|
+
("identifier", "field_identifier", "qualified_identifier", "destructor_name", "operator_name")), None)
|
|
107
|
+
d = nxt
|
|
108
|
+
else:
|
|
109
|
+
return d, fd
|
|
110
|
+
return None, fd
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _qual_parts(src, n) -> tuple[list[str], str]:
|
|
114
|
+
"""qualified_identifier A::B::f -> ([A, B], 'f'); other name nodes -> ([], text)."""
|
|
115
|
+
scope = []
|
|
116
|
+
while n is not None and n.type == "qualified_identifier":
|
|
117
|
+
s = n.child_by_field_name("scope")
|
|
118
|
+
if s is not None:
|
|
119
|
+
st = text(src, s)
|
|
120
|
+
scope.append(re.sub(r"<.*>", "", st).strip())
|
|
121
|
+
n = n.child_by_field_name("name")
|
|
122
|
+
if n is None:
|
|
123
|
+
return scope, "?"
|
|
124
|
+
if n.type == "template_function":
|
|
125
|
+
n = n.child_by_field_name("name") or n
|
|
126
|
+
return scope, re.sub(r"\s+", "", text(src, n))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _params_sig(src, fd) -> str:
|
|
130
|
+
if fd is None:
|
|
131
|
+
return ""
|
|
132
|
+
pl = fd.child_by_field_name("parameters")
|
|
133
|
+
if pl is None:
|
|
134
|
+
return ""
|
|
135
|
+
parts = []
|
|
136
|
+
for p in pl.named_children:
|
|
137
|
+
if p.type in ("parameter_declaration", "optional_parameter_declaration"):
|
|
138
|
+
ty = p.child_by_field_name("type")
|
|
139
|
+
t = text(src, ty) if ty is not None else "?"
|
|
140
|
+
d = p.child_by_field_name("declarator")
|
|
141
|
+
if d is not None and d.type in ("pointer_declarator", "reference_declarator"):
|
|
142
|
+
t += "*" if d.type == "pointer_declarator" else "&"
|
|
143
|
+
parts.append(re.sub(r"\s+", " ", t))
|
|
144
|
+
elif p.type == "variadic_parameter":
|
|
145
|
+
parts.append("...")
|
|
146
|
+
return ",".join(parts)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Extractor:
|
|
150
|
+
def __init__(self, path: str, src: bytes, lang: str, module: str, blank: re.Pattern | None = None):
|
|
151
|
+
self.src, self.path, self.module = src, path, module
|
|
152
|
+
self.blank = blank
|
|
153
|
+
self.f = CFile(path, lang)
|
|
154
|
+
|
|
155
|
+
def run(self) -> CFile:
|
|
156
|
+
tree = parser(self.f.lang).parse(mask_annotations(self.src, self.blank) if self.blank is not None else self.src)
|
|
157
|
+
self.f.lines = self.src.decode("utf-8", "replace").split("\n")
|
|
158
|
+
if tree.root_node.has_error:
|
|
159
|
+
from ...core.syntax_errors import tree_spans
|
|
160
|
+
self.f.error_spans = tree_spans(tree.root_node)
|
|
161
|
+
self.scan_directives()
|
|
162
|
+
self.container(tree.root_node, {"ns": [], "cls": None, "access": None, "anon": False, "extern_c": False})
|
|
163
|
+
lines = self.f.lines
|
|
164
|
+
for i, ln in enumerate(lines):
|
|
165
|
+
if (ln[:1].isalpha() or ln[:1] == "_") and "(" in ln:
|
|
166
|
+
m = TOP_CALL.match(ln)
|
|
167
|
+
if m and m.group(1) not in C_KEYWORDS:
|
|
168
|
+
self.f.top_calls.append((m.group(1), m.group(2), i + 1, 0, i + 1))
|
|
169
|
+
continue
|
|
170
|
+
# `NAME(a,` continued on the next lines (up to 12)
|
|
171
|
+
if re.match(r"[A-Za-z_]\w*\s*\(", ln) and ln.count("(") > ln.count(")") and not ln.rstrip().endswith(("{", ";")):
|
|
172
|
+
j, buf = i, ln
|
|
173
|
+
while buf.count("(") > buf.count(")") and j + 1 < len(lines) and j - i < 12:
|
|
174
|
+
j += 1
|
|
175
|
+
buf += " " + lines[j].strip()
|
|
176
|
+
m = TOP_CALL.match(buf)
|
|
177
|
+
if m and m.group(1) not in C_KEYWORDS:
|
|
178
|
+
self.f.top_calls.append((m.group(1), m.group(2), i + 1, 0, j + 1))
|
|
179
|
+
if tree.root_node.has_error:
|
|
180
|
+
self.recover()
|
|
181
|
+
return self.f
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------- error recovery
|
|
184
|
+
def recover(self):
|
|
185
|
+
"""Top-level definitions in or after a region tree-sitter could not parse (unbalanced macro arguments,
|
|
186
|
+
unknown syntax): every brace block at file level that no extracted item starts in is parsed again on its own
|
|
187
|
+
(same line / column positions); a block that still fails gives a function from its head (calls by name)."""
|
|
188
|
+
txt = self.src.decode("utf-8", "replace")
|
|
189
|
+
spans = [(it.start, it.end) for it in self.f.items if it.kind != "macro"]
|
|
190
|
+
names = {it.name for it in self.f.items if it.kind in ("function", "method")}
|
|
191
|
+
line_at = _line_index(txt)
|
|
192
|
+
rec_spans = []
|
|
193
|
+
for a, z in _toplevel_blocks(txt):
|
|
194
|
+
sl, el = line_at(a), line_at(z)
|
|
195
|
+
# covered: an item starts in the block (or just before it: a macro / attribute line on top)
|
|
196
|
+
if any(sl - 3 <= x <= el and y >= sl for x, y in spans):
|
|
197
|
+
continue
|
|
198
|
+
scol = a - txt.rfind("\n", 0, a) - 1
|
|
199
|
+
chunk = "\n" * (sl - 1) + " " * scol + _unwrap(txt[a:z + 1])
|
|
200
|
+
sub = Extractor(self.path, chunk.encode("utf-8"), self.f.lang, self.module, self.blank)
|
|
201
|
+
sub.f = self.f
|
|
202
|
+
n0 = len(self.f.items)
|
|
203
|
+
t = parser(self.f.lang).parse(mask_annotations(sub.src, self.blank) if self.blank is not None else sub.src)
|
|
204
|
+
if not t.root_node.has_error:
|
|
205
|
+
sub.container(t.root_node, {"ns": [], "cls": None, "access": None, "anon": False, "extern_c": False})
|
|
206
|
+
if len(self.f.items) == n0:
|
|
207
|
+
self._head_fn(txt, a, z, sl, el, line_at)
|
|
208
|
+
new = self.f.items[n0:]
|
|
209
|
+
# a name the file already defines (the main parse placed it elsewhere): keep the first
|
|
210
|
+
keep = [it for it in new if not (it.kind in ("function", "method") and it.name in names)]
|
|
211
|
+
if len(keep) != len(new):
|
|
212
|
+
drop = {id(it) for it in new} - {id(it) for it in keep}
|
|
213
|
+
self.f.items[n0:] = keep
|
|
214
|
+
self.f.calls = [c for c in self.f.calls if id(c[0]) not in drop]
|
|
215
|
+
self.f.idents = [c for c in self.f.idents if id(c[0]) not in drop]
|
|
216
|
+
self.f.type_refs = [c for c in self.f.type_refs if id(c[0]) not in drop]
|
|
217
|
+
for it in keep:
|
|
218
|
+
it.attrs.setdefault("recovered", True)
|
|
219
|
+
names.add(it.name)
|
|
220
|
+
rec_spans.append((it.start, it.end, id(it)))
|
|
221
|
+
self.f.recovered += len(keep)
|
|
222
|
+
if rec_spans:
|
|
223
|
+
# a definition the main parse ran on into the recovered ones: clipped, and the references inside the
|
|
224
|
+
# recovered definitions belong to them only
|
|
225
|
+
rec_ids = {r[2] for r in rec_spans}
|
|
226
|
+
first = min(r[0] for r in rec_spans)
|
|
227
|
+
for it in self.f.items:
|
|
228
|
+
if id(it) not in rec_ids and it.start < first <= it.end and it.kind in ("function", "method"):
|
|
229
|
+
it.end = max(it.start, max((y for x, y in _block_spans_before(txt, line_at, first)), default=it.start))
|
|
230
|
+
|
|
231
|
+
def inside(line, owner):
|
|
232
|
+
return id(owner) not in rec_ids and any(x <= line <= y for x, y, _ in rec_spans)
|
|
233
|
+
self.f.calls = [c for c in self.f.calls if not inside(c[4], c[0])]
|
|
234
|
+
self.f.idents = [c for c in self.f.idents if not inside(c[2], c[0])]
|
|
235
|
+
self.f.type_refs = [c for c in self.f.type_refs if not inside(c[2], c[0])]
|
|
236
|
+
|
|
237
|
+
def _head_fn(self, txt, a, z, sl, el, line_at):
|
|
238
|
+
b = txt.find("{", a, z + 1)
|
|
239
|
+
head = re.sub(r"\s+", " ", _unwrap(txt[a:b])).strip()
|
|
240
|
+
m = HEAD_FN.search(head)
|
|
241
|
+
if not m or m.group(1) in C_KEYWORDS or re.match(r"(typedef|struct|union|enum|class|namespace|extern)\b", head) or "=" in head:
|
|
242
|
+
return
|
|
243
|
+
name = m.group(1)
|
|
244
|
+
npos = txt.find(name, a, b)
|
|
245
|
+
ln = line_at(npos)
|
|
246
|
+
col = npos - txt.rfind("\n", 0, npos) - 1
|
|
247
|
+
static = head.startswith("static ") or " static " in head[:m.start()]
|
|
248
|
+
it = CItem("function", name, f"{self.path}#{name}" if static else name, self.path, ln, col, sl, el, self.module,
|
|
249
|
+
name, static=static, attrs={"recovered": "head"})
|
|
250
|
+
self.f.items.append(it)
|
|
251
|
+
for c in CALL_RE.finditer(txt, b, z):
|
|
252
|
+
nm = c.group(1)
|
|
253
|
+
if nm not in C_KEYWORDS:
|
|
254
|
+
p = c.start(1)
|
|
255
|
+
self.f.calls.append((it, "name", nm, nm, line_at(p), p - txt.rfind("\n", 0, p) - 1))
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------- preprocessor regions (line based)
|
|
258
|
+
def scan_directives(self):
|
|
259
|
+
lines = self.f.lines
|
|
260
|
+
stack = []
|
|
261
|
+
first_directive = True
|
|
262
|
+
i = 0
|
|
263
|
+
n = len(lines)
|
|
264
|
+
while i < n:
|
|
265
|
+
raw = lines[i]
|
|
266
|
+
ln = i + 1
|
|
267
|
+
s = raw.strip()
|
|
268
|
+
while s.endswith("\\") and i + 1 < n:
|
|
269
|
+
i += 1
|
|
270
|
+
s = s[:-1] + " " + lines[i].strip()
|
|
271
|
+
i += 1
|
|
272
|
+
if not s.startswith("#"):
|
|
273
|
+
if s and not s.startswith(("//", "/*", "*")):
|
|
274
|
+
first_directive = False
|
|
275
|
+
continue
|
|
276
|
+
m = re.match(r"#\s*(\w+)\s*(.*)", s)
|
|
277
|
+
if not m:
|
|
278
|
+
continue
|
|
279
|
+
d, rest = m.group(1), re.sub(r"/\*.*?\*/|//.*$", "", m.group(2)).strip()
|
|
280
|
+
if d in ("if", "ifdef", "ifndef"):
|
|
281
|
+
cond = rest if d == "if" else (f"defined({rest.split()[0]})" if d == "ifdef" else f"!defined({rest.split()[0] if rest else '?'})")
|
|
282
|
+
guard = None
|
|
283
|
+
if d == "ifndef" and first_directive and rest:
|
|
284
|
+
nxt = next((x.strip() for x in lines[i:i + 3] if x.strip()), "")
|
|
285
|
+
if re.match(rf"#\s*define\s+{re.escape(rest.split()[0])}\b", nxt):
|
|
286
|
+
guard = rest.split()[0]
|
|
287
|
+
self.f.guards.add(guard)
|
|
288
|
+
stack.append({"start": ln, "conds": [cond], "cur": cond, "line": ln, "branch": "if", "guard": guard})
|
|
289
|
+
elif d in ("elif", "else", "elifdef", "elifndef") and stack:
|
|
290
|
+
fr = stack[-1]
|
|
291
|
+
self._close(fr, ln - 1)
|
|
292
|
+
prev = " && ".join(f"!({c})" for c in fr["conds"])
|
|
293
|
+
if d == "else":
|
|
294
|
+
cond = prev
|
|
295
|
+
else:
|
|
296
|
+
e = rest if d == "elif" else (f"defined({rest})" if d == "elifdef" else f"!defined({rest})")
|
|
297
|
+
cond = f"{prev} && ({e})"
|
|
298
|
+
fr["conds"].append(e)
|
|
299
|
+
fr.update({"start": ln, "cur": cond, "line": ln, "branch": d})
|
|
300
|
+
elif d == "endif" and stack:
|
|
301
|
+
fr = stack.pop()
|
|
302
|
+
self._close(fr, ln - 1)
|
|
303
|
+
first_directive = first_directive and d in ("ifndef", "define", "pragma", "if", "ifdef")
|
|
304
|
+
|
|
305
|
+
def _close(self, fr, end):
|
|
306
|
+
if fr.get("guard"):
|
|
307
|
+
return
|
|
308
|
+
if end >= fr["start"]:
|
|
309
|
+
self.f.regions.append((fr["start"], end, fr["cur"], fr["line"], fr["branch"]))
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------- declarations
|
|
312
|
+
def _qual(self, ctx, parts: list[str]) -> str:
|
|
313
|
+
return "::".join([p for p in ctx["ns"] if p] + parts)
|
|
314
|
+
|
|
315
|
+
def _key(self, ctx, qual: str, static: bool) -> str:
|
|
316
|
+
if static or ctx["anon"]:
|
|
317
|
+
return f"{self.path}#{qual}"
|
|
318
|
+
return qual
|
|
319
|
+
|
|
320
|
+
def _add(self, kind, name, key, qual, n, name_node, **kw) -> CItem:
|
|
321
|
+
line = (name_node or n).start_point[0] + 1
|
|
322
|
+
col = (name_node or n).start_point[1]
|
|
323
|
+
it = CItem(kind, name, key, self.path, line, col, n.start_point[0] + 1, n.end_point[0] + 1, self.module, qual, **kw)
|
|
324
|
+
it.doc = self._doc(n)
|
|
325
|
+
self.f.items.append(it)
|
|
326
|
+
return it
|
|
327
|
+
|
|
328
|
+
def _doc(self, n) -> str | None:
|
|
329
|
+
p = n.prev_sibling
|
|
330
|
+
docs = []
|
|
331
|
+
while p is not None and p.type == "comment" and p.end_point[0] >= n.start_point[0] - 1 - len(docs) * 0:
|
|
332
|
+
docs.insert(0, text(self.src, p))
|
|
333
|
+
if len(docs) > 6:
|
|
334
|
+
break
|
|
335
|
+
p = p.prev_sibling
|
|
336
|
+
if not docs:
|
|
337
|
+
return None
|
|
338
|
+
s = "\n".join(re.sub(r"^\s*(/\*+|\*+/|\*|//+!?|///?)", "", ln).rstrip(" */") for d in docs for ln in d.splitlines())
|
|
339
|
+
s = s.strip()
|
|
340
|
+
return s[:1500] or None
|
|
341
|
+
|
|
342
|
+
def container(self, node, ctx):
|
|
343
|
+
for c in node.children:
|
|
344
|
+
self.decl(c, ctx)
|
|
345
|
+
|
|
346
|
+
def decl(self, n, ctx):
|
|
347
|
+
t = n.type
|
|
348
|
+
src = self.src
|
|
349
|
+
if t == "ERROR":
|
|
350
|
+
# a `#define` tree-sitter gave up on (a comment inside a continued macro body): the macro still exists
|
|
351
|
+
kids = n.children
|
|
352
|
+
for i, c in enumerate(kids):
|
|
353
|
+
if c.type == "#define" and i + 1 < len(kids) and kids[i + 1].type == "identifier":
|
|
354
|
+
name = text(src, kids[i + 1])
|
|
355
|
+
if name not in self.f.guards:
|
|
356
|
+
fl = i + 2 < len(kids) and kids[i + 2].type == "preproc_params"
|
|
357
|
+
it = self._add("macro", name, name, name, c, kids[i + 1], attrs={"function_like": fl})
|
|
358
|
+
ln = c.start_point[0]
|
|
359
|
+
body = self.f.lines[ln].split(name, 1)[-1] if ln < len(self.f.lines) else ""
|
|
360
|
+
if fl:
|
|
361
|
+
body = re.sub(r"^\s*\([^()]*\)", "", body)
|
|
362
|
+
it.attrs["value"] = body.strip().rstrip("\\").strip()[:120] or None
|
|
363
|
+
if t in ("preproc_if", "preproc_ifdef", "preproc_elif", "preproc_else", "preproc_elifdef", "ERROR", "declaration_list"):
|
|
364
|
+
self.container(n, ctx)
|
|
365
|
+
return
|
|
366
|
+
if t == "preproc_include":
|
|
367
|
+
p = n.child_by_field_name("path")
|
|
368
|
+
if p is not None:
|
|
369
|
+
s = text(src, p)
|
|
370
|
+
self.f.includes.append((s.strip('<>"'), n.start_point[0] + 1, s.startswith("<")))
|
|
371
|
+
return
|
|
372
|
+
if t in ("preproc_def", "preproc_function_def"):
|
|
373
|
+
nm = n.child_by_field_name("name")
|
|
374
|
+
if nm is not None:
|
|
375
|
+
name = text(src, nm)
|
|
376
|
+
if name not in self.f.guards:
|
|
377
|
+
it = self._add("macro", name, name, name, n, nm, attrs={"function_like": t == "preproc_function_def"})
|
|
378
|
+
val = n.child_by_field_name("value")
|
|
379
|
+
it.attrs["value"] = text(src, val)[:120] if val is not None else None
|
|
380
|
+
ps = n.child_by_field_name("parameters")
|
|
381
|
+
if val is not None and ps is not None and "{" in text(src, val):
|
|
382
|
+
params = [x.strip() for x in text(src, ps).strip("()").split(",") if x.strip()]
|
|
383
|
+
if generated_names(params, text(src, val), params):
|
|
384
|
+
self.f.generators[name] = (params, text(src, val))
|
|
385
|
+
return
|
|
386
|
+
if t == "namespace_definition":
|
|
387
|
+
nm = n.child_by_field_name("name")
|
|
388
|
+
body = n.child_by_field_name("body")
|
|
389
|
+
name = text(src, nm) if nm is not None else ""
|
|
390
|
+
c2 = dict(ctx, ns=ctx["ns"] + (name.split("::") if name else []), anon=ctx["anon"] or not name)
|
|
391
|
+
if body is not None:
|
|
392
|
+
self.container(body, c2)
|
|
393
|
+
return
|
|
394
|
+
if t == "linkage_specification":
|
|
395
|
+
body = n.child_by_field_name("body")
|
|
396
|
+
c2 = dict(ctx, extern_c=True)
|
|
397
|
+
if body is not None:
|
|
398
|
+
if body.type == "declaration_list":
|
|
399
|
+
self.container(body, c2)
|
|
400
|
+
else:
|
|
401
|
+
self.decl(body, c2)
|
|
402
|
+
return
|
|
403
|
+
if t == "template_declaration":
|
|
404
|
+
for c in n.named_children:
|
|
405
|
+
if c.type not in ("template_parameter_list",):
|
|
406
|
+
self.decl(c, ctx)
|
|
407
|
+
return
|
|
408
|
+
if t in ("class_specifier", "struct_specifier", "union_specifier", "enum_specifier"):
|
|
409
|
+
self.type_spec(n, ctx)
|
|
410
|
+
return
|
|
411
|
+
if t == "function_definition":
|
|
412
|
+
self.func_def(n, ctx)
|
|
413
|
+
return
|
|
414
|
+
if t in ("declaration", "field_declaration"):
|
|
415
|
+
self.declaration(n, ctx)
|
|
416
|
+
return
|
|
417
|
+
if t == "type_definition":
|
|
418
|
+
ty = n.child_by_field_name("type")
|
|
419
|
+
if ty is not None and ty.type in ("struct_specifier", "union_specifier", "enum_specifier", "class_specifier"):
|
|
420
|
+
tname = None
|
|
421
|
+
for d in n.children_by_field_name("declarator"):
|
|
422
|
+
nm, _ = _declarator_name(src, d)
|
|
423
|
+
if nm is not None and nm.type in ("type_identifier", "identifier"):
|
|
424
|
+
tname = text(src, nm)
|
|
425
|
+
break
|
|
426
|
+
self.type_spec(ty, ctx, alias=tname)
|
|
427
|
+
for d in n.children_by_field_name("declarator"):
|
|
428
|
+
nm, fd = _declarator_name(src, d)
|
|
429
|
+
if nm is not None and nm.type in ("type_identifier", "identifier", "primitive_type"):
|
|
430
|
+
name = text(src, nm)
|
|
431
|
+
q = f"{ctx['cls']['qual']}::{name}" if ctx["cls"] else self._qual(ctx, [name])
|
|
432
|
+
if not any(x.kind in ("struct", "union", "enum", "class") and x.key == q and x.line == nm.start_point[0] + 1 for x in self.f.items):
|
|
433
|
+
self._add("typedef", name, q, q, n, nm, attrs={"of": re.sub(r"\s+", " ", text(src, ty))[:80] if ty is not None else None})
|
|
434
|
+
return
|
|
435
|
+
if t == "alias_declaration":
|
|
436
|
+
nm = n.child_by_field_name("name")
|
|
437
|
+
if nm is not None:
|
|
438
|
+
name = text(src, nm)
|
|
439
|
+
q = f"{ctx['cls']['qual']}::{name}" if ctx["cls"] else self._qual(ctx, [name])
|
|
440
|
+
self._add("typedef", name, q, q, n, nm, parent=ctx["cls"]["key"] if ctx["cls"] else None)
|
|
441
|
+
return
|
|
442
|
+
if t in ("access_specifier",):
|
|
443
|
+
ctx["access"] = text(src, n).strip(": ")
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
def type_spec(self, n, ctx, alias: str | None = None):
|
|
447
|
+
src = self.src
|
|
448
|
+
body = n.child_by_field_name("body")
|
|
449
|
+
nm = n.child_by_field_name("name")
|
|
450
|
+
if body is None:
|
|
451
|
+
return
|
|
452
|
+
kind = {"class_specifier": "class", "struct_specifier": "struct", "union_specifier": "union", "enum_specifier": "enum"}[n.type]
|
|
453
|
+
spec = ""
|
|
454
|
+
if nm is not None and nm.type == "template_type":
|
|
455
|
+
# explicit / partial specialization `struct S<T*, int>`: name S, key S<T*,int>
|
|
456
|
+
args = nm.child_by_field_name("arguments")
|
|
457
|
+
spec = re.sub(r"\s+", "", text(src, args)) if args is not None else ""
|
|
458
|
+
nm = nm.child_by_field_name("name") or nm
|
|
459
|
+
elif nm is not None and nm.type == "qualified_identifier":
|
|
460
|
+
nn = nm.child_by_field_name("name")
|
|
461
|
+
nm = nn if nn is not None and nn.type == "type_identifier" else nm
|
|
462
|
+
name = text(src, nm) if nm is not None else alias
|
|
463
|
+
if not name:
|
|
464
|
+
name = None
|
|
465
|
+
if name:
|
|
466
|
+
parent_q = ctx["cls"]["qual"] if ctx["cls"] else None
|
|
467
|
+
q = (f"{parent_q}::{name}" if parent_q else self._qual(ctx, [name])) + spec
|
|
468
|
+
it = self._add(kind, name, self._key(ctx, q, False), q, n, nm if nm is not None else n, parent=ctx["cls"]["key"] if ctx["cls"] else None)
|
|
469
|
+
key = it.key
|
|
470
|
+
else:
|
|
471
|
+
q = key = None
|
|
472
|
+
if kind == "enum":
|
|
473
|
+
for e in body.named_children:
|
|
474
|
+
if e.type == "enumerator":
|
|
475
|
+
en = e.child_by_field_name("name")
|
|
476
|
+
if en is not None:
|
|
477
|
+
ename = text(src, en)
|
|
478
|
+
eq = f"{q}::{ename}" if q and n.children and any(c.type in ("class", "struct") for c in n.children) else self._qual(ctx, [ename])
|
|
479
|
+
self._add("enumerator", ename, self._key(ctx, eq, False), eq, e, en, parent=key)
|
|
480
|
+
return
|
|
481
|
+
bc = _first(n, "base_class_clause")
|
|
482
|
+
if bc is not None and key:
|
|
483
|
+
access = None
|
|
484
|
+
for c in bc.children:
|
|
485
|
+
if c.type == "access_specifier":
|
|
486
|
+
access = text(src, c)
|
|
487
|
+
elif c.type in ("type_identifier", "qualified_identifier", "template_type"):
|
|
488
|
+
self.f.bases.append((key, re.sub(r"<.*>", "", text(src, c)), c.start_point[0] + 1, access))
|
|
489
|
+
access = None
|
|
490
|
+
if key is None:
|
|
491
|
+
# anonymous struct/union: members belong to the enclosing scope; still walk for nested types
|
|
492
|
+
c2 = ctx
|
|
493
|
+
else:
|
|
494
|
+
c2 = dict(ctx, cls={"qual": q, "key": key, "kind": kind}, access="private" if kind == "class" else "public")
|
|
495
|
+
for c in body.children:
|
|
496
|
+
if c.type == "access_specifier":
|
|
497
|
+
c2["access"] = text(src, c).strip(": ")
|
|
498
|
+
continue
|
|
499
|
+
if c.type == "field_declaration" and key:
|
|
500
|
+
self.field_decl(c, c2)
|
|
501
|
+
elif c.type in ("function_definition", "template_declaration", "declaration", "class_specifier", "struct_specifier",
|
|
502
|
+
"union_specifier", "enum_specifier", "type_definition", "alias_declaration", "preproc_if", "preproc_ifdef",
|
|
503
|
+
"friend_declaration"):
|
|
504
|
+
if c.type == "friend_declaration":
|
|
505
|
+
continue
|
|
506
|
+
self.decl(c, c2)
|
|
507
|
+
|
|
508
|
+
def field_decl(self, n, ctx):
|
|
509
|
+
src = self.src
|
|
510
|
+
ty = n.child_by_field_name("type")
|
|
511
|
+
if ty is not None and ty.type in ("struct_specifier", "union_specifier", "enum_specifier", "class_specifier") and ty.child_by_field_name("body") is not None:
|
|
512
|
+
self.type_spec(ty, ctx)
|
|
513
|
+
txt_types = [c.type for c in n.children]
|
|
514
|
+
for d in n.children_by_field_name("declarator"):
|
|
515
|
+
nm, fd = _declarator_name(src, d)
|
|
516
|
+
if nm is None:
|
|
517
|
+
continue
|
|
518
|
+
if fd is not None: # method declaration
|
|
519
|
+
scope, name = _qual_parts(src, nm)
|
|
520
|
+
virt = "virtual" in txt_types or any(c.type == "virtual" for c in n.children)
|
|
521
|
+
vs = _first(fd, "virtual_specifier")
|
|
522
|
+
override = vs is not None
|
|
523
|
+
pure = n.child_by_field_name("default_value") is not None and text(src, n.child_by_field_name("default_value")) == "0"
|
|
524
|
+
q = f"{ctx['cls']['qual']}::{name}"
|
|
525
|
+
self.f.decls.append(CDecl(name, q, self.path, nm.start_point[0] + 1, nm.start_point[1], "method",
|
|
526
|
+
export=False, access=ctx.get("access"), virtual=virt or override, pure=pure,
|
|
527
|
+
override=override, static="static" in [text(src, c) for c in n.children if c.type == "storage_class_specifier"]))
|
|
528
|
+
self.f.decl_pos.add((nm.start_point[0] + 1, nm.start_point[1]))
|
|
529
|
+
elif nm.type in ("field_identifier", "identifier"):
|
|
530
|
+
name = text(src, nm)
|
|
531
|
+
q = f"{ctx['cls']['qual']}::{name}"
|
|
532
|
+
fit = self._add("field", name, f"{ctx['cls']['key']}::{name}", q, n, nm, parent=ctx["cls"]["key"],
|
|
533
|
+
attrs={"access": ctx.get("access")} if self.f.lang == "cpp" else {})
|
|
534
|
+
if ty is not None:
|
|
535
|
+
self._type_refs(ty, fit)
|
|
536
|
+
|
|
537
|
+
def declaration(self, n, ctx):
|
|
538
|
+
src = self.src
|
|
539
|
+
if n.type == "field_declaration" and ctx.get("cls"):
|
|
540
|
+
self.field_decl(n, ctx)
|
|
541
|
+
return
|
|
542
|
+
storage = [text(src, c) for c in n.children if c.type == "storage_class_specifier"]
|
|
543
|
+
static = "static" in storage
|
|
544
|
+
ty = n.child_by_field_name("type")
|
|
545
|
+
if ty is not None and ty.type in ("struct_specifier", "union_specifier", "enum_specifier", "class_specifier") and ty.child_by_field_name("body") is not None:
|
|
546
|
+
self.type_spec(ty, ctx)
|
|
547
|
+
for d in n.children_by_field_name("declarator"):
|
|
548
|
+
nm, fd = _declarator_name(src, d)
|
|
549
|
+
if nm is None:
|
|
550
|
+
continue
|
|
551
|
+
scope, name = _qual_parts(src, nm)
|
|
552
|
+
if fd is not None:
|
|
553
|
+
prefix = src[n.start_byte:nm.start_byte].decode("utf-8", "replace")
|
|
554
|
+
exp = bool(EXPORT_RE.search(prefix)) and not all(m.group(0) in NOT_EXPORT for m in EXPORT_RE.finditer(prefix))
|
|
555
|
+
q = self._qual(ctx, scope + [name]) if not ctx.get("cls") else f"{ctx['cls']['qual']}::{name}"
|
|
556
|
+
self.f.decls.append(CDecl(name, q, self.path, nm.start_point[0] + 1, nm.start_point[1],
|
|
557
|
+
"method" if ctx.get("cls") else "function", export=exp, access=ctx.get("access"),
|
|
558
|
+
static=static, extern_c=ctx["extern_c"]))
|
|
559
|
+
self.f.decl_pos.add((nm.start_point[0] + 1, nm.start_point[1]))
|
|
560
|
+
continue
|
|
561
|
+
if "extern" in storage or "typedef" in storage:
|
|
562
|
+
self.f.decl_pos.add((nm.start_point[0] + 1, nm.start_point[1]))
|
|
563
|
+
continue
|
|
564
|
+
if nm.type not in ("identifier", "qualified_identifier", "field_identifier"):
|
|
565
|
+
continue
|
|
566
|
+
q = self._qual(ctx, scope + [name])
|
|
567
|
+
it = self._add("global", name, self._key(ctx, q, static), q, n, nm, static=static,
|
|
568
|
+
attrs={"const": any(text(src, c) in ("const", "constexpr") for c in n.children if c.type == "type_qualifier")})
|
|
569
|
+
val = d.child_by_field_name("value") if d.type == "init_declarator" else None
|
|
570
|
+
if val is not None:
|
|
571
|
+
self.body(val, it)
|
|
572
|
+
if ty is not None:
|
|
573
|
+
self._type_refs(ty, it)
|
|
574
|
+
|
|
575
|
+
def func_def(self, n, ctx):
|
|
576
|
+
src = self.src
|
|
577
|
+
ty = n.child_by_field_name("type")
|
|
578
|
+
if ty is not None and text(src, ty) in ("namespace", "inline namespace"):
|
|
579
|
+
# error recovery sometimes reads `namespace x {` as a function `namespace x() {}`
|
|
580
|
+
d = n.child_by_field_name("declarator")
|
|
581
|
+
body = n.child_by_field_name("body")
|
|
582
|
+
name = text(src, d) if d is not None and d.type in ("identifier", "field_identifier", "type_identifier") else ""
|
|
583
|
+
if body is not None:
|
|
584
|
+
self.container(body, dict(ctx, ns=ctx["ns"] + ([name] if name else []), anon=ctx["anon"] or not name))
|
|
585
|
+
return
|
|
586
|
+
d = n.child_by_field_name("declarator")
|
|
587
|
+
nm, fd = _declarator_name(src, d)
|
|
588
|
+
if nm is None:
|
|
589
|
+
return
|
|
590
|
+
storage = [text(src, c) for c in n.children if c.type == "storage_class_specifier"]
|
|
591
|
+
static = "static" in storage
|
|
592
|
+
scope, name = _qual_parts(src, nm)
|
|
593
|
+
attrs = {}
|
|
594
|
+
if name in TEST_MACROS and fd is not None:
|
|
595
|
+
args = [re.sub(r"\s+", "", text(src, p)) for p in fd.child_by_field_name("parameters").named_children] if fd.child_by_field_name("parameters") is not None else []
|
|
596
|
+
label = ".".join(a.strip('"') for a in args[:2]) or name
|
|
597
|
+
q = f"{name}({label})"
|
|
598
|
+
it = self._add("function", label, f"{self.path}#{q}", q, n, nm, attrs={"test_macro": name})
|
|
599
|
+
body = n.child_by_field_name("body")
|
|
600
|
+
if body is not None:
|
|
601
|
+
self.body(body, it)
|
|
602
|
+
return
|
|
603
|
+
prefix = src[n.start_byte:nm.start_byte].decode("utf-8", "replace")
|
|
604
|
+
if EXPORT_RE.search(prefix) and not all(m.group(0) in NOT_EXPORT for m in EXPORT_RE.finditer(prefix)):
|
|
605
|
+
attrs["export"] = True
|
|
606
|
+
if any(c.type == "virtual" for c in n.children):
|
|
607
|
+
attrs["virtual"] = True
|
|
608
|
+
vs = _first(fd, "virtual_specifier") if fd is not None else None
|
|
609
|
+
if vs is not None:
|
|
610
|
+
attrs["override"] = True
|
|
611
|
+
attrs["virtual"] = True
|
|
612
|
+
if ctx["extern_c"]:
|
|
613
|
+
attrs["extern_c"] = True
|
|
614
|
+
sig = _params_sig(src, fd)
|
|
615
|
+
if ctx.get("cls"):
|
|
616
|
+
q = f"{ctx['cls']['qual']}::{name}"
|
|
617
|
+
kind = "method"
|
|
618
|
+
if ctx.get("access") and self.f.lang == "cpp":
|
|
619
|
+
attrs["access"] = ctx["access"]
|
|
620
|
+
it = self._add(kind, name, f"{ctx['cls']['key']}::{name}", q, n, nm, parent=ctx["cls"]["key"], attrs=attrs, sig=sig)
|
|
621
|
+
else:
|
|
622
|
+
q = self._qual(ctx, scope + [name])
|
|
623
|
+
kind = "method" if scope else "function" # refined later (scope may be a namespace)
|
|
624
|
+
it = self._add(kind, name, self._key(ctx, q, static), q, n, nm, static=static, attrs=attrs, scope=scope,
|
|
625
|
+
ns=list(ctx["ns"]), sig=sig)
|
|
626
|
+
body = n.child_by_field_name("body")
|
|
627
|
+
if body is not None:
|
|
628
|
+
self.body(body, it)
|
|
629
|
+
# constructor initializer lists
|
|
630
|
+
fil = _first(n, "field_initializer_list")
|
|
631
|
+
if fil is not None:
|
|
632
|
+
self.body(fil, it)
|
|
633
|
+
|
|
634
|
+
def _type_refs(self, node, owner):
|
|
635
|
+
stack = [node]
|
|
636
|
+
while stack:
|
|
637
|
+
x = stack.pop()
|
|
638
|
+
if x.type == "type_identifier":
|
|
639
|
+
self.f.type_refs.append((owner, text(self.src, x), x.start_point[0] + 1, x.start_point[1]))
|
|
640
|
+
stack.extend(x.children)
|
|
641
|
+
|
|
642
|
+
# ---------------------------------------------------------------- bodies
|
|
643
|
+
def body(self, node, owner):
|
|
644
|
+
src = self.src
|
|
645
|
+
stack = [node]
|
|
646
|
+
while stack:
|
|
647
|
+
x = stack.pop()
|
|
648
|
+
t = x.type
|
|
649
|
+
if t == "call_expression":
|
|
650
|
+
fn = x.child_by_field_name("function")
|
|
651
|
+
if fn is not None:
|
|
652
|
+
if fn.type == "identifier":
|
|
653
|
+
nm = text(src, fn)
|
|
654
|
+
self.f.calls.append((owner, "name", nm, nm, fn.start_point[0] + 1, fn.start_point[1]))
|
|
655
|
+
elif fn.type == "qualified_identifier":
|
|
656
|
+
scope, nm = _qual_parts(src, fn)
|
|
657
|
+
nn = fn
|
|
658
|
+
while nn.type == "qualified_identifier" and nn.child_by_field_name("name") is not None:
|
|
659
|
+
nn = nn.child_by_field_name("name")
|
|
660
|
+
self.f.calls.append((owner, "qualified", "::".join(scope + [nm]), nm, nn.start_point[0] + 1, nn.start_point[1]))
|
|
661
|
+
elif fn.type == "field_expression":
|
|
662
|
+
fld = fn.child_by_field_name("field")
|
|
663
|
+
if fld is not None:
|
|
664
|
+
nm = text(src, fld)
|
|
665
|
+
self.f.calls.append((owner, "member", nm, nm, fld.start_point[0] + 1, fld.start_point[1]))
|
|
666
|
+
elif fn.type == "template_function":
|
|
667
|
+
nmn = fn.child_by_field_name("name")
|
|
668
|
+
if nmn is not None:
|
|
669
|
+
nm = text(src, nmn)
|
|
670
|
+
self.f.calls.append((owner, "name", nm, nm.split("::")[-1], nmn.start_point[0] + 1, nmn.start_point[1]))
|
|
671
|
+
ftxt = re.sub(r"\s+", "", text(src, fn))
|
|
672
|
+
if ftxt in GETENV:
|
|
673
|
+
args = x.child_by_field_name("arguments")
|
|
674
|
+
if args is not None and args.named_children:
|
|
675
|
+
a0 = args.named_children[0]
|
|
676
|
+
k = string_value(src, a0) if a0.type == "string_literal" else None
|
|
677
|
+
if k is None and a0.type == "concatenated_string":
|
|
678
|
+
k = "".join(string_value(src, s) or "" for s in a0.named_children if s.type == "string_literal") or None
|
|
679
|
+
if ftxt in ("_dupenv_s", "getenv_s") and len(args.named_children) >= 3:
|
|
680
|
+
k = string_value(src, args.named_children[-1])
|
|
681
|
+
if k:
|
|
682
|
+
self.f.env.append((k, x.start_point[0] + 1, owner))
|
|
683
|
+
elif t == "identifier":
|
|
684
|
+
p = x.parent
|
|
685
|
+
if p is not None and p.type != "qualified_identifier" and not (p.type == "call_expression" and p.child_by_field_name("function") == x):
|
|
686
|
+
if p.type not in ("init_declarator", "parameter_declaration", "pointer_declarator", "array_declarator",
|
|
687
|
+
"function_declarator", "declaration") or (p.type == "init_declarator" and p.child_by_field_name("value") == x):
|
|
688
|
+
self.f.idents.append((owner, text(src, x), x.start_point[0] + 1, x.start_point[1]))
|
|
689
|
+
elif t == "type_identifier":
|
|
690
|
+
self.f.type_refs.append((owner, text(src, x), x.start_point[0] + 1, x.start_point[1]))
|
|
691
|
+
elif t in ("lambda_expression",):
|
|
692
|
+
pass
|
|
693
|
+
stack.extend(x.children)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
C_KEYWORDS = {"if", "while", "for", "switch", "return", "sizeof", "do", "else", "case", "defined", "_Alignof",
|
|
697
|
+
"alignof", "decltype", "typeof", "__typeof__", "__attribute__", "__declspec", "static_assert",
|
|
698
|
+
"_Static_assert", "catch", "noexcept", "throw", "new", "delete", "operator"}
|
|
699
|
+
TOP_CALL = re.compile(r"([A-Za-z_]\w*)\s*\(((?:[^()]|\([^()]*\))*)\)\s*;?\s*(?://.*|/\*.*\*/)?$")
|
|
700
|
+
HEAD_FN = re.compile(r"([A-Za-z_]\w*)\s*\((?:[^()]|\([^()]*\))*\)\s*(?:const\s*)?$")
|
|
701
|
+
CALL_RE = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
|
|
702
|
+
# a function definition inside a macro value: `int get_##name(void) {`, `static void fn(int x) {`
|
|
703
|
+
GEN_DEF = re.compile(r"(?:^|[\s*;}])((?:[A-Za-z_]\w*)(?:\s*##\s*[A-Za-z_]\w*)*)\s*\(([^;{}()]*(?:\([^()]*\)[^;{}()]*)*)\)\s*\{")
|
|
704
|
+
LEX = re.compile(r"//[^\n]*|/\*.*?\*/|\"(?:\\.|[^\"\\\n])*\"|'(?:\\.|[^'\\\n])*'|^[ \t]*#(?:\\\n|[^\n])*|[{};]", re.S | re.M)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def generated_names(params: list[str], value: str, args: list[str]) -> list[tuple[str, bool, str]]:
|
|
708
|
+
"""Functions a function-like macro defines when expanded with `args`: [(name, static, parameter types)]. `##`
|
|
709
|
+
pastes tokens, parameters are replaced by their argument."""
|
|
710
|
+
sub = dict(zip(params, (a.strip() for a in args)))
|
|
711
|
+
out = []
|
|
712
|
+
for m in GEN_DEF.finditer(value):
|
|
713
|
+
expr = m.group(1)
|
|
714
|
+
parts = [x.strip() for x in expr.split("##")]
|
|
715
|
+
if len(parts) == 1 and parts[0] in C_KEYWORDS:
|
|
716
|
+
continue
|
|
717
|
+
if not any(p in sub for p in parts) or (len(parts) == 1 and m.start(1) == len(value) - len(value.lstrip())):
|
|
718
|
+
continue # a name that does not depend on the arguments, or no return type before it
|
|
719
|
+
name = "".join(sub.get(p, p) for p in parts)
|
|
720
|
+
if re.fullmatch(r"[A-Za-z_]\w*", name) and name not in C_KEYWORDS:
|
|
721
|
+
ptxt = re.sub(r"\s*##\s*", "", re.sub(r"[A-Za-z_]\w*", lambda q: sub.get(q.group(0), q.group(0)), m.group(2)))
|
|
722
|
+
sig = []
|
|
723
|
+
for prm in (x.strip() for x in ptxt.split(",") if x.strip()):
|
|
724
|
+
if prm == "void":
|
|
725
|
+
continue
|
|
726
|
+
prm = re.sub(r"\s*([*&])\s*", r"\1 ", prm).strip()
|
|
727
|
+
ws = prm.split()
|
|
728
|
+
ty = " ".join(ws[:-1]) if len(ws) > 1 and re.fullmatch(r"\w+", ws[-1]) else prm
|
|
729
|
+
sig.append(re.sub(r"\b(const|struct|volatile)\b\s*", lambda q: q.group(0) if q.group(1) == "struct" else "", ty).replace(" *", "*").replace("* ", "*").strip())
|
|
730
|
+
out.append((name, "static" in value[:m.start(1)].split(";")[-1].split("}")[-1], ",".join(sig)))
|
|
731
|
+
return out
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _unwrap(chunk: str) -> str:
|
|
735
|
+
"""`HEAP_EXPORT(void heap_insert(struct heap* h)) {` -> the wrapper macro and its parentheses blanked (same
|
|
736
|
+
length), so the declaration inside parses as the definition's head."""
|
|
737
|
+
m = re.match(r"\s*([A-Z][A-Z0-9_]*)\s*\(", chunk)
|
|
738
|
+
b = chunk.find("{")
|
|
739
|
+
if not m or b < 0:
|
|
740
|
+
return chunk
|
|
741
|
+
depth, close = 0, None
|
|
742
|
+
for i in range(m.end() - 1, b):
|
|
743
|
+
if chunk[i] == "(":
|
|
744
|
+
depth += 1
|
|
745
|
+
elif chunk[i] == ")":
|
|
746
|
+
depth -= 1
|
|
747
|
+
if depth == 0:
|
|
748
|
+
close = i
|
|
749
|
+
break
|
|
750
|
+
inner = chunk[m.end():close] if close else ""
|
|
751
|
+
if close is None or "(" not in inner or chunk[close + 1:b].strip():
|
|
752
|
+
return chunk
|
|
753
|
+
return " " * m.end() + inner + " " + chunk[close + 1:]
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _block_spans_before(txt: str, line_at, line: int):
|
|
757
|
+
"""Line spans of the file-level blocks that end before `line`."""
|
|
758
|
+
for a, z in _toplevel_blocks(txt):
|
|
759
|
+
if line_at(z) < line:
|
|
760
|
+
yield line_at(a), line_at(z)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _line_index(txt: str):
|
|
764
|
+
import bisect
|
|
765
|
+
nl = [i for i, ch in enumerate(txt) if ch == "\n"]
|
|
766
|
+
return lambda pos: bisect.bisect_right(nl, pos - 1) + 1
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _toplevel_blocks(txt: str):
|
|
770
|
+
"""(start, end) offsets of file-level constructs that contain a brace block (a definition: head ... `}`),
|
|
771
|
+
skipping comments, strings and preprocessor lines; stray closing braces are ignored."""
|
|
772
|
+
depth, start, last_end = 0, None, 0
|
|
773
|
+
pp = [] # per #if level: True while inside an #elif / #else branch (its braces are not counted)
|
|
774
|
+
for m in LEX.finditer(txt):
|
|
775
|
+
tok = m.group(0)
|
|
776
|
+
if tok.lstrip().startswith("#"):
|
|
777
|
+
d = re.match(r"\s*#\s*(\w*)", tok).group(1)
|
|
778
|
+
if d in ("if", "ifdef", "ifndef"):
|
|
779
|
+
pp.append(False)
|
|
780
|
+
elif d in ("elif", "else", "elifdef", "elifndef") and pp:
|
|
781
|
+
pp[-1] = True
|
|
782
|
+
elif d == "endif" and pp:
|
|
783
|
+
pp.pop()
|
|
784
|
+
if any(pp):
|
|
785
|
+
continue
|
|
786
|
+
if len(tok) != 1 or tok not in "{};": # comment, string, preprocessor line
|
|
787
|
+
if depth == 0 and start is None:
|
|
788
|
+
if tok[:1] in "\"'":
|
|
789
|
+
start = m.start()
|
|
790
|
+
else:
|
|
791
|
+
last_end = m.end()
|
|
792
|
+
continue
|
|
793
|
+
if depth == 0 and start is None:
|
|
794
|
+
seg = txt[last_end:m.start()]
|
|
795
|
+
start = last_end + len(seg) - len(seg.lstrip())
|
|
796
|
+
if tok == "{":
|
|
797
|
+
depth += 1
|
|
798
|
+
elif tok == "}":
|
|
799
|
+
if depth == 0:
|
|
800
|
+
start, last_end = None, m.end()
|
|
801
|
+
continue
|
|
802
|
+
depth -= 1
|
|
803
|
+
if depth == 0:
|
|
804
|
+
yield start, m.end() - 1
|
|
805
|
+
start, last_end = None, m.end()
|
|
806
|
+
elif depth == 0:
|
|
807
|
+
start, last_end = None, m.end()
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def extract(path: str, src: bytes, lang: str, module: str, blank: re.Pattern | None = None) -> CFile:
|
|
811
|
+
return Extractor(path, src, lang, module, blank).run()
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
# ---------------------------------------------------------------- annotation macros
|
|
815
|
+
# Declaration annotations hidden behind macros (FOO_API, FOO_CONSTEXPR, FOO_INLINE, FOO_NODISCARD ...) are the main
|
|
816
|
+
# reason tree-sitter mis-parses real C/C++ headers. They are blanked (same byte length, so every position is kept)
|
|
817
|
+
# before parsing: names that match these suffixes, plus every object-like macro the project defines whose value is
|
|
818
|
+
# empty or only attributes/keywords.
|
|
819
|
+
ANNOT_NAME = re.compile(rb"^[A-Z][A-Z0-9_]*_(?:API|EXPORT|EXPORTS|IMPORT|INLINE|FORCEINLINE|FORCE_INLINE|ALWAYS_INLINE|NOINLINE|"
|
|
820
|
+
rb"CONSTEXPR\d*|CONSTEXPR_\w+|CONSTEVAL|NODISCARD|NOEXCEPT|NORETURN|DEPRECATED|MAYBE_UNUSED|UNUSED|"
|
|
821
|
+
rb"NO_UNIQUE_ADDRESS|VISIBILITY\w*|VISIBLE|HIDDEN|EXTERN|EXTERN_C|LOCAL|PUBLIC_API|STATIC_INLINE|"
|
|
822
|
+
rb"INLINE_VAR|ATTRIBUTE|ATTR|WARN_UNUSED_RESULT|MUST_USE_RESULT|PURE|HOT|COLD)_?$")
|
|
823
|
+
ANNOT_VALUE = re.compile(rb"^(?:\s|__attribute__\s*\(\(.*?\)\)|__declspec\s*\([^)]*\)|\[\[[^\]]*\]\]|inline|__inline|"
|
|
824
|
+
rb"__inline__|__forceinline|static|extern|\"C\"|constexpr|consteval|noexcept|explicit|virtual|"
|
|
825
|
+
rb"__cdecl|__stdcall|__fastcall|WINAPI|[A-Z][A-Z0-9_]*_(?:API|EXPORT|INLINE|CONSTEXPR\d*))*$")
|
|
826
|
+
NS_WRAP = re.compile(rb"^\s*(?:(?:inline\s+)?namespace\s+[\w:]+\s*\{\s*|\}\s*)+$")
|
|
827
|
+
DEFINE_RE = re.compile(rb"^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(?![\w(])[ \t]*(.*?)\\?$", re.M)
|
|
828
|
+
WORD_RE = re.compile(rb"\b[A-Z][A-Z0-9_]{2,}\b")
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def annotation_macros(sources) -> set:
|
|
832
|
+
"""Collect project macros that are pure declaration annotations (see above)."""
|
|
833
|
+
names, values_bad, numeric = set(), set(), set()
|
|
834
|
+
for src in sources:
|
|
835
|
+
for m in DEFINE_RE.finditer(src):
|
|
836
|
+
nm, val = m.group(1), m.group(2).strip()
|
|
837
|
+
if m.group(0).rstrip().endswith(b"\\"):
|
|
838
|
+
# multi-line value: only `namespace a { inline namespace b {` / `}}` wrappers count (masked in pairs)
|
|
839
|
+
end = src.find(b"\n", m.end())
|
|
840
|
+
full = m.group(2)
|
|
841
|
+
pos = m.end()
|
|
842
|
+
while full.rstrip().endswith(b"\\") or src[pos - 1:pos] == b"\\":
|
|
843
|
+
nxt = src.find(b"\n", pos + 1)
|
|
844
|
+
if nxt < 0:
|
|
845
|
+
break
|
|
846
|
+
full += b" " + src[pos + 1:nxt].rstrip(b"\\")
|
|
847
|
+
pos = nxt
|
|
848
|
+
if not src[nxt - 1:nxt] == b"\\":
|
|
849
|
+
break
|
|
850
|
+
if NS_WRAP.match(full.replace(b"\\", b" ")):
|
|
851
|
+
names.add(nm)
|
|
852
|
+
else:
|
|
853
|
+
values_bad.add(nm)
|
|
854
|
+
continue
|
|
855
|
+
if ANNOT_VALUE.match(val):
|
|
856
|
+
names.add(nm)
|
|
857
|
+
else:
|
|
858
|
+
values_bad.add(nm)
|
|
859
|
+
if re.match(rb"^\(?-?\d", val):
|
|
860
|
+
numeric.add(nm)
|
|
861
|
+
for w in set(WORD_RE.findall(src)):
|
|
862
|
+
if ANNOT_NAME.match(w):
|
|
863
|
+
names.add(w)
|
|
864
|
+
# a macro that is an annotation in one #if branch but a value in another stays only if its name says so
|
|
865
|
+
return {n for n in names if n not in numeric and (n not in values_bad or ANNOT_NAME.match(n))}
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def annotation_regex(names: set) -> re.Pattern | None:
|
|
869
|
+
if not names:
|
|
870
|
+
return None
|
|
871
|
+
alt = b"|".join(re.escape(n) for n in sorted(names, key=len, reverse=True))
|
|
872
|
+
return re.compile(rb"\b(?:" + alt + rb")\b")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def mask_annotations(src: bytes, rx: re.Pattern) -> bytes:
|
|
876
|
+
out, cont = [], False
|
|
877
|
+
for line in src.split(b"\n"):
|
|
878
|
+
directive = cont or line.lstrip().startswith(b"#")
|
|
879
|
+
cont = directive and line.rstrip().endswith(b"\\")
|
|
880
|
+
out.append(line if directive else rx.sub(lambda m: b" " * (m.end() - m.start()), line))
|
|
881
|
+
return b"\n".join(out)
|