Path:
strictdoc/core/file_traceability_index.py
Lines:
1237
Non-empty lines:
1110
Non-empty lines covered with requirements:
1110 / 1110 (100.0%)
Functions:
29
Functions covered by requirements:
29 / 29 (100.0%)
- "4.1. Traceability index" (REQUIREMENT)
- "7.1. Link requirements with source files" (REQUIREMENT)
1
"""2
@relation(SDOC-SRS-28, SDOC-SRS-33, scope=file)3
"""4
5
import re
6
from copy import copy
7
from typing import (
8
TYPE_CHECKING,
9
Dict,
10
Iterator,
11
List,
12
Optional,
13
Set,
14
Tuple,
15
Union,
16
)17
18
from strictdoc.backend.sdoc.document_reference import DocumentReference
19
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
20
from strictdoc.backend.sdoc.models.document_grammar import (
21
DocumentGrammar,
22
)23
from strictdoc.backend.sdoc.models.model import SDocDocumentIF
24
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
25
from strictdoc.backend.sdoc.models.reference import FileEntry, FileReference
26
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
27
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
28
ForwardLanguageItemMarker,
29
LanguageItemMarker,
30
RangeMarkerType,
31
)32
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
33
from strictdoc.backend.sdoc_source_code.models.range_marker import (
34
ForwardFileMarker,
35
ForwardRangeMarker,
36
RangeMarker,
37
)38
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
39
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
40
RelationMarkerType,
41
SourceFileTraceabilityInfo,
42
)43
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
44
from strictdoc.core.constants import GraphLinkType
45
from strictdoc.core.document_iterator import SDocDocumentIterator
46
from strictdoc.core.file_system.source_tree import SourceFile
47
from strictdoc.core.project_config import ProjectConfig, SourceNodesEntry
48
from strictdoc.helpers.cast import assert_cast
49
from strictdoc.helpers.exception import StrictDocException
50
from strictdoc.helpers.google_test import convert_function_name_to_gtest_macro
51
from strictdoc.helpers.mid import MID
52
from strictdoc.helpers.ordered_set import OrderedSet
53
54
if TYPE_CHECKING:
55
from strictdoc.core.traceability_index import (
56
TraceabilityIndex,
57
)58
59
60
class FileTraceabilityIndex:
61
def __init__(self) -> None:
62
# "file.py" -> List[SDocNode]63
self.map_paths_to_reqs: Dict[str, OrderedSet[SDocNode]] = {}
64
65
# "REQ-001" -> {"file.py", ...}66
self.map_reqs_uids_to_paths: Dict[str, OrderedSet[str]] = {}
67
68
# "file.py" -> SourceFileTraceabilityInfo.69
self.map_paths_to_source_file_traceability_info: Dict[
70
str, SourceFileTraceabilityInfo
71
] = {}
72
73
# "file.py" -> { { "foo" -> [("REQ-1", "Impl"), ("REQ-2", "Test")] }, ... }74
self.map_file_function_names_to_reqs_uids: Dict[
75
str, Dict[str, List[Tuple[str, Optional[str]]]]
76
] = {}
77
self.map_file_class_names_to_reqs_uids: Dict[
78
str, Dict[str, List[Tuple[str, Optional[str]]]]
79
] = {}
80
81
# This is only public non-static functions from languages like C.82
self.map_all_function_names_to_definition_functions: Dict[
83
str, List[LanguageItem]
84
] = {}
85
86
# "file.py" -> [SDocNode] # noqa: ERA00187
self.source_file_reqs_cache: Dict[str, Optional[List[SDocNode]]] = {}
88
89
self.requirements_with_forward_links: OrderedSet[SDocNode] = (
90
OrderedSet()
91
)92
self.trace_infos: List[SourceFileTraceabilityInfo] = []
93
94
def has_source_file_reqs(self, source_file_rel_path: str) -> bool:
95
path_reqs = self.map_paths_to_reqs.get(source_file_rel_path)
96
if path_reqs is not None and len(path_reqs) > 0:
97
return True
98
file_trace_info = self.map_paths_to_source_file_traceability_info[
99
source_file_rel_path100
]101
return len(file_trace_info.markers) > 0
102
103
def get_requirement_file_links(
104
self, requirement: SDocNode
105
) -> List[Tuple[str, List[RelationMarkerType]]]:
106
if requirement.reserved_uid not in self.map_reqs_uids_to_paths:
107
return []
108
109
matching_links_with_markers: List[
110
Tuple[str, List[RelationMarkerType]]
111
] = []
112
requirement_source_paths: OrderedSet[str] = self.map_reqs_uids_to_paths[
113
requirement.reserved_uid
114
]115
116
# Now that one requirement can have multiple File-relations to the same file.117
# This can be multiple FUNCTION: or RANGE: forward-relations.118
# To avoid duplication of results, visit each unique file link path only once.119
visited_file_links: Set[str] = set()
120
for requirement_source_path_ in requirement_source_paths:
121
if requirement_source_path_ in visited_file_links:
122
continue123
visited_file_links.add(requirement_source_path_)
124
125
source_file_traceability_info: Optional[
126
SourceFileTraceabilityInfo127
] = self.map_paths_to_source_file_traceability_info.get(
128
requirement_source_path_129
)130
assert source_file_traceability_info is not None, (
131
f"Requirement {requirement.reserved_uid} references a file"
132
f" that does not exist: {requirement_source_path_}."
133
)134
markers = source_file_traceability_info.ng_map_reqs_to_markers.get(
135
requirement.reserved_uid
136
)137
if markers is None or len(markers) == 0:
138
matching_links_with_markers.append(
139
(requirement_source_path_, [])
140
)141
continue142
matching_links_with_markers.append(
143
(requirement_source_path_, markers)
144
)145
146
return matching_links_with_markers
147
148
def indexed_source_files(self) -> Iterator[SourceFile]:
149
for _, sfti in self.map_paths_to_source_file_traceability_info.items():
150
if sfti.source_file is not None:
151
yield sfti.source_file
152
153
def get_source_file_reqs(
154
self, source_file_rel_path: str
155
) -> Optional[List[SDocNode]]:
156
assert (
157
source_file_rel_path158
in self.map_paths_to_source_file_traceability_info
159
)160
if source_file_rel_path in self.source_file_reqs_cache:
161
return self.source_file_reqs_cache[source_file_rel_path]
162
163
source_file_traceability_info: SourceFileTraceabilityInfo = (
164
self.map_paths_to_source_file_traceability_info[
165
source_file_rel_path166
]167
)168
169
if source_file_rel_path not in self.map_paths_to_reqs:
170
self.source_file_reqs_cache[source_file_rel_path] = None
171
return None
172
173
requirements = self.map_paths_to_reqs[source_file_rel_path]
174
assert len(requirements) > 0
175
range_requirements = []
176
177
for requirement in requirements:
178
if (
179
requirement.reserved_uid
180
in source_file_traceability_info.ng_map_reqs_to_markers
181
):182
range_requirements.append(requirement)
183
184
self.source_file_reqs_cache[source_file_rel_path] = range_requirements
185
return range_requirements
186
187
def get_coverage_info(
188
self, source_file_rel_path: str
189
) -> SourceFileTraceabilityInfo:
190
assert (
191
source_file_rel_path192
in self.map_paths_to_source_file_traceability_info
193
), source_file_rel_path
194
source_file_tr_info: SourceFileTraceabilityInfo = (
195
self.map_paths_to_source_file_traceability_info[
196
source_file_rel_path197
]198
)199
return source_file_tr_info
200
201
def get_coverage_info_weak(
202
self, source_file_rel_path: str
203
) -> Optional[SourceFileTraceabilityInfo]:
204
source_file_tr_info: Optional[SourceFileTraceabilityInfo] = (
205
self.map_paths_to_source_file_traceability_info.get(
206
source_file_rel_path207
)208
)209
return source_file_tr_info
210
211
def validate_and_resolve(
212
self,
213
traceability_index: "TraceabilityIndex",
214
project_config: ProjectConfig,
215
) -> None:
216
"""
217
Resolve all source code traceability after the index is fully built.218
"""219
220
#221
# STEP: Collect minimal information that will help to resolve the222
# forward-declared paths/function names at the step 2.223
#224
for trace_info_ in self.trace_infos:
225
source_file: SourceFile = assert_cast(
226
trace_info_.source_file, SourceFile
227
)228
229
self.map_paths_to_source_file_traceability_info[
230
source_file.in_doctree_source_file_rel_path_posix
231
] = trace_info_
232
233
for function_ in trace_info_.functions:
234
if function_.is_definition() and function_.is_public():
235
self.map_all_function_names_to_definition_functions.setdefault(
236
function_.name, []
237
).append(function_)
238
239
#240
# STEP: Auto-generated SDocNodes from source file comments and register241
# their UIDs. This must happen before marker validation so that242
# source files can reference these UIDs via @relation.243
#244
documents_with_generated_content = set()
245
246
section_cache: Dict[str, Union[SDocDocumentIF, SDocNode]] = {}
247
source_nodes_config: List[SourceNodesEntry] = (
248
project_config.source_nodes
249
)250
unused_source_node_paths = {
251
config_entry_.path for config_entry_ in source_nodes_config
252
}253
for (
254
path_to_source_file_,
255
traceability_info_,
256
) in self.map_paths_to_source_file_traceability_info.items():
257
if len(traceability_info_.source_nodes) == 0:
258
continue259
260
if len(source_nodes_config) == 0:
261
continue262
263
relevant_source_node_entry = (
264
project_config.get_relevant_source_nodes_entry(
265
path_to_source_file_266
)267
)268
if relevant_source_node_entry is not None:
269
unused_source_node_paths.discard(
270
relevant_source_node_entry.path
271
)272
else:
273
continue274
275
document_uid = relevant_source_node_entry.uid
276
document = traceability_index.get_node_by_uid(document_uid)
277
documents_with_generated_content.add(document)
278
current_top_node = None
279
280
for source_node_ in traceability_info_.source_nodes:
281
if len(source_node_.fields) == 0:
282
continue283
284
assert source_node_.entity_name is not None
285
sdoc_node = None
286
sdoc_node_uid = source_node_.get_sdoc_field(
287
"UID", relevant_source_node_entry
288
)289
mid = source_node_.get_sdoc_field(
290
"MID", relevant_source_node_entry
291
)292
293
# First merge criterion: Merge if SDoc node with same MID exists.294
if mid is not None:
295
sdoc_node_mid = MID(mid)
296
merge_candidate_sdoc_node = (
297
traceability_index.get_node_by_mid_weak(sdoc_node_mid)
298
)299
if isinstance(merge_candidate_sdoc_node, SDocNode):
300
sdoc_node = merge_candidate_sdoc_node
301
sdoc_node_uid = sdoc_node.reserved_uid
302
303
if sdoc_node is None:
304
# If no UID from source code field or merge-by-MID, create UID by conventional scheme.305
if sdoc_node_uid is None:
306
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
307
# Second merge criterion: Merge if SDoc node with same UID exists.308
tmp_sdoc_node = traceability_index.get_node_by_uid_weak(
309
sdoc_node_uid310
)311
if isinstance(tmp_sdoc_node, SDocNode):
312
sdoc_node = tmp_sdoc_node
313
314
assert sdoc_node_uid is not None
315
if sdoc_node is not None:
316
sdoc_node = assert_cast(sdoc_node, SDocNode)
317
self.merge_sdoc_node_with_source_node(
318
relevant_source_node_entry,
319
source_node_,
320
sdoc_node,
321
document,
322
)323
else:
324
sdoc_node = self.create_sdoc_node_from_source_node(
325
source_node_,
326
relevant_source_node_entry,
327
sdoc_node_uid,
328
document,
329
)330
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
331
if current_top_node is None:
332
current_top_node, created_sections = (
333
FileTraceabilityIndex.create_source_node_section(
334
document,
335
path_to_source_file_,
336
section_cache,
337
)338
)339
for created_section in created_sections:
340
traceability_index.graph_database.create_link(
341
link_type=GraphLinkType.MID_TO_NODE,
342
lhs_node=created_section.reserved_mid,
343
rhs_node=created_section,
344
)345
current_top_node.section_contents.append(sdoc_node)
346
347
self.connect_source_node_function(
348
source_node_, sdoc_node_uid, traceability_info_
349
)350
self.connect_sdoc_node_with_file_path(
351
sdoc_node, path_to_source_file_
352
)353
self.connect_source_node_requirements(
354
source_node_, sdoc_node, traceability_index
355
)356
357
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration358
for unused_source_node_path in unused_source_node_paths:
359
print( # noqa: T201
360
f"warning: source_node path {unused_source_node_path} doesn't match any source file. "
361
"Hint: Check include_source_paths."362
)363
364
# Iterate over all generated documents to calculate all node levels.365
for document_ in documents_with_generated_content:
366
document_iterator = SDocDocumentIterator(document_)
367
for _, _ in document_iterator.all_content(
368
print_fragments=False,
369
):370
pass371
372
#373
# STEP: Resolve requirements that have forward links.374
# Some requirements can come from the SDoc documents generated375
# on the fly from JUnit XML documents.376
#377
for forward_requirement_ in self.requirements_with_forward_links:
378
assert forward_requirement_.reserved_uid is not None
379
380
for relation_ in forward_requirement_.relations:
381
if not isinstance(relation_, FileReference):
382
continue383
384
file_reference: FileReference = assert_cast(
385
relation_, FileReference
386
)387
file_posix_path = file_reference.get_posix_path()
388
389
if file_posix_path == "#FORWARD#":
390
test_function = (
391
forward_requirement_.get_meta_field_value_by_title(
392
"TEST_FUNCTION"393
)394
)395
assert test_function is not None
396
397
functions: List[LanguageItem]
398
if test_function.startswith("#GTEST#"):
399
test_function = test_function.removeprefix("#GTEST#")
400
possible_gtest_functions = (
401
convert_function_name_to_gtest_macro(test_function)
402
)403
for (
404
possible_gtest_function_405
) in possible_gtest_functions:
406
if (
407
possible_gtest_function_408
in self.map_all_function_names_to_definition_functions
409
):410
test_function = possible_gtest_function_
411
break412
else:
413
raise RuntimeError(
414
"Could not find a matching Google Test function: "415
f"{possible_gtest_functions}"
416
) # pragma: no cover
417
forward_requirement_.set_field_value(
418
field_name="TEST_FUNCTION",
419
form_field_index=0,
420
value=test_function,
421
)422
functions = (
423
self.map_all_function_names_to_definition_functions[
424
test_function425
]426
)427
assert len(functions) == 1
428
429
function: LanguageItem = functions[0]
430
resolved_path_to_function_file = function.parent.source_file.in_doctree_source_file_rel_path_posix
431
file_posix_path = resolved_path_to_function_file
432
433
file_reference.g_file_entry = FileEntry(
434
relation_,
435
g_file_format=relation_.g_file_entry.g_file_format,
436
g_file_path=resolved_path_to_function_file,
437
g_line_range=None,
438
element="function",
439
id=test_function,
440
)441
442
forward_requirement_.set_field_value(
443
field_name="TEST_PATH",
444
form_field_index=0,
445
value=resolved_path_to_function_file,
446
)447
448
#449
# This transitively connects requirements and test results450
# through the test source files.451
#452
for language_item_marker_ in function.markers:
453
for req_ in language_item_marker_.reqs:
454
node = traceability_index.get_node_by_uid_weak2(
455
req_456
)457
traceability_index.graph_database.create_link(
458
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
459
lhs_node=forward_requirement_,
460
rhs_node=node,
461
edge="Satisfies",
462
)463
traceability_index.graph_database.create_link(
464
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
465
lhs_node=node,
466
rhs_node=forward_requirement_,
467
edge="IsSatisfiedBy",
468
)469
#470
# Validate that all requirements reference existing files.471
#472
source_file_traceability_info: Optional[
473
SourceFileTraceabilityInfo474
] = self.map_paths_to_source_file_traceability_info.get(
475
file_posix_path476
)477
if source_file_traceability_info is None:
478
raise StrictDocException(
479
f"Requirement {forward_requirement_.reserved_uid} "
480
"references a file that does not exist: "481
f"{file_posix_path}."
482
)483
484
#485
# Now that the test reports related fixups are done, the486
# following code registers the requirements with forward links.487
#488
self.map_paths_to_reqs.setdefault(
489
file_posix_path, OrderedSet()
490
).add(forward_requirement_)
491
492
assert forward_requirement_.reserved_uid is not None
493
self.map_reqs_uids_to_paths.setdefault(
494
forward_requirement_.reserved_uid, OrderedSet()
495
).add(file_posix_path)
496
497
if (
498
file_reference.g_file_entry.element == "function"
499
and file_reference.g_file_entry.id is not None
500
):501
one_file_function_name_to_reqs_uids = (
502
self.map_file_function_names_to_reqs_uids.setdefault(
503
file_posix_path, {}
504
)505
)506
one_file_function_name_to_reqs_uids.setdefault(
507
file_reference.g_file_entry.id, []
508
).append(
509
(forward_requirement_.reserved_uid, relation_.role)
510
)511
elif (
512
file_reference.g_file_entry.element == "class"
513
and file_reference.g_file_entry.id is not None
514
):515
one_file_class_name_to_reqs_uids = (
516
self.map_file_class_names_to_reqs_uids.setdefault(
517
file_posix_path, {}
518
)519
)520
one_file_class_name_to_reqs_uids.setdefault(
521
file_reference.g_file_entry.id, []
522
).append(
523
(forward_requirement_.reserved_uid, relation_.role)
524
)525
elif file_reference.g_file_entry.line_range is not None:
526
line_range = file_reference.g_file_entry.line_range
527
uid = forward_requirement_.reserved_uid
528
source_file_info = (
529
self.map_paths_to_source_file_traceability_info[
530
file_posix_path531
]532
)533
start_marker, end_marker = (
534
self.forward_range_markers_from_range(
535
line_range, uid, relation_.role
536
)537
)538
source_file_info.ng_map_reqs_to_markers.setdefault(
539
uid, []
540
).append(start_marker)
541
source_file_info.markers.append(start_marker)
542
source_file_info.markers.append(end_marker)
543
else:
544
uid = forward_requirement_.reserved_uid
545
source_file_info = (
546
self.map_paths_to_source_file_traceability_info[
547
file_posix_path548
]549
)550
forward_file_marker = (
551
self.forward_file_marker_from_file_info(
552
source_file_info,
553
uid,
554
relation_.role,
555
)556
)557
source_file_info.ng_map_reqs_to_markers.setdefault(
558
forward_requirement_.reserved_uid, []
559
).append(forward_file_marker)
560
source_file_info.markers.append(forward_file_marker)
561
562
#563
# STEP: Add markers for forward relations to functions and classes564
#565
for trace_info_ in self.trace_infos:
566
source_file = assert_cast(trace_info_.source_file, SourceFile)
567
568
self.map_paths_to_source_file_traceability_info[
569
source_file.in_doctree_source_file_rel_path_posix
570
] = trace_info_
571
572
for function_ in trace_info_.functions:
573
# FIXME: Using display_name, not name. A separate exercise is574
# to disambiguate forward links to C++ overloaded functions.575
if (
576
reqs_uids := self.get_req_uids_by_function_name(
577
source_file.in_doctree_source_file_rel_path_posix,
578
function_.display_name,
579
)580
) is not None:
581
self.create_traceability_info_shared_markers_for_function(
582
trace_info_,
583
function_,
584
RangeMarkerType.FUNCTION,
585
reqs_uids,
586
)587
if (
588
reqs_uids := self.get_req_uids_by_class_name(
589
source_file.in_doctree_source_file_rel_path_posix,
590
function_.display_name,
591
)592
) is not None:
593
self.create_traceability_info_shared_markers_for_function(
594
trace_info_,
595
function_,
596
RangeMarkerType.CLASS,
597
reqs_uids,
598
)599
600
marker_: Union[
601
LanguageItemMarker, LineMarker, RangeMarker, ForwardRangeMarker
602
]603
for marker_ in copy(trace_info_.markers):
604
# FIXME: Is this 'continue' needed here?605
if isinstance(marker_, ForwardRangeMarker):
606
continue607
for requirement_uid_ in marker_.reqs:
608
node = traceability_index.get_node_by_uid_weak2(
609
requirement_uid_610
)611
if node is None:
612
raise StrictDocException(
613
f"Source file {source_file.in_doctree_source_file_rel_path_posix} references "
614
f"a requirement that does not exist: {requirement_uid_}."
615
)616
617
self.map_reqs_uids_to_paths.setdefault(
618
requirement_uid_, OrderedSet()
619
).add(source_file.in_doctree_source_file_rel_path_posix)
620
621
self.map_paths_to_reqs.setdefault(
622
source_file.in_doctree_source_file_rel_path_posix,
623
OrderedSet(),
624
).add(node)
625
626
if isinstance(marker_, LanguageItemMarker):
627
marker_copy = marker_.create_end_marker()
628
trace_info_.markers.append(marker_copy)
629
630
#631
# Resolve definitions to declarations (only applicable for C and C++).632
#633
634
reversed_trace_info = {
635
value: key
636
for key, value in self.map_paths_to_source_file_traceability_info.items()
637
}638
639
for (
640
traceability_info_641
) in self.map_paths_to_source_file_traceability_info.values():
642
for function_ in traceability_info_.functions:
643
if (
644
function_.is_declaration()
645
and function_.name
646
in self.map_all_function_names_to_definition_functions
647
):648
definition_functions: List[LanguageItem] = []
649
if not function_.is_public():
650
definition_function = traceability_info_.ng_map_names_to_definition_functions.get(
651
function_.name, None
652
)653
if definition_function is not None:
654
definition_functions.append(definition_function)
655
else:
656
mapped_definition_functions = (
657
self.map_all_function_names_to_definition_functions[
658
function_.name
659
]660
)661
definition_functions.extend(mapped_definition_functions)
662
if len(definition_functions) == 0:
663
continue664
665
for definition_function_ in definition_functions:
666
definition_function_trace_info: SourceFileTraceabilityInfo = definition_function_.parent
667
668
for marker_ in function_.markers:
669
language_item_marker = self.forward_marker_from_language_item(
670
function=definition_function_,
671
marker_type=RangeMarkerType.FUNCTION,
672
reqs=marker_.reqs_objs,
673
role=marker_.role,
674
description=f"function {function_.display_name}()",
675
)676
677
for req_uid_ in marker_.reqs:
678
definition_function_trace_info.ng_map_reqs_to_markers.setdefault(
679
req_uid_, []
680
).append(language_item_marker)
681
682
path_to_info = reversed_trace_info[
683
definition_function_trace_info684
]685
self.map_reqs_uids_to_paths.setdefault(
686
req_uid_, OrderedSet()
687
).add(path_to_info)
688
689
node = traceability_index.get_node_by_uid(
690
req_uid_691
)692
self.map_paths_to_reqs.setdefault(
693
path_to_info, OrderedSet()
694
).add(node)
695
696
definition_function_trace_info.markers.append(
697
language_item_marker698
)699
700
#701
# STEP: Calculate requirements coverage by code. Sort nodes.702
#703
self.calculate_code_coverage_and_sort_nodes(traceability_index)
704
705
def create_requirement_with_forward_source_links(
706
self, requirement: SDocNode
707
) -> None:
708
self.requirements_with_forward_links.add(requirement)
709
710
def create_traceability_info(
711
self,
712
source_file: SourceFile,
713
traceability_info: SourceFileTraceabilityInfo,
714
) -> None:
715
assert isinstance(traceability_info, SourceFileTraceabilityInfo)
716
traceability_info.source_file = source_file
717
718
self.trace_infos.append(traceability_info)
719
- "3.2.2. Forward links to macro definitions using regexes" (REQUIREMENT)
720
def get_req_uids_by_function_name(
721
self, rel_path_posix: str, name: str
722
) -> Optional[List[Tuple[str, Optional[str]]]]:
723
"""
724
@relation(SDOC-LLR-207, scope=function)725
"""726
727
if rel_path_posix not in self.map_file_function_names_to_reqs_uids:
728
return None
729
730
function_names_to_reqs_uids = self.map_file_function_names_to_reqs_uids[
731
rel_path_posix732
]733
734
matching_req_uids: List[Tuple[str, Optional[str]]] = []
735
exact_matching_req_uids = function_names_to_reqs_uids.get(name, None)
736
if exact_matching_req_uids is not None:
737
matching_req_uids.extend(exact_matching_req_uids)
738
739
for function_name_, req_uids_ in function_names_to_reqs_uids.items():
740
if not FileTraceabilityIndex.is_regex_function_name(function_name_):
741
continue742
743
regex_pattern = function_name_[1:-1]
744
try:
745
if re.search(regex_pattern, name) is not None:
746
matching_req_uids.extend(req_uids_)
747
except re.error as exception:
748
raise StrictDocException(
749
"Invalid regular expression in FUNCTION relation "750
f"{function_name_}: {exception}."
751
) from exception
752
753
return matching_req_uids if len(matching_req_uids) > 0 else None
754
755
@staticmethod756
def is_regex_function_name(name: str) -> bool:
757
return len(name) >= 2 and name[0] == "/" and name[-1] == "/"
758
759
def get_req_uids_by_class_name(
760
self, rel_path_posix: str, name: str
761
) -> Optional[List[Tuple[str, Optional[str]]]]:
762
if rel_path_posix in self.map_file_class_names_to_reqs_uids:
763
return self.map_file_class_names_to_reqs_uids[rel_path_posix].get(
764
name, None
765
)766
return None
767
768
@staticmethod769
def create_traceability_info_shared_markers_for_function(
770
traceability_info: SourceFileTraceabilityInfo,
771
function: LanguageItem,
772
marker_type: RangeMarkerType,
773
reqs_uids: List[Tuple[str, Optional[str]]],
774
) -> None:
775
markers_by_role = {}
776
for req_uid_, role in reqs_uids:
777
req = Req(None, req_uid_)
778
if role not in markers_by_role:
779
markers_by_role[role] = (
780
FileTraceabilityIndex.forward_marker_from_language_item(
781
function, marker_type, [req], role
782
)783
)784
else:
785
markers_by_role[role].reqs_objs.append(req)
786
787
for req_uid_, role in reqs_uids:
788
markers = traceability_info.ng_map_reqs_to_markers.setdefault(
789
req_uid_, []
790
)791
markers.append(markers_by_role[role])
792
793
traceability_info.markers.extend(markers_by_role.values())
794
795
@staticmethod796
def forward_marker_from_language_item(
797
function: LanguageItem,
798
marker_type: RangeMarkerType,
799
reqs: List[Req],
800
role: Optional[str],
801
description: Optional[str] = None,
802
) -> ForwardLanguageItemMarker:
803
language_item_marker = ForwardLanguageItemMarker(
804
parent=None, reqs_objs=reqs, scope=marker_type.value
805
)806
language_item_marker.ng_source_line_begin = function.line_begin
807
language_item_marker.ng_range_line_begin = function.line_begin
808
language_item_marker.ng_range_line_end = function.line_end
809
language_item_marker.role = role
810
if description is not None:
811
language_item_marker.set_description(description)
812
elif marker_type == RangeMarkerType.FUNCTION:
813
language_item_marker.set_description(
814
f"function {function.display_name}()"
815
)816
elif marker_type == RangeMarkerType.CLASS:
817
language_item_marker.set_description(f"class {function.name}")
818
return language_item_marker
819
820
@staticmethod821
def forward_range_markers_from_range(
822
file_range: Tuple[int, int], requirement_uid_: str, role: Optional[str]
823
) -> Tuple[ForwardRangeMarker, ForwardRangeMarker]:
824
start_marker = ForwardRangeMarker(
825
start_or_end=True,
826
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
827
role=role,
828
)829
start_marker.ng_range_line_begin = file_range[0]
830
start_marker.ng_source_line_begin = file_range[0]
831
start_marker.ng_range_line_end = file_range[1]
832
833
end_marker = ForwardRangeMarker(
834
start_or_end=False,
835
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
836
role=role,
837
)838
end_marker.ng_source_line_begin = file_range[1]
839
end_marker.ng_range_line_begin = file_range[0]
840
end_marker.ng_range_line_end = file_range[1]
841
842
return start_marker, end_marker
843
844
@staticmethod845
def forward_file_marker_from_file_info(
846
file_info: SourceFileTraceabilityInfo,
847
requirement_uid_: str,
848
role: Optional[str],
849
) -> ForwardFileMarker:
850
marker = ForwardFileMarker(
851
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
852
role=role,
853
)854
marker.ng_range_line_begin = 1
855
marker.ng_source_line_begin = 1
856
marker.ng_range_line_end = file_info.file_stats.lines_total
857
return marker
858
859
def calculate_code_coverage_and_sort_nodes(
860
self, traceability_index: "TraceabilityIndex"
861
) -> None:
862
"""
863
Finalize code coverage and sort all nodes.864
865
For each trace info object:866
- Sort the markers according to their source location.867
- Calculate coverage information.868
"""869
870
for (
871
path,
872
traceability_info_,
873
) in self.map_paths_to_source_file_traceability_info.items():
874
875
def marker_comparator_start(
876
marker: RelationMarkerType,
877
) -> int:
878
assert marker.ng_range_line_begin is not None
879
return marker.ng_range_line_begin
880
881
sorted_markers = sorted(
882
traceability_info_.markers, key=marker_comparator_start
883
)884
885
traceability_info_.markers = sorted_markers
886
# Finding how many lines are covered by the requirements in the file.887
# Quick and dirty: https://stackoverflow.com/a/15273749/598057888
merged_ranges: List[List[int]] = []
889
for marker_ in traceability_info_.markers:
890
assert isinstance(
891
marker_,
892
(893
LanguageItemMarker,
894
ForwardRangeMarker,
895
RangeMarker,
896
LineMarker,
897
),898
), marker_
899
if marker_.ng_is_nodoc:
900
continue901
if not marker_.is_begin():
902
continue903
begin, end = (
904
assert_cast(marker_.ng_range_line_begin, int),
905
assert_cast(marker_.ng_range_line_end, int),
906
)907
if merged_ranges and merged_ranges[-1][1] >= (begin - 1):
908
merged_ranges[-1][1] = max(merged_ranges[-1][1], end)
909
else:
910
merged_ranges.append([begin, end])
911
coverage = 0
912
for merged_range in merged_ranges:
913
for line_ in range(merged_range[0], merged_range[1] + 1):
914
if traceability_info_.file_stats.lines_info[line_]:
915
coverage += 1
916
917
for function_ in traceability_info_.functions:
918
for merged_range in merged_ranges:
919
if (
920
function_.line_begin >= merged_range[0]
921
and function_.line_end <= merged_range[1]
922
):923
traceability_info_.covered_functions += 1
924
break925
926
traceability_info_.set_coverage_stats(merged_ranges, coverage)
927
928
for (
929
req_uid_,
930
markers_,
931
) in traceability_info_.ng_map_reqs_to_markers.items():
932
933
def marker_comparator_range(
934
marker: RelationMarkerType,
935
) -> Tuple[int, int]:
936
assert marker.ng_range_line_begin is not None
937
assert marker.ng_range_line_end is not None
938
return marker.ng_range_line_begin, marker.ng_range_line_end
939
940
markers_.sort(key=marker_comparator_range)
941
942
# Validate here, SDocNode.relations doesn't track marker roles.943
node = traceability_index.get_node_by_uid(req_uid_)
944
document = node.get_document()
945
assert document is not None
946
assert document.grammar is not None
947
grammar_element = document.grammar.elements_by_type[
948
node.node_type
949
]950
for marker in markers_:
951
# Backwards markers do not require referenced node grammar952
# to have the relation/role registered in the grammar.953
if isinstance(marker, (LanguageItemMarker, RangeMarker)):
954
continue955
956
if not grammar_element.has_relation_type_role(
957
relation_type="File",
958
relation_role=marker.role,
959
):960
raise StrictDocSemanticError.invalid_marker_role(
961
node=node,
962
marker=marker,
963
path_to_src_file=path,
964
)965
966
# Sort by paths alphabetically.967
for paths_with_role in self.map_reqs_uids_to_paths.values():
968
paths_with_role.sort()
969
970
# Sort by node UID alphabetically.971
for path_requirements_ in self.map_paths_to_reqs.values():
972
973
def compare_sdocnode_by_uid(node_: SDocNode) -> str:
974
return assert_cast(node_.reserved_uid, str)
975
976
path_requirements_.sort(key=compare_sdocnode_by_uid)
977
978
def connect_source_node_function(
979
self,
980
source_node: SourceNode,
981
source_sdoc_node_uid: str,
982
traceability_info: SourceFileTraceabilityInfo,
983
) -> None:
984
source_node_function = source_node.function
985
assert source_node_function is not None
986
987
language_item_marker = self.forward_marker_from_language_item(
988
function=source_node_function,
989
marker_type=RangeMarkerType.FUNCTION,
990
reqs=[Req(None, source_sdoc_node_uid)],
991
role=None,
992
description=f"function {source_node_function.display_name}()",
993
)994
995
traceability_info.ng_map_reqs_to_markers.setdefault(
996
source_sdoc_node_uid, []
997
).append(language_item_marker)
998
language_item_marker_copy = language_item_marker.create_end_marker()
999
traceability_info.markers.append(language_item_marker)
1000
traceability_info.markers.append(language_item_marker_copy)
1001
1002
@staticmethod1003
def create_sdoc_node_from_source_node(
1004
source_node: SourceNode,
1005
source_node_config_entry: SourceNodesEntry,
1006
sdoc_node_uid: str,
1007
parent_document: SDocDocumentIF,
1008
) -> SDocNode:
1009
sdoc_node = SDocNode(
1010
parent=parent_document,
1011
node_type=source_node_config_entry.node_type,
1012
fields=[],
1013
relations=[],
1014
# It is important that this autogenerated node is marked as such.1015
autogen=True,
1016
)1017
sdoc_node.ng_document_reference = DocumentReference()
1018
sdoc_node.ng_document_reference.set_document(parent_document)
1019
sdoc_node.ng_including_document_reference = DocumentReference()
1020
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1021
sdoc_node_fields["UID"] = sdoc_node_uid
1022
if (
1023
"TITLE" not in sdoc_node_fields
1024
and source_node.entity_name is not None
1025
):1026
sdoc_node_fields["TITLE"] = source_node.entity_name
1027
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1028
return sdoc_node
1029
1030
@staticmethod1031
def merge_sdoc_node_with_source_node(
1032
source_node_config_entry: SourceNodesEntry,
1033
source_node: SourceNode,
1034
sdoc_node: SDocNode,
1035
parent_document: SDocDocumentIF,
1036
) -> None:
1037
# First check if grammar element definitions are compatible.1038
source_node_type = source_node_config_entry.node_type
1039
source_node_grammar = assert_cast(
1040
parent_document.grammar, DocumentGrammar
1041
)1042
source_node_grammar_element = source_node_grammar.elements_by_type[
1043
source_node_type1044
]1045
sdoc_node_document = assert_cast(
1046
sdoc_node.get_document(), SDocDocumentIF
1047
)1048
sdoc_node_grammar = assert_cast(
1049
sdoc_node_document.grammar, DocumentGrammar
1050
)1051
sdoc_node_grammar_element = sdoc_node_grammar.elements_by_type[
1052
source_node_type1053
]1054
if source_node_grammar_element != sdoc_node_grammar_element:
1055
raise StrictDocException(
1056
f"Can't merge node {sdoc_node.reserved_uid} with source portion: "
1057
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
1058
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
1059
)1060
# Merge strategy: overwrite any field if there's a field with same name from custom tags.1061
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1062
1063
# Sanity check: Nor UID neither MID must conflict (early auto-MID is allowed to be overwritten)1064
if (
1065
"MID" in sdoc_node.ordered_fields_lookup
1066
and "MID" in sdoc_node_fields
1067
):1068
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1069
if sdoc_mid_field != sdoc_node_fields["MID"]:
1070
raise StrictDocException(
1071
f"Can't merge node by UID {sdoc_node.reserved_uid}: "
1072
f"Conflicting MID: {sdoc_mid_field} != {sdoc_node_fields['MID']}"
1073
)1074
if sdoc_node.reserved_uid is not None and "UID" in sdoc_node_fields:
1075
if sdoc_node.reserved_uid != sdoc_node_fields["UID"]:
1076
raise StrictDocException(
1077
f"Can't merge node by MID {sdoc_node.reserved_mid}: "
1078
f"Conflicting UID: {sdoc_node.reserved_uid} != {sdoc_node_fields['UID']}"
1079
)1080
1081
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1082
source_node.sdoc_node = sdoc_node
1083
1084
@staticmethod1085
def set_sdoc_node_fields(
1086
sdoc_node: SDocNode, sdoc_node_fields: dict[str, str]
1087
) -> None:
1088
for field_name, field_value in sdoc_node_fields.items():
1089
sdoc_node.set_field_value(
1090
field_name=field_name,
1091
form_field_index=0,
1092
value=field_value,
1093
)1094
1095
# As we overwrite the field's content from the source code,1096
# we mark the field as source_origin here.1097
new_field: SDocNodeField = sdoc_node.ordered_fields_lookup[
1098
field_name1099
][0]
1100
new_field.mark_as_source_origin()
1101
1102
@staticmethod1103
def create_source_node_section(
1104
document: SDocDocumentIF,
1105
path_to_source_file: str,
1106
section_cache: Dict[str, Union[SDocDocumentIF, SDocNode]],
1107
) -> Tuple[Union[SDocDocumentIF, SDocNode], List[SDocNode]]:
1108
"""
1109
Add a subsection for each path components in a given file path.1110
"""1111
current_top_node: Union[SDocDocumentIF, SDocNode] = document
1112
created_sections: List[SDocNode] = []
1113
path_components = path_to_source_file.split("/")
1114
for path_component_idx_, path_component_ in enumerate(path_components):
1115
if path_component_ not in section_cache:
1116
path_component_title = (
1117
path_component_ + "/"
1118
if path_component_idx_ < (len(path_components) - 1)
1119
else path_component_
1120
)1121
current_section = SDocNode(
1122
parent=current_top_node,
1123
node_type="SECTION",
1124
fields=[],
1125
relations=[],
1126
is_composite=True,
1127
node_type_close="SECTION",
1128
# It is important that this autogenerated node is marked as such.1129
autogen=True,
1130
)1131
current_section.ng_document_reference = DocumentReference()
1132
current_section.ng_document_reference.set_document(document)
1133
current_section.ng_including_document_reference = (
1134
DocumentReference()
1135
)1136
current_section.set_field_value(
1137
field_name="TITLE",
1138
form_field_index=0,
1139
value=path_component_title,
1140
)1141
1142
current_top_node.section_contents.append(current_section)
1143
section_cache[path_component_] = current_section
1144
created_sections.append(current_section)
1145
current_top_node = section_cache[path_component_]
1146
return current_top_node, created_sections
1147
1148
def connect_sdoc_node_with_file_path(
1149
self, sdoc_node: SDocNode, path_to_source_file_: str
1150
) -> None:
1151
uid = sdoc_node.reserved_uid
1152
assert uid is not None
1153
self.map_reqs_uids_to_paths.setdefault(uid, OrderedSet()).add(
1154
path_to_source_file_1155
)1156
self.map_paths_to_reqs.setdefault(
1157
path_to_source_file_, OrderedSet()
1158
).add(sdoc_node)
1159
1160
@staticmethod1161
def connect_source_node_requirements(
1162
source_node: SourceNode,
1163
sdoc_node: SDocNode,
1164
traceability_index: "TraceabilityIndex",
1165
) -> None:
1166
"""
1167
Connect auto-generated requirement with function marker and with marker target requirement.1168
1169
If function comment has @relation(REQ, scope=function), connections shall become1170
[REQ] <-parent- [auto-generated/merged sdoc_node] -file-> [function marker]1171
1172
Here we link REQ and sdoc_node bidirectional.1173
"""1174
if (
1175
sdoc_node.reserved_uid is not None
1176
and not traceability_index.graph_database.has_link(
1177
link_type=GraphLinkType.UID_TO_NODE,
1178
lhs_node=sdoc_node.reserved_uid,
1179
rhs_node=sdoc_node,
1180
)1181
):1182
traceability_index.graph_database.create_link(
1183
link_type=GraphLinkType.UID_TO_NODE,
1184
lhs_node=sdoc_node.reserved_uid,
1185
rhs_node=sdoc_node,
1186
)1187
1188
# A merge procedure may have overwritten the MID,1189
# in which case the graph database and search index needs an update.1190
if "MID" in sdoc_node.ordered_fields_lookup != sdoc_node.reserved_mid:
1191
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1192
if sdoc_mid_field != sdoc_node.reserved_mid:
1193
# TODO:1194
# If we really want to support changing the auto-assigned MID,1195
# at least the graph database and the document search index need an update (remove old MID, add new MID).1196
# I currently struggle to update the search index.1197
parent_document = sdoc_node.get_parent_or_including_document()
1198
sdoc_node.reserved_mid = MID(sdoc_mid_field)
1199
if parent_document.config.enable_mid:
1200
sdoc_node.mid_permanent = True
1201
1202
if not traceability_index.graph_database.has_link(
1203
link_type=GraphLinkType.MID_TO_NODE,
1204
lhs_node=sdoc_node.reserved_mid,
1205
rhs_node=sdoc_node,
1206
):1207
traceability_index.graph_database.create_link(
1208
link_type=GraphLinkType.MID_TO_NODE,
1209
lhs_node=sdoc_node.reserved_mid,
1210
rhs_node=sdoc_node,
1211
)1212
1213
for marker_ in source_node.markers:
1214
if not isinstance(marker_, LanguageItemMarker):
1215
continue1216
for req_ in marker_.reqs:
1217
node = traceability_index.get_node_by_uid_weak2(req_)
1218
if not traceability_index.graph_database.has_link(
1219
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1220
lhs_node=sdoc_node,
1221
rhs_node=node,
1222
):1223
traceability_index.graph_database.create_link(
1224
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1225
lhs_node=sdoc_node,
1226
rhs_node=node,
1227
)1228
if not traceability_index.graph_database.has_link(
1229
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1230
lhs_node=node,
1231
rhs_node=sdoc_node,
1232
):1233
traceability_index.graph_database.create_link(
1234
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1235
lhs_node=node,
1236
rhs_node=sdoc_node,
1237
)