kconfig-preprocessor-parser 0.1.0__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.
- kconfig_preprocessor_parser/__init__.py +33 -0
- kconfig_preprocessor_parser/parser.py +1130 -0
- kconfig_preprocessor_parser/py.typed +0 -0
- kconfig_preprocessor_parser-0.1.0.dist-info/METADATA +155 -0
- kconfig_preprocessor_parser-0.1.0.dist-info/RECORD +7 -0
- kconfig_preprocessor_parser-0.1.0.dist-info/WHEEL +4 -0
- kconfig_preprocessor_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from kconfig_preprocessor_parser.parser import (
|
|
2
|
+
ConditionInfo,
|
|
3
|
+
EffectiveConditionInfo,
|
|
4
|
+
PreprocBlock,
|
|
5
|
+
extract_all_tracked_lines,
|
|
6
|
+
extract_preproc_branch_blocks_from_file,
|
|
7
|
+
extract_preproc_blocks_from_file,
|
|
8
|
+
extract_preproc_blocks_from_covered_functions,
|
|
9
|
+
find_function_for_line,
|
|
10
|
+
get_all_parseable_preproc_lines_in_covered_functions,
|
|
11
|
+
get_all_lines_in_preproc,
|
|
12
|
+
get_function_name,
|
|
13
|
+
parse_enabled_configs,
|
|
14
|
+
parse_function_spans,
|
|
15
|
+
write_effective_conditions_for_requested_lines,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ConditionInfo",
|
|
20
|
+
"EffectiveConditionInfo",
|
|
21
|
+
"PreprocBlock",
|
|
22
|
+
"extract_all_tracked_lines",
|
|
23
|
+
"extract_preproc_branch_blocks_from_file",
|
|
24
|
+
"extract_preproc_blocks_from_file",
|
|
25
|
+
"extract_preproc_blocks_from_covered_functions",
|
|
26
|
+
"find_function_for_line",
|
|
27
|
+
"get_all_parseable_preproc_lines_in_covered_functions",
|
|
28
|
+
"get_all_lines_in_preproc",
|
|
29
|
+
"get_function_name",
|
|
30
|
+
"parse_enabled_configs",
|
|
31
|
+
"parse_function_spans",
|
|
32
|
+
"write_effective_conditions_for_requested_lines",
|
|
33
|
+
]
|
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import tree_sitter_c as tsc
|
|
9
|
+
from tree_sitter import Language, Parser
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PREPROC_TYPES = {"preproc_if", "preproc_ifdef", "preproc_elif"}
|
|
13
|
+
SKIP_TYPES = {"#ifdef", "#ifndef", "#else", "#if", "#elif", "#endif", "\n"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ConditionInfo:
|
|
18
|
+
symbol: str | None = None
|
|
19
|
+
polarity: Literal["positive", "negative"] = "positive"
|
|
20
|
+
parseable: bool = False
|
|
21
|
+
raw_condition: str | None = None
|
|
22
|
+
failure_reason: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PreprocBlock:
|
|
27
|
+
type: str
|
|
28
|
+
start_line: int
|
|
29
|
+
end_line: int
|
|
30
|
+
body_lines: list[int] = field(default_factory=list)
|
|
31
|
+
condition: ConditionInfo = field(default_factory=ConditionInfo)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class EffectiveConditionInfo:
|
|
36
|
+
raw_expression: str | None = None
|
|
37
|
+
parseable: bool = False
|
|
38
|
+
failure_reason: str | None = None
|
|
39
|
+
terms: list[ConditionInfo] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ParserStats:
|
|
44
|
+
parseable_lines: int = 0
|
|
45
|
+
parseable_blocks: int = 0
|
|
46
|
+
unparseable_lines: int = 0
|
|
47
|
+
unparseable_blocks: int = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ParseableLines:
|
|
52
|
+
parseable_lines: dict[str, list[int]]
|
|
53
|
+
stats: ParserStats
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _default_condition_info() -> ConditionInfo:
|
|
57
|
+
return ConditionInfo()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _condition_failure(raw_condition: str | None, reason: str) -> ConditionInfo:
|
|
61
|
+
return ConditionInfo(
|
|
62
|
+
raw_condition=raw_condition,
|
|
63
|
+
failure_reason=reason,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _collect_body_lines(
|
|
68
|
+
node, preproc_types: set[str], skip_types: set[str]
|
|
69
|
+
) -> set[int]:
|
|
70
|
+
lines: set[int] = set()
|
|
71
|
+
for child in node.children:
|
|
72
|
+
if child.type in preproc_types:
|
|
73
|
+
continue
|
|
74
|
+
if child.type in {"preproc_elif", "preproc_else"}:
|
|
75
|
+
lines.update(_collect_body_lines(child, preproc_types, skip_types))
|
|
76
|
+
continue
|
|
77
|
+
if child.type in skip_types:
|
|
78
|
+
continue
|
|
79
|
+
if child == node.child_by_field_name("name"):
|
|
80
|
+
continue
|
|
81
|
+
if child == node.child_by_field_name("condition"):
|
|
82
|
+
continue
|
|
83
|
+
start = child.start_point[0] + 1
|
|
84
|
+
end = child.end_point[0] + 1
|
|
85
|
+
for line in range(start, end + 1):
|
|
86
|
+
lines.add(line)
|
|
87
|
+
return lines
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _collect_branch_body_lines(
|
|
91
|
+
node, preproc_types: set[str], skip_types: set[str]
|
|
92
|
+
) -> set[int]:
|
|
93
|
+
lines: set[int] = set()
|
|
94
|
+
for child in node.children:
|
|
95
|
+
if child.type in preproc_types:
|
|
96
|
+
continue
|
|
97
|
+
if child.type in {"preproc_elif", "preproc_else"}:
|
|
98
|
+
continue
|
|
99
|
+
if child.type in skip_types:
|
|
100
|
+
continue
|
|
101
|
+
if child == node.child_by_field_name("name"):
|
|
102
|
+
continue
|
|
103
|
+
if child == node.child_by_field_name("condition"):
|
|
104
|
+
continue
|
|
105
|
+
start = child.start_point[0] + 1
|
|
106
|
+
end = child.end_point[0] + 1
|
|
107
|
+
for line in range(start, end + 1):
|
|
108
|
+
lines.add(line)
|
|
109
|
+
return lines
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _clone_condition_info(condition: ConditionInfo) -> ConditionInfo:
|
|
113
|
+
return ConditionInfo(
|
|
114
|
+
symbol=condition.symbol,
|
|
115
|
+
polarity=condition.polarity,
|
|
116
|
+
parseable=condition.parseable,
|
|
117
|
+
raw_condition=condition.raw_condition,
|
|
118
|
+
failure_reason=condition.failure_reason,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _negate_condition(condition: ConditionInfo) -> ConditionInfo:
|
|
123
|
+
negated_polarity: Literal["positive", "negative"] = (
|
|
124
|
+
"negative" if condition.polarity == "positive" else "positive"
|
|
125
|
+
)
|
|
126
|
+
return ConditionInfo(
|
|
127
|
+
symbol=condition.symbol,
|
|
128
|
+
polarity=negated_polarity,
|
|
129
|
+
parseable=condition.parseable,
|
|
130
|
+
raw_condition=condition.raw_condition,
|
|
131
|
+
failure_reason=condition.failure_reason,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _condition_term_to_expression(term: ConditionInfo) -> str | None:
|
|
136
|
+
if term.parseable and term.symbol:
|
|
137
|
+
if term.polarity == "negative":
|
|
138
|
+
return f"not {term.symbol}"
|
|
139
|
+
return term.symbol
|
|
140
|
+
|
|
141
|
+
raw_condition = term.raw_condition.strip() if term.raw_condition else None
|
|
142
|
+
if not raw_condition:
|
|
143
|
+
return None
|
|
144
|
+
if term.polarity == "negative":
|
|
145
|
+
return f"!({raw_condition})"
|
|
146
|
+
return raw_condition
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _build_effective_condition_info(
|
|
150
|
+
terms: list[ConditionInfo],
|
|
151
|
+
) -> EffectiveConditionInfo:
|
|
152
|
+
expression_terms: list[str] = []
|
|
153
|
+
all_parseable = True
|
|
154
|
+
failure_reason: str | None = None
|
|
155
|
+
|
|
156
|
+
for term in terms:
|
|
157
|
+
expression_term = _condition_term_to_expression(term)
|
|
158
|
+
if expression_term:
|
|
159
|
+
expression_terms.append(expression_term)
|
|
160
|
+
|
|
161
|
+
if not term.parseable or not term.symbol:
|
|
162
|
+
all_parseable = False
|
|
163
|
+
if not failure_reason:
|
|
164
|
+
failure_reason = term.failure_reason or "contains_unparseable_term"
|
|
165
|
+
|
|
166
|
+
raw_expression = " and ".join(expression_terms) if expression_terms else None
|
|
167
|
+
if not terms:
|
|
168
|
+
all_parseable = False
|
|
169
|
+
failure_reason = "empty_effective_condition"
|
|
170
|
+
|
|
171
|
+
return EffectiveConditionInfo(
|
|
172
|
+
raw_expression=raw_expression,
|
|
173
|
+
parseable=all_parseable,
|
|
174
|
+
failure_reason=failure_reason,
|
|
175
|
+
terms=[_clone_condition_info(term) for term in terms],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _find_next_branch_node(node):
|
|
180
|
+
for child in node.children:
|
|
181
|
+
if child.type in {"preproc_elif", "preproc_else"}:
|
|
182
|
+
return child
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _collect_chain_nodes(root_node) -> tuple[list[Any], Any | None]:
|
|
187
|
+
elif_nodes: list = []
|
|
188
|
+
else_node = None
|
|
189
|
+
|
|
190
|
+
next_branch = _find_next_branch_node(root_node)
|
|
191
|
+
while next_branch:
|
|
192
|
+
if next_branch.type == "preproc_elif":
|
|
193
|
+
elif_nodes.append(next_branch)
|
|
194
|
+
next_branch = _find_next_branch_node(next_branch)
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
else_node = next_branch
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
return elif_nodes, else_node
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _iter_branch_children_for_traversal(node):
|
|
204
|
+
for child in node.children:
|
|
205
|
+
if child.type in {"preproc_elif", "preproc_else"}:
|
|
206
|
+
continue
|
|
207
|
+
if child.type in SKIP_TYPES:
|
|
208
|
+
continue
|
|
209
|
+
if child == node.child_by_field_name("name"):
|
|
210
|
+
continue
|
|
211
|
+
if child == node.child_by_field_name("condition"):
|
|
212
|
+
continue
|
|
213
|
+
yield child
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build_else_local_condition(chain_conditions: list[ConditionInfo]) -> ConditionInfo:
|
|
217
|
+
if len(chain_conditions) == 1:
|
|
218
|
+
return _negate_condition(chain_conditions[0])
|
|
219
|
+
|
|
220
|
+
negated_terms = [_negate_condition(condition) for condition in chain_conditions]
|
|
221
|
+
expression_terms = [
|
|
222
|
+
term
|
|
223
|
+
for term in (
|
|
224
|
+
_condition_term_to_expression(condition) for condition in negated_terms
|
|
225
|
+
)
|
|
226
|
+
if term
|
|
227
|
+
]
|
|
228
|
+
raw_condition = " and ".join(expression_terms) if expression_terms else None
|
|
229
|
+
return ConditionInfo(
|
|
230
|
+
raw_condition=raw_condition,
|
|
231
|
+
parseable=False,
|
|
232
|
+
failure_reason="compound_else_branch_condition",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _branch_end_line(node, body_lines: list[int]) -> int:
|
|
237
|
+
if body_lines:
|
|
238
|
+
return body_lines[-1]
|
|
239
|
+
return node.start_point[0] + 1
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _emit_branch_blocks_for_chain(
|
|
243
|
+
node,
|
|
244
|
+
preproc_types: set[str],
|
|
245
|
+
skip_types: set[str],
|
|
246
|
+
source_code: bytes,
|
|
247
|
+
ancestor_terms: list[ConditionInfo],
|
|
248
|
+
blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]],
|
|
249
|
+
) -> None:
|
|
250
|
+
elif_nodes, else_node = _collect_chain_nodes(node)
|
|
251
|
+
branch_nodes = [node, *elif_nodes]
|
|
252
|
+
|
|
253
|
+
chain_conditions = [
|
|
254
|
+
_parse_preproc_condition(branch_node, source_code)
|
|
255
|
+
for branch_node in branch_nodes
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
for branch_index, branch_node in enumerate(branch_nodes):
|
|
259
|
+
negated_prefix = [
|
|
260
|
+
_negate_condition(condition)
|
|
261
|
+
for condition in chain_conditions[:branch_index]
|
|
262
|
+
]
|
|
263
|
+
local_terms = [*negated_prefix, chain_conditions[branch_index]]
|
|
264
|
+
effective_terms = [*ancestor_terms, *local_terms]
|
|
265
|
+
|
|
266
|
+
nested_blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]] = []
|
|
267
|
+
for child in _iter_branch_children_for_traversal(branch_node):
|
|
268
|
+
_find_branch_blocks_in_node(
|
|
269
|
+
child,
|
|
270
|
+
preproc_types,
|
|
271
|
+
skip_types,
|
|
272
|
+
source_code,
|
|
273
|
+
effective_terms,
|
|
274
|
+
nested_blocks,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
nested_body_lines = {
|
|
278
|
+
line
|
|
279
|
+
for nested_block, _ in nested_blocks
|
|
280
|
+
for line in nested_block.body_lines
|
|
281
|
+
}
|
|
282
|
+
raw_branch_body_lines = set(
|
|
283
|
+
_collect_branch_body_lines(branch_node, preproc_types, skip_types)
|
|
284
|
+
)
|
|
285
|
+
body_lines = sorted(raw_branch_body_lines - nested_body_lines)
|
|
286
|
+
|
|
287
|
+
block = PreprocBlock(
|
|
288
|
+
type=branch_node.type,
|
|
289
|
+
start_line=branch_node.start_point[0] + 1,
|
|
290
|
+
end_line=_branch_end_line(branch_node, body_lines),
|
|
291
|
+
body_lines=body_lines,
|
|
292
|
+
condition=_clone_condition_info(chain_conditions[branch_index]),
|
|
293
|
+
)
|
|
294
|
+
blocks.append((block, _build_effective_condition_info(effective_terms)))
|
|
295
|
+
blocks.extend(nested_blocks)
|
|
296
|
+
|
|
297
|
+
if not else_node:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
else_local_terms = [_negate_condition(condition) for condition in chain_conditions]
|
|
301
|
+
else_effective_terms = [*ancestor_terms, *else_local_terms]
|
|
302
|
+
else_nested_blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]] = []
|
|
303
|
+
for child in _iter_branch_children_for_traversal(else_node):
|
|
304
|
+
_find_branch_blocks_in_node(
|
|
305
|
+
child,
|
|
306
|
+
preproc_types,
|
|
307
|
+
skip_types,
|
|
308
|
+
source_code,
|
|
309
|
+
else_effective_terms,
|
|
310
|
+
else_nested_blocks,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
else_nested_body_lines = {
|
|
314
|
+
line
|
|
315
|
+
for nested_block, _ in else_nested_blocks
|
|
316
|
+
for line in nested_block.body_lines
|
|
317
|
+
}
|
|
318
|
+
raw_else_body_lines = set(
|
|
319
|
+
_collect_branch_body_lines(else_node, preproc_types, skip_types)
|
|
320
|
+
)
|
|
321
|
+
else_body_lines = sorted(raw_else_body_lines - else_nested_body_lines)
|
|
322
|
+
|
|
323
|
+
else_block = PreprocBlock(
|
|
324
|
+
type=else_node.type,
|
|
325
|
+
start_line=else_node.start_point[0] + 1,
|
|
326
|
+
end_line=_branch_end_line(else_node, else_body_lines),
|
|
327
|
+
body_lines=else_body_lines,
|
|
328
|
+
condition=_build_else_local_condition(chain_conditions),
|
|
329
|
+
)
|
|
330
|
+
blocks.append((else_block, _build_effective_condition_info(else_effective_terms)))
|
|
331
|
+
blocks.extend(else_nested_blocks)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _find_branch_blocks_in_node(
|
|
335
|
+
node,
|
|
336
|
+
preproc_types: set[str],
|
|
337
|
+
skip_types: set[str],
|
|
338
|
+
source_code: bytes,
|
|
339
|
+
ancestor_terms: list[ConditionInfo],
|
|
340
|
+
blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]],
|
|
341
|
+
) -> None:
|
|
342
|
+
if node.type in {"preproc_if", "preproc_ifdef"}:
|
|
343
|
+
_emit_branch_blocks_for_chain(
|
|
344
|
+
node,
|
|
345
|
+
preproc_types,
|
|
346
|
+
skip_types,
|
|
347
|
+
source_code,
|
|
348
|
+
ancestor_terms,
|
|
349
|
+
blocks,
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
for child in node.children:
|
|
354
|
+
_find_branch_blocks_in_node(
|
|
355
|
+
child,
|
|
356
|
+
preproc_types,
|
|
357
|
+
skip_types,
|
|
358
|
+
source_code,
|
|
359
|
+
ancestor_terms,
|
|
360
|
+
blocks,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _strip_outer_parens_once(text: str) -> str:
|
|
365
|
+
stripped = text.strip()
|
|
366
|
+
if not (stripped.startswith("(") and stripped.endswith(")")):
|
|
367
|
+
return stripped
|
|
368
|
+
|
|
369
|
+
depth = 0
|
|
370
|
+
for idx, char in enumerate(stripped):
|
|
371
|
+
if char == "(":
|
|
372
|
+
depth += 1
|
|
373
|
+
elif char == ")":
|
|
374
|
+
depth -= 1
|
|
375
|
+
if depth < 0:
|
|
376
|
+
return stripped
|
|
377
|
+
if depth == 0 and idx != len(stripped) - 1:
|
|
378
|
+
return stripped
|
|
379
|
+
|
|
380
|
+
if depth != 0:
|
|
381
|
+
return stripped
|
|
382
|
+
|
|
383
|
+
return stripped[1:-1].strip()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _node_text(node, source_code: bytes) -> str:
|
|
387
|
+
return source_code[node.start_byte : node.end_byte].decode("utf-8", errors="ignore")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _normalize_defined_term_text(term_text: str) -> str | None:
|
|
391
|
+
normalized_term = _strip_outer_parens_once(term_text)
|
|
392
|
+
normalized_term = re.sub(r"\s+", " ", normalized_term).strip()
|
|
393
|
+
if not normalized_term:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
defined_match = re.fullmatch(
|
|
397
|
+
r"defined\s*(?:\(\s*(CONFIG_[A-Za-z0-9_]+)\s*\)|\s+(CONFIG_[A-Za-z0-9_]+))",
|
|
398
|
+
normalized_term,
|
|
399
|
+
)
|
|
400
|
+
if defined_match:
|
|
401
|
+
symbol = defined_match.group(1) or defined_match.group(2)
|
|
402
|
+
return f"({symbol})"
|
|
403
|
+
|
|
404
|
+
if re.fullmatch(r"CONFIG_[A-Za-z0-9_]+", normalized_term):
|
|
405
|
+
return f"({normalized_term})"
|
|
406
|
+
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _binary_expression_parts(node, source_code: bytes):
|
|
411
|
+
operator_node = None
|
|
412
|
+
operands: list[Any] = []
|
|
413
|
+
|
|
414
|
+
for child in node.children:
|
|
415
|
+
if child.type in {"\n", "(", ")"}:
|
|
416
|
+
continue
|
|
417
|
+
if child.type in {"&&", "||"}:
|
|
418
|
+
operator_node = child
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
child_text = _node_text(child, source_code).strip()
|
|
422
|
+
if child_text in {"&&", "||"}:
|
|
423
|
+
operator_node = child
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
operands.append(child)
|
|
427
|
+
|
|
428
|
+
if len(operands) != 2 or not operator_node:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
return operands[0], operator_node, operands[1]
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _normalize_condition_expression_node(node, source_code: bytes) -> str | None:
|
|
435
|
+
if node.type == "parenthesized_expression":
|
|
436
|
+
inner_nodes = [
|
|
437
|
+
child for child in node.children if child.type not in {"\n", "(", ")"}
|
|
438
|
+
]
|
|
439
|
+
if len(inner_nodes) != 1:
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
inner_expression = _normalize_condition_expression_node(
|
|
443
|
+
inner_nodes[0], source_code
|
|
444
|
+
)
|
|
445
|
+
if not inner_expression:
|
|
446
|
+
return None
|
|
447
|
+
return inner_expression
|
|
448
|
+
|
|
449
|
+
if node.type == "binary_expression":
|
|
450
|
+
expression_parts = _binary_expression_parts(node, source_code)
|
|
451
|
+
if not expression_parts:
|
|
452
|
+
return None
|
|
453
|
+
left_node, operator_node, right_node = expression_parts
|
|
454
|
+
|
|
455
|
+
left_expression = _normalize_condition_expression_node(left_node, source_code)
|
|
456
|
+
right_expression = _normalize_condition_expression_node(right_node, source_code)
|
|
457
|
+
if not left_expression or not right_expression:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
operator_text = _node_text(operator_node, source_code).strip()
|
|
461
|
+
if operator_text == "&&":
|
|
462
|
+
operator_keyword = "and"
|
|
463
|
+
elif operator_text == "||":
|
|
464
|
+
operator_keyword = "or"
|
|
465
|
+
else:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
return f"({left_expression} {operator_keyword} {right_expression})"
|
|
469
|
+
|
|
470
|
+
return _normalize_defined_term_text(_node_text(node, source_code))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _parse_condition_text(condition_text: str) -> ConditionInfo:
|
|
474
|
+
condition_info = _default_condition_info()
|
|
475
|
+
raw_condition = condition_text.strip()
|
|
476
|
+
if not raw_condition:
|
|
477
|
+
return _condition_failure(None, "empty_condition")
|
|
478
|
+
|
|
479
|
+
condition_info.raw_condition = raw_condition
|
|
480
|
+
normalized = raw_condition
|
|
481
|
+
negated = False
|
|
482
|
+
|
|
483
|
+
if normalized.startswith("!"):
|
|
484
|
+
negated = True
|
|
485
|
+
normalized = normalized[1:].strip()
|
|
486
|
+
|
|
487
|
+
normalized = _strip_outer_parens_once(normalized)
|
|
488
|
+
normalized = re.sub(r"\s+", " ", normalized).strip()
|
|
489
|
+
if not normalized:
|
|
490
|
+
return _condition_failure(raw_condition, "empty_condition")
|
|
491
|
+
|
|
492
|
+
defined_paren_match = re.match(
|
|
493
|
+
r"^defined\s*\(\s*(CONFIG_[A-Za-z0-9_]+)\s*\)$",
|
|
494
|
+
normalized,
|
|
495
|
+
)
|
|
496
|
+
if defined_paren_match:
|
|
497
|
+
symbol = defined_paren_match.group(1)
|
|
498
|
+
condition_info.symbol = symbol
|
|
499
|
+
condition_info.polarity = "negative" if negated else "positive"
|
|
500
|
+
canonical_raw = f"defined({symbol})"
|
|
501
|
+
condition_info.raw_condition = (
|
|
502
|
+
f"not {canonical_raw}" if negated else canonical_raw
|
|
503
|
+
)
|
|
504
|
+
condition_info.parseable = True
|
|
505
|
+
return condition_info
|
|
506
|
+
|
|
507
|
+
defined_space_match = re.match(
|
|
508
|
+
r"^defined\s+(CONFIG_[A-Za-z0-9_]+)$",
|
|
509
|
+
normalized,
|
|
510
|
+
)
|
|
511
|
+
if defined_space_match:
|
|
512
|
+
symbol = defined_space_match.group(1)
|
|
513
|
+
condition_info.symbol = symbol
|
|
514
|
+
condition_info.polarity = "negative" if negated else "positive"
|
|
515
|
+
canonical_raw = f"defined({symbol})"
|
|
516
|
+
condition_info.raw_condition = (
|
|
517
|
+
f"not {canonical_raw}" if negated else canonical_raw
|
|
518
|
+
)
|
|
519
|
+
condition_info.parseable = True
|
|
520
|
+
return condition_info
|
|
521
|
+
|
|
522
|
+
is_enabled_match = re.match(
|
|
523
|
+
r"^IS_ENABLED\s*\(\s*(CONFIG_[A-Za-z0-9_]+)\s*\)$",
|
|
524
|
+
normalized,
|
|
525
|
+
)
|
|
526
|
+
if is_enabled_match:
|
|
527
|
+
symbol = is_enabled_match.group(1)
|
|
528
|
+
condition_info.symbol = symbol
|
|
529
|
+
condition_info.polarity = "negative" if negated else "positive"
|
|
530
|
+
canonical_raw = f"IS_ENABLED({symbol})"
|
|
531
|
+
condition_info.raw_condition = (
|
|
532
|
+
f"not {canonical_raw}" if negated else canonical_raw
|
|
533
|
+
)
|
|
534
|
+
condition_info.parseable = True
|
|
535
|
+
return condition_info
|
|
536
|
+
|
|
537
|
+
bare_match = re.match(
|
|
538
|
+
r"^\(*\s*(CONFIG_[A-Za-z0-9_]+)\s*\)*$",
|
|
539
|
+
normalized,
|
|
540
|
+
)
|
|
541
|
+
if bare_match:
|
|
542
|
+
symbol = bare_match.group(1)
|
|
543
|
+
condition_info.symbol = symbol
|
|
544
|
+
condition_info.polarity = "negative" if negated else "positive"
|
|
545
|
+
condition_info.raw_condition = f"not {symbol}" if negated else symbol
|
|
546
|
+
condition_info.parseable = True
|
|
547
|
+
return condition_info
|
|
548
|
+
|
|
549
|
+
return _condition_failure(raw_condition, "unsupported_condition_expression")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _parse_preproc_condition(node, source_code: bytes) -> ConditionInfo:
|
|
553
|
+
condition_info = _default_condition_info()
|
|
554
|
+
|
|
555
|
+
if node.type == "preproc_ifdef":
|
|
556
|
+
directive_text = source_code[node.start_byte : node.end_byte].decode(
|
|
557
|
+
"utf-8", errors="ignore"
|
|
558
|
+
)
|
|
559
|
+
raw_condition = directive_text.strip()
|
|
560
|
+
name_node = node.child_by_field_name("name")
|
|
561
|
+
if not name_node:
|
|
562
|
+
return _condition_failure(raw_condition, "missing_name_node")
|
|
563
|
+
symbol = name_node.text.decode("utf-8")
|
|
564
|
+
if not symbol.startswith("CONFIG_"):
|
|
565
|
+
return _condition_failure(symbol, "non_config_symbol")
|
|
566
|
+
condition_info.symbol = symbol
|
|
567
|
+
condition_info.raw_condition = raw_condition
|
|
568
|
+
condition_info.polarity = (
|
|
569
|
+
"negative" if raw_condition.startswith("#ifndef") else "positive"
|
|
570
|
+
)
|
|
571
|
+
condition_info.parseable = True
|
|
572
|
+
return condition_info
|
|
573
|
+
|
|
574
|
+
if node.type in {"preproc_if", "preproc_elif"}:
|
|
575
|
+
condition_node = node.child_by_field_name("condition")
|
|
576
|
+
if not condition_node:
|
|
577
|
+
raw_condition = source_code[node.start_byte : node.end_byte].decode(
|
|
578
|
+
"utf-8", errors="ignore"
|
|
579
|
+
)
|
|
580
|
+
return _condition_failure(raw_condition.strip(), "missing_condition_node")
|
|
581
|
+
|
|
582
|
+
condition_text = source_code[
|
|
583
|
+
condition_node.start_byte : condition_node.end_byte
|
|
584
|
+
].decode("utf-8", errors="ignore")
|
|
585
|
+
parsed_condition = _parse_condition_text(condition_text)
|
|
586
|
+
if parsed_condition.parseable:
|
|
587
|
+
return parsed_condition
|
|
588
|
+
|
|
589
|
+
normalized_expression = _normalize_condition_expression_node(
|
|
590
|
+
condition_node,
|
|
591
|
+
source_code,
|
|
592
|
+
)
|
|
593
|
+
if not normalized_expression:
|
|
594
|
+
return parsed_condition
|
|
595
|
+
|
|
596
|
+
return ConditionInfo(
|
|
597
|
+
symbol=normalized_expression,
|
|
598
|
+
polarity="positive",
|
|
599
|
+
parseable=True,
|
|
600
|
+
raw_condition=normalized_expression,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
return condition_info
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _find_blocks_in_node(
|
|
607
|
+
node,
|
|
608
|
+
preproc_types: set[str],
|
|
609
|
+
skip_types: set[str],
|
|
610
|
+
source_code: bytes,
|
|
611
|
+
blocks: list[PreprocBlock],
|
|
612
|
+
) -> None:
|
|
613
|
+
if node.type in preproc_types:
|
|
614
|
+
condition_info = _parse_preproc_condition(node, source_code)
|
|
615
|
+
body_lines = _collect_body_lines(node, preproc_types, skip_types)
|
|
616
|
+
blocks.append(
|
|
617
|
+
PreprocBlock(
|
|
618
|
+
type=node.type,
|
|
619
|
+
start_line=node.start_point[0] + 1,
|
|
620
|
+
end_line=node.end_point[0] + 1,
|
|
621
|
+
body_lines=sorted(body_lines),
|
|
622
|
+
condition=condition_info,
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
for child in node.children:
|
|
626
|
+
_find_blocks_in_node(child, preproc_types, skip_types, source_code, blocks)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def get_function_name(node):
|
|
630
|
+
"""Extract function name from a function_definition node."""
|
|
631
|
+
for child in node.children:
|
|
632
|
+
if child.type == "function_declarator":
|
|
633
|
+
for subchild in child.children:
|
|
634
|
+
if subchild.type == "identifier":
|
|
635
|
+
return subchild.text.decode("utf-8")
|
|
636
|
+
elif child.type == "pointer_declarator":
|
|
637
|
+
return get_function_name(child)
|
|
638
|
+
elif child.type == "identifier":
|
|
639
|
+
return child.text.decode("utf-8")
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def parse_function_spans(c_file: Path) -> dict[str, tuple[int, int]]:
|
|
644
|
+
"""Extract start and end line numbers for every function in a C file.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
c_file: Path to the C source file.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
A dict mapping function names to (start_line, end_line) tuples.
|
|
651
|
+
Line numbers are 1-indexed.
|
|
652
|
+
"""
|
|
653
|
+
c_language = Language(tsc.language())
|
|
654
|
+
parser = Parser(c_language)
|
|
655
|
+
|
|
656
|
+
source_code = c_file.read_bytes()
|
|
657
|
+
tree = parser.parse(source_code)
|
|
658
|
+
|
|
659
|
+
result: dict[str, tuple[int, int]] = {}
|
|
660
|
+
|
|
661
|
+
stack = [tree.root_node]
|
|
662
|
+
while stack:
|
|
663
|
+
node = stack.pop()
|
|
664
|
+
if node.type == "function_definition":
|
|
665
|
+
func_name = get_function_name(node)
|
|
666
|
+
if func_name:
|
|
667
|
+
# tree-sitter uses 0-indexed lines, convert to 1-indexed
|
|
668
|
+
start_line = node.start_point[0] + 1
|
|
669
|
+
end_line = node.end_point[0] + 1
|
|
670
|
+
result[func_name] = (start_line, end_line)
|
|
671
|
+
stack.extend(node.children)
|
|
672
|
+
return result
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def extract_preproc_blocks_from_covered_functions(
|
|
676
|
+
kernel_src: Path,
|
|
677
|
+
c_file_path: Path,
|
|
678
|
+
covered_functions: set[str],
|
|
679
|
+
) -> list[PreprocBlock]:
|
|
680
|
+
"""Extract preprocessor directive blocks from covered functions."""
|
|
681
|
+
c_language = Language(tsc.language())
|
|
682
|
+
parser = Parser(c_language)
|
|
683
|
+
|
|
684
|
+
source_code = (kernel_src / c_file_path).read_bytes()
|
|
685
|
+
tree = parser.parse(source_code)
|
|
686
|
+
|
|
687
|
+
blocks: list[PreprocBlock] = []
|
|
688
|
+
|
|
689
|
+
stack = [tree.root_node]
|
|
690
|
+
while stack:
|
|
691
|
+
node = stack.pop()
|
|
692
|
+
if node.type == "function_definition":
|
|
693
|
+
func_name = get_function_name(node)
|
|
694
|
+
if func_name and func_name in covered_functions:
|
|
695
|
+
_find_blocks_in_node(
|
|
696
|
+
node,
|
|
697
|
+
PREPROC_TYPES,
|
|
698
|
+
SKIP_TYPES,
|
|
699
|
+
source_code,
|
|
700
|
+
blocks,
|
|
701
|
+
)
|
|
702
|
+
else:
|
|
703
|
+
stack.extend(node.children)
|
|
704
|
+
|
|
705
|
+
return blocks
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def extract_preproc_blocks_from_file(
|
|
709
|
+
kernel_src: Path,
|
|
710
|
+
c_file_path: Path,
|
|
711
|
+
) -> list[PreprocBlock]:
|
|
712
|
+
"""Extract all preprocessor directive blocks from a C file."""
|
|
713
|
+
c_language = Language(tsc.language())
|
|
714
|
+
parser = Parser(c_language)
|
|
715
|
+
|
|
716
|
+
source_code = (kernel_src / c_file_path).read_bytes()
|
|
717
|
+
tree = parser.parse(source_code)
|
|
718
|
+
|
|
719
|
+
blocks: list[PreprocBlock] = []
|
|
720
|
+
_find_blocks_in_node(
|
|
721
|
+
tree.root_node,
|
|
722
|
+
PREPROC_TYPES,
|
|
723
|
+
SKIP_TYPES,
|
|
724
|
+
source_code,
|
|
725
|
+
blocks,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
return blocks
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def extract_preproc_branch_blocks_from_file(
|
|
732
|
+
kernel_src: Path,
|
|
733
|
+
c_file_path: Path,
|
|
734
|
+
) -> list[tuple[PreprocBlock, EffectiveConditionInfo]]:
|
|
735
|
+
"""Extract preprocessor directive branches with full effective conditions."""
|
|
736
|
+
c_language = Language(tsc.language())
|
|
737
|
+
parser = Parser(c_language)
|
|
738
|
+
|
|
739
|
+
source_code = (kernel_src / c_file_path).read_bytes()
|
|
740
|
+
tree = parser.parse(source_code)
|
|
741
|
+
|
|
742
|
+
blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]] = []
|
|
743
|
+
_find_branch_blocks_in_node(
|
|
744
|
+
tree.root_node,
|
|
745
|
+
PREPROC_TYPES,
|
|
746
|
+
SKIP_TYPES,
|
|
747
|
+
source_code,
|
|
748
|
+
[],
|
|
749
|
+
blocks,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
return blocks
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _line_raw_expression(
|
|
756
|
+
line_number: int,
|
|
757
|
+
branch_blocks: list[tuple[PreprocBlock, EffectiveConditionInfo]],
|
|
758
|
+
rel_path: str,
|
|
759
|
+
) -> tuple[str | None, bool, str | None]:
|
|
760
|
+
matches = [
|
|
761
|
+
(block, effective_condition)
|
|
762
|
+
for block, effective_condition in branch_blocks
|
|
763
|
+
if line_number in block.body_lines
|
|
764
|
+
]
|
|
765
|
+
|
|
766
|
+
if not matches:
|
|
767
|
+
return None, False, None
|
|
768
|
+
|
|
769
|
+
if len(matches) > 1:
|
|
770
|
+
matches_by_start = sorted(matches, key=lambda match: match[0].start_line, reverse=True)
|
|
771
|
+
top_start_line = matches_by_start[0][0].start_line
|
|
772
|
+
top_matches = [
|
|
773
|
+
(block, effective_condition)
|
|
774
|
+
for block, effective_condition in matches_by_start
|
|
775
|
+
if block.start_line == top_start_line
|
|
776
|
+
]
|
|
777
|
+
if len(top_matches) == 1:
|
|
778
|
+
effective_condition = top_matches[0][1]
|
|
779
|
+
is_unresolved = not effective_condition.parseable
|
|
780
|
+
failure_reason = (
|
|
781
|
+
effective_condition.failure_reason if is_unresolved else None
|
|
782
|
+
)
|
|
783
|
+
return effective_condition.raw_expression, is_unresolved, failure_reason
|
|
784
|
+
|
|
785
|
+
canonical_matches = {
|
|
786
|
+
(
|
|
787
|
+
effective_condition.raw_expression,
|
|
788
|
+
effective_condition.parseable,
|
|
789
|
+
effective_condition.failure_reason,
|
|
790
|
+
)
|
|
791
|
+
for _, effective_condition in top_matches
|
|
792
|
+
}
|
|
793
|
+
if len(canonical_matches) == 1:
|
|
794
|
+
effective_condition = top_matches[0][1]
|
|
795
|
+
is_unresolved = not effective_condition.parseable
|
|
796
|
+
failure_reason = (
|
|
797
|
+
effective_condition.failure_reason if is_unresolved else None
|
|
798
|
+
)
|
|
799
|
+
return effective_condition.raw_expression, is_unresolved, failure_reason
|
|
800
|
+
|
|
801
|
+
raise ValueError(
|
|
802
|
+
"Found ambiguous effective conditions for "
|
|
803
|
+
f"{rel_path}:{line_number}; candidates={len(top_matches)}"
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
effective_condition = matches[0][1]
|
|
807
|
+
is_unresolved = not effective_condition.parseable
|
|
808
|
+
failure_reason = effective_condition.failure_reason if is_unresolved else None
|
|
809
|
+
return effective_condition.raw_expression, is_unresolved, failure_reason
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _kernel_relative_from_absolute_filename(
|
|
813
|
+
filename: str | Path,
|
|
814
|
+
kernel_src: Path,
|
|
815
|
+
strip_prefix: str | Path | None = None,
|
|
816
|
+
) -> Path | None:
|
|
817
|
+
"""Map a path recorded at build time onto a path relative to `kernel_src`.
|
|
818
|
+
|
|
819
|
+
Coverage reports record absolute paths from the machine the kernel was built
|
|
820
|
+
on, which rarely match the tree being analyzed now. Three strategies are
|
|
821
|
+
tried in order:
|
|
822
|
+
|
|
823
|
+
1. `strip_prefix`, when given, is removed from the front of the path.
|
|
824
|
+
2. A path already relative, or already under `kernel_src`, is used directly.
|
|
825
|
+
3. Otherwise the longest trailing portion of the path that exists under
|
|
826
|
+
`kernel_src` is used.
|
|
827
|
+
|
|
828
|
+
Returns None when no strategy yields a path that exists, so a single
|
|
829
|
+
unrecognized entry skips instead of aborting the whole report.
|
|
830
|
+
"""
|
|
831
|
+
filename_path = Path(filename)
|
|
832
|
+
|
|
833
|
+
if strip_prefix is not None:
|
|
834
|
+
try:
|
|
835
|
+
return filename_path.relative_to(Path(strip_prefix))
|
|
836
|
+
except ValueError:
|
|
837
|
+
pass
|
|
838
|
+
|
|
839
|
+
if not filename_path.is_absolute():
|
|
840
|
+
return filename_path
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
return filename_path.relative_to(kernel_src)
|
|
844
|
+
except ValueError:
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
# Longest trailing portion that resolves under kernel_src wins, so
|
|
848
|
+
# "fs/ext4/inode.c" is preferred over the ambiguous "ext4/inode.c".
|
|
849
|
+
parts = filename_path.parts
|
|
850
|
+
for idx in range(1, len(parts)):
|
|
851
|
+
candidate = Path(*parts[idx:])
|
|
852
|
+
if (kernel_src / candidate).exists():
|
|
853
|
+
return candidate
|
|
854
|
+
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _build_effective_conditions_results(
|
|
859
|
+
kernel_src: Path,
|
|
860
|
+
requested_lines: dict[str, list[int]],
|
|
861
|
+
) -> tuple[dict[str, list[dict]], dict[str, list[dict]], dict[str, int], set[str]]:
|
|
862
|
+
resolved_results: dict[str, list[dict]] = {}
|
|
863
|
+
unresolved_results: dict[str, list[dict]] = {}
|
|
864
|
+
raw_expressions: set[str] = set()
|
|
865
|
+
total_requested_lines = 0
|
|
866
|
+
resolved_lines = 0
|
|
867
|
+
unresolved_lines = 0
|
|
868
|
+
|
|
869
|
+
for rel_path, lines in sorted(requested_lines.items()):
|
|
870
|
+
if not rel_path.endswith(".c"):
|
|
871
|
+
continue
|
|
872
|
+
|
|
873
|
+
full_path = kernel_src / rel_path
|
|
874
|
+
if not full_path.exists():
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
branch_blocks = extract_preproc_branch_blocks_from_file(
|
|
878
|
+
kernel_src,
|
|
879
|
+
Path(rel_path),
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
resolved_entries: list[dict] = []
|
|
883
|
+
for line_number in lines:
|
|
884
|
+
total_requested_lines += 1
|
|
885
|
+
raw_expression, is_unresolved, failure_reason = _line_raw_expression(
|
|
886
|
+
line_number,
|
|
887
|
+
branch_blocks,
|
|
888
|
+
rel_path,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if is_unresolved:
|
|
892
|
+
unresolved_lines += 1
|
|
893
|
+
unresolved_entry = {
|
|
894
|
+
"line": line_number,
|
|
895
|
+
"raw_expression": raw_expression,
|
|
896
|
+
"failure_reason": failure_reason,
|
|
897
|
+
}
|
|
898
|
+
if rel_path not in unresolved_results:
|
|
899
|
+
unresolved_results[rel_path] = []
|
|
900
|
+
unresolved_results[rel_path].append(unresolved_entry)
|
|
901
|
+
continue
|
|
902
|
+
|
|
903
|
+
resolved_lines += 1
|
|
904
|
+
resolved_entries.append(
|
|
905
|
+
{
|
|
906
|
+
"line": line_number,
|
|
907
|
+
"raw_expression": raw_expression,
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
if raw_expression:
|
|
911
|
+
raw_expressions.add(raw_expression)
|
|
912
|
+
|
|
913
|
+
if resolved_entries:
|
|
914
|
+
resolved_results[rel_path] = resolved_entries
|
|
915
|
+
|
|
916
|
+
summary = {
|
|
917
|
+
"total_requested_lines": total_requested_lines,
|
|
918
|
+
"resolved_lines": resolved_lines,
|
|
919
|
+
"unresolved_lines": unresolved_lines,
|
|
920
|
+
}
|
|
921
|
+
return resolved_results, unresolved_results, summary, raw_expressions
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def write_effective_conditions_for_requested_lines(
|
|
925
|
+
kernel_src: Path,
|
|
926
|
+
requested_lines: dict[str, list[int]],
|
|
927
|
+
output_dir: Path,
|
|
928
|
+
) -> dict[str, Path]:
|
|
929
|
+
(
|
|
930
|
+
resolved_results,
|
|
931
|
+
unresolved_results,
|
|
932
|
+
summary,
|
|
933
|
+
raw_expressions,
|
|
934
|
+
) = _build_effective_conditions_results(kernel_src, requested_lines)
|
|
935
|
+
|
|
936
|
+
raw_output_path = (output_dir / "raw_effective_conditions.txt").absolute()
|
|
937
|
+
resolved_output_path = (output_dir / "resolved_lines.json").absolute()
|
|
938
|
+
unresolved_output_path = (output_dir / "unresolved_lines.json").absolute()
|
|
939
|
+
summary_output_path = (output_dir / "summary.json").absolute()
|
|
940
|
+
|
|
941
|
+
raw_output_path.write_text(
|
|
942
|
+
"\n".join(raw_expressions) + ("\n" if raw_expressions else "")
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
resolved_payload = {
|
|
946
|
+
"kernel_src": str(kernel_src),
|
|
947
|
+
"resolved_results": resolved_results,
|
|
948
|
+
}
|
|
949
|
+
resolved_output_path.write_text(json.dumps(resolved_payload, indent=2))
|
|
950
|
+
|
|
951
|
+
unresolved_payload = {
|
|
952
|
+
"kernel_src": str(kernel_src),
|
|
953
|
+
"unresolved_results": unresolved_results,
|
|
954
|
+
}
|
|
955
|
+
unresolved_output_path.write_text(json.dumps(unresolved_payload, indent=2))
|
|
956
|
+
|
|
957
|
+
summary_payload = {
|
|
958
|
+
"kernel_src": str(kernel_src),
|
|
959
|
+
"summary": summary,
|
|
960
|
+
}
|
|
961
|
+
summary_output_path.write_text(json.dumps(summary_payload, indent=2))
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
"raw_effective_conditions": raw_output_path,
|
|
965
|
+
"resolved_lines": resolved_output_path,
|
|
966
|
+
"unresolved_lines": unresolved_output_path,
|
|
967
|
+
"summary": summary_output_path,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def parse_enabled_configs(config_path: Path) -> set[str]:
|
|
972
|
+
"""Parse enabled CONFIG_* symbols from a kernel .config file."""
|
|
973
|
+
enabled: set[str] = set()
|
|
974
|
+
for line in config_path.read_text(errors="ignore").splitlines():
|
|
975
|
+
line = line.strip()
|
|
976
|
+
if not line or line.startswith("#"):
|
|
977
|
+
continue
|
|
978
|
+
if not line.startswith("CONFIG_"):
|
|
979
|
+
continue
|
|
980
|
+
if "=" not in line:
|
|
981
|
+
continue
|
|
982
|
+
key, value = line.split("=", 1)
|
|
983
|
+
if value in {"y", "m"}:
|
|
984
|
+
enabled.add(key)
|
|
985
|
+
return enabled
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def extract_all_tracked_lines(coverage_file: Path) -> defaultdict[str, set[int]]:
|
|
989
|
+
"""Extract all tracked line numbers from a syzkaller coverage JSON report.
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
coverage_file: Path to the syzkaller coverage JSON file.
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
A defaultdict mapping filenames to sets of all tracked line numbers.
|
|
996
|
+
Includes lines from "Covered", "Uncovered", and "Both" categories.
|
|
997
|
+
"""
|
|
998
|
+
result: defaultdict[str, set[int]] = defaultdict(set)
|
|
999
|
+
|
|
1000
|
+
with open(coverage_file) as f:
|
|
1001
|
+
data = json.load(f)
|
|
1002
|
+
|
|
1003
|
+
for entry in data:
|
|
1004
|
+
filename = entry.get("Filename")
|
|
1005
|
+
if filename:
|
|
1006
|
+
covered = entry.get("Covered", [])
|
|
1007
|
+
uncovered = entry.get("Uncovered", [])
|
|
1008
|
+
both = entry.get("Both", [])
|
|
1009
|
+
result[filename].update(covered + uncovered + both)
|
|
1010
|
+
|
|
1011
|
+
return result
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def find_function_for_line(
|
|
1015
|
+
line: int, function_spans: dict[str, tuple[int, int]]
|
|
1016
|
+
) -> str | None:
|
|
1017
|
+
"""Find which function contains a given line number.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
line: The line number to look up.
|
|
1021
|
+
function_spans: Dict mapping function names to (start_line, end_line) tuples.
|
|
1022
|
+
|
|
1023
|
+
Returns:
|
|
1024
|
+
The function name if found, None otherwise.
|
|
1025
|
+
"""
|
|
1026
|
+
for func_name, (start, end) in function_spans.items():
|
|
1027
|
+
if start <= line <= end:
|
|
1028
|
+
return func_name
|
|
1029
|
+
return None
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def get_all_parseable_preproc_lines_in_covered_functions(
|
|
1033
|
+
coverage_file: Path,
|
|
1034
|
+
kernel_src: Path,
|
|
1035
|
+
strip_prefix: str | Path | None = None,
|
|
1036
|
+
) -> ParseableLines:
|
|
1037
|
+
"""Collect all parseable preprocessor body lines inside covered functions.
|
|
1038
|
+
|
|
1039
|
+
Returns lines for both enabled and disabled parseable directives, plus stats.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
coverage_file: Path to the syzkaller coverage JSON report.
|
|
1043
|
+
kernel_src: Root of the kernel tree to analyze.
|
|
1044
|
+
strip_prefix: Build-time path prefix to strip from the paths recorded in
|
|
1045
|
+
the report. Optional; when omitted, paths are matched against
|
|
1046
|
+
`kernel_src` by longest trailing portion. Files that cannot be
|
|
1047
|
+
mapped onto `kernel_src` are skipped.
|
|
1048
|
+
"""
|
|
1049
|
+
|
|
1050
|
+
all_tracked_lines = extract_all_tracked_lines(coverage_file)
|
|
1051
|
+
parseable_lines = defaultdict(list)
|
|
1052
|
+
stats = ParserStats()
|
|
1053
|
+
result = ParseableLines(parseable_lines=parseable_lines, stats=stats)
|
|
1054
|
+
|
|
1055
|
+
for filename, tracked in all_tracked_lines.items():
|
|
1056
|
+
if not filename.endswith(".c"):
|
|
1057
|
+
continue
|
|
1058
|
+
if not tracked:
|
|
1059
|
+
continue
|
|
1060
|
+
|
|
1061
|
+
rel_c_file_path = _kernel_relative_from_absolute_filename(
|
|
1062
|
+
filename, kernel_src, strip_prefix
|
|
1063
|
+
)
|
|
1064
|
+
if rel_c_file_path is None:
|
|
1065
|
+
continue
|
|
1066
|
+
c_file_path = kernel_src / rel_c_file_path
|
|
1067
|
+
if not c_file_path.exists():
|
|
1068
|
+
continue
|
|
1069
|
+
|
|
1070
|
+
function_spans = parse_function_spans(c_file_path)
|
|
1071
|
+
if not function_spans:
|
|
1072
|
+
continue
|
|
1073
|
+
|
|
1074
|
+
covered_functions: set[str] = set()
|
|
1075
|
+
for line in tracked:
|
|
1076
|
+
func_name = find_function_for_line(line, function_spans)
|
|
1077
|
+
if func_name:
|
|
1078
|
+
covered_functions.add(func_name)
|
|
1079
|
+
|
|
1080
|
+
if not covered_functions:
|
|
1081
|
+
continue
|
|
1082
|
+
|
|
1083
|
+
blocks = extract_preproc_blocks_from_covered_functions(
|
|
1084
|
+
kernel_src, rel_c_file_path, covered_functions
|
|
1085
|
+
)
|
|
1086
|
+
if not blocks:
|
|
1087
|
+
continue
|
|
1088
|
+
|
|
1089
|
+
lines: set[int] = set()
|
|
1090
|
+
for block in blocks:
|
|
1091
|
+
body_lines = block.body_lines
|
|
1092
|
+
condition = block.condition
|
|
1093
|
+
if not condition.parseable:
|
|
1094
|
+
stats.unparseable_blocks += 1
|
|
1095
|
+
stats.unparseable_lines += len(body_lines)
|
|
1096
|
+
continue
|
|
1097
|
+
|
|
1098
|
+
symbol = condition.symbol
|
|
1099
|
+
if not symbol:
|
|
1100
|
+
stats.unparseable_blocks += 1
|
|
1101
|
+
stats.unparseable_lines += len(body_lines)
|
|
1102
|
+
continue
|
|
1103
|
+
|
|
1104
|
+
stats.parseable_blocks += 1
|
|
1105
|
+
stats.parseable_lines += len(body_lines)
|
|
1106
|
+
lines.update(body_lines)
|
|
1107
|
+
|
|
1108
|
+
if lines:
|
|
1109
|
+
parseable_lines[str(rel_c_file_path)] = sorted(lines)
|
|
1110
|
+
|
|
1111
|
+
return result
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def get_all_lines_in_preproc(
|
|
1115
|
+
coverage_file: Path,
|
|
1116
|
+
kernel_src: Path,
|
|
1117
|
+
output_file: Path,
|
|
1118
|
+
strip_prefix: str | Path | None = None,
|
|
1119
|
+
) -> ParseableLines:
|
|
1120
|
+
parseable_result = get_all_parseable_preproc_lines_in_covered_functions(
|
|
1121
|
+
coverage_file, kernel_src, strip_prefix
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1125
|
+
with open(output_file, "w") as f:
|
|
1126
|
+
for file_path, lines in sorted(parseable_result.parseable_lines.items()):
|
|
1127
|
+
line_nums = ",".join(str(line) for line in lines)
|
|
1128
|
+
f.write(f"{file_path}:[{line_nums}]\n")
|
|
1129
|
+
|
|
1130
|
+
return parseable_result
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kconfig-preprocessor-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extract C preprocessor conditionals and resolve each branch to its effective CONFIG_* condition
|
|
5
|
+
Keywords: c,preprocessor,tree-sitter,kconfig,linux-kernel,ifdef
|
|
6
|
+
Author: s4nsec
|
|
7
|
+
Author-email: s4nsec <s4nsec@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: C
|
|
19
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
20
|
+
Classifier: Topic :: Software Development :: Pre-processors
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: tree-sitter>=0.23
|
|
23
|
+
Requires-Dist: tree-sitter-c>=0.23
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Project-URL: Homepage, https://github.com/appleseedlab/kconfig-preprocessor-parser
|
|
26
|
+
Project-URL: Repository, https://github.com/appleseedlab/kconfig-preprocessor-parser
|
|
27
|
+
Project-URL: Issues, https://github.com/appleseedlab/kconfig-preprocessor-parser/issues
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# kconfig-preprocessor-parser
|
|
31
|
+
|
|
32
|
+
Extract C preprocessor conditional blocks (`#ifdef` / `#if` / `#elif` / `#else`)
|
|
33
|
+
from source files using [tree-sitter](https://tree-sitter.github.io/), and
|
|
34
|
+
resolve each branch to the **effective condition** that must hold for its lines
|
|
35
|
+
to compile.
|
|
36
|
+
|
|
37
|
+
Built for Linux kernel configuration analysis, where knowing *which* `CONFIG_*`
|
|
38
|
+
symbols guard a given line matters. Scope is deliberately narrow: only
|
|
39
|
+
`CONFIG_*` symbols are resolved (see [Limitations](#limitations)).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv add kconfig-preprocessor-parser
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Python 3.10+.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Given `s.c`:
|
|
52
|
+
|
|
53
|
+
```c
|
|
54
|
+
int fn(void) {
|
|
55
|
+
#ifdef CONFIG_A
|
|
56
|
+
a();
|
|
57
|
+
#elif defined(CONFIG_B)
|
|
58
|
+
b();
|
|
59
|
+
#else
|
|
60
|
+
c();
|
|
61
|
+
#endif
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Each branch resolves to the full condition, including the negated preceding
|
|
67
|
+
branches:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
from kconfig_preprocessor_parser import extract_preproc_branch_blocks_from_file
|
|
72
|
+
|
|
73
|
+
for block, cond in extract_preproc_branch_blocks_from_file(
|
|
74
|
+
kernel_src=Path("."), c_file_path=Path("s.c")
|
|
75
|
+
):
|
|
76
|
+
print(block.type, block.body_lines, "->", cond.raw_expression)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
preproc_ifdef [3] -> CONFIG_A
|
|
81
|
+
preproc_elif [5] -> not CONFIG_A and CONFIG_B
|
|
82
|
+
preproc_else [7] -> not CONFIG_A and not CONFIG_B
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Note `raw_expression` for the `#elif` and `#else` branches: the parser carries
|
|
86
|
+
the negation of every earlier branch in the chain, so the expression is the
|
|
87
|
+
complete guard for those lines, not just the local directive.
|
|
88
|
+
|
|
89
|
+
Nested blocks compose the same way — a `#ifdef CONFIG_B` inside a
|
|
90
|
+
`#ifdef CONFIG_A` yields `CONFIG_A and CONFIG_B`.
|
|
91
|
+
|
|
92
|
+
Boolean operators are supported: `defined(A) && defined(B)` resolves to
|
|
93
|
+
`((A) and (B))`, and `||` resolves to `or`.
|
|
94
|
+
|
|
95
|
+
## Limitations
|
|
96
|
+
|
|
97
|
+
**Value comparisons are not resolved.** Conditions are reduced in terms of
|
|
98
|
+
whether a symbol is defined, so any comparison against a value fails:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
#if LINUX_VERSION_CODE > 100 -> parseable=False, "unsupported_condition_expression"
|
|
102
|
+
#if CONFIG_NR_CPUS > 4 -> parseable=False, "unsupported_condition_expression"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Unresolved conditions are never silently dropped. They come back with
|
|
106
|
+
`parseable=False`, a `failure_reason`, and the original text in
|
|
107
|
+
`raw_expression`, so you can filter or handle them yourself.
|
|
108
|
+
|
|
109
|
+
**Only `CONFIG_*` symbols are resolved.** Any other identifier is reported with
|
|
110
|
+
`parseable=False` and `failure_reason="non_config_symbol"`:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
#ifdef CONFIG_A -> parseable=True
|
|
114
|
+
#ifdef __KERNEL__ -> parseable=False, "non_config_symbol"
|
|
115
|
+
#ifdef DEBUG -> parseable=False, "non_config_symbol"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Conditions are read syntactically from one file.** Macros are not expanded and
|
|
119
|
+
`#include` is not followed, so `#define MY_FLAG CONFIG_A` followed by
|
|
120
|
+
`#ifdef MY_FLAG` reports `MY_FLAG`, not `CONFIG_A`. Symbols are reported as
|
|
121
|
+
guards regardless of whether they are actually defined — resolve them against a
|
|
122
|
+
real config with `parse_enabled_configs`.
|
|
123
|
+
|
|
124
|
+
**C only.** Backed by `tree-sitter-c`; C++ is not supported.
|
|
125
|
+
|
|
126
|
+
## Coverage reports
|
|
127
|
+
|
|
128
|
+
`get_all_lines_in_preproc` and
|
|
129
|
+
`get_all_parseable_preproc_lines_in_covered_functions` read syzkaller coverage
|
|
130
|
+
JSON. Reports record absolute paths from the machine the kernel was built on,
|
|
131
|
+
which rarely match the tree you are analyzing, so paths are mapped onto
|
|
132
|
+
`kernel_src` by longest matching suffix:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
get_all_parseable_preproc_lines_in_covered_functions(
|
|
136
|
+
coverage_file=Path("coverage.json"),
|
|
137
|
+
kernel_src=Path("/src/linux"),
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Pass `strip_prefix` to map them explicitly instead:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
get_all_parseable_preproc_lines_in_covered_functions(
|
|
145
|
+
coverage_file=Path("coverage.json"),
|
|
146
|
+
kernel_src=Path("/src/linux"),
|
|
147
|
+
strip_prefix="/build/ci/kernel-6.1",
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Files that cannot be mapped onto `kernel_src` are skipped, not fatal.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
kconfig_preprocessor_parser/__init__.py,sha256=GodWXvpjzzYY_Wv9rDWSu6RivWKOMg3hNwj9TI2hVz0,1042
|
|
2
|
+
kconfig_preprocessor_parser/parser.py,sha256=vgjH2Jpt-1Hm_2qOHZ9sU5vw1moaA5SnfPIsEeh5gfc,36051
|
|
3
|
+
kconfig_preprocessor_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
kconfig_preprocessor_parser-0.1.0.dist-info/licenses/LICENSE,sha256=iNbwKomd6MBtGjgTMPi8aT_aLP7YdT4e0KyCYSgYBgo,1063
|
|
5
|
+
kconfig_preprocessor_parser-0.1.0.dist-info/WHEEL,sha256=ZFFp7t7R4RYQ5KYZkmiFWoQvHay7SrTmn-6ZYfoFZ3U,80
|
|
6
|
+
kconfig_preprocessor_parser-0.1.0.dist-info/METADATA,sha256=bIT9P1yyqYH3Kwz7p-E1S-ywaod-imaA9kN9y_k-6ao,5059
|
|
7
|
+
kconfig_preprocessor_parser-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 s4nsec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|