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
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from bisect import bisect_right
|
|
5
|
+
import gc
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
from .fast_parser import FastEligibility
|
|
10
|
+
from .model import (
|
|
11
|
+
CallSiteRecord,
|
|
12
|
+
Diagnostic,
|
|
13
|
+
FileParseArtifact,
|
|
14
|
+
FunctionRecord,
|
|
15
|
+
ParseStatus,
|
|
16
|
+
SourceRange,
|
|
17
|
+
TranslationUnit,
|
|
18
|
+
)
|
|
19
|
+
from .parser import ParserBackend, _preprocessor_guard_regions
|
|
20
|
+
from .parser_environment import load_parser_dependency_lock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class TreeSitterPrepared:
|
|
25
|
+
source_bytes: bytes
|
|
26
|
+
node_types: frozenset[str]
|
|
27
|
+
eligibility: FastEligibility
|
|
28
|
+
has_error_or_missing: bool
|
|
29
|
+
root_type: str
|
|
30
|
+
artifact: FileParseArtifact
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TreeSitterCParser(ParserBackend):
|
|
34
|
+
"""Canonical fast C syntax front-end backed by tree-sitter-c.
|
|
35
|
+
|
|
36
|
+
This backend deliberately owns syntax only. Constructs that require C type
|
|
37
|
+
or compiler semantics are surfaced through ``eligibility.reasons`` so the
|
|
38
|
+
HybridParser can route them to selective Clang refinement. The syntax tree
|
|
39
|
+
itself is still usable for fail-soft evidence when Clang is unavailable.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = "tree-sitter-c-v1"
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
try:
|
|
46
|
+
from tree_sitter import Language, Parser, Query, QueryCursor
|
|
47
|
+
import tree_sitter_c
|
|
48
|
+
except Exception as exc: # pragma: no cover - exercised on wheel env
|
|
49
|
+
raise RuntimeError("locked tree-sitter runtime is not available") from exc
|
|
50
|
+
|
|
51
|
+
language = Language(tree_sitter_c.language())
|
|
52
|
+
lock = load_parser_dependency_lock()
|
|
53
|
+
expected_abi = int(lock["packages"]["tree-sitter-c"]["grammar_abi"])
|
|
54
|
+
observed_abi = int(getattr(language, "abi_version", -1))
|
|
55
|
+
if observed_abi != expected_abi:
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
f"tree-sitter-c ABI mismatch: observed={observed_abi} expected={expected_abi}"
|
|
58
|
+
)
|
|
59
|
+
self.language = language
|
|
60
|
+
self.parser = Parser(language)
|
|
61
|
+
self._QueryCursor = QueryCursor
|
|
62
|
+
self._function_query = Query(language, "(function_definition declarator: (function_declarator declarator: (identifier) @name) @decl) @fn")
|
|
63
|
+
self._syntax_evidence_query = Query(
|
|
64
|
+
language,
|
|
65
|
+
"[(function_definition) (call_expression) (return_statement) "
|
|
66
|
+
"(break_statement) (if_statement) (while_statement) (for_statement) "
|
|
67
|
+
"(field_expression) (struct_specifier) (type_definition) (preproc_ifdef)] @node",
|
|
68
|
+
)
|
|
69
|
+
self._all_call_query = Query(language, "(call_expression) @call")
|
|
70
|
+
self._direct_call_query = Query(language, "(call_expression function: (identifier)) @call")
|
|
71
|
+
self._call_detail_query = Query(language, "(call_expression function: (_) @target) @call")
|
|
72
|
+
|
|
73
|
+
def prepare(self, translation_unit: TranslationUnit) -> TreeSitterPrepared:
|
|
74
|
+
if translation_unit.source_text is not None:
|
|
75
|
+
source = translation_unit.source_text.encode("utf-8")
|
|
76
|
+
else:
|
|
77
|
+
source = translation_unit.source.path.read_bytes()
|
|
78
|
+
source_text = source.decode("utf-8", errors="replace")
|
|
79
|
+
line_starts = _byte_line_starts(source)
|
|
80
|
+
guarded_regions = _preprocessor_guard_regions(
|
|
81
|
+
source, translation_unit.source.project_relative_path
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
tree = self.parser.parse(source)
|
|
85
|
+
root = tree.root_node
|
|
86
|
+
syntax_cursor = self._QueryCursor(self._syntax_evidence_query)
|
|
87
|
+
syntax_captures = syntax_cursor.captures(root)
|
|
88
|
+
evidence_nodes = syntax_captures.get("node", ())
|
|
89
|
+
all_call_cursor = self._QueryCursor(self._all_call_query)
|
|
90
|
+
all_call_captures = all_call_cursor.captures(root)
|
|
91
|
+
all_calls = all_call_captures.get("call", ())
|
|
92
|
+
direct_call_cursor = self._QueryCursor(self._direct_call_query)
|
|
93
|
+
direct_call_captures = direct_call_cursor.captures(root)
|
|
94
|
+
direct_calls = direct_call_captures.get("call", ())
|
|
95
|
+
function_cursor = self._QueryCursor(self._function_query)
|
|
96
|
+
function_matches = function_cursor.matches(root)
|
|
97
|
+
call_detail_cursor = self._QueryCursor(self._call_detail_query)
|
|
98
|
+
call_matches = call_detail_cursor.matches(root)
|
|
99
|
+
|
|
100
|
+
gc_was_enabled = gc.isenabled()
|
|
101
|
+
if gc_was_enabled:
|
|
102
|
+
gc.disable()
|
|
103
|
+
raw_functions: list[tuple] = []
|
|
104
|
+
raw_calls: list[tuple] = []
|
|
105
|
+
raw_problems: list[tuple] = []
|
|
106
|
+
try:
|
|
107
|
+
node_types_set: set[str] = {root.type}
|
|
108
|
+
node_types_set.update(node.type for node in evidence_nodes)
|
|
109
|
+
node_types = frozenset(node_types_set)
|
|
110
|
+
indirect_call_seen = len(direct_calls) != len(all_calls)
|
|
111
|
+
has_error_or_missing = bool(root.has_error)
|
|
112
|
+
root_type = root.type
|
|
113
|
+
|
|
114
|
+
reasons: list[str] = []
|
|
115
|
+
if node_types.intersection({"type_definition", "struct_specifier", "field_expression"}):
|
|
116
|
+
reasons.append("TYPE_AMBIGUOUS")
|
|
117
|
+
if re.search(rb"\b__(?:[A-Za-z_]\w*)\b", source):
|
|
118
|
+
reasons.append("COMPILER_EXTENSION")
|
|
119
|
+
if re.search(rb"(?m)^\s*#\s*(?:define|if|ifdef|ifndef|elif|else|endif)\b", source):
|
|
120
|
+
reasons.append("PREPROCESSOR_SEMANTICS")
|
|
121
|
+
if re.search(rb"\(\s*\*\s*[A-Za-z_]\w*\s*\)\s*\(", source):
|
|
122
|
+
reasons.append("INDIRECT_CALL")
|
|
123
|
+
if indirect_call_seen:
|
|
124
|
+
reasons.append("INDIRECT_CALL")
|
|
125
|
+
eligibility = FastEligibility(not reasons, tuple(dict.fromkeys(reasons)))
|
|
126
|
+
|
|
127
|
+
fn_spans: list[tuple[int, int, str, str]] = []
|
|
128
|
+
for _pattern_index, captures in function_matches:
|
|
129
|
+
fn_node = captures["fn"][0]
|
|
130
|
+
name_node = captures["name"][0]
|
|
131
|
+
declarator = captures["decl"][0]
|
|
132
|
+
fn_start, fn_end = fn_node.start_byte, fn_node.end_byte
|
|
133
|
+
name_start, name_end = name_node.start_byte, name_node.end_byte
|
|
134
|
+
decl_start, decl_end = declarator.start_byte, declarator.end_byte
|
|
135
|
+
name = source[name_start:name_end].decode("utf-8", errors="replace")
|
|
136
|
+
prefix = source[fn_start:decl_start].decode("utf-8", errors="replace")
|
|
137
|
+
sm = re.search(r"\b(static|extern)\b", prefix)
|
|
138
|
+
storage = sm.group(1) if sm else None
|
|
139
|
+
type_qualifier = prefix.strip() or None
|
|
140
|
+
decl_text = source[decl_start:decl_end].decode("utf-8", errors="replace")
|
|
141
|
+
params = _parameter_names_from_declarator_text(decl_text)
|
|
142
|
+
name_line, name_col = _line_col_from_byte(name_start, line_starts)
|
|
143
|
+
qid = _function_id(
|
|
144
|
+
translation_unit, name, name_line, name_col, storage, type_qualifier
|
|
145
|
+
)
|
|
146
|
+
raw_functions.append(
|
|
147
|
+
(name, qid, storage, type_qualifier, params, fn_start, fn_end)
|
|
148
|
+
)
|
|
149
|
+
fn_spans.append((fn_start, fn_end, qid, name))
|
|
150
|
+
|
|
151
|
+
fn_starts = [x[0] for x in fn_spans]
|
|
152
|
+
for _pattern_index, captures in call_matches:
|
|
153
|
+
call_node = captures["call"][0]
|
|
154
|
+
target = captures["target"][0]
|
|
155
|
+
call_start, call_end = call_node.start_byte, call_node.end_byte
|
|
156
|
+
pos = bisect_right(fn_starts, call_start) - 1
|
|
157
|
+
if pos < 0 or call_end > fn_spans[pos][1]:
|
|
158
|
+
continue
|
|
159
|
+
_fs, _fe, caller_id, caller_name = fn_spans[pos]
|
|
160
|
+
callee = (
|
|
161
|
+
source[target.start_byte:target.end_byte].decode("utf-8", errors="replace")
|
|
162
|
+
if target.type == "identifier" else None
|
|
163
|
+
)
|
|
164
|
+
raw_calls.append(
|
|
165
|
+
(
|
|
166
|
+
caller_id, caller_name, callee,
|
|
167
|
+
source[call_start:call_end].decode("utf-8", errors="replace"),
|
|
168
|
+
call_start, call_end, callee is None,
|
|
169
|
+
_control_context(call_node, source),
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if has_error_or_missing:
|
|
174
|
+
for node in _walk(root):
|
|
175
|
+
is_problem = (
|
|
176
|
+
node.type == "ERROR"
|
|
177
|
+
or bool(getattr(node, "is_error", False))
|
|
178
|
+
or bool(getattr(node, "is_missing", False))
|
|
179
|
+
)
|
|
180
|
+
if is_problem:
|
|
181
|
+
raw_problems.append(
|
|
182
|
+
(
|
|
183
|
+
node.start_byte, node.end_byte, node.type,
|
|
184
|
+
bool(getattr(node, "is_missing", False)),
|
|
185
|
+
source[node.start_byte:node.end_byte].decode("utf-8", errors="replace"),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
finally:
|
|
189
|
+
captures = None
|
|
190
|
+
fn_node = name_node = declarator = call_node = target = node = None
|
|
191
|
+
call_matches = call_detail_cursor = None
|
|
192
|
+
function_matches = function_cursor = None
|
|
193
|
+
evidence_nodes = syntax_captures = syntax_cursor = None
|
|
194
|
+
all_calls = all_call_captures = all_call_cursor = None
|
|
195
|
+
direct_calls = direct_call_captures = direct_call_cursor = None
|
|
196
|
+
root = tree = None
|
|
197
|
+
if gc_was_enabled:
|
|
198
|
+
gc.enable()
|
|
199
|
+
if gc_was_enabled:
|
|
200
|
+
gc.collect()
|
|
201
|
+
|
|
202
|
+
functions = [
|
|
203
|
+
FunctionRecord(
|
|
204
|
+
name=name, qualified_id=qid,
|
|
205
|
+
source_path=translation_unit.source.project_relative_path,
|
|
206
|
+
source_range=_range_from_bytes(start, end, line_starts),
|
|
207
|
+
storage_class=storage, type_qualifier=type_qualifier, parameters=params,
|
|
208
|
+
)
|
|
209
|
+
for name, qid, storage, type_qualifier, params, start, end in raw_functions
|
|
210
|
+
]
|
|
211
|
+
callsites = [
|
|
212
|
+
CallSiteRecord(
|
|
213
|
+
caller_id=caller_id, caller_name=caller_name, callee_name=callee,
|
|
214
|
+
raw_text=raw_text, source_path=translation_unit.source.project_relative_path,
|
|
215
|
+
source_range=_range_from_bytes(start, end, line_starts),
|
|
216
|
+
unknown_possible=unknown, control_context=context,
|
|
217
|
+
)
|
|
218
|
+
for caller_id, caller_name, callee, raw_text, start, end, unknown, context in raw_calls
|
|
219
|
+
]
|
|
220
|
+
diagnostics: list[Diagnostic] = []
|
|
221
|
+
opaque_regions: list[SourceRange] = []
|
|
222
|
+
for start, end, node_type, missing, raw in raw_problems:
|
|
223
|
+
opaque_regions.append(_range_from_bytes(start, end, line_starts))
|
|
224
|
+
diagnostics.append(
|
|
225
|
+
Diagnostic(
|
|
226
|
+
severity="error",
|
|
227
|
+
message=f"tree-sitter syntax recovery node type={node_type} missing={missing}",
|
|
228
|
+
raw=raw,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
status = (
|
|
232
|
+
ParseStatus.ERROR_RECOVERED if has_error_or_missing and (functions or callsites)
|
|
233
|
+
else ParseStatus.PARTIAL if has_error_or_missing
|
|
234
|
+
else ParseStatus.OK
|
|
235
|
+
)
|
|
236
|
+
artifact = FileParseArtifact(
|
|
237
|
+
translation_unit=translation_unit, status=status, functions=functions,
|
|
238
|
+
callsites=callsites, diagnostics=diagnostics, opaque_regions=opaque_regions,
|
|
239
|
+
guarded_regions=guarded_regions, parser_backend=self.name,
|
|
240
|
+
)
|
|
241
|
+
return TreeSitterPrepared(
|
|
242
|
+
source_bytes=source, node_types=node_types, eligibility=eligibility,
|
|
243
|
+
has_error_or_missing=has_error_or_missing, root_type=root_type, artifact=artifact,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def parse(self, translation_unit: TranslationUnit) -> FileParseArtifact:
|
|
247
|
+
prepared = self.prepare(translation_unit)
|
|
248
|
+
if not prepared.eligibility.eligible:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"semantic refinement required: " + ", ".join(prepared.eligibility.reasons)
|
|
251
|
+
)
|
|
252
|
+
return self.parse_prepared(translation_unit, prepared)
|
|
253
|
+
|
|
254
|
+
def parse_prepared(
|
|
255
|
+
self, translation_unit: TranslationUnit, prepared: TreeSitterPrepared
|
|
256
|
+
) -> FileParseArtifact:
|
|
257
|
+
# ``prepare`` closes the native Tree-sitter lifetime boundary and stores
|
|
258
|
+
# only pure Python evidence/artifacts. No Node/Cursor crosses this API.
|
|
259
|
+
return prepared.artifact
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _walk(root: Any) -> Iterable[Any]:
|
|
263
|
+
# TreeCursor avoids materialising each node's full ``children`` tuple.
|
|
264
|
+
# That is both cheaper and avoids a Windows py-tree-sitter access-violation
|
|
265
|
+
# observed on large translation units with hundreds of function siblings.
|
|
266
|
+
cursor = root.walk()
|
|
267
|
+
while True:
|
|
268
|
+
yield cursor.node
|
|
269
|
+
if cursor.goto_first_child():
|
|
270
|
+
continue
|
|
271
|
+
if cursor.goto_next_sibling():
|
|
272
|
+
continue
|
|
273
|
+
while True:
|
|
274
|
+
if not cursor.goto_parent():
|
|
275
|
+
return
|
|
276
|
+
if cursor.goto_next_sibling():
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _text(source: bytes, node: Any | None) -> str:
|
|
281
|
+
if node is None:
|
|
282
|
+
return ""
|
|
283
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _range(node: Any) -> SourceRange:
|
|
287
|
+
return SourceRange(
|
|
288
|
+
node.start_point.row + 1,
|
|
289
|
+
node.start_point.column + 1,
|
|
290
|
+
node.end_point.row + 1,
|
|
291
|
+
node.end_point.column + 1,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _declarator_identifier(node: Any | None) -> Any | None:
|
|
296
|
+
if node is None:
|
|
297
|
+
return None
|
|
298
|
+
if node.type == "identifier":
|
|
299
|
+
return node
|
|
300
|
+
# C declarator nesting is represented through a named `declarator` field.
|
|
301
|
+
child = node.child_by_field_name("declarator")
|
|
302
|
+
if child is not None:
|
|
303
|
+
found = _declarator_identifier(child)
|
|
304
|
+
if found is not None:
|
|
305
|
+
return found
|
|
306
|
+
# Conservative fallback for parenthesized/pointer declarators. Avoid the
|
|
307
|
+
# parameter list so parameter identifiers are not mistaken for function names.
|
|
308
|
+
for candidate in getattr(node, "named_children", ()) or ():
|
|
309
|
+
if candidate.type == "parameter_list":
|
|
310
|
+
continue
|
|
311
|
+
found = _declarator_identifier(candidate)
|
|
312
|
+
if found is not None:
|
|
313
|
+
return found
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _parameter_names(source: bytes, declarator: Any | None) -> tuple[str, ...]:
|
|
318
|
+
if declarator is None:
|
|
319
|
+
return ()
|
|
320
|
+
parameter_list = next((n for n in _walk(declarator) if n.type == "parameter_list"), None)
|
|
321
|
+
if parameter_list is None:
|
|
322
|
+
return ()
|
|
323
|
+
out: list[str] = []
|
|
324
|
+
for child in getattr(parameter_list, "named_children", ()) or ():
|
|
325
|
+
if child.type != "parameter_declaration":
|
|
326
|
+
continue
|
|
327
|
+
decl = child.child_by_field_name("declarator")
|
|
328
|
+
ident = _declarator_identifier(decl)
|
|
329
|
+
if ident is not None:
|
|
330
|
+
out.append(_text(source, ident))
|
|
331
|
+
return tuple(out)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _byte_line_starts(source: bytes) -> tuple[int, ...]:
|
|
335
|
+
starts = [0]
|
|
336
|
+
starts.extend(i + 1 for i, b in enumerate(source) if b == 10)
|
|
337
|
+
return tuple(starts)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _line_col_from_byte(offset: int, line_starts: tuple[int, ...]) -> tuple[int, int]:
|
|
341
|
+
line_index = bisect_right(line_starts, offset) - 1
|
|
342
|
+
return line_index + 1, offset - line_starts[line_index] + 1
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _range_from_bytes(start: int, end: int, line_starts: tuple[int, ...]) -> SourceRange:
|
|
346
|
+
sl, sc = _line_col_from_byte(start, line_starts)
|
|
347
|
+
el, ec = _line_col_from_byte(end, line_starts)
|
|
348
|
+
return SourceRange(sl, sc, el, ec)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _parameter_names_from_declarator_text(declarator: str) -> tuple[str, ...]:
|
|
352
|
+
left = declarator.find("(")
|
|
353
|
+
right = declarator.rfind(")")
|
|
354
|
+
if left < 0 or right <= left:
|
|
355
|
+
return ()
|
|
356
|
+
text = declarator[left + 1:right].strip()
|
|
357
|
+
if not text or text == "void":
|
|
358
|
+
return ()
|
|
359
|
+
names: list[str] = []
|
|
360
|
+
for part in text.split(","):
|
|
361
|
+
ids = re.findall(r"[A-Za-z_]\w*", part)
|
|
362
|
+
if not ids:
|
|
363
|
+
continue
|
|
364
|
+
candidate = ids[-1]
|
|
365
|
+
if candidate not in {"void", "char", "short", "int", "long", "float", "double", "signed", "unsigned", "const", "volatile", "struct", "union", "enum"}:
|
|
366
|
+
names.append(candidate)
|
|
367
|
+
return tuple(names)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _storage_class(source: bytes, function_node: Any, declarator: Any | None = None) -> str | None:
|
|
371
|
+
# Avoid named_children enumeration here: Windows py-tree-sitter 0.26.0
|
|
372
|
+
# can access-violate on large sibling sets. Storage-class keywords occur
|
|
373
|
+
# before the function declarator, so the raw prefix is sufficient.
|
|
374
|
+
end = declarator.start_byte if declarator is not None else function_node.end_byte
|
|
375
|
+
prefix = source[function_node.start_byte:end].decode("utf-8", errors="replace")
|
|
376
|
+
match = re.search(r"\b(static|extern)\b", prefix)
|
|
377
|
+
return match.group(1) if match else None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _type_qualifier(source: bytes, function_node: Any, declarator: Any | None) -> str | None:
|
|
381
|
+
if declarator is None:
|
|
382
|
+
return None
|
|
383
|
+
prefix = source[function_node.start_byte : declarator.start_byte].decode("utf-8", errors="replace").strip()
|
|
384
|
+
return prefix or None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _function_id(
|
|
388
|
+
tu: TranslationUnit,
|
|
389
|
+
name: str,
|
|
390
|
+
line: int,
|
|
391
|
+
col: int,
|
|
392
|
+
storage: str | None,
|
|
393
|
+
qual_type: str | None,
|
|
394
|
+
) -> str:
|
|
395
|
+
return f"{tu.source.project_relative_path}:{line}:{col}:{storage or 'external'}:{name}:{qual_type or '?'}"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _control_context(node: Any, source: bytes) -> tuple[str, ...]:
|
|
399
|
+
contexts: list[str] = []
|
|
400
|
+
child = node
|
|
401
|
+
parent = getattr(node, "parent", None)
|
|
402
|
+
while parent is not None and parent.type != "function_definition":
|
|
403
|
+
if parent.type == "if_statement":
|
|
404
|
+
condition = parent.child_by_field_name("condition")
|
|
405
|
+
cond = " ".join(_text(source, condition).split()) if condition is not None else ""
|
|
406
|
+
base = f"IF:if{cond}" if cond.startswith("(") else f"IF:if({cond})"
|
|
407
|
+
alt = parent.child_by_field_name("alternative")
|
|
408
|
+
branch = "/else" if alt is not None and _node_contains(alt, child) else "/then"
|
|
409
|
+
contexts.append(base + branch)
|
|
410
|
+
elif parent.type in {"while_statement", "for_statement", "do_statement"}:
|
|
411
|
+
raw = " ".join(_text(source, parent).split())
|
|
412
|
+
condition = parent.child_by_field_name("condition")
|
|
413
|
+
if parent.type == "while_statement":
|
|
414
|
+
cond = " ".join(_text(source, condition).split()) if condition is not None else ""
|
|
415
|
+
header = f"while {cond}" if cond.startswith("(") else f"while ({cond})"
|
|
416
|
+
compact = "".join(header.split()).lower()
|
|
417
|
+
contexts.append("LOOP:" + (("[infinite] " if compact in {"while(1)", "while(true)"} else "") + header))
|
|
418
|
+
elif parent.type == "for_statement":
|
|
419
|
+
header = raw.split("{",1)[0].strip()
|
|
420
|
+
compact = "".join(header.split()).lower()
|
|
421
|
+
contexts.append("LOOP:" + (("[infinite] " if compact == "for(;;)" else "") + header))
|
|
422
|
+
else:
|
|
423
|
+
compact = "".join(raw.split()).lower()
|
|
424
|
+
infinite = compact.endswith("while(1);") or compact.endswith("while(true);")
|
|
425
|
+
contexts.append("LOOP:" + (("[infinite] " if infinite else "") + ("do/while(1)" if infinite else "do/while")))
|
|
426
|
+
elif parent.type == "case_statement":
|
|
427
|
+
labels = _tree_sitter_case_labels(parent, source)
|
|
428
|
+
contexts.append("DEFAULT" if labels == ["DEFAULT"] else "CASE:" + "|".join(labels))
|
|
429
|
+
elif parent.type == "switch_statement":
|
|
430
|
+
condition = parent.child_by_field_name("condition")
|
|
431
|
+
cond = " ".join(_text(source, condition).split()) if condition is not None else ""
|
|
432
|
+
contexts.append(f"SWITCH:switch{cond}" if cond.startswith("(") else f"SWITCH:switch({cond})")
|
|
433
|
+
child = parent
|
|
434
|
+
parent = getattr(parent, "parent", None)
|
|
435
|
+
contexts.reverse()
|
|
436
|
+
return tuple(contexts)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _node_contains(ancestor: Any, node: Any) -> bool:
|
|
440
|
+
return ancestor.start_byte <= node.start_byte and node.end_byte <= ancestor.end_byte
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _tree_sitter_case_labels(case_node: Any, source: bytes) -> list[str]:
|
|
444
|
+
value = case_node.child_by_field_name("value")
|
|
445
|
+
if value is None:
|
|
446
|
+
return ["DEFAULT"]
|
|
447
|
+
labels = [_text(source, value).strip() or "?"]
|
|
448
|
+
parent = getattr(case_node, "parent", None)
|
|
449
|
+
if parent is None:
|
|
450
|
+
return labels
|
|
451
|
+
named = list(getattr(parent, "named_children", ()))
|
|
452
|
+
try:
|
|
453
|
+
pos = named.index(case_node)
|
|
454
|
+
except ValueError:
|
|
455
|
+
return labels
|
|
456
|
+
i = pos - 1
|
|
457
|
+
prefix: list[str] = []
|
|
458
|
+
while i >= 0 and named[i].type == "case_statement":
|
|
459
|
+
prev = named[i]
|
|
460
|
+
prev_value = prev.child_by_field_name("value")
|
|
461
|
+
# A label-only case has no named child other than its value. It falls
|
|
462
|
+
# through to the current case and therefore represents an alternative.
|
|
463
|
+
if prev_value is None or len(getattr(prev, "named_children", ())) != 1:
|
|
464
|
+
break
|
|
465
|
+
prefix.append(_text(source, prev_value).strip() or "?")
|
|
466
|
+
i -= 1
|
|
467
|
+
prefix.reverse()
|
|
468
|
+
return prefix + labels
|
cseq/valueflow.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
|
|
5
|
+
from .model import (
|
|
6
|
+
CallSiteRecord,
|
|
7
|
+
CallTargetKind,
|
|
8
|
+
FlowEdge,
|
|
9
|
+
FlowEdgeKind,
|
|
10
|
+
FunctionRecord,
|
|
11
|
+
ProjectIndex,
|
|
12
|
+
ValueSlotKind,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def bind_direct_call_arguments(index: ProjectIndex) -> None:
|
|
17
|
+
target_calls = [
|
|
18
|
+
c for c in index.callsites
|
|
19
|
+
if c.target_kind == CallTargetKind.INTERNAL_EXACT and c.target_function_id
|
|
20
|
+
]
|
|
21
|
+
if not target_calls:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Demand-driven: only callees of exact direct calls need parameter metadata.
|
|
25
|
+
# Avoid a project-wide qualified-id dictionary on million-function projects.
|
|
26
|
+
needed_names = {c.callee_name for c in target_calls if c.callee_name}
|
|
27
|
+
needed_ids = {c.target_function_id for c in target_calls if c.target_function_id}
|
|
28
|
+
functions_by_id = {}
|
|
29
|
+
for f in index.functions_named(needed_names):
|
|
30
|
+
if f.name in needed_names and f.qualified_id in needed_ids:
|
|
31
|
+
functions_by_id[f.qualified_id] = f
|
|
32
|
+
if len(functions_by_id) == len(needed_ids):
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
existing = {(e.source_key, e.target_key, e.kind) for e in index.flow_edges}
|
|
36
|
+
new_edges: list[FlowEdge] = []
|
|
37
|
+
|
|
38
|
+
for call in target_calls:
|
|
39
|
+
target = functions_by_id.get(call.target_function_id)
|
|
40
|
+
if not target:
|
|
41
|
+
continue
|
|
42
|
+
for arg_slot, param_name in zip(call.argument_slot_keys, target.parameters):
|
|
43
|
+
if not arg_slot:
|
|
44
|
+
continue
|
|
45
|
+
param_key = f"{target.qualified_id}::param::{param_name}"
|
|
46
|
+
key = (arg_slot, param_key, FlowEdgeKind.ARG_TO_PARAM)
|
|
47
|
+
if key not in existing:
|
|
48
|
+
new_edges.append(
|
|
49
|
+
FlowEdge(
|
|
50
|
+
arg_slot,
|
|
51
|
+
param_key,
|
|
52
|
+
FlowEdgeKind.ARG_TO_PARAM,
|
|
53
|
+
call.source_path,
|
|
54
|
+
call.source_range,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
existing.add(key)
|
|
58
|
+
index.flow_edges.extend(new_edges)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_indirect_calls(index: ProjectIndex) -> None:
|
|
62
|
+
"""Resolve indirect calls with a small demand-driven ValueSlot solver.
|
|
63
|
+
|
|
64
|
+
The PoC supports:
|
|
65
|
+
* local function-pointer assignments/initializers
|
|
66
|
+
* conditional initializers (multiple function sources)
|
|
67
|
+
* struct-field callback assignment propagated through direct-call args
|
|
68
|
+
* global callback arrays, exact and unknown indices
|
|
69
|
+
|
|
70
|
+
Unknown sources remain representable and can be extended later without
|
|
71
|
+
changing the CallSite model.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
indirect_calls = [
|
|
75
|
+
c for c in index.callsites
|
|
76
|
+
if c.callee_slot_key is not None and c.target_kind == CallTargetKind.UNKNOWN
|
|
77
|
+
]
|
|
78
|
+
if not indirect_calls:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
needed_function_names = {
|
|
82
|
+
edge.source_key.split("::", 1)[1]
|
|
83
|
+
for edge in index.flow_edges
|
|
84
|
+
if edge.source_key.startswith("function::")
|
|
85
|
+
}
|
|
86
|
+
function_by_name: dict[str, list[FunctionRecord]] = {}
|
|
87
|
+
if needed_function_names:
|
|
88
|
+
for fn in index.functions_named(needed_function_names):
|
|
89
|
+
if fn.name in needed_function_names:
|
|
90
|
+
function_by_name.setdefault(fn.name, []).append(fn)
|
|
91
|
+
|
|
92
|
+
incoming: dict[str, list[FlowEdge]] = {}
|
|
93
|
+
for edge in index.flow_edges:
|
|
94
|
+
incoming.setdefault(edge.target_key, []).append(edge)
|
|
95
|
+
|
|
96
|
+
slot_kind = {slot.key: slot.kind for slot in index.value_slots}
|
|
97
|
+
|
|
98
|
+
def resolve_slot(slot_key: str, seen: set[str] | None = None) -> tuple[set[str], bool]:
|
|
99
|
+
seen = set() if seen is None else set(seen)
|
|
100
|
+
if slot_key in seen:
|
|
101
|
+
return set(), True
|
|
102
|
+
seen.add(slot_key)
|
|
103
|
+
|
|
104
|
+
targets: set[str] = set()
|
|
105
|
+
unknown = False
|
|
106
|
+
had_evidence = False
|
|
107
|
+
|
|
108
|
+
for edge in incoming.get(slot_key, []):
|
|
109
|
+
had_evidence = True
|
|
110
|
+
if edge.source_key.startswith("function::"):
|
|
111
|
+
name = edge.source_key.split("::", 1)[1]
|
|
112
|
+
matches = function_by_name.get(name, [])
|
|
113
|
+
if matches:
|
|
114
|
+
targets.update(f.qualified_id for f in matches)
|
|
115
|
+
else:
|
|
116
|
+
unknown = True
|
|
117
|
+
else:
|
|
118
|
+
sub_targets, sub_unknown = resolve_slot(edge.source_key, seen)
|
|
119
|
+
targets.update(sub_targets)
|
|
120
|
+
unknown |= sub_unknown
|
|
121
|
+
|
|
122
|
+
# Field-sensitive propagation through object/parameter alias bindings.
|
|
123
|
+
# Example: main::local::h.cb -> dispatch::param::h.cb, where the base
|
|
124
|
+
# object binding main::local::h -> dispatch::param::h is ARG_TO_PARAM.
|
|
125
|
+
if "." in slot_key:
|
|
126
|
+
base_key, field = slot_key.rsplit(".", 1)
|
|
127
|
+
for edge in incoming.get(base_key, []):
|
|
128
|
+
if edge.kind not in {FlowEdgeKind.ARG_TO_PARAM, FlowEdgeKind.ASSIGN, FlowEdgeKind.INIT}:
|
|
129
|
+
continue
|
|
130
|
+
# An allocation-site edge records provenance of the base pointer;
|
|
131
|
+
# it is not another named object whose field should be recursively
|
|
132
|
+
# read. Actual field writes through aliases are handled by the named
|
|
133
|
+
# pointer/object edges.
|
|
134
|
+
if slot_kind.get(edge.source_key) == ValueSlotKind.HEAP_ALLOCATION:
|
|
135
|
+
continue
|
|
136
|
+
had_evidence = True
|
|
137
|
+
aliased_field = f"{edge.source_key}.{field}"
|
|
138
|
+
sub_targets, sub_unknown = resolve_slot(aliased_field, seen)
|
|
139
|
+
targets.update(sub_targets)
|
|
140
|
+
unknown |= sub_unknown
|
|
141
|
+
|
|
142
|
+
# No evidence for a non-function slot means unknown remains possible.
|
|
143
|
+
if not had_evidence and slot_kind.get(slot_key) not in {
|
|
144
|
+
ValueSlotKind.GLOBAL_ARRAY_ELEMENT,
|
|
145
|
+
ValueSlotKind.GLOBAL_ARRAY_SUMMARY,
|
|
146
|
+
}:
|
|
147
|
+
unknown = True
|
|
148
|
+
return targets, unknown
|
|
149
|
+
|
|
150
|
+
resolved_calls: list[CallSiteRecord] = []
|
|
151
|
+
for call in index.callsites:
|
|
152
|
+
if call.callee_slot_key is None or call.target_kind != CallTargetKind.UNKNOWN:
|
|
153
|
+
resolved_calls.append(call)
|
|
154
|
+
continue
|
|
155
|
+
targets, unknown = resolve_slot(call.callee_slot_key)
|
|
156
|
+
ordered = tuple(sorted(targets))
|
|
157
|
+
if len(ordered) == 1 and not unknown:
|
|
158
|
+
resolved_calls.append(
|
|
159
|
+
replace(
|
|
160
|
+
call,
|
|
161
|
+
target_kind=CallTargetKind.INDIRECT_EXACT,
|
|
162
|
+
target_function_id=ordered[0],
|
|
163
|
+
target_function_ids=ordered,
|
|
164
|
+
unknown_possible=False,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
elif ordered:
|
|
168
|
+
resolved_calls.append(
|
|
169
|
+
replace(
|
|
170
|
+
call,
|
|
171
|
+
target_kind=CallTargetKind.INDIRECT_POSSIBLE,
|
|
172
|
+
target_function_id=None,
|
|
173
|
+
target_function_ids=ordered,
|
|
174
|
+
unknown_possible=unknown,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
resolved_calls.append(replace(call, unknown_possible=True))
|
|
179
|
+
index.callsites[:] = resolved_calls
|