cseq 0.0.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.
- cseq/__init__.py +9 -0
- cseq/acceptance.py +162 -0
- cseq/api.py +328 -0
- cseq/binary.py +122 -0
- cseq/cache.py +420 -0
- cseq/cli.py +895 -0
- cseq/compile_db.py +98 -0
- cseq/config.py +138 -0
- cseq/cpu_rules.py +48 -0
- cseq/dependencies.py +202 -0
- cseq/docs.py +21 -0
- cseq/dwarf.py +115 -0
- cseq/event_store.py +378 -0
- cseq/explain.py +113 -0
- cseq/fast_parser.py +286 -0
- cseq/html.py +485 -0
- cseq/html_bundle.py +464 -0
- cseq/hybrid_parser.py +120 -0
- cseq/linker.py +117 -0
- cseq/marker.py +294 -0
- cseq/model.py +367 -0
- cseq/parser.py +797 -0
- cseq/parser_contract_cases.json +69 -0
- cseq/parser_dependency_lock.json +59 -0
- cseq/parser_environment.py +186 -0
- cseq/parser_migration.py +126 -0
- cseq/plugin.py +216 -0
- cseq/project.py +305 -0
- cseq/query.py +62 -0
- cseq/resources/README.md +171 -0
- cseq/resources/design.md +6197 -0
- cseq/resources/verification_report.html +26 -0
- cseq/runtime.py +177 -0
- cseq/runtime_address.py +79 -0
- cseq/runtime_cpu.py +87 -0
- cseq/scanner.py +54 -0
- cseq/sequence.py +281 -0
- cseq/server.py +104 -0
- cseq/source_index.py +227 -0
- cseq/static_store.py +280 -0
- cseq/trace_analysis.py +336 -0
- cseq/trace_diff.py +56 -0
- cseq/tree_sitter_parser.py +468 -0
- cseq/valueflow.py +179 -0
- cseq-0.0.1.dist-info/METADATA +181 -0
- cseq-0.0.1.dist-info/RECORD +49 -0
- cseq-0.0.1.dist-info/WHEEL +5 -0
- cseq-0.0.1.dist-info/entry_points.txt +2 -0
- cseq-0.0.1.dist-info/top_level.txt +1 -0
cseq/parser.py
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .model import (
|
|
12
|
+
CallSiteRecord,
|
|
13
|
+
Diagnostic,
|
|
14
|
+
FileParseArtifact,
|
|
15
|
+
FlowEdge,
|
|
16
|
+
FlowEdgeKind,
|
|
17
|
+
FunctionRecord,
|
|
18
|
+
GuardedSourceRegion,
|
|
19
|
+
ParseStatus,
|
|
20
|
+
SourceRange,
|
|
21
|
+
TranslationUnit,
|
|
22
|
+
ValueSlot,
|
|
23
|
+
ValueSlotKind,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParserBackend(ABC):
|
|
28
|
+
name: str
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def parse(self, translation_unit: TranslationUnit) -> FileParseArtifact:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClangJsonParser(ParserBackend):
|
|
36
|
+
"""PoC parser backend using clang's JSON AST dump.
|
|
37
|
+
|
|
38
|
+
The backend boundary is intentional: a Tree-sitter fast parser can be added
|
|
39
|
+
later without changing ProjectIndex / Program IR consumers.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = "clang-json"
|
|
43
|
+
|
|
44
|
+
def __init__(self, clang_executable: str | None = None) -> None:
|
|
45
|
+
exe = clang_executable or shutil.which("clang")
|
|
46
|
+
if not exe:
|
|
47
|
+
raise RuntimeError("clang executable was not found")
|
|
48
|
+
self.clang_executable = exe
|
|
49
|
+
|
|
50
|
+
def parse(self, translation_unit: TranslationUnit) -> FileParseArtifact:
|
|
51
|
+
source = translation_unit.source.path
|
|
52
|
+
cmd = [
|
|
53
|
+
self.clang_executable,
|
|
54
|
+
"-Xclang", "-ast-dump=json",
|
|
55
|
+
"-fsyntax-only", "-Wno-everything",
|
|
56
|
+
*translation_unit.arguments,
|
|
57
|
+
str(source),
|
|
58
|
+
]
|
|
59
|
+
proc = subprocess.run(
|
|
60
|
+
cmd,
|
|
61
|
+
cwd=translation_unit.working_directory,
|
|
62
|
+
stdout=subprocess.PIPE,
|
|
63
|
+
stderr=subprocess.PIPE,
|
|
64
|
+
text=True,
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
errors="replace",
|
|
67
|
+
check=False,
|
|
68
|
+
)
|
|
69
|
+
diagnostics = _parse_clang_diagnostics(proc.stderr)
|
|
70
|
+
opaque_regions = _opaque_regions_from_clang_diagnostics(proc.stderr)
|
|
71
|
+
guarded_regions = _preprocessor_guard_regions(source.read_bytes(), translation_unit.source.project_relative_path)
|
|
72
|
+
root = _load_ast_prefix(proc.stdout)
|
|
73
|
+
functions: list[FunctionRecord] = []
|
|
74
|
+
callsites: list[CallSiteRecord] = []
|
|
75
|
+
value_slots: list[ValueSlot] = []
|
|
76
|
+
flow_edges: list[FlowEdge] = []
|
|
77
|
+
if root is not None:
|
|
78
|
+
_collect_functions(root, translation_unit, functions)
|
|
79
|
+
callsites, value_slots, flow_edges = collect_program_facts(root, translation_unit, functions)
|
|
80
|
+
|
|
81
|
+
if proc.returncode == 0 and root is not None:
|
|
82
|
+
status = ParseStatus.OK
|
|
83
|
+
elif root is not None and functions:
|
|
84
|
+
status = ParseStatus.ERROR_RECOVERED
|
|
85
|
+
elif root is not None:
|
|
86
|
+
status = ParseStatus.PARTIAL
|
|
87
|
+
else:
|
|
88
|
+
status = ParseStatus.OPAQUE_REGION
|
|
89
|
+
|
|
90
|
+
return FileParseArtifact(
|
|
91
|
+
translation_unit=translation_unit,
|
|
92
|
+
status=status,
|
|
93
|
+
functions=functions,
|
|
94
|
+
callsites=callsites,
|
|
95
|
+
value_slots=value_slots,
|
|
96
|
+
flow_edges=flow_edges,
|
|
97
|
+
diagnostics=diagnostics,
|
|
98
|
+
opaque_regions=opaque_regions,
|
|
99
|
+
guarded_regions=guarded_regions,
|
|
100
|
+
parser_backend=self.name,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_ast_prefix(stdout: str) -> dict[str, Any] | None:
|
|
105
|
+
if not stdout.strip():
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
value = json.loads(stdout)
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
return None
|
|
111
|
+
return value if isinstance(value, dict) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _opaque_regions_from_clang_diagnostics(stderr: str) -> list[SourceRange]:
|
|
116
|
+
"""Convert hard compiler diagnostics into explicit uncertain source ranges.
|
|
117
|
+
|
|
118
|
+
Clang often still emits a useful partial AST after an error. The range is
|
|
119
|
+
intentionally narrow (diagnostic line/column) so callers can keep all other
|
|
120
|
+
recovered facts while still showing that this location is not trustworthy.
|
|
121
|
+
"""
|
|
122
|
+
rx = re.compile(r"^(.*?):(\d+):(\d+):\s*(fatal error|error):", re.MULTILINE)
|
|
123
|
+
seen: set[tuple[int, int]] = set()
|
|
124
|
+
out: list[SourceRange] = []
|
|
125
|
+
for m in rx.finditer(stderr):
|
|
126
|
+
line, col = int(m.group(2)), int(m.group(3))
|
|
127
|
+
key = (line, col)
|
|
128
|
+
if key in seen:
|
|
129
|
+
continue
|
|
130
|
+
seen.add(key)
|
|
131
|
+
out.append(SourceRange(line, col, line, col))
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
def _parse_clang_diagnostics(stderr: str) -> list[Diagnostic]:
|
|
135
|
+
result: list[Diagnostic] = []
|
|
136
|
+
rx = re.compile(r"\b(fatal error|error|warning|note):\s*(.*)$")
|
|
137
|
+
for raw in stderr.splitlines():
|
|
138
|
+
m = rx.search(raw)
|
|
139
|
+
if not m:
|
|
140
|
+
continue
|
|
141
|
+
sev = m.group(1).replace("fatal error", "fatal")
|
|
142
|
+
result.append(Diagnostic(severity=sev, message=m.group(2).strip(), raw=raw))
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _location_entry(location: dict[str, Any] | None) -> dict[str, Any]:
|
|
147
|
+
"""Return the effective source location from Clang JSON location variants."""
|
|
148
|
+
if not isinstance(location, dict):
|
|
149
|
+
return {}
|
|
150
|
+
expansion = location.get("expansionLoc")
|
|
151
|
+
if isinstance(expansion, dict):
|
|
152
|
+
return expansion
|
|
153
|
+
spelling = location.get("spellingLoc")
|
|
154
|
+
if isinstance(spelling, dict):
|
|
155
|
+
return spelling
|
|
156
|
+
return location
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _same_path(a: str, b: Path, working_directory: Path) -> bool:
|
|
160
|
+
try:
|
|
161
|
+
pa = Path(a)
|
|
162
|
+
if not pa.is_absolute():
|
|
163
|
+
pa = working_directory / pa
|
|
164
|
+
return pa.resolve() == b.resolve()
|
|
165
|
+
except (OSError, RuntimeError, ValueError):
|
|
166
|
+
return Path(a).name.lower() == b.name.lower()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _node_belongs_to_translation_unit(node: dict[str, Any], tu: TranslationUnit) -> bool:
|
|
170
|
+
"""Exclude definitions physically coming from included headers.
|
|
171
|
+
|
|
172
|
+
Clang may omit ``file`` for consecutive locations in the primary source, so
|
|
173
|
+
absence of ``file`` is accepted only when there is no ``includedFrom`` chain.
|
|
174
|
+
Macro locations prefer expansionLoc, which represents the user's call site.
|
|
175
|
+
"""
|
|
176
|
+
loc = _location_entry(node.get("loc"))
|
|
177
|
+
rng = node.get("range") or {}
|
|
178
|
+
begin = _location_entry(rng.get("begin"))
|
|
179
|
+
candidates = [x for x in (loc, begin) if isinstance(x, dict) and x]
|
|
180
|
+
saw_primary_without_file = False
|
|
181
|
+
for entry in candidates:
|
|
182
|
+
file_name = entry.get("file")
|
|
183
|
+
if file_name:
|
|
184
|
+
if _same_path(str(file_name), tu.source.path, tu.working_directory):
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
if entry.get("includedFrom"):
|
|
188
|
+
return False
|
|
189
|
+
saw_primary_without_file = True
|
|
190
|
+
return saw_primary_without_file
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _collect_functions(node: dict[str, Any], tu: TranslationUnit, out: list[FunctionRecord]) -> None:
|
|
194
|
+
if node.get("kind") == "FunctionDecl" and _has_body(node) and _node_belongs_to_translation_unit(node, tu):
|
|
195
|
+
loc = node.get("loc") or {}
|
|
196
|
+
rng = node.get("range") or {}
|
|
197
|
+
begin = rng.get("begin") or loc
|
|
198
|
+
end = rng.get("end") or loc
|
|
199
|
+
start_line = _int_or_none(begin.get("line")) or _int_or_none(loc.get("line"))
|
|
200
|
+
start_col = _int_or_none(begin.get("col")) or _int_or_none(loc.get("col"))
|
|
201
|
+
name = str(node.get("name") or "<anonymous>")
|
|
202
|
+
storage = node.get("storageClass")
|
|
203
|
+
type_info = node.get("type") or {}
|
|
204
|
+
params = tuple(
|
|
205
|
+
str(c.get("name"))
|
|
206
|
+
for c in node.get("inner") or ()
|
|
207
|
+
if isinstance(c, dict) and c.get("kind") == "ParmVarDecl" and c.get("name")
|
|
208
|
+
)
|
|
209
|
+
out.append(FunctionRecord(
|
|
210
|
+
name=name,
|
|
211
|
+
qualified_id=_function_id(tu, name, start_line, start_col, storage, type_info.get("qualType")),
|
|
212
|
+
source_path=tu.source.project_relative_path,
|
|
213
|
+
source_range=SourceRange(start_line, start_col, _int_or_none(end.get("line")), _int_or_none(end.get("col"))),
|
|
214
|
+
storage_class=str(storage) if storage else None,
|
|
215
|
+
type_qualifier=str(type_info.get("qualType")) if type_info.get("qualType") else None,
|
|
216
|
+
parameters=params,
|
|
217
|
+
))
|
|
218
|
+
for child in node.get("inner") or ():
|
|
219
|
+
if isinstance(child, dict):
|
|
220
|
+
_collect_functions(child, tu, out)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def collect_program_facts(root: dict[str, Any], tu: TranslationUnit, functions: list[FunctionRecord]) -> tuple[list[CallSiteRecord], list[ValueSlot], list[FlowEdge]]:
|
|
224
|
+
source_bytes = tu.source.path.read_bytes()
|
|
225
|
+
guard_by_line = _preprocessor_guard_map(source_bytes)
|
|
226
|
+
calls: list[CallSiteRecord] = []
|
|
227
|
+
slots: dict[str, ValueSlot] = {}
|
|
228
|
+
edges: list[FlowEdge] = []
|
|
229
|
+
funcs_by_name = {f.name: f for f in functions}
|
|
230
|
+
|
|
231
|
+
def add_slot(slot: ValueSlot | None) -> str | None:
|
|
232
|
+
if slot is None:
|
|
233
|
+
return None
|
|
234
|
+
slots.setdefault(slot.key, slot)
|
|
235
|
+
return slot.key
|
|
236
|
+
|
|
237
|
+
def add_function_source(name: str) -> str:
|
|
238
|
+
key = f"function::{name}"
|
|
239
|
+
slots.setdefault(key, ValueSlot(key, ValueSlotKind.FUNCTION, name))
|
|
240
|
+
return key
|
|
241
|
+
|
|
242
|
+
def add_sources_to_target(expr: dict[str, Any] | None, target: ValueSlot | None, kind: FlowEdgeKind, current: FunctionRecord | None) -> None:
|
|
243
|
+
target_key = add_slot(target)
|
|
244
|
+
if not target_key or not expr:
|
|
245
|
+
return
|
|
246
|
+
fn_names = _expr_function_names(expr)
|
|
247
|
+
for fn in sorted(fn_names):
|
|
248
|
+
edges.append(FlowEdge(add_function_source(fn), target_key, kind, tu.source.project_relative_path, _range(expr)))
|
|
249
|
+
src_slot = _expr_slot(expr, current, tu)
|
|
250
|
+
src_key = add_slot(src_slot)
|
|
251
|
+
if src_key and src_key != target_key:
|
|
252
|
+
edges.append(FlowEdge(src_key, target_key, kind, tu.source.project_relative_path, _range(expr)))
|
|
253
|
+
|
|
254
|
+
# Global array initializers first.
|
|
255
|
+
for node in _walk_nodes(root):
|
|
256
|
+
if node.get("kind") != "VarDecl" or not _is_global_var(node, root):
|
|
257
|
+
continue
|
|
258
|
+
name = node.get("name")
|
|
259
|
+
if not name:
|
|
260
|
+
continue
|
|
261
|
+
init = next((c for c in node.get("inner") or () if isinstance(c, dict) and c.get("kind") == "InitListExpr"), None)
|
|
262
|
+
if not init:
|
|
263
|
+
continue
|
|
264
|
+
for idx, elem in enumerate(init.get("inner") or ()):
|
|
265
|
+
if not isinstance(elem, dict):
|
|
266
|
+
continue
|
|
267
|
+
exact = ValueSlot(f"global::{name}[{idx}]", ValueSlotKind.GLOBAL_ARRAY_ELEMENT, str(name), index=idx)
|
|
268
|
+
summary = ValueSlot(f"global::{name}[*]", ValueSlotKind.GLOBAL_ARRAY_SUMMARY, str(name))
|
|
269
|
+
add_sources_to_target(elem, exact, FlowEdgeKind.INIT, None)
|
|
270
|
+
add_sources_to_target(elem, summary, FlowEdgeKind.INIT, None)
|
|
271
|
+
|
|
272
|
+
def walk(node: dict[str, Any], current: FunctionRecord | None, control_context: tuple[str, ...] = ()) -> None:
|
|
273
|
+
next_current = current
|
|
274
|
+
next_context = control_context
|
|
275
|
+
kind = node.get("kind")
|
|
276
|
+
control = None if kind == "IfStmt" else _control_context_label(node, source_bytes)
|
|
277
|
+
if control is not None:
|
|
278
|
+
next_context = (*control_context, control)
|
|
279
|
+
if node.get("kind") == "FunctionDecl" and _has_body(node):
|
|
280
|
+
if not _node_belongs_to_translation_unit(node, tu):
|
|
281
|
+
return
|
|
282
|
+
name = str(node.get("name") or "<anonymous>")
|
|
283
|
+
loc = _location_entry(node.get("loc"))
|
|
284
|
+
line = _int_or_none(loc.get("line"))
|
|
285
|
+
next_current = next((f for f in functions if f.name == name and (line is None or f.source_range.start_line == line)), current)
|
|
286
|
+
if next_current:
|
|
287
|
+
for param in node.get("inner") or ():
|
|
288
|
+
if isinstance(param, dict) and param.get("kind") == "ParmVarDecl" and param.get("name"):
|
|
289
|
+
add_slot(_decl_slot(param, next_current, tu))
|
|
290
|
+
|
|
291
|
+
if next_current is not None:
|
|
292
|
+
if node.get("kind") == "VarDecl" and node.get("name"):
|
|
293
|
+
target = _decl_slot(node, next_current, tu)
|
|
294
|
+
add_slot(target)
|
|
295
|
+
init = next((c for c in node.get("inner") or () if isinstance(c, dict)), None)
|
|
296
|
+
if init:
|
|
297
|
+
add_sources_to_target(init, target, FlowEdgeKind.INIT, next_current)
|
|
298
|
+
|
|
299
|
+
if node.get("kind") == "BinaryOperator" and node.get("opcode") == "=":
|
|
300
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
301
|
+
if len(inner) >= 2:
|
|
302
|
+
target = _expr_slot(inner[0], next_current, tu)
|
|
303
|
+
add_sources_to_target(inner[1], target, FlowEdgeKind.ASSIGN, next_current)
|
|
304
|
+
|
|
305
|
+
if node.get("kind") == "CallExpr":
|
|
306
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
307
|
+
callee_expr = inner[0] if inner else None
|
|
308
|
+
direct = _direct_callee_name(node)
|
|
309
|
+
callee_slot = None if direct else _expr_slot(callee_expr, next_current, tu)
|
|
310
|
+
callee_slot_key = add_slot(callee_slot)
|
|
311
|
+
arg_slots: list[str | None] = []
|
|
312
|
+
for arg in inner[1:]:
|
|
313
|
+
arg_slots.append(add_slot(_argument_object_slot(arg, next_current, tu)))
|
|
314
|
+
calls.append(CallSiteRecord(
|
|
315
|
+
caller_id=next_current.qualified_id,
|
|
316
|
+
caller_name=next_current.name,
|
|
317
|
+
callee_name=direct,
|
|
318
|
+
raw_text=_raw_text(node, source_bytes),
|
|
319
|
+
source_path=tu.source.project_relative_path,
|
|
320
|
+
source_range=_range(node, source_bytes),
|
|
321
|
+
callee_slot_key=callee_slot_key,
|
|
322
|
+
argument_slot_keys=tuple(arg_slots),
|
|
323
|
+
control_context=next_context,
|
|
324
|
+
macro_expanded=_is_macro_expanded(node),
|
|
325
|
+
spelling_range=_range_with_loc_kind(node, "spellingLoc"),
|
|
326
|
+
expansion_range=_range_with_loc_kind(node, "expansionLoc"),
|
|
327
|
+
guard_expression=guard_by_line.get((_range(node, source_bytes).start_line or 0)),
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
children = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
331
|
+
if kind == "IfStmt":
|
|
332
|
+
raw_if = _raw_text(node, source_bytes).replace("\n", " ").strip()
|
|
333
|
+
if len(raw_if) > 80:
|
|
334
|
+
raw_if = raw_if[:77] + "..."
|
|
335
|
+
base = "if:" + raw_if
|
|
336
|
+
for i, child in enumerate(children):
|
|
337
|
+
if i == 1:
|
|
338
|
+
child_ctx = (*control_context, base + "/then")
|
|
339
|
+
elif i >= 2:
|
|
340
|
+
child_ctx = (*control_context, base + "/else")
|
|
341
|
+
else:
|
|
342
|
+
child_ctx = control_context
|
|
343
|
+
walk(child, next_current, child_ctx)
|
|
344
|
+
return
|
|
345
|
+
if kind == "CaseStmt":
|
|
346
|
+
labels, body = _case_chain(node, source_bytes)
|
|
347
|
+
label = "|".join(labels) if labels else "?"
|
|
348
|
+
if body is not None:
|
|
349
|
+
walk(body, next_current, (*next_context, f"CASE:{label}"))
|
|
350
|
+
return
|
|
351
|
+
if kind == "DefaultStmt":
|
|
352
|
+
for child in children:
|
|
353
|
+
walk(child, next_current, (*next_context, "DEFAULT"))
|
|
354
|
+
return
|
|
355
|
+
for child in children:
|
|
356
|
+
walk(child, next_current, next_context)
|
|
357
|
+
|
|
358
|
+
walk(root, None, ())
|
|
359
|
+
return calls, list(slots.values()), edges
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _case_chain(node: dict[str, Any], source_bytes: bytes) -> tuple[list[str], dict[str, Any] | None]:
|
|
365
|
+
"""Return consecutive case labels and the first non-CaseStmt body.
|
|
366
|
+
|
|
367
|
+
Clang represents fall-through labels such as ``case 2: case 3: body`` as
|
|
368
|
+
nested CaseStmt nodes. Treating the nesting as conjunction would be wrong:
|
|
369
|
+
the body is reachable from either label. We therefore collapse the label
|
|
370
|
+
chain into one alternative region (``CASE:2|3``).
|
|
371
|
+
"""
|
|
372
|
+
labels: list[str] = []
|
|
373
|
+
current: dict[str, Any] | None = node
|
|
374
|
+
while isinstance(current, dict) and current.get("kind") == "CaseStmt":
|
|
375
|
+
children = [c for c in current.get("inner") or () if isinstance(c, dict)]
|
|
376
|
+
if not children:
|
|
377
|
+
return labels, None
|
|
378
|
+
label_node = children[0]
|
|
379
|
+
label = _raw_text(label_node, source_bytes).strip() or "?"
|
|
380
|
+
labels.append(label)
|
|
381
|
+
body = children[1] if len(children) >= 2 else None
|
|
382
|
+
if isinstance(body, dict) and body.get("kind") == "CaseStmt":
|
|
383
|
+
current = body
|
|
384
|
+
continue
|
|
385
|
+
return labels, body
|
|
386
|
+
return labels, current
|
|
387
|
+
|
|
388
|
+
def _control_context_label(node: dict[str, Any], source_bytes: bytes) -> str | None:
|
|
389
|
+
kind = node.get("kind")
|
|
390
|
+
if kind not in {"IfStmt", "SwitchStmt", "ForStmt", "WhileStmt", "DoStmt"}:
|
|
391
|
+
return None
|
|
392
|
+
raw = _raw_text(node, source_bytes).replace("\n", " ").strip()
|
|
393
|
+
if kind == "IfStmt":
|
|
394
|
+
return f"IF:{_header_expression(raw, 'if')}"
|
|
395
|
+
if kind == "SwitchStmt":
|
|
396
|
+
return f"SWITCH:{_header_expression(raw, 'switch')}"
|
|
397
|
+
if kind == "ForStmt":
|
|
398
|
+
header = _header_expression(raw, "for")
|
|
399
|
+
compact = "".join(header.split())
|
|
400
|
+
return "LOOP:" + (("[infinite] " if compact == "for(;;)" else "") + header)
|
|
401
|
+
if kind == "WhileStmt":
|
|
402
|
+
header = _header_expression(raw, "while")
|
|
403
|
+
compact = "".join(header.split())
|
|
404
|
+
return "LOOP:" + (("[infinite] " if compact in {"while(1)", "while(true)"} else "") + header)
|
|
405
|
+
if kind == "DoStmt":
|
|
406
|
+
compact = "".join(raw.split())
|
|
407
|
+
infinite = compact.endswith("while(1)") or compact.endswith("while(1);") or compact.endswith("while(true)") or compact.endswith("while(true);")
|
|
408
|
+
return "LOOP:" + (("[infinite] " if infinite else "") + ("do/while(1)" if infinite else "do/while"))
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _header_expression(raw: str, keyword: str) -> str:
|
|
413
|
+
pos = raw.find("(")
|
|
414
|
+
if pos < 0:
|
|
415
|
+
return keyword
|
|
416
|
+
depth = 0
|
|
417
|
+
for i in range(pos, len(raw)):
|
|
418
|
+
ch = raw[i]
|
|
419
|
+
if ch == "(": depth += 1
|
|
420
|
+
elif ch == ")":
|
|
421
|
+
depth -= 1
|
|
422
|
+
if depth == 0: return raw[: i + 1].strip()
|
|
423
|
+
return raw[:120].strip()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _walk_nodes(node: dict[str, Any]):
|
|
427
|
+
yield node
|
|
428
|
+
for child in node.get("inner") or ():
|
|
429
|
+
if isinstance(child, dict):
|
|
430
|
+
yield from _walk_nodes(child)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _is_global_var(node: dict[str, Any], root: dict[str, Any]) -> bool:
|
|
434
|
+
# Clang top-level VarDecls appear directly below TranslationUnitDecl.
|
|
435
|
+
return any(child is node for child in root.get("inner") or ())
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _expr_function_names(node: dict[str, Any] | None) -> set[str]:
|
|
439
|
+
out: set[str] = set()
|
|
440
|
+
if not isinstance(node, dict):
|
|
441
|
+
return out
|
|
442
|
+
if node.get("kind") == "DeclRefExpr":
|
|
443
|
+
ref = node.get("referencedDecl") or {}
|
|
444
|
+
if ref.get("kind") == "FunctionDecl" and ref.get("name"):
|
|
445
|
+
out.add(str(ref["name"]))
|
|
446
|
+
for child in node.get("inner") or ():
|
|
447
|
+
if isinstance(child, dict):
|
|
448
|
+
out |= _expr_function_names(child)
|
|
449
|
+
return out
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _expr_slot(node: dict[str, Any] | None, current: FunctionRecord | None, tu: TranslationUnit) -> ValueSlot | None:
|
|
453
|
+
if not isinstance(node, dict):
|
|
454
|
+
return None
|
|
455
|
+
kind = node.get("kind")
|
|
456
|
+
if kind in {"ImplicitCastExpr", "ParenExpr", "CStyleCastExpr", "UnaryOperator"}:
|
|
457
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
458
|
+
if inner:
|
|
459
|
+
return _expr_slot(inner[0], current, tu)
|
|
460
|
+
if kind == "DeclRefExpr":
|
|
461
|
+
ref = node.get("referencedDecl") or {}
|
|
462
|
+
if ref.get("kind") in {"VarDecl", "ParmVarDecl"} and ref.get("name"):
|
|
463
|
+
return _decl_ref_slot(ref, current, tu)
|
|
464
|
+
if kind == "MemberExpr" and node.get("name"):
|
|
465
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
466
|
+
base_node = inner[0] if inner else None
|
|
467
|
+
base = _expr_slot(base_node, current, tu) if base_node else None
|
|
468
|
+
if base:
|
|
469
|
+
field = str(node["name"])
|
|
470
|
+
if _is_union_typed(base_node):
|
|
471
|
+
return ValueSlot(
|
|
472
|
+
f"{base.key}.<union>", ValueSlotKind.UNION_SUMMARY,
|
|
473
|
+
f"{base.name}.<union>", base.function_id, field_name="<union>",
|
|
474
|
+
)
|
|
475
|
+
return ValueSlot(f"{base.key}.{field}", ValueSlotKind.STRUCT_FIELD, f"{base.name}.{field}", base.function_id, field_name=field)
|
|
476
|
+
if kind == "CallExpr" and current is not None:
|
|
477
|
+
allocator = _direct_callee_name(node)
|
|
478
|
+
if allocator in {"malloc", "calloc", "realloc", "aligned_alloc"}:
|
|
479
|
+
rng = _range(node)
|
|
480
|
+
where = f"{tu.source.project_relative_path}:{rng.start_line or '?'}:{rng.start_column or '?'}"
|
|
481
|
+
key = f"heap::{current.qualified_id}::{where}:{allocator}"
|
|
482
|
+
return ValueSlot(key, ValueSlotKind.HEAP_ALLOCATION, f"{allocator}@{where}", current.qualified_id)
|
|
483
|
+
if kind == "ArraySubscriptExpr":
|
|
484
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
485
|
+
if len(inner) >= 2:
|
|
486
|
+
base_name = _declref_name(inner[0])
|
|
487
|
+
if base_name:
|
|
488
|
+
idx = _integer_value(inner[1])
|
|
489
|
+
if idx is not None:
|
|
490
|
+
return ValueSlot(f"global::{base_name}[{idx}]", ValueSlotKind.GLOBAL_ARRAY_ELEMENT, base_name, index=idx)
|
|
491
|
+
return ValueSlot(f"global::{base_name}[*]", ValueSlotKind.GLOBAL_ARRAY_SUMMARY, base_name)
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _is_union_typed(node: dict[str, Any] | None) -> bool:
|
|
496
|
+
if not isinstance(node, dict):
|
|
497
|
+
return False
|
|
498
|
+
type_info = node.get("type") or {}
|
|
499
|
+
qual = str(type_info.get("qualType") or type_info.get("desugaredQualType") or "")
|
|
500
|
+
if "union " in qual:
|
|
501
|
+
return True
|
|
502
|
+
# Cast/paren nodes may carry a pointer type while the inner DeclRef has the
|
|
503
|
+
# actual union spelling, so inspect their first child conservatively.
|
|
504
|
+
for child in node.get("inner") or ():
|
|
505
|
+
if isinstance(child, dict) and _is_union_typed(child):
|
|
506
|
+
return True
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _argument_object_slot(node: dict[str, Any], current: FunctionRecord, tu: TranslationUnit) -> ValueSlot | None:
|
|
511
|
+
# &h and h are both treated as the same abstract object for the PoC alias binding.
|
|
512
|
+
if node.get("kind") == "UnaryOperator" and node.get("opcode") == "&":
|
|
513
|
+
inner = [c for c in node.get("inner") or () if isinstance(c, dict)]
|
|
514
|
+
return _expr_slot(inner[0], current, tu) if inner else None
|
|
515
|
+
return _expr_slot(node, current, tu)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _decl_slot(node: dict[str, Any], current: FunctionRecord, tu: TranslationUnit) -> ValueSlot | None:
|
|
519
|
+
name = node.get("name")
|
|
520
|
+
if not name:
|
|
521
|
+
return None
|
|
522
|
+
if node.get("kind") == "ParmVarDecl":
|
|
523
|
+
key = f"{current.qualified_id}::param::{name}"
|
|
524
|
+
return ValueSlot(key, ValueSlotKind.PARAMETER, str(name), current.qualified_id)
|
|
525
|
+
key = f"{current.qualified_id}::local::{name}"
|
|
526
|
+
return ValueSlot(key, ValueSlotKind.LOCAL, str(name), current.qualified_id)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _decl_ref_slot(ref: dict[str, Any], current: FunctionRecord | None, tu: TranslationUnit) -> ValueSlot | None:
|
|
530
|
+
if current is None or not ref.get("name"):
|
|
531
|
+
return None
|
|
532
|
+
name = str(ref["name"])
|
|
533
|
+
if ref.get("kind") == "ParmVarDecl":
|
|
534
|
+
return ValueSlot(f"{current.qualified_id}::param::{name}", ValueSlotKind.PARAMETER, name, current.qualified_id)
|
|
535
|
+
return ValueSlot(f"{current.qualified_id}::local::{name}", ValueSlotKind.LOCAL, name, current.qualified_id)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _declref_name(node: dict[str, Any] | None) -> str | None:
|
|
539
|
+
if not isinstance(node, dict):
|
|
540
|
+
return None
|
|
541
|
+
if node.get("kind") == "DeclRefExpr":
|
|
542
|
+
ref = node.get("referencedDecl") or {}
|
|
543
|
+
if ref.get("name"):
|
|
544
|
+
return str(ref["name"])
|
|
545
|
+
for child in node.get("inner") or ():
|
|
546
|
+
if isinstance(child, dict):
|
|
547
|
+
found = _declref_name(child)
|
|
548
|
+
if found:
|
|
549
|
+
return found
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _integer_value(node: dict[str, Any] | None) -> int | None:
|
|
554
|
+
if not isinstance(node, dict):
|
|
555
|
+
return None
|
|
556
|
+
if node.get("kind") == "IntegerLiteral":
|
|
557
|
+
value = node.get("value")
|
|
558
|
+
try:
|
|
559
|
+
return int(value)
|
|
560
|
+
except (TypeError, ValueError):
|
|
561
|
+
return None
|
|
562
|
+
for child in node.get("inner") or ():
|
|
563
|
+
if isinstance(child, dict):
|
|
564
|
+
v = _integer_value(child)
|
|
565
|
+
if v is not None:
|
|
566
|
+
return v
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _is_macro_expanded(node: dict[str, Any]) -> bool:
|
|
572
|
+
rng = node.get("range") or {}
|
|
573
|
+
for part in (rng.get("begin") or {}, rng.get("end") or {}):
|
|
574
|
+
if isinstance(part, dict) and ("spellingLoc" in part or "expansionLoc" in part):
|
|
575
|
+
return True
|
|
576
|
+
return False
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _range_with_loc_kind(node: dict[str, Any], key: str) -> SourceRange | None:
|
|
580
|
+
rng = node.get("range") or {}
|
|
581
|
+
begin_raw = rng.get("begin") or {}
|
|
582
|
+
end_raw = rng.get("end") or {}
|
|
583
|
+
begin = begin_raw.get(key) if isinstance(begin_raw, dict) else None
|
|
584
|
+
end = end_raw.get(key) if isinstance(end_raw, dict) else None
|
|
585
|
+
if not isinstance(begin, dict) and not isinstance(end, dict):
|
|
586
|
+
return None
|
|
587
|
+
begin = begin if isinstance(begin, dict) else {}
|
|
588
|
+
end = end if isinstance(end, dict) else begin
|
|
589
|
+
start_line = _int_or_none(begin.get("line")) or _int_or_none(end.get("line"))
|
|
590
|
+
end_line = _int_or_none(end.get("line")) or start_line
|
|
591
|
+
return SourceRange(start_line, _int_or_none(begin.get("col")), end_line, _int_or_none(end.get("col")))
|
|
592
|
+
|
|
593
|
+
def _direct_callee_name(call_node: dict[str, Any]) -> str | None:
|
|
594
|
+
def find(node: Any) -> str | None:
|
|
595
|
+
if not isinstance(node, dict):
|
|
596
|
+
return None
|
|
597
|
+
if node.get("kind") == "DeclRefExpr":
|
|
598
|
+
ref = node.get("referencedDecl") or {}
|
|
599
|
+
if ref.get("kind") == "FunctionDecl" and ref.get("name"):
|
|
600
|
+
return str(ref["name"])
|
|
601
|
+
for child in node.get("inner") or ():
|
|
602
|
+
found = find(child)
|
|
603
|
+
if found:
|
|
604
|
+
return found
|
|
605
|
+
return None
|
|
606
|
+
inner = call_node.get("inner") or []
|
|
607
|
+
return find(inner[0]) if inner else None
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _raw_text(node: dict[str, Any], source_bytes: bytes) -> str:
|
|
611
|
+
rng = node.get("range") or {}
|
|
612
|
+
begin = rng.get("begin") or {}
|
|
613
|
+
end = rng.get("end") or {}
|
|
614
|
+
start = begin.get("offset") if isinstance(begin, dict) else None
|
|
615
|
+
stop = end.get("offset") if isinstance(end, dict) else None
|
|
616
|
+
tok_len = end.get("tokLen") if isinstance(end, dict) else None
|
|
617
|
+
tok_len = tok_len or 1
|
|
618
|
+
if isinstance(start, int) and isinstance(stop, int):
|
|
619
|
+
return source_bytes[start:stop + int(tok_len)].decode("utf-8", errors="replace")
|
|
620
|
+
|
|
621
|
+
# Macro-expanded AST ranges often store expansionLoc/spellingLoc instead of
|
|
622
|
+
# direct offsets. Prefer the expansion location and reconstruct only the
|
|
623
|
+
# invocation token sequence from the original source.
|
|
624
|
+
expansion = begin.get("expansionLoc") if isinstance(begin, dict) else None
|
|
625
|
+
if not isinstance(expansion, dict):
|
|
626
|
+
expansion = end.get("expansionLoc") if isinstance(end, dict) else None
|
|
627
|
+
if isinstance(expansion, dict) and isinstance(expansion.get("offset"), int):
|
|
628
|
+
return _macro_invocation_text(source_bytes, int(expansion["offset"]))
|
|
629
|
+
return "<call>"
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _macro_invocation_text(source_bytes: bytes, start: int) -> str:
|
|
633
|
+
text = source_bytes.decode("utf-8", errors="replace")
|
|
634
|
+
start = max(0, min(start, len(text)))
|
|
635
|
+
i = start
|
|
636
|
+
while i < len(text) and (text[i].isalnum() or text[i] == "_"):
|
|
637
|
+
i += 1
|
|
638
|
+
while i < len(text) and text[i].isspace() and text[i] not in "\r\n":
|
|
639
|
+
i += 1
|
|
640
|
+
if i >= len(text) or text[i] != "(":
|
|
641
|
+
return text[start:i].strip() or "<call>"
|
|
642
|
+
depth = 0
|
|
643
|
+
quote: str | None = None
|
|
644
|
+
escaped = False
|
|
645
|
+
j = i
|
|
646
|
+
while j < len(text):
|
|
647
|
+
ch = text[j]
|
|
648
|
+
if quote is not None:
|
|
649
|
+
if escaped:
|
|
650
|
+
escaped = False
|
|
651
|
+
elif ch == "\\":
|
|
652
|
+
escaped = True
|
|
653
|
+
elif ch == quote:
|
|
654
|
+
quote = None
|
|
655
|
+
else:
|
|
656
|
+
if ch in {"\"", "'"}:
|
|
657
|
+
quote = ch
|
|
658
|
+
elif ch == "(":
|
|
659
|
+
depth += 1
|
|
660
|
+
elif ch == ")":
|
|
661
|
+
depth -= 1
|
|
662
|
+
if depth == 0:
|
|
663
|
+
return text[start:j + 1].strip()
|
|
664
|
+
elif ch in "\r\n;" and depth <= 0:
|
|
665
|
+
break
|
|
666
|
+
j += 1
|
|
667
|
+
return text[start:j].strip() or "<call>"
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _range(node: dict[str, Any], source_bytes: bytes | None = None) -> SourceRange:
|
|
671
|
+
rng = node.get("range") or {}
|
|
672
|
+
begin = rng.get("begin") or node.get("loc") or {}
|
|
673
|
+
end = rng.get("end") or node.get("loc") or {}
|
|
674
|
+
if isinstance(begin, dict) and isinstance(begin.get("expansionLoc"), dict):
|
|
675
|
+
begin = begin["expansionLoc"]
|
|
676
|
+
elif isinstance(begin, dict) and isinstance(begin.get("spellingLoc"), dict):
|
|
677
|
+
begin = begin["spellingLoc"]
|
|
678
|
+
if isinstance(end, dict) and isinstance(end.get("expansionLoc"), dict):
|
|
679
|
+
end = end["expansionLoc"]
|
|
680
|
+
elif isinstance(end, dict) and isinstance(end.get("spellingLoc"), dict):
|
|
681
|
+
end = end["spellingLoc"]
|
|
682
|
+
start_line = _int_or_none(begin.get("line")) or _int_or_none(end.get("line"))
|
|
683
|
+
start_col = _int_or_none(begin.get("col"))
|
|
684
|
+
end_line = _int_or_none(end.get("line")) or start_line
|
|
685
|
+
end_col = _int_or_none(end.get("col"))
|
|
686
|
+
if source_bytes is not None and start_line is None and isinstance(begin.get("offset"), int):
|
|
687
|
+
start_line, start_col = _line_col_from_offset(source_bytes, int(begin["offset"]))
|
|
688
|
+
if source_bytes is not None and end_line is None and isinstance(end.get("offset"), int):
|
|
689
|
+
end_line, end_col = _line_col_from_offset(source_bytes, int(end["offset"]))
|
|
690
|
+
return SourceRange(start_line, start_col, end_line, end_col)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _line_col_from_offset(source_bytes: bytes, offset: int) -> tuple[int, int]:
|
|
694
|
+
offset = max(0, min(offset, len(source_bytes)))
|
|
695
|
+
prefix = source_bytes[:offset]
|
|
696
|
+
line = prefix.count(b"\n") + 1
|
|
697
|
+
last_nl = prefix.rfind(b"\n")
|
|
698
|
+
col = offset + 1 if last_nl < 0 else offset - last_nl
|
|
699
|
+
return line, col
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _preprocessor_guard_map(source_bytes: bytes) -> dict[int, str]:
|
|
703
|
+
"""Map active source lines to symbolic preprocessor guards.
|
|
704
|
+
|
|
705
|
+
This scanner does not try to evaluate C preprocessor expressions. It keeps
|
|
706
|
+
them as symbolic evidence so the active AST retains the reason a callsite
|
|
707
|
+
exists in the selected build.
|
|
708
|
+
"""
|
|
709
|
+
text = source_bytes.decode("utf-8", errors="replace")
|
|
710
|
+
stack: list[dict[str, object]] = []
|
|
711
|
+
result: dict[int, str] = {}
|
|
712
|
+
|
|
713
|
+
def current_guard() -> str | None:
|
|
714
|
+
parts = [str(frame["current"]) for frame in stack if frame.get("current")]
|
|
715
|
+
return " && ".join(f"({p})" for p in parts) if parts else None
|
|
716
|
+
|
|
717
|
+
for lineno, raw in enumerate(text.splitlines(), 1):
|
|
718
|
+
stripped = raw.strip()
|
|
719
|
+
if stripped.startswith("#ifdef "):
|
|
720
|
+
expr = f"defined({stripped[7:].strip()})"
|
|
721
|
+
stack.append({"branches": [expr], "current": expr})
|
|
722
|
+
continue
|
|
723
|
+
if stripped.startswith("#ifndef "):
|
|
724
|
+
expr = f"!defined({stripped[8:].strip()})"
|
|
725
|
+
stack.append({"branches": [expr], "current": expr})
|
|
726
|
+
continue
|
|
727
|
+
if stripped.startswith("#if "):
|
|
728
|
+
expr = stripped[4:].strip() or "<empty>"
|
|
729
|
+
stack.append({"branches": [expr], "current": expr})
|
|
730
|
+
continue
|
|
731
|
+
if stripped.startswith("#elif ") and stack:
|
|
732
|
+
expr = stripped[6:].strip() or "<empty>"
|
|
733
|
+
branches = list(stack[-1].get("branches") or [])
|
|
734
|
+
prior = " || ".join(f"({x})" for x in branches)
|
|
735
|
+
stack[-1]["current"] = f"!({prior}) && ({expr})"
|
|
736
|
+
branches.append(expr)
|
|
737
|
+
stack[-1]["branches"] = branches
|
|
738
|
+
continue
|
|
739
|
+
if stripped.startswith("#else") and stack:
|
|
740
|
+
branches = list(stack[-1].get("branches") or [])
|
|
741
|
+
prior = " || ".join(f"({x})" for x in branches) or "<none>"
|
|
742
|
+
stack[-1]["current"] = f"!({prior})"
|
|
743
|
+
continue
|
|
744
|
+
if stripped.startswith("#endif") and stack:
|
|
745
|
+
stack.pop()
|
|
746
|
+
continue
|
|
747
|
+
guard = current_guard()
|
|
748
|
+
if guard is not None:
|
|
749
|
+
result[lineno] = guard
|
|
750
|
+
return result
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _preprocessor_guard_regions(source_bytes: bytes, source_path: str) -> list[GuardedSourceRegion]:
|
|
754
|
+
"""Return contiguous guarded source regions for *all* preprocessor branches.
|
|
755
|
+
|
|
756
|
+
This is intentionally independent from Clang's active AST. Even source lines
|
|
757
|
+
excluded by the selected build remain represented as symbolic evidence, so an
|
|
758
|
+
inactive branch is distinguishable from code that never existed.
|
|
759
|
+
"""
|
|
760
|
+
by_line = _preprocessor_guard_map(source_bytes)
|
|
761
|
+
if not by_line:
|
|
762
|
+
return []
|
|
763
|
+
regions: list[GuardedSourceRegion] = []
|
|
764
|
+
start: int | None = None
|
|
765
|
+
prev: int | None = None
|
|
766
|
+
expr: str | None = None
|
|
767
|
+
for line in sorted(by_line):
|
|
768
|
+
current = by_line[line]
|
|
769
|
+
if start is None:
|
|
770
|
+
start = prev = line
|
|
771
|
+
expr = current
|
|
772
|
+
continue
|
|
773
|
+
if current == expr and line == (prev or line) + 1:
|
|
774
|
+
prev = line
|
|
775
|
+
continue
|
|
776
|
+
regions.append(GuardedSourceRegion(source_path, SourceRange(start, 1, prev, None), str(expr)))
|
|
777
|
+
start = prev = line
|
|
778
|
+
expr = current
|
|
779
|
+
if start is not None:
|
|
780
|
+
regions.append(GuardedSourceRegion(source_path, SourceRange(start, 1, prev, None), str(expr)))
|
|
781
|
+
return regions
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _has_body(node: dict[str, Any]) -> bool:
|
|
785
|
+
return any(isinstance(c, dict) and c.get("kind") == "CompoundStmt" for c in node.get("inner") or ())
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _function_id(tu: TranslationUnit, name: str, line: int | None, col: int | None, storage: Any, qual_type: Any) -> str:
|
|
789
|
+
line = line if line is not None else "?"
|
|
790
|
+
col = col if col is not None else "?"
|
|
791
|
+
storage_text = storage or "external"
|
|
792
|
+
type_text = qual_type or "?"
|
|
793
|
+
return f"{tu.source.project_relative_path}:{line}:{col}:{storage_text}:{name}:{type_text}"
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _int_or_none(value: Any) -> int | None:
|
|
797
|
+
return value if isinstance(value, int) else None
|