StrictDoc Documentation
strictdoc/core/file_traceability_index.py
Source file coverage
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%)
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: ERA001
93
        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_path
106
        ]
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
                continue
129
            visited_file_links.add(requirement_source_path_)
130
 
131
            source_file_traceability_info: Optional[
132
                SourceFileTraceabilityInfo
133
            ] = 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
                continue
148
            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_path
164
            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_path
172
            ]
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_path
198
            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_path
203
            ]
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_path
213
            )
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 the
228
        #       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 register
247
        #       their UIDs. This must happen before marker validation so that
248
        #       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
                continue
265
 
266
            if len(source_nodes_config) == 0:
267
                continue
268
 
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
                continue
280
 
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
                    continue
289
 
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_uid
316
                    )
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 misconfiguration
379
        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
                pass
392
 
393
        #
394
        # STEP: Resolve requirements that have forward links.
395
        #       Some requirements can come from the SDoc documents generated
396
        #       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
                    continue
404
 
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
                                break
433
                        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
                                break
466
                        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_function
480
                        ]
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 results
505
                    # 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 node
511
                    # whenever that node was itself auto-generated (or
512
                    # merged) from a source_node parsed off the same test
513
                    # 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 test
542
                    # function are linked to the TEST_CASE node when one
543
                    # 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 config
547
                    # for this file), fall back to linking the requirement
548
                    # 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 merged
560
                                    # from this same function, the generic
561
                                    # requirement-to-source-traceability
562
                                    # mechanism has already linked it to
563
                                    # this requirement via an unlabeled
564
                                    # Parent/Child edge (see the
565
                                    # source_node.markers loop above in
566
                                    # this file). Replace that unlabeled
567
                                    # edge with the labeled verifies/
568
                                    # is verified by one so the relation is
569
                                    # 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
                    SourceFileTraceabilityInfo
611
                ] = self.map_paths_to_source_file_traceability_info.get(
612
                    file_posix_path
613
                )
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, the
623
                # 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_path
668
                        ]
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_path
685
                        ]
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 classes
701
        #
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
                    continue
742
                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
                        continue
799
 
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_info
819
                                ]
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_marker
833
                            )
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
 
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_posix
867
        ]
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
                continue
877
 
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
                continue
902
            for req_uid_ in reqs_uids:
903
                if req_uid_ in seen_req_uids:
904
                    continue
905
                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
    @staticmethod
911
    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
    @staticmethod
919
    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
    @staticmethod
932
    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
    @staticmethod
959
    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
    @staticmethod
984
    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
    @staticmethod
1008
    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/598057
1051
            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
                    continue
1064
                if not marker_.is_begin():
1065
                    continue
1066
                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
                        break
1088
 
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 grammar
1115
                    # to have the relation/role registered in the grammar.
1116
                    if isinstance(marker, (LanguageItemMarker, RangeMarker)):
1117
                        continue
1118
 
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
    @staticmethod
1166
    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 this
1189
        # 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
    @staticmethod
1195
    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_type
1208
        ]
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_type
1217
        ]
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
    @staticmethod
1251
    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_name
1287
            ][0]
1288
            new_field.mark_as_source_origin()
1289
 
1290
    @staticmethod
1291
    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
    @staticmethod
1349
    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 become
1358
        [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
                continue
1413
            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
                    )