Path:
strictdoc/core/file_traceability_index.py
Lines:
1434
Non-empty lines:
1297
Non-empty lines covered with requirements:
1297 / 1297 (100.0%)
Functions:
31
Functions covered by requirements:
31 / 31 (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.gcov.helpers import convert_function_name_to_gcovr_style
19
from strictdoc.backend.sdoc.document_reference import DocumentReference
20
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
21
from strictdoc.backend.sdoc.free_text_reader import SDFreeTextReader
22
from strictdoc.backend.sdoc.models.anchor import Anchor
23
from strictdoc.backend.sdoc.models.document_grammar import (
24
DocumentGrammar,
25
)26
from strictdoc.backend.sdoc.models.model import SDocDocumentIF
27
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
28
from strictdoc.backend.sdoc.models.reference import FileEntry, FileReference
29
from strictdoc.backend.sdoc_source_code.models.language import LanguageItem
30
from strictdoc.backend.sdoc_source_code.models.language_item_marker import (
31
ForwardLanguageItemMarker,
32
LanguageItemMarker,
33
RangeMarkerType,
34
)35
from strictdoc.backend.sdoc_source_code.models.line_marker import LineMarker
36
from strictdoc.backend.sdoc_source_code.models.range_marker import (
37
ForwardFileMarker,
38
ForwardRangeMarker,
39
RangeMarker,
40
)41
from strictdoc.backend.sdoc_source_code.models.requirement_marker import Req
42
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
43
RelationMarkerType,
44
SourceFileTraceabilityInfo,
45
)46
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
47
from strictdoc.core.constants import GraphEdgeLabel, GraphLinkType
48
from strictdoc.core.document_iterator import SDocDocumentIterator
49
from strictdoc.core.file_system.source_tree import SourceFile
50
from strictdoc.core.project_config import ProjectConfig, SourceNodesEntry
51
from strictdoc.helpers.cargo_nextest import (
52
convert_nextest_test_to_rust_canonical_paths,
53
)54
from strictdoc.helpers.cast import assert_cast
55
from strictdoc.helpers.exception import StrictDocException
56
from strictdoc.helpers.google_test import convert_function_name_to_gtest_macro
57
from strictdoc.helpers.mid import MID
58
from strictdoc.helpers.ordered_set import OrderedSet
59
60
if TYPE_CHECKING:
61
from strictdoc.core.traceability_index import (
62
TraceabilityIndex,
63
)64
65
66
class FileTraceabilityIndex:
67
def __init__(self) -> None:
68
# "file.py" -> List[SDocNode]69
self.map_paths_to_reqs: Dict[str, OrderedSet[SDocNode]] = {}
70
71
# "REQ-001" -> {"file.py", ...}72
self.map_reqs_uids_to_paths: Dict[str, OrderedSet[str]] = {}
73
74
# "file.py" -> SourceFileTraceabilityInfo.75
self.map_paths_to_source_file_traceability_info: Dict[
76
str, SourceFileTraceabilityInfo
77
] = {}
78
79
# "file.py" -> { { "foo" -> [("REQ-1", "Impl"), ("REQ-2", "Test")] }, ... }80
self.map_file_function_names_to_reqs_uids: Dict[
81
str, Dict[str, List[Tuple[str, Optional[str]]]]
82
] = {}
83
self.map_file_class_names_to_reqs_uids: Dict[
84
str, Dict[str, List[Tuple[str, Optional[str]]]]
85
] = {}
86
87
# This is only public non-static functions from languages like C.88
self.map_all_function_names_to_definition_functions: Dict[
89
str, List[LanguageItem]
90
] = {}
91
92
# "file.py" -> [SDocNode] # noqa: ERA00193
self.source_file_reqs_cache: Dict[str, Optional[List[SDocNode]]] = {}
94
95
self.requirements_with_forward_links: OrderedSet[SDocNode] = (
96
OrderedSet()
97
)98
self.trace_infos: List[SourceFileTraceabilityInfo] = []
99
100
def has_source_file_reqs(self, source_file_rel_path: str) -> bool:
101
path_reqs = self.map_paths_to_reqs.get(source_file_rel_path)
102
if path_reqs is not None and len(path_reqs) > 0:
103
return True
104
file_trace_info = self.map_paths_to_source_file_traceability_info[
105
source_file_rel_path106
]107
return len(file_trace_info.markers) > 0
108
109
def get_requirement_file_links(
110
self, requirement: SDocNode
111
) -> List[Tuple[str, List[RelationMarkerType]]]:
112
if requirement.reserved_uid not in self.map_reqs_uids_to_paths:
113
return []
114
115
matching_links_with_markers: List[
116
Tuple[str, List[RelationMarkerType]]
117
] = []
118
requirement_source_paths: OrderedSet[str] = self.map_reqs_uids_to_paths[
119
requirement.reserved_uid
120
]121
122
# Now that one requirement can have multiple File-relations to the same file.123
# This can be multiple FUNCTION: or RANGE: forward-relations.124
# To avoid duplication of results, visit each unique file link path only once.125
visited_file_links: Set[str] = set()
126
for requirement_source_path_ in requirement_source_paths:
127
if requirement_source_path_ in visited_file_links:
128
continue129
visited_file_links.add(requirement_source_path_)
130
131
source_file_traceability_info: Optional[
132
SourceFileTraceabilityInfo133
] = self.map_paths_to_source_file_traceability_info.get(
134
requirement_source_path_135
)136
assert source_file_traceability_info is not None, (
137
f"Requirement {requirement.reserved_uid} references a file"
138
f" that does not exist: {requirement_source_path_}."
139
)140
markers = source_file_traceability_info.ng_map_reqs_to_markers.get(
141
requirement.reserved_uid
142
)143
if markers is None or len(markers) == 0:
144
matching_links_with_markers.append(
145
(requirement_source_path_, [])
146
)147
continue148
matching_links_with_markers.append(
149
(requirement_source_path_, markers)
150
)151
152
return matching_links_with_markers
153
154
def indexed_source_files(self) -> Iterator[SourceFile]:
155
for _, sfti in self.map_paths_to_source_file_traceability_info.items():
156
if sfti.source_file is not None:
157
yield sfti.source_file
158
159
def get_source_file_reqs(
160
self, source_file_rel_path: str
161
) -> Optional[List[SDocNode]]:
162
assert (
163
source_file_rel_path164
in self.map_paths_to_source_file_traceability_info
165
)166
if source_file_rel_path in self.source_file_reqs_cache:
167
return self.source_file_reqs_cache[source_file_rel_path]
168
169
source_file_traceability_info: SourceFileTraceabilityInfo = (
170
self.map_paths_to_source_file_traceability_info[
171
source_file_rel_path172
]173
)174
175
if source_file_rel_path not in self.map_paths_to_reqs:
176
self.source_file_reqs_cache[source_file_rel_path] = None
177
return None
178
179
requirements = self.map_paths_to_reqs[source_file_rel_path]
180
assert len(requirements) > 0
181
range_requirements = []
182
183
for requirement in requirements:
184
if (
185
requirement.reserved_uid
186
in source_file_traceability_info.ng_map_reqs_to_markers
187
):188
range_requirements.append(requirement)
189
190
self.source_file_reqs_cache[source_file_rel_path] = range_requirements
191
return range_requirements
192
193
def get_coverage_info(
194
self, source_file_rel_path: str
195
) -> SourceFileTraceabilityInfo:
196
assert (
197
source_file_rel_path198
in self.map_paths_to_source_file_traceability_info
199
), source_file_rel_path
200
source_file_tr_info: SourceFileTraceabilityInfo = (
201
self.map_paths_to_source_file_traceability_info[
202
source_file_rel_path203
]204
)205
return source_file_tr_info
206
207
def get_coverage_info_weak(
208
self, source_file_rel_path: str
209
) -> Optional[SourceFileTraceabilityInfo]:
210
source_file_tr_info: Optional[SourceFileTraceabilityInfo] = (
211
self.map_paths_to_source_file_traceability_info.get(
212
source_file_rel_path213
)214
)215
return source_file_tr_info
216
217
def validate_and_resolve(
218
self,
219
traceability_index: "TraceabilityIndex",
220
project_config: ProjectConfig,
221
) -> None:
222
"""
223
Resolve all source code traceability after the index is fully built.224
"""225
226
#227
# STEP: Collect minimal information that will help to resolve the228
# forward-declared paths/function names at the step 2.229
#230
for trace_info_ in self.trace_infos:
231
source_file: SourceFile = assert_cast(
232
trace_info_.source_file, SourceFile
233
)234
235
self.map_paths_to_source_file_traceability_info[
236
source_file.in_doctree_source_file_rel_path_posix
237
] = trace_info_
238
239
for function_ in trace_info_.functions:
240
if function_.is_definition() and function_.is_public():
241
self.map_all_function_names_to_definition_functions.setdefault(
242
function_.name, []
243
).append(function_)
244
245
#246
# STEP: Auto-generated SDocNodes from source file comments and register247
# their UIDs. This must happen before marker validation so that248
# source files can reference these UIDs via @relation.249
#250
documents_with_generated_content = set()
251
252
section_cache: Dict[str, Union[SDocDocumentIF, SDocNode]] = {}
253
source_nodes_config: List[SourceNodesEntry] = (
254
project_config.source_nodes
255
)256
unused_source_node_paths = {
257
config_entry_.path for config_entry_ in source_nodes_config
258
}259
for (
260
path_to_source_file_,
261
traceability_info_,
262
) in self.map_paths_to_source_file_traceability_info.items():
263
if len(traceability_info_.source_nodes) == 0:
264
continue265
266
if len(source_nodes_config) == 0:
267
continue268
269
relevant_source_node_entry = (
270
project_config.get_relevant_source_nodes_entry(
271
path_to_source_file_272
)273
)274
if relevant_source_node_entry is not None:
275
unused_source_node_paths.discard(
276
relevant_source_node_entry.path
277
)278
else:
279
continue280
281
document_uid = relevant_source_node_entry.uid
282
document = traceability_index.get_node_by_uid(document_uid)
283
documents_with_generated_content.add(document)
284
current_top_node = None
285
286
for source_node_ in traceability_info_.source_nodes:
287
if len(source_node_.fields) == 0:
288
continue289
290
assert source_node_.entity_name is not None
291
sdoc_node = None
292
sdoc_node_uid = source_node_.get_sdoc_field(
293
"UID", relevant_source_node_entry
294
)295
mid = source_node_.get_sdoc_field(
296
"MID", relevant_source_node_entry
297
)298
299
# First merge criterion: Merge if SDoc node with same MID exists.300
if mid is not None:
301
sdoc_node_mid = MID(mid)
302
merge_candidate_sdoc_node = (
303
traceability_index.get_node_by_mid_weak(sdoc_node_mid)
304
)305
if isinstance(merge_candidate_sdoc_node, SDocNode):
306
sdoc_node = merge_candidate_sdoc_node
307
sdoc_node_uid = sdoc_node.reserved_uid
308
309
if sdoc_node is None:
310
# If no UID from source code field or merge-by-MID, create UID by conventional scheme.311
if sdoc_node_uid is None:
312
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
313
# Second merge criterion: Merge if SDoc node with same UID exists.314
tmp_sdoc_node = traceability_index.get_node_by_uid_weak(
315
sdoc_node_uid316
)317
if isinstance(tmp_sdoc_node, SDocNode):
318
sdoc_node = tmp_sdoc_node
319
320
assert sdoc_node_uid is not None
321
if sdoc_node is not None:
322
sdoc_node = assert_cast(sdoc_node, SDocNode)
323
self.merge_sdoc_node_with_source_node(
324
relevant_source_node_entry,
325
source_node_,
326
sdoc_node,
327
document,
328
)329
else:
330
sdoc_node = self.create_sdoc_node_from_source_node(
331
source_node_,
332
relevant_source_node_entry,
333
sdoc_node_uid,
334
document,
335
)336
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
337
if current_top_node is None:
338
current_top_node, created_sections = (
339
FileTraceabilityIndex.create_source_node_section(
340
document,
341
path_to_source_file_,
342
section_cache,
343
)344
)345
for created_section in created_sections:
346
traceability_index.graph_database.create_link(
347
link_type=GraphLinkType.MID_TO_NODE,
348
lhs_node=created_section.reserved_mid,
349
rhs_node=created_section,
350
)351
current_top_node.section_contents.append(sdoc_node)
352
353
# Register [ANCHOR]s from source node fields as linkable targets.354
for node_field_ in sdoc_node.enumerate_fields():
355
for part_ in node_field_.parts:
356
if isinstance(part_, Anchor):
357
traceability_index.graph_database.create_link(
358
link_type=GraphLinkType.MID_TO_NODE,
359
lhs_node=part_.mid,
360
rhs_node=part_,
361
)362
traceability_index.graph_database.create_link(
363
link_type=GraphLinkType.UID_TO_NODE,
364
lhs_node=part_.value,
365
rhs_node=part_,
366
)367
368
self.connect_source_node_function(
369
source_node_, sdoc_node_uid, traceability_info_
370
)371
self.connect_sdoc_node_with_file_path(
372
sdoc_node, path_to_source_file_
373
)374
self.connect_source_node_requirements(
375
source_node_, sdoc_node, traceability_index
376
)377
378
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration379
for unused_source_node_path in unused_source_node_paths:
380
print( # noqa: T201
381
f"warning: source_node path {unused_source_node_path} doesn't match any source file. "
382
"Hint: Check include_source_paths."383
)384
385
# Iterate over all generated documents to calculate all node levels.386
for document_ in documents_with_generated_content:
387
document_iterator = SDocDocumentIterator(document_)
388
for _, _ in document_iterator.all_content(
389
print_fragments=False,
390
):391
pass392
393
#394
# STEP: Resolve requirements that have forward links.395
# Some requirements can come from the SDoc documents generated396
# on the fly from JUnit XML documents.397
#398
for forward_requirement_ in self.requirements_with_forward_links:
399
assert forward_requirement_.reserved_uid is not None
400
401
for relation_ in forward_requirement_.relations:
402
if not isinstance(relation_, FileReference):
403
continue404
405
file_reference: FileReference = assert_cast(
406
relation_, FileReference
407
)408
file_posix_path = file_reference.get_posix_path()
409
410
if file_posix_path == "#FORWARD#":
411
test_function = (
412
forward_requirement_.get_meta_field_value_by_title(
413
"TEST_FUNCTION"414
)415
)416
assert test_function is not None
417
418
functions: List[LanguageItem]
419
if test_function.startswith("#GTEST#"):
420
test_function = test_function.removeprefix("#GTEST#")
421
possible_gtest_functions = (
422
convert_function_name_to_gtest_macro(test_function)
423
)424
for (
425
possible_gtest_function_426
) in possible_gtest_functions:
427
if (
428
possible_gtest_function_429
in self.map_all_function_names_to_definition_functions
430
):431
test_function = possible_gtest_function_
432
break433
else:
434
raise RuntimeError(
435
"Could not find a matching Google Test function: "436
f"{possible_gtest_functions}"
437
) # pragma: no cover
438
forward_requirement_.set_field_value(
439
field_name="TEST_FUNCTION",
440
form_field_index=0,
441
value=test_function,
442
)443
elif test_function.startswith("#NEXTEST#"):
444
# The payload after #NEXTEST# is "<classname>|<name>"445
# as taken straight from the cargo-nextest JUnit XML.446
nextest_payload = test_function.removeprefix(
447
"#NEXTEST#"448
)449
nextest_classname, _, nextest_name = (
450
nextest_payload.partition("|")
451
)452
possible_nextest_functions = (
453
convert_nextest_test_to_rust_canonical_paths(
454
nextest_classname, nextest_name
455
)456
)457
for (
458
possible_nextest_function_459
) in possible_nextest_functions:
460
if (
461
possible_nextest_function_462
in self.map_all_function_names_to_definition_functions
463
):464
test_function = possible_nextest_function_
465
break466
else:
467
raise RuntimeError(
468
"Could not find a matching Rust function for "469
"cargo-nextest test: "470
f"{possible_nextest_functions}"
471
) # pragma: no cover
472
forward_requirement_.set_field_value(
473
field_name="TEST_FUNCTION",
474
form_field_index=0,
475
value=test_function,
476
)477
functions = (
478
self.map_all_function_names_to_definition_functions[
479
test_function480
]481
)482
assert len(functions) == 1
483
484
function: LanguageItem = functions[0]
485
resolved_path_to_function_file = function.parent.source_file.in_doctree_source_file_rel_path_posix
486
file_posix_path = resolved_path_to_function_file
487
488
file_reference.g_file_entry = FileEntry(
489
relation_,
490
g_file_format=relation_.g_file_entry.g_file_format,
491
g_file_path=resolved_path_to_function_file,
492
g_line_range=None,
493
element="function",
494
id=test_function,
495
)496
497
forward_requirement_.set_field_value(
498
field_name="TEST_PATH",
499
form_field_index=0,
500
value=resolved_path_to_function_file,
501
)502
503
# ==========================================================504
# Transitively connect requirements and test results505
# through the test source files:506
# [REQUIREMENT] <-> [TEST_CASE] <-> [TEST_RESULT].507
# ==========================================================508
509
#510
# A TEST_RESULT is linked to a TEST_SPEC/TEST_CASE node511
# whenever that node was itself auto-generated (or512
# merged) from a source_node parsed off the same test513
# function.514
#515
testcase_nodes: List[SDocNode] = []
516
for source_node_ in function.parent.source_nodes:
517
if (
518
source_node_.function is function
519
and source_node_.sdoc_node is not None
520
):521
testcase_nodes.append(source_node_.sdoc_node)
522
523
for testcase_node_ in testcase_nodes:
524
traceability_index.graph_database.create_link(
525
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
526
lhs_node=forward_requirement_,
527
rhs_node=testcase_node_,
528
edge=GraphEdgeLabel.IS_RESULT_OF,
529
)530
traceability_index.graph_database.create_link(
531
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
532
lhs_node=testcase_node_,
533
rhs_node=forward_requirement_,
534
edge=GraphEdgeLabel.HAS_RESULT,
535
)536
537
#538
# Two different user project setups:539
#540
# 1) A user does use the autogenerated test case nodes.541
# Requirements marked with @relation(REQ) on the test542
# function are linked to the TEST_CASE node when one543
# exists: [REQUIREMENT] <-> [TEST_CASE].544
#545
# 2) A user DOES NOT use the autogenerated test case nodes.546
# When no TEST_CASE node exists (e.g. no source_nodes config547
# for this file), fall back to linking the requirement548
# directly to the TEST_RESULT, as before:549
# [REQUIREMENT] <-> [TEST_RESULT].550
#551
for language_item_marker_ in function.markers:
552
for req_ in language_item_marker_.reqs:
553
node = traceability_index.get_node_by_uid_weak2(
554
req_555
)556
if len(testcase_nodes) > 0:
557
for testcase_node_ in testcase_nodes:
558
#559
# When the TEST_CASE node was merged560
# from this same function, the generic561
# requirement-to-source-traceability562
# mechanism has already linked it to563
# this requirement via an unlabeled564
# Parent/Child edge (see the565
# source_node.markers loop above in566
# this file). Replace that unlabeled567
# edge with the labeled verifies/568
# is verified by one so the relation is569
# not shown twice.570
#571
traceability_index.graph_database.delete_link_weak(
572
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
573
lhs_node=testcase_node_,
574
rhs_node=node,
575
)576
traceability_index.graph_database.delete_link_weak(
577
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
578
lhs_node=node,
579
rhs_node=testcase_node_,
580
)581
traceability_index.graph_database.create_link(
582
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
583
lhs_node=testcase_node_,
584
rhs_node=node,
585
edge=GraphEdgeLabel.VERIFIES,
586
)587
traceability_index.graph_database.create_link(
588
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
589
lhs_node=node,
590
rhs_node=testcase_node_,
591
edge=GraphEdgeLabel.IS_VERIFIED_BY,
592
)593
else:
594
traceability_index.graph_database.create_link(
595
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
596
lhs_node=forward_requirement_,
597
rhs_node=node,
598
edge=GraphEdgeLabel.SATISFIES,
599
)600
traceability_index.graph_database.create_link(
601
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
602
lhs_node=node,
603
rhs_node=forward_requirement_,
604
edge=GraphEdgeLabel.IS_SATISFIED_BY,
605
)606
#607
# Validate that all requirements reference existing files.608
#609
source_file_traceability_info: Optional[
610
SourceFileTraceabilityInfo611
] = self.map_paths_to_source_file_traceability_info.get(
612
file_posix_path613
)614
if source_file_traceability_info is None:
615
raise StrictDocException(
616
f"Requirement {forward_requirement_.reserved_uid} "
617
"references a file that does not exist: "618
f"{file_posix_path}."
619
)620
621
#622
# Now that the test reports related fixups are done, the623
# following code registers the requirements with forward links.624
#625
self.map_paths_to_reqs.setdefault(
626
file_posix_path, OrderedSet()
627
).add(forward_requirement_)
628
629
assert forward_requirement_.reserved_uid is not None
630
self.map_reqs_uids_to_paths.setdefault(
631
forward_requirement_.reserved_uid, OrderedSet()
632
).add(file_posix_path)
633
634
if (
635
file_reference.g_file_entry.element == "function"
636
and file_reference.g_file_entry.id is not None
637
):638
one_file_function_name_to_reqs_uids = (
639
self.map_file_function_names_to_reqs_uids.setdefault(
640
file_posix_path, {}
641
)642
)643
one_file_function_name_to_reqs_uids.setdefault(
644
file_reference.g_file_entry.id, []
645
).append(
646
(forward_requirement_.reserved_uid, relation_.role)
647
)648
elif (
649
file_reference.g_file_entry.element == "class"
650
and file_reference.g_file_entry.id is not None
651
):652
one_file_class_name_to_reqs_uids = (
653
self.map_file_class_names_to_reqs_uids.setdefault(
654
file_posix_path, {}
655
)656
)657
one_file_class_name_to_reqs_uids.setdefault(
658
file_reference.g_file_entry.id, []
659
).append(
660
(forward_requirement_.reserved_uid, relation_.role)
661
)662
elif file_reference.g_file_entry.line_range is not None:
663
line_range = file_reference.g_file_entry.line_range
664
uid = forward_requirement_.reserved_uid
665
source_file_info = (
666
self.map_paths_to_source_file_traceability_info[
667
file_posix_path668
]669
)670
start_marker, end_marker = (
671
self.forward_range_markers_from_range(
672
line_range, uid, relation_.role
673
)674
)675
source_file_info.ng_map_reqs_to_markers.setdefault(
676
uid, []
677
).append(start_marker)
678
source_file_info.markers.append(start_marker)
679
source_file_info.markers.append(end_marker)
680
else:
681
uid = forward_requirement_.reserved_uid
682
source_file_info = (
683
self.map_paths_to_source_file_traceability_info[
684
file_posix_path685
]686
)687
forward_file_marker = (
688
self.forward_file_marker_from_file_info(
689
source_file_info,
690
uid,
691
relation_.role,
692
)693
)694
source_file_info.ng_map_reqs_to_markers.setdefault(
695
forward_requirement_.reserved_uid, []
696
).append(forward_file_marker)
697
source_file_info.markers.append(forward_file_marker)
698
699
#700
# STEP: Add markers for forward relations to functions and classes701
#702
for trace_info_ in self.trace_infos:
703
source_file = assert_cast(trace_info_.source_file, SourceFile)
704
705
self.map_paths_to_source_file_traceability_info[
706
source_file.in_doctree_source_file_rel_path_posix
707
] = trace_info_
708
709
for function_ in trace_info_.functions:
710
if (
711
reqs_uids := self.get_req_uids_by_function_names(
712
source_file.in_doctree_source_file_rel_path_posix,
713
self.get_function_matching_names(function_),
714
)715
) is not None:
716
self.create_traceability_info_shared_markers_for_function(
717
trace_info_,
718
function_,
719
RangeMarkerType.FUNCTION,
720
reqs_uids,
721
)722
if (
723
reqs_uids := self.get_req_uids_by_class_name(
724
source_file.in_doctree_source_file_rel_path_posix,
725
function_.display_name,
726
)727
) is not None:
728
self.create_traceability_info_shared_markers_for_function(
729
trace_info_,
730
function_,
731
RangeMarkerType.CLASS,
732
reqs_uids,
733
)734
735
marker_: Union[
736
LanguageItemMarker, LineMarker, RangeMarker, ForwardRangeMarker
737
]738
for marker_ in copy(trace_info_.markers):
739
# FIXME: Is this 'continue' needed here?740
if isinstance(marker_, ForwardRangeMarker):
741
continue742
for requirement_uid_ in marker_.reqs:
743
node = traceability_index.get_node_by_uid_weak2(
744
requirement_uid_745
)746
if node is None:
747
raise StrictDocException(
748
f"Source file {source_file.in_doctree_source_file_rel_path_posix} references "
749
f"a requirement that does not exist: {requirement_uid_}."
750
)751
752
self.map_reqs_uids_to_paths.setdefault(
753
requirement_uid_, OrderedSet()
754
).add(source_file.in_doctree_source_file_rel_path_posix)
755
756
self.map_paths_to_reqs.setdefault(
757
source_file.in_doctree_source_file_rel_path_posix,
758
OrderedSet(),
759
).add(node)
760
761
if isinstance(marker_, LanguageItemMarker):
762
marker_copy = marker_.create_end_marker()
763
trace_info_.markers.append(marker_copy)
764
765
#766
# Resolve definitions to declarations (only applicable for C and C++).767
#768
769
reversed_trace_info = {
770
value: key
771
for key, value in self.map_paths_to_source_file_traceability_info.items()
772
}773
774
for (
775
traceability_info_776
) in self.map_paths_to_source_file_traceability_info.values():
777
for function_ in traceability_info_.functions:
778
if (
779
function_.is_declaration()
780
and function_.name
781
in self.map_all_function_names_to_definition_functions
782
):783
definition_functions: List[LanguageItem] = []
784
if not function_.is_public():
785
definition_function = traceability_info_.ng_map_names_to_definition_functions.get(
786
function_.name, None
787
)788
if definition_function is not None:
789
definition_functions.append(definition_function)
790
else:
791
mapped_definition_functions = (
792
self.map_all_function_names_to_definition_functions[
793
function_.name
794
]795
)796
definition_functions.extend(mapped_definition_functions)
797
if len(definition_functions) == 0:
798
continue799
800
for definition_function_ in definition_functions:
801
definition_function_trace_info: SourceFileTraceabilityInfo = definition_function_.parent
802
803
for marker_ in function_.markers:
804
language_item_marker = self.forward_marker_from_language_item(
805
function=definition_function_,
806
marker_type=RangeMarkerType.FUNCTION,
807
reqs=marker_.reqs_objs,
808
role=marker_.role,
809
description=f"function {function_.display_name}()",
810
)811
812
for req_uid_ in marker_.reqs:
813
definition_function_trace_info.ng_map_reqs_to_markers.setdefault(
814
req_uid_, []
815
).append(language_item_marker)
816
817
path_to_info = reversed_trace_info[
818
definition_function_trace_info819
]820
self.map_reqs_uids_to_paths.setdefault(
821
req_uid_, OrderedSet()
822
).add(path_to_info)
823
824
node = traceability_index.get_node_by_uid(
825
req_uid_826
)827
self.map_paths_to_reqs.setdefault(
828
path_to_info, OrderedSet()
829
).add(node)
830
831
definition_function_trace_info.markers.append(
832
language_item_marker833
)834
835
#836
# STEP: Calculate requirements coverage by code. Sort nodes.837
#838
self.calculate_code_coverage_and_sort_nodes(traceability_index)
839
840
def create_requirement_with_forward_source_links(
841
self, requirement: SDocNode
842
) -> None:
843
self.requirements_with_forward_links.add(requirement)
844
845
def create_traceability_info(
846
self,
847
source_file: SourceFile,
848
traceability_info: SourceFileTraceabilityInfo,
849
) -> None:
850
assert isinstance(traceability_info, SourceFileTraceabilityInfo)
851
traceability_info.source_file = source_file
852
853
self.trace_infos.append(traceability_info)
854
- "3.2.2. Forward links to macro definitions using regexes" (REQUIREMENT)
855
def get_req_uids_by_function_name(
856
self, rel_path_posix: str, name: str
857
) -> Optional[List[Tuple[str, Optional[str]]]]:
858
"""
859
@relation(SDOC-LLR-207, scope=function)860
"""861
862
if rel_path_posix not in self.map_file_function_names_to_reqs_uids:
863
return None
864
865
function_names_to_reqs_uids = self.map_file_function_names_to_reqs_uids[
866
rel_path_posix867
]868
869
matching_req_uids: List[Tuple[str, Optional[str]]] = []
870
exact_matching_req_uids = function_names_to_reqs_uids.get(name, None)
871
if exact_matching_req_uids is not None:
872
matching_req_uids.extend(exact_matching_req_uids)
873
874
for function_name_, req_uids_ in function_names_to_reqs_uids.items():
875
if not FileTraceabilityIndex.is_regex_function_name(function_name_):
876
continue877
878
regex_pattern = function_name_[1:-1]
879
try:
880
if re.search(regex_pattern, name) is not None:
881
matching_req_uids.extend(req_uids_)
882
except re.error as exception:
883
raise StrictDocException(
884
"Invalid regular expression in FUNCTION relation "885
f"{function_name_}: {exception}."
886
) from exception
887
888
return matching_req_uids if len(matching_req_uids) > 0 else None
889
890
def get_req_uids_by_function_names(
891
self, rel_path_posix: str, names: List[str]
892
) -> Optional[List[Tuple[str, Optional[str]]]]:
893
matching_req_uids: List[Tuple[str, Optional[str]]] = []
894
seen_req_uids: Set[Tuple[str, Optional[str]]] = set()
895
896
for name_ in names:
897
reqs_uids = self.get_req_uids_by_function_name(
898
rel_path_posix, name_
899
)900
if reqs_uids is None:
901
continue902
for req_uid_ in reqs_uids:
903
if req_uid_ in seen_req_uids:
904
continue905
seen_req_uids.add(req_uid_)
906
matching_req_uids.append(req_uid_)
907
908
return matching_req_uids if len(matching_req_uids) > 0 else None
909
910
@staticmethod911
def get_function_matching_names(function: LanguageItem) -> List[str]:
912
names = [function.display_name, function.name]
913
gcovr_name = convert_function_name_to_gcovr_style(function.name)
914
if gcovr_name not in names:
915
names.append(gcovr_name)
916
return names
917
918
@staticmethod919
def is_regex_function_name(name: str) -> bool:
920
return len(name) >= 2 and name[0] == "/" and name[-1] == "/"
921
922
def get_req_uids_by_class_name(
923
self, rel_path_posix: str, name: str
924
) -> Optional[List[Tuple[str, Optional[str]]]]:
925
if rel_path_posix in self.map_file_class_names_to_reqs_uids:
926
return self.map_file_class_names_to_reqs_uids[rel_path_posix].get(
927
name, None
928
)929
return None
930
931
@staticmethod932
def create_traceability_info_shared_markers_for_function(
933
traceability_info: SourceFileTraceabilityInfo,
934
function: LanguageItem,
935
marker_type: RangeMarkerType,
936
reqs_uids: List[Tuple[str, Optional[str]]],
937
) -> None:
938
markers_by_role = {}
939
for req_uid_, role in reqs_uids:
940
req = Req(None, req_uid_)
941
if role not in markers_by_role:
942
markers_by_role[role] = (
943
FileTraceabilityIndex.forward_marker_from_language_item(
944
function, marker_type, [req], role
945
)946
)947
else:
948
markers_by_role[role].reqs_objs.append(req)
949
950
for req_uid_, role in reqs_uids:
951
markers = traceability_info.ng_map_reqs_to_markers.setdefault(
952
req_uid_, []
953
)954
markers.append(markers_by_role[role])
955
956
traceability_info.markers.extend(markers_by_role.values())
957
958
@staticmethod959
def forward_marker_from_language_item(
960
function: LanguageItem,
961
marker_type: RangeMarkerType,
962
reqs: List[Req],
963
role: Optional[str],
964
description: Optional[str] = None,
965
) -> ForwardLanguageItemMarker:
966
language_item_marker = ForwardLanguageItemMarker(
967
parent=None, reqs_objs=reqs, scope=marker_type.value
968
)969
language_item_marker.ng_source_line_begin = function.line_begin
970
language_item_marker.ng_range_line_begin = function.line_begin
971
language_item_marker.ng_range_line_end = function.line_end
972
language_item_marker.role = role
973
if description is not None:
974
language_item_marker.set_description(description)
975
elif marker_type == RangeMarkerType.FUNCTION:
976
language_item_marker.set_description(
977
f"function {function.display_name}()"
978
)979
elif marker_type == RangeMarkerType.CLASS:
980
language_item_marker.set_description(f"class {function.name}")
981
return language_item_marker
982
983
@staticmethod984
def forward_range_markers_from_range(
985
file_range: Tuple[int, int], requirement_uid_: str, role: Optional[str]
986
) -> Tuple[ForwardRangeMarker, ForwardRangeMarker]:
987
start_marker = ForwardRangeMarker(
988
start_or_end=True,
989
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
990
role=role,
991
)992
start_marker.ng_range_line_begin = file_range[0]
993
start_marker.ng_source_line_begin = file_range[0]
994
start_marker.ng_range_line_end = file_range[1]
995
996
end_marker = ForwardRangeMarker(
997
start_or_end=False,
998
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
999
role=role,
1000
)1001
end_marker.ng_source_line_begin = file_range[1]
1002
end_marker.ng_range_line_begin = file_range[0]
1003
end_marker.ng_range_line_end = file_range[1]
1004
1005
return start_marker, end_marker
1006
1007
@staticmethod1008
def forward_file_marker_from_file_info(
1009
file_info: SourceFileTraceabilityInfo,
1010
requirement_uid_: str,
1011
role: Optional[str],
1012
) -> ForwardFileMarker:
1013
marker = ForwardFileMarker(
1014
reqs_objs=[Req(parent=None, uid=requirement_uid_)],
1015
role=role,
1016
)1017
marker.ng_range_line_begin = 1
1018
marker.ng_source_line_begin = 1
1019
marker.ng_range_line_end = file_info.file_stats.lines_total
1020
return marker
1021
1022
def calculate_code_coverage_and_sort_nodes(
1023
self, traceability_index: "TraceabilityIndex"
1024
) -> None:
1025
"""
1026
Finalize code coverage and sort all nodes.1027
1028
For each trace info object:1029
- Sort the markers according to their source location.1030
- Calculate coverage information.1031
"""1032
1033
for (
1034
path,
1035
traceability_info_,
1036
) in self.map_paths_to_source_file_traceability_info.items():
1037
1038
def marker_comparator_start(
1039
marker: RelationMarkerType,
1040
) -> int:
1041
assert marker.ng_range_line_begin is not None
1042
return marker.ng_range_line_begin
1043
1044
sorted_markers = sorted(
1045
traceability_info_.markers, key=marker_comparator_start
1046
)1047
1048
traceability_info_.markers = sorted_markers
1049
# Finding how many lines are covered by the requirements in the file.1050
# Quick and dirty: https://stackoverflow.com/a/15273749/5980571051
merged_ranges: List[List[int]] = []
1052
for marker_ in traceability_info_.markers:
1053
assert isinstance(
1054
marker_,
1055
(1056
LanguageItemMarker,
1057
ForwardRangeMarker,
1058
RangeMarker,
1059
LineMarker,
1060
),1061
), marker_
1062
if marker_.ng_is_nodoc:
1063
continue1064
if not marker_.is_begin():
1065
continue1066
begin, end = (
1067
assert_cast(marker_.ng_range_line_begin, int),
1068
assert_cast(marker_.ng_range_line_end, int),
1069
)1070
if merged_ranges and merged_ranges[-1][1] >= (begin - 1):
1071
merged_ranges[-1][1] = max(merged_ranges[-1][1], end)
1072
else:
1073
merged_ranges.append([begin, end])
1074
coverage = 0
1075
for merged_range in merged_ranges:
1076
for line_ in range(merged_range[0], merged_range[1] + 1):
1077
if traceability_info_.file_stats.lines_info[line_]:
1078
coverage += 1
1079
1080
for function_ in traceability_info_.functions:
1081
for merged_range in merged_ranges:
1082
if (
1083
function_.line_begin >= merged_range[0]
1084
and function_.line_end <= merged_range[1]
1085
):1086
traceability_info_.covered_functions += 1
1087
break1088
1089
traceability_info_.set_coverage_stats(merged_ranges, coverage)
1090
1091
for (
1092
req_uid_,
1093
markers_,
1094
) in traceability_info_.ng_map_reqs_to_markers.items():
1095
1096
def marker_comparator_range(
1097
marker: RelationMarkerType,
1098
) -> Tuple[int, int]:
1099
assert marker.ng_range_line_begin is not None
1100
assert marker.ng_range_line_end is not None
1101
return marker.ng_range_line_begin, marker.ng_range_line_end
1102
1103
markers_.sort(key=marker_comparator_range)
1104
1105
# Validate here, SDocNode.relations doesn't track marker roles.1106
node = traceability_index.get_node_by_uid(req_uid_)
1107
document = node.get_document()
1108
assert document is not None
1109
assert document.grammar is not None
1110
grammar_element = document.grammar.elements_by_type[
1111
node.node_type
1112
]1113
for marker in markers_:
1114
# Backwards markers do not require referenced node grammar1115
# to have the relation/role registered in the grammar.1116
if isinstance(marker, (LanguageItemMarker, RangeMarker)):
1117
continue1118
1119
if not grammar_element.has_relation_type_role(
1120
relation_type="File",
1121
relation_role=marker.role,
1122
):1123
raise StrictDocSemanticError.invalid_marker_role(
1124
node=node,
1125
marker=marker,
1126
path_to_src_file=path,
1127
)1128
1129
# Sort by paths alphabetically.1130
for paths_with_role in self.map_reqs_uids_to_paths.values():
1131
paths_with_role.sort()
1132
1133
# Sort by node UID alphabetically.1134
for path_requirements_ in self.map_paths_to_reqs.values():
1135
1136
def compare_sdocnode_by_uid(node_: SDocNode) -> str:
1137
return assert_cast(node_.reserved_uid, str)
1138
1139
path_requirements_.sort(key=compare_sdocnode_by_uid)
1140
1141
def connect_source_node_function(
1142
self,
1143
source_node: SourceNode,
1144
source_sdoc_node_uid: str,
1145
traceability_info: SourceFileTraceabilityInfo,
1146
) -> None:
1147
source_node_function = source_node.function
1148
assert source_node_function is not None
1149
1150
language_item_marker = self.forward_marker_from_language_item(
1151
function=source_node_function,
1152
marker_type=RangeMarkerType.FUNCTION,
1153
reqs=[Req(None, source_sdoc_node_uid)],
1154
role=None,
1155
description=f"function {source_node_function.display_name}()",
1156
)1157
1158
traceability_info.ng_map_reqs_to_markers.setdefault(
1159
source_sdoc_node_uid, []
1160
).append(language_item_marker)
1161
language_item_marker_copy = language_item_marker.create_end_marker()
1162
traceability_info.markers.append(language_item_marker)
1163
traceability_info.markers.append(language_item_marker_copy)
1164
1165
@staticmethod1166
def create_sdoc_node_from_source_node(
1167
source_node: SourceNode,
1168
source_node_config_entry: SourceNodesEntry,
1169
sdoc_node_uid: str,
1170
parent_document: SDocDocumentIF,
1171
) -> SDocNode:
1172
sdoc_node = SDocNode(
1173
parent=parent_document,
1174
node_type=source_node_config_entry.node_type,
1175
fields=[],
1176
relations=[],
1177
# It is important that this autogenerated node is marked as such.1178
autogen=True,
1179
)1180
sdoc_node.ng_document_reference = DocumentReference()
1181
sdoc_node.ng_document_reference.set_document(parent_document)
1182
sdoc_node.ng_including_document_reference = DocumentReference()
1183
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1184
sdoc_node_fields["UID"] = sdoc_node_uid
1185
FileTraceabilityIndex.set_sdoc_node_fields(
1186
sdoc_node, sdoc_node_fields, source_node
1187
)1188
# Recorded so that a later TEST_RESULT resolution pass can find this1189
# node by its source_node/function and link it as a test case,1190
# even though no explicit @relation marker exists for it.1191
source_node.sdoc_node = sdoc_node
1192
return sdoc_node
1193
1194
@staticmethod1195
def merge_sdoc_node_with_source_node(
1196
source_node_config_entry: SourceNodesEntry,
1197
source_node: SourceNode,
1198
sdoc_node: SDocNode,
1199
parent_document: SDocDocumentIF,
1200
) -> None:
1201
# First check if grammar element definitions are compatible.1202
source_node_type = source_node_config_entry.node_type
1203
source_node_grammar = assert_cast(
1204
parent_document.grammar, DocumentGrammar
1205
)1206
source_node_grammar_element = source_node_grammar.elements_by_type[
1207
source_node_type1208
]1209
sdoc_node_document = assert_cast(
1210
sdoc_node.get_document(), SDocDocumentIF
1211
)1212
sdoc_node_grammar = assert_cast(
1213
sdoc_node_document.grammar, DocumentGrammar
1214
)1215
sdoc_node_grammar_element = sdoc_node_grammar.elements_by_type[
1216
source_node_type1217
]1218
if source_node_grammar_element != sdoc_node_grammar_element:
1219
raise StrictDocException(
1220
f"Can't merge node {sdoc_node.reserved_uid} with source portion: "
1221
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
1222
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
1223
)1224
# Merge strategy: overwrite any field if there's a field with same name from custom tags.1225
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1226
1227
# Sanity check: Nor UID neither MID must conflict (early auto-MID is allowed to be overwritten)1228
if (
1229
"MID" in sdoc_node.ordered_fields_lookup
1230
and "MID" in sdoc_node_fields
1231
):1232
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1233
if sdoc_mid_field != sdoc_node_fields["MID"]:
1234
raise StrictDocException(
1235
f"Can't merge node by UID {sdoc_node.reserved_uid}: "
1236
f"Conflicting MID: {sdoc_mid_field} != {sdoc_node_fields['MID']}"
1237
)1238
if sdoc_node.reserved_uid is not None and "UID" in sdoc_node_fields:
1239
if sdoc_node.reserved_uid != sdoc_node_fields["UID"]:
1240
raise StrictDocException(
1241
f"Can't merge node by MID {sdoc_node.reserved_mid}: "
1242
f"Conflicting UID: {sdoc_node.reserved_uid} != {sdoc_node_fields['UID']}"
1243
)1244
1245
FileTraceabilityIndex.set_sdoc_node_fields(
1246
sdoc_node, sdoc_node_fields, source_node
1247
)1248
source_node.sdoc_node = sdoc_node
1249
1250
@staticmethod1251
def set_sdoc_node_fields(
1252
sdoc_node: SDocNode,
1253
sdoc_node_fields: Dict[str, str],
1254
source_node: SourceNode,
1255
) -> None:
1256
document = assert_cast(sdoc_node.get_document(), SDocDocumentIF)
1257
grammar = assert_cast(document.grammar, DocumentGrammar)
1258
element = grammar.elements_by_type[sdoc_node.node_type]
1259
1260
# Fall back to parser-suggested auto title as last option.1261
if (
1262
"TITLE" not in sdoc_node_fields
1263
and "TITLE" not in sdoc_node.ordered_fields_lookup
1264
and source_node.entity_name is not None
1265
):1266
sdoc_node_fields["TITLE"] = source_node.entity_name
1267
1268
for field_name, field_value in sdoc_node_fields.items():
1269
multiline = element.is_field_multiline(field_name)
1270
free_text_container = SDFreeTextReader.read(field_value)
1271
sdoc_node_field = SDocNodeField.from_parts(
1272
sdoc_node,
1273
field_name=field_name,
1274
parts=free_text_container.parts,
1275
multiline=multiline,
1276
)1277
sdoc_node.set_field_value(
1278
field_name=field_name,
1279
form_field_index=0,
1280
value=sdoc_node_field,
1281
)1282
1283
# As we overwrite the field's content from the source code,1284
# we mark the field as source_origin here.1285
new_field: SDocNodeField = sdoc_node.ordered_fields_lookup[
1286
field_name1287
][0]
1288
new_field.mark_as_source_origin()
1289
1290
@staticmethod1291
def create_source_node_section(
1292
document: SDocDocumentIF,
1293
path_to_source_file: str,
1294
section_cache: Dict[str, Union[SDocDocumentIF, SDocNode]],
1295
) -> Tuple[Union[SDocDocumentIF, SDocNode], List[SDocNode]]:
1296
"""
1297
Add a subsection for each path components in a given file path.1298
"""1299
current_top_node: Union[SDocDocumentIF, SDocNode] = document
1300
created_sections: List[SDocNode] = []
1301
path_components = path_to_source_file.split("/")
1302
for path_component_idx_, path_component_ in enumerate(path_components):
1303
if path_component_ not in section_cache:
1304
path_component_title = (
1305
path_component_ + "/"
1306
if path_component_idx_ < (len(path_components) - 1)
1307
else path_component_
1308
)1309
current_section = SDocNode(
1310
parent=current_top_node,
1311
node_type="SECTION",
1312
fields=[],
1313
relations=[],
1314
is_composite=True,
1315
node_type_close="SECTION",
1316
# It is important that this autogenerated node is marked as such.1317
autogen=True,
1318
)1319
current_section.ng_document_reference = DocumentReference()
1320
current_section.ng_document_reference.set_document(document)
1321
current_section.ng_including_document_reference = (
1322
DocumentReference()
1323
)1324
current_section.set_field_value(
1325
field_name="TITLE",
1326
form_field_index=0,
1327
value=path_component_title,
1328
)1329
1330
current_top_node.section_contents.append(current_section)
1331
section_cache[path_component_] = current_section
1332
created_sections.append(current_section)
1333
current_top_node = section_cache[path_component_]
1334
return current_top_node, created_sections
1335
1336
def connect_sdoc_node_with_file_path(
1337
self, sdoc_node: SDocNode, path_to_source_file_: str
1338
) -> None:
1339
uid = sdoc_node.reserved_uid
1340
assert uid is not None
1341
self.map_reqs_uids_to_paths.setdefault(uid, OrderedSet()).add(
1342
path_to_source_file_1343
)1344
self.map_paths_to_reqs.setdefault(
1345
path_to_source_file_, OrderedSet()
1346
).add(sdoc_node)
1347
1348
@staticmethod1349
def connect_source_node_requirements(
1350
source_node: SourceNode,
1351
sdoc_node: SDocNode,
1352
traceability_index: "TraceabilityIndex",
1353
) -> None:
1354
"""
1355
Connect auto-generated requirement with function marker and with marker target requirement.1356
1357
If function comment has @relation(REQ, scope=function), connections shall become1358
[REQ] <-parent- [auto-generated/merged sdoc_node] -file-> [function marker]1359
1360
Here we link REQ and sdoc_node bidirectional.1361
"""1362
if (
1363
sdoc_node.reserved_uid is not None
1364
and not traceability_index.graph_database.has_link(
1365
link_type=GraphLinkType.UID_TO_NODE,
1366
lhs_node=sdoc_node.reserved_uid,
1367
rhs_node=sdoc_node,
1368
)1369
):1370
traceability_index.graph_database.create_link(
1371
link_type=GraphLinkType.UID_TO_NODE,
1372
lhs_node=sdoc_node.reserved_uid,
1373
rhs_node=sdoc_node,
1374
)1375
1376
# A merge procedure may have overwritten the MID,1377
# in which case the graph database and search index needs an update.1378
if "MID" in sdoc_node.ordered_fields_lookup != sdoc_node.reserved_mid:
1379
sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
1380
if sdoc_mid_field != sdoc_node.reserved_mid:
1381
# TODO:1382
# If we really want to support changing the auto-assigned MID,1383
# at least the graph database and the document search index need an update (remove old MID, add new MID).1384
# I currently struggle to update the search index.1385
parent_document = sdoc_node.get_parent_or_including_document()
1386
sdoc_node.reserved_mid = MID(sdoc_mid_field)
1387
assert parent_document.grammar is not None
1388
node_grammar_element = (
1389
parent_document.grammar.elements_by_type.get(
1390
sdoc_node.node_type
1391
)1392
)1393
if parent_document.config.enable_mid or (
1394
node_grammar_element is not None
1395
and "MID" in node_grammar_element.fields_map
1396
):1397
sdoc_node.mid_permanent = True
1398
1399
if not traceability_index.graph_database.has_link(
1400
link_type=GraphLinkType.MID_TO_NODE,
1401
lhs_node=sdoc_node.reserved_mid,
1402
rhs_node=sdoc_node,
1403
):1404
traceability_index.graph_database.create_link(
1405
link_type=GraphLinkType.MID_TO_NODE,
1406
lhs_node=sdoc_node.reserved_mid,
1407
rhs_node=sdoc_node,
1408
)1409
1410
for marker_ in source_node.markers:
1411
if not isinstance(marker_, LanguageItemMarker):
1412
continue1413
for req_ in marker_.reqs:
1414
node = traceability_index.get_node_by_uid_weak2(req_)
1415
if not traceability_index.graph_database.has_link(
1416
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1417
lhs_node=sdoc_node,
1418
rhs_node=node,
1419
):1420
traceability_index.graph_database.create_link(
1421
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1422
lhs_node=sdoc_node,
1423
rhs_node=node,
1424
)1425
if not traceability_index.graph_database.has_link(
1426
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1427
lhs_node=node,
1428
rhs_node=sdoc_node,
1429
):1430
traceability_index.graph_database.create_link(
1431
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1432
lhs_node=node,
1433
rhs_node=sdoc_node,
1434
)