Path:
strictdoc/backend/sdoc_source_code/reader_c.py
Lines:
530
Non-empty lines:
474
Non-empty lines covered with requirements:
474 / 474 (100.0%)
Functions:
7
Functions covered by requirements:
7 / 7 (100.0%)
- "7.2.1. Language-aware parsing of source code" (REQUIREMENT)
- "7.2.2. Language-aware parsing of C/C++ code" (REQUIREMENT)
1
"""2
@relation(SDOC-SRS-142, SDOC-SRS-146, scope=file)3
"""4
5
from typing import Final, List, Optional, Sequence
6
7
import tree_sitter_c
8
import tree_sitter_cpp
9
from tree_sitter import Language, Node, Parser
10
11
from strictdoc.backend.sdoc_source_code.constants import FunctionAttribute
12
from strictdoc.backend.sdoc_source_code.marker_parser import MarkerParser
13
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
14
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
15
LanguageItemMarker,
16
RangeMarkerType,
17
)18
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
19
from strictdoc.backend.sdoc_source_code.models.range_marker import (
20
RangeMarker,
21
)22
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
23
SourceFileTraceabilityInfo,
24
)25
from strictdoc.backend.sdoc_source_code.models.source_location import ByteRange
26
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
27
from strictdoc.backend.sdoc_source_code.parse_context import ParseContext
28
from strictdoc.backend.sdoc_source_code.processors.general_language_marker_processors import (
29
language_item_marker_processor,
30
line_marker_processor,
31
range_marker_processor,
32
source_file_traceability_info_processor,
33
)34
from strictdoc.backend.sdoc_source_code.tree_sitter_helpers import (
35
traverse_tree,
36
ts_find_child_node_by_type,
37
ts_find_child_nodes_by_type,
38
)39
from strictdoc.helpers.cast import assert_cast
40
from strictdoc.helpers.file_stats import SourceFileStats
41
from strictdoc.helpers.file_system import file_open_read_bytes
42
43
KNOWN_FUNCTION_DEFINITION_MACROS: Final[frozenset[str]] = frozenset(
44
(45
# Linux46
"COMPAT_SYSCALL_DEFINE0",
47
"COMPAT_SYSCALL_DEFINE1",
48
"COMPAT_SYSCALL_DEFINE2",
49
"COMPAT_SYSCALL_DEFINE3",
50
"COMPAT_SYSCALL_DEFINE4",
51
"COMPAT_SYSCALL_DEFINE5",
52
"COMPAT_SYSCALL_DEFINE6",
53
"FIXTURE_SETUP",
54
"FIXTURE_TEARDOWN",
55
"SYSCALL_DEFINE0",
56
"SYSCALL_DEFINE1",
57
"SYSCALL_DEFINE2",
58
"SYSCALL_DEFINE3",
59
"SYSCALL_DEFINE4",
60
"SYSCALL_DEFINE5",
61
"SYSCALL_DEFINE6",
62
# Google Benchmark63
"BENCHMARK",
64
"BENCHMARK_F",
65
"BENCHMARK_DEFINE_F",
66
"BENCHMARK_TEMPLATE",
67
"BENCHMARK_TEMPLATE_F",
68
"BENCHMARK_TEMPLATE_DEFINE_F",
69
# Google Test and Linux70
"TEST",
71
"TEST_F",
72
"TEST_P",
73
"TEST_F_SIGNAL",
74
"TYPED_TEST",
75
# Zephyr76
"ZTEST_USER",
77
)78
)79
80
81
class SourceFileTraceabilityReader_C:
82
@staticmethod83
def supported_elements() -> list[str]:
84
return ["function", "class"]
85
86
def __init__(self, custom_tags: Optional[set[str]] = None) -> None:
87
self.custom_tags: Optional[set[str]] = custom_tags
88
89
def read(
90
self,
91
input_buffer: bytes,
92
file_path: Optional[str] = None,
93
) -> SourceFileTraceabilityInfo:
94
assert isinstance(input_buffer, bytes)
95
96
file_stats = SourceFileStats.create(input_buffer)
97
parse_context = ParseContext(file_path, file_stats)
98
99
language_arg: object
100
if file_path is not None and file_path.endswith(".c"):
101
language_arg = tree_sitter_c.language()
102
else:
103
language_arg = tree_sitter_cpp.language()
104
py_language = Language(language_arg)
105
parser = Parser(py_language)
106
107
tree = parser.parse(input_buffer)
108
109
traceability_info = SourceFileTraceabilityInfo([])
110
111
nodes = traverse_tree(tree)
112
113
source_node: Optional[SourceNode]
114
for node_ in nodes:
115
function_name: str
116
language_item_markers: List[LanguageItemMarker]
117
function_comment_node: Optional[Node]
118
if node_.type == "translation_unit":
119
if (
120
len(node_.children) > 0
121
and node_.children[0].type == "comment"
122
and (comment_node := node_.children[0])
123
):124
if comment_node.text is not None:
125
comment_text = comment_node.text.decode("utf-8")
126
source_node = MarkerParser.parse(
127
input_string=comment_text,
128
line_start=node_.start_point[0] + 1,
129
# It is important that +1 is not present here because130
# currently StrictDoc does not display the last empty line (\n is 10).131
line_end=node_.end_point[0]
132
if input_buffer[-1] == 10
133
else node_.end_point[0] + 1,
134
comment_line_start=node_.start_point[0] + 1,
135
comment_byte_range=ByteRange.create_from_ts_node(
136
comment_node137
),138
filename=parse_context.filename,
139
custom_tags=self.custom_tags,
140
)141
for marker_ in source_node.markers:
142
if not isinstance(marker_, LanguageItemMarker):
143
continue144
# At the top level, only accept the scope=file markers.145
# Everything else will be handled by functions and classes.146
if marker_.scope != RangeMarkerType.FILE:
147
continue148
if isinstance(marker_, LanguageItemMarker) and (
149
language_item_marker_ := marker_
150
):151
language_item_marker_processor(
152
language_item_marker_, parse_context
153
)154
traceability_info.markers.append(
155
language_item_marker_156
)157
158
elif node_.type in ("declaration", "field_declaration"):
159
function_declarator_node = ts_find_child_node_by_type(
160
node_, "function_declarator"
161
)162
163
# C++ reference declaration wrap the function declaration one time.164
if function_declarator_node is None:
165
# Example: "TrkVertex& operator-=(const TrkVertex& c);".166
reference_declarator_node = ts_find_child_node_by_type(
167
node_, "reference_declarator"
168
)169
if reference_declarator_node is None:
170
continue171
172
function_declarator_node = ts_find_child_node_by_type(
173
reference_declarator_node, "function_declarator"
174
)175
if function_declarator_node is None:
176
continue177
178
# For normal C functions the identifier is "identifier".179
# For C++, there are:180
# Class function declarations: bool CanSend(const CanFrame &frame); # noqa: ERA001181
# Operators: TrkVertex& operator-=(const TrkVertex& c); # noqa: ERA001182
# Destructors: ~TrkVertex(); # noqa: ERA001183
function_identifier_node = self._get_function_name_node(
184
function_declarator_node,
185
)186
if function_identifier_node is None:
187
continue188
189
if function_identifier_node.text is None:
190
continue191
192
assert function_identifier_node.text is not None, node_.text
193
function_display_name = function_identifier_node.text.decode(
194
"utf8"195
)196
197
assert function_declarator_node.text is not None, node_.text
198
function_name = function_declarator_node.text.decode("utf8")
199
assert function_name is not None, node_.text
200
# Remove extra trailing spaces, newlines etc added by code-formatting or linters201
function_name = " ".join(function_name.split())
202
203
parent_names = self.get_node_ns(node_)
204
if len(parent_names) > 0:
205
function_name = (
206
f"{'::'.join(parent_names)}::{function_name}"
207
)208
function_display_name = (
209
f"{'::'.join(parent_names)}::{function_display_name}"
210
)211
212
function_attributes = {FunctionAttribute.DECLARATION}
213
for specifier_node_ in ts_find_child_nodes_by_type(
214
node_, "storage_class_specifier"
215
):216
if specifier_node_.text == b"static":
217
function_attributes.add(FunctionAttribute.STATIC)
218
219
source_node = None
220
language_item_markers = []
221
function_comment_node = None
222
if (
223
node_.prev_sibling is not None
224
and node_.prev_sibling.type == "comment"
225
):226
function_comment_node = node_.prev_sibling
227
assert function_comment_node.text is not None, node_.text
228
function_comment_text = function_comment_node.text.decode(
229
"utf8"230
)231
232
function_last_line = node_.end_point[0] + 1
233
234
source_node = MarkerParser.parse(
235
input_string=function_comment_text,
236
line_start=function_comment_node.start_point[0] + 1,
237
line_end=function_last_line,
238
comment_line_start=function_comment_node.start_point[0]
239
+ 1,
240
comment_byte_range=ByteRange.create_from_ts_node(
241
function_comment_node242
),243
filename=parse_context.filename,
244
entity_name=function_display_name,
245
custom_tags=self.custom_tags,
246
)247
for marker_ in source_node.markers:
248
if isinstance(marker_, LanguageItemMarker) and (
249
language_item_marker_ := marker_
250
):251
language_item_marker_processor(
252
language_item_marker_, parse_context
253
)254
traceability_info.markers.append(
255
language_item_marker_256
)257
language_item_markers.append(marker_)
258
259
# The function range includes the top comment if it exists.260
new_function = LanguageItem(
261
parent=traceability_info,
262
name=function_name,
263
display_name=function_display_name,
264
line_begin=function_comment_node.start_point[0] + 1
265
if function_comment_node is not None
266
else node_.range.start_point[0] + 1,
267
line_end=node_.range.end_point[0] + 1,
268
code_byte_range=ByteRange.create_from_ts_node(node_),
269
child_functions=[],
270
markers=language_item_markers,
271
attributes=function_attributes,
272
)273
if source_node is not None:
274
source_node.function = new_function
275
traceability_info.functions.append(new_function)
276
277
elif node_.type == "function_definition":
278
function_name = ""
279
280
try:
281
function_declarator_node = ts_find_child_node_by_type(
282
node_, "function_declarator", raise_on_error=True
283
)284
except LookupError:
285
# Probably confused by macro, skip node to avoid processing random subtrees.286
continue287
# C++ reference declaration wrap the function declaration one time.288
if function_declarator_node is None:
289
# Example: Foo& Foo::operator+(const Foo& c) { return *this; }290
reference_declarator_node = ts_find_child_node_by_type(
291
node_, "reference_declarator"
292
)293
if reference_declarator_node is None:
294
continue295
296
function_declarator_node = ts_find_child_node_by_type(
297
reference_declarator_node, "function_declarator"
298
)299
if function_declarator_node is None:
300
continue301
302
assert function_declarator_node is not None, node_.text
303
304
assert function_declarator_node.text is not None, node_.text
305
function_name = function_declarator_node.text.decode("utf8")
306
307
identifier_node = self._get_function_name_node(
308
function_declarator_node309
)310
if identifier_node is None:
311
print( # noqa: T201
312
"warning: C/C++ source reader: skipping unsupported "313
"function declarator at "314
f"{parse_context.filename}:"
315
f"{function_declarator_node.start_point[0] + 1}: "
316
f"{function_declarator_node}"
317
)318
continue319
320
assert identifier_node.text is not None, node_.text
321
function_display_name = identifier_node.text.decode("utf8")
322
323
assert function_name is not None, node_.text
324
# Remove extra trailing spaces, newlines etc added by code-formatting or linters325
function_name = " ".join(function_name.split())
326
parent_names = self.get_node_ns(node_)
327
328
# The first if branch handles a special case where selected329
# macros are actually function definitions. Typical examples of330
# such functions:331
# 1) Google Test TEST(...)332
# 2) Zephyr RTOS ZTEST_USER(...)333
# 3) Linux SYSCALL2_DEFINE etc.334
if function_display_name in KNOWN_FUNCTION_DEFINITION_MACROS:
335
# Make the display name to include the entire macro/function336
# signature.337
# Example of this case:338
# function_name: ZTEST_USER(semaphore, test_k_sem_correct_count_limit) # noqa: ERA001339
# function_display_name (before assignment): ZTEST_USER340
function_display_name = function_name
341
elif len(parent_names) > 0:
342
function_name = (
343
f"{'::'.join(parent_names)}::{function_name}"
344
)345
function_display_name = (
346
f"{'::'.join(parent_names)}::{function_display_name}"
347
)348
349
source_node = None
350
language_item_markers = []
351
function_comment_node = None
352
function_comment_text = None
353
354
# In the condition below, it is important that the comment is355
# considered a function comment only if it there are no empty356
# lines between the comment and function.357
if (
358
node_.prev_sibling is not None
359
and node_.prev_sibling.type == "comment"
360
and (node_.prev_sibling.end_point[0] + 1)
361
== node_.start_point[0]
362
):363
function_comment_node = node_.prev_sibling
364
assert function_comment_node.text is not None, node_.text
365
function_comment_text = function_comment_node.text.decode(
366
"utf8"367
)368
369
function_last_line = node_.end_point[0] + 1
370
371
source_node = MarkerParser.parse(
372
input_string=function_comment_text,
373
line_start=function_comment_node.start_point[0] + 1,
374
line_end=function_last_line,
375
comment_line_start=function_comment_node.start_point[0]
376
+ 1,
377
comment_byte_range=ByteRange.create_from_ts_node(
378
function_comment_node379
),380
filename=parse_context.filename,
381
entity_name=function_display_name,
382
custom_tags=self.custom_tags,
383
)384
385
traceability_info.source_nodes.append(source_node)
386
for marker_ in source_node.markers:
387
if isinstance(marker_, LanguageItemMarker):
388
language_item_marker_processor(
389
marker_, parse_context
390
)391
traceability_info.markers.append(marker_)
392
language_item_markers.append(marker_)
393
394
# The function range includes the top comment if it exists.395
new_function = LanguageItem(
396
parent=traceability_info,
397
name=function_name,
398
display_name=function_display_name,
399
line_begin=function_comment_node.start_point[0] + 1
400
if function_comment_node is not None
401
else node_.range.start_point[0] + 1,
402
line_end=node_.range.end_point[0] + 1,
403
code_byte_range=ByteRange.create_from_ts_node(node_),
404
child_functions=[],
405
markers=language_item_markers,
406
attributes={FunctionAttribute.DEFINITION},
407
)408
traceability_info.functions.append(new_function)
409
if len(language_item_markers) > 0:
410
traceability_info.ng_map_names_to_markers[function_name] = (
411
# FIXME: Cannot win the fight with mypy without assert_cast.412
assert_cast(language_item_markers, list)
413
)414
traceability_info.ng_map_names_to_definition_functions[
415
function_name416
] = new_function
417
if source_node is not None:
418
source_node.function = new_function
419
elif node_.type == "comment":
420
#421
# FIXME: Here parsing of function comments can happen as well422
# but this time the focus is ONLY on range and line markers.423
# The case which is handled here is when a user adds a424
# range_start marker in a function comment.425
# It is not good that parsing of function comments426
# happens twice.427
#428
429
assert node_.text is not None, (
430
f"Comment without a text: {node_}"
431
)432
433
node_text_string = node_.text.decode("utf8")
434
435
source_node = MarkerParser.parse(
436
input_string=node_text_string,
437
line_start=node_.start_point[0] + 1,
438
line_end=node_.end_point[0] + 1,
439
comment_line_start=node_.start_point[0] + 1,
440
comment_byte_range=ByteRange.create_from_ts_node(node_),
441
filename=parse_context.filename,
442
custom_tags=None,
443
)444
445
for marker_ in source_node.markers:
446
if isinstance(marker_, RangeMarker) and (
447
range_marker_ := marker_
448
):449
range_marker_processor(range_marker_, parse_context)
450
elif isinstance(marker_, LineMarker) and (
451
line_marker_ := marker_
452
):453
line_marker_processor(line_marker_, parse_context)
454
else:
455
pass456
else:
457
pass458
459
source_file_traceability_info_processor(
460
traceability_info, parse_context
461
)462
463
traceability_info.ng_map_reqs_to_markers = (
464
parse_context.map_reqs_to_markers
465
)466
467
return traceability_info
468
469
def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo:
470
with file_open_read_bytes(file_path) as file:
471
sdoc_content = file.read()
472
sdoc = self.read(sdoc_content, file_path=file_path)
473
return sdoc
474
475
@staticmethod476
def _get_function_name_node(
477
function_declarator_node: Node,
478
) -> Optional[Node]:
479
assert function_declarator_node.type == "function_declarator"
480
function_identifier_node = ts_find_child_node_by_type(
481
function_declarator_node,
482
node_type=(
483
"identifier",
484
"field_identifier",
485
"operator_name",
486
"destructor_name",
487
"qualified_identifier",
488
),489
)490
if function_identifier_node is not None:
491
return function_identifier_node
492
493
nested_function_declarator_node = ts_find_child_node_by_type(
494
function_declarator_node,
495
"function_declarator",
496
)497
if nested_function_declarator_node is not None:
498
return SourceFileTraceabilityReader_C._get_function_name_node(
499
nested_function_declarator_node500
)501
502
return None
503
504
@staticmethod505
def get_node_ns(node: Node) -> Sequence[str]:
506
"""
507
Walk up the tree and find parent classes.508
"""509
parent_scopes = []
510
cursor: Optional[Node] = node
511
while cursor is not None:
512
if cursor.type == "class_specifier" and len(cursor.children) > 1:
513
second_node_or_none = cursor.children[1]
514
if (
515
second_node_or_none.type == "type_identifier"
516
and second_node_or_none.text is not None
517
):518
parent_class_name = second_node_or_none.text.decode("utf8")
519
parent_scopes.append(parent_class_name)
520
elif cursor.type == "namespace_definition":
521
for c in cursor.children:
522
if c.type == "namespace_identifier" and c.text is not None:
523
parent_class_name = c.text.decode("utf8")
524
parent_scopes.append(parent_class_name)
525
break526
527
cursor = cursor.parent
528
529
parent_scopes.reverse()
530
return parent_scopes