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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.