cg-code-graph 0.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,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)