Skip to content

Commit 035fc5b

Browse files
committed
feat(backend/sdoc_source_code): Support merging sdoc nodes with source nodes
This allows to have a (maybe incomplete) definition of a requirement anywhere in the static document tree which is then merged with content from annotated source code at runtime. Previously, StrictDoc would always have auto-generated a new node in an auto-determined document position. Match criterion is currently the UID, this should be enhanced to MID in an upcoming change. If the grammar element of sdoc and source code node are different, an error will be raised. There are two use cases: - Control the position (document, section) of a generated source node, as opposed to letting the node generator use an auto generated section. - Support "side-car" files where parts of a requirement are defined in *.sdoc, while other parts of the same requirement are inlined with source code. This is a concept from the Linux kernel requirement template proposal.
1 parent d48030e commit 035fc5b

File tree

6 files changed

+306
-82
lines changed

6 files changed

+306
-82
lines changed

strictdoc/core/file_traceability_index.py

Lines changed: 192 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from strictdoc.backend.sdoc.document_reference import DocumentReference
1818
from strictdoc.backend.sdoc.error_handling import StrictDocSemanticError
1919
from strictdoc.backend.sdoc.models.document import SDocDocument
20+
from strictdoc.backend.sdoc.models.document_grammar import (
21+
DocumentGrammar,
22+
)
2023
from strictdoc.backend.sdoc.models.model import SDocDocumentIF, SDocNodeIF
2124
from strictdoc.backend.sdoc.models.node import SDocNode
2225
from strictdoc.backend.sdoc.models.reference import FileEntry, FileReference
@@ -37,6 +40,7 @@
3740
RelationMarkerType,
3841
SourceFileTraceabilityInfo,
3942
)
43+
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
4044
from strictdoc.core.constants import GraphLinkType
4145
from strictdoc.core.document_iterator import SDocDocumentIterator
4246
from strictdoc.core.project_config import ProjectConfig
@@ -638,95 +642,40 @@ def create_folder_section(
638642
current_top_node = section_cache[path_component_]
639643

640644
for source_node_ in traceability_info_.source_nodes:
641-
source_sdoc_node = SDocNode(
642-
parent=document,
643-
node_type=relevant_source_node_entry["node_type"],
644-
fields=[],
645-
relations=[],
646-
# It is important that this autogenerated node is marked as such.
647-
autogen=True,
648-
)
649645
assert source_node_.entity_name is not None
650646
source_sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
651-
source_sdoc_node.ng_document_reference = DocumentReference()
652-
source_sdoc_node.ng_document_reference.set_document(document)
653-
source_sdoc_node.ng_including_document_reference = (
654-
DocumentReference()
655-
)
656-
source_sdoc_node.set_field_value(
657-
field_name="UID",
658-
form_field_index=0,
659-
value=source_sdoc_node_uid,
660-
)
661-
source_sdoc_node.set_field_value(
662-
field_name="TITLE",
663-
form_field_index=0,
664-
value=source_node_.entity_name,
665-
)
666647

667-
for node_name_, node_value_ in source_node_.fields.items():
668-
source_sdoc_node.set_field_value(
669-
field_name=node_name_,
670-
form_field_index=0,
671-
value=node_value_,
648+
source_sdoc_node = traceability_index.get_node_by_uid_weak(
649+
source_sdoc_node_uid
650+
)
651+
if source_sdoc_node is not None:
652+
source_sdoc_node = assert_cast(source_sdoc_node, SDocNode)
653+
self.merge_sdoc_node_with_source_node(
654+
source_sdoc_node, source_node_, document, config_entry_
655+
)
656+
else:
657+
source_sdoc_node = self.create_sdoc_node_from_source_node(
658+
source_node_,
659+
source_sdoc_node_uid,
660+
document,
661+
relevant_source_node_entry,
662+
)
663+
current_top_node.section_contents.append(source_sdoc_node)
664+
traceability_index.graph_database.create_link(
665+
link_type=GraphLinkType.UID_TO_NODE,
666+
lhs_node=source_sdoc_node_uid,
667+
rhs_node=source_sdoc_node,
672668
)
673-
current_top_node.section_contents.append(source_sdoc_node)
674669

675-
traceability_index.graph_database.create_link(
676-
link_type=GraphLinkType.UID_TO_NODE,
677-
lhs_node=source_sdoc_node_uid,
678-
rhs_node=source_sdoc_node,
670+
self.connect_source_node_function(
671+
source_node_, source_sdoc_node_uid, traceability_info_
679672
)
680-
681-
source_node_function = source_node_.function
682-
assert source_node_function is not None
683-
684-
function_marker = self.forward_function_marker_from_function(
685-
function=source_node_function,
686-
marker_type=RangeMarkerType.FUNCTION,
687-
reqs=[Req(None, source_sdoc_node_uid)],
688-
role=None,
689-
description=f"function {source_node_function.display_name}()",
673+
self.connect_sdoc_node_with_file_path(
674+
source_sdoc_node, path_to_source_file_
675+
)
676+
self.connect_source_node_requirements(
677+
source_node_, source_sdoc_node, traceability_index
690678
)
691-
692-
traceability_info_.ng_map_reqs_to_markers.setdefault(
693-
source_sdoc_node_uid, []
694-
).append(function_marker)
695-
696-
self.map_reqs_uids_to_paths.setdefault(
697-
source_sdoc_node_uid, OrderedSet()
698-
).add(path_to_source_file_)
699-
700-
self.map_paths_to_reqs.setdefault(
701-
path_to_source_file_, OrderedSet()
702-
).add(source_sdoc_node)
703-
704-
function_marker_copy = function_marker.create_end_marker()
705-
706-
traceability_info_.markers.append(function_marker)
707-
traceability_info_.markers.append(function_marker_copy)
708-
709-
#
710-
# This connects:
711-
# - Source nodes and auto-generated requirements.
712-
# - Source nodes-related requirements and auto-generated requirements.
713-
#
714-
for marker_ in source_node_.markers:
715-
if not isinstance(marker_, FunctionRangeMarker):
716-
continue
717-
718-
for req_ in marker_.reqs:
719-
node = traceability_index.get_node_by_uid_weak2(req_)
720-
traceability_index.graph_database.create_link(
721-
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
722-
lhs_node=source_sdoc_node,
723-
rhs_node=node,
724-
)
725-
traceability_index.graph_database.create_link(
726-
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
727-
lhs_node=node,
728-
rhs_node=source_sdoc_node,
729-
)
730679

731680
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration
732681
for unused_source_node_path in unused_source_node_paths:
@@ -990,3 +939,164 @@ def compare_sdocnode_by_uid(node_: SDocNode) -> str:
990939
return assert_cast(node_.reserved_uid, str)
991940

992941
path_requirements_.sort(key=compare_sdocnode_by_uid)
942+
943+
def connect_source_node_function(
944+
self,
945+
source_node: SourceNode,
946+
source_sdoc_node_uid: str,
947+
traceability_info: SourceFileTraceabilityInfo,
948+
) -> None:
949+
source_node_function = source_node.function
950+
assert source_node_function is not None
951+
952+
function_marker = self.forward_function_marker_from_function(
953+
function=source_node_function,
954+
marker_type=RangeMarkerType.FUNCTION,
955+
reqs=[Req(None, source_sdoc_node_uid)],
956+
role=None,
957+
description=f"function {source_node_function.display_name}()",
958+
)
959+
960+
traceability_info.ng_map_reqs_to_markers.setdefault(
961+
source_sdoc_node_uid, []
962+
).append(function_marker)
963+
function_marker_copy = function_marker.create_end_marker()
964+
traceability_info.markers.append(function_marker)
965+
traceability_info.markers.append(function_marker_copy)
966+
967+
@staticmethod
968+
def create_sdoc_node_from_source_node(
969+
source_node: SourceNode,
970+
uid: str,
971+
parent_document: SDocDocumentIF,
972+
relevant_source_node_entry: dict[str, str],
973+
) -> SDocNode:
974+
source_sdoc_node = SDocNode(
975+
parent=parent_document,
976+
node_type=relevant_source_node_entry["node_type"],
977+
fields=[],
978+
relations=[],
979+
# It is important that this autogenerated node is marked as such.
980+
autogen=True,
981+
)
982+
source_sdoc_node.ng_document_reference = DocumentReference()
983+
source_sdoc_node.ng_document_reference.set_document(parent_document)
984+
source_sdoc_node.ng_including_document_reference = DocumentReference()
985+
source_sdoc_node.set_field_value(
986+
field_name="UID",
987+
form_field_index=0,
988+
value=uid,
989+
)
990+
source_sdoc_node.set_field_value(
991+
field_name="TITLE",
992+
form_field_index=0,
993+
value=source_node.entity_name,
994+
)
995+
for node_name_, node_value_ in source_node.fields.items():
996+
source_sdoc_node.set_field_value(
997+
field_name=node_name_,
998+
form_field_index=0,
999+
value=node_value_,
1000+
)
1001+
return source_sdoc_node
1002+
1003+
@staticmethod
1004+
def merge_sdoc_node_with_source_node(
1005+
sdoc_node: SDocNode,
1006+
source_node: SourceNode,
1007+
parent_document: SDocDocumentIF,
1008+
source_node_config_entry: dict[str, str],
1009+
) -> None:
1010+
# First check if grammar element definitions are compatible.
1011+
source_node_type = source_node_config_entry["node_type"]
1012+
source_node_grammar = assert_cast(
1013+
parent_document.grammar, DocumentGrammar
1014+
)
1015+
source_node_grammar_element = source_node_grammar.elements_by_type[
1016+
source_node_type
1017+
]
1018+
sdoc_node_document = assert_cast(
1019+
sdoc_node.get_document(), SDocDocumentIF
1020+
)
1021+
sdoc_node_grammar = assert_cast(
1022+
sdoc_node_document.grammar, DocumentGrammar
1023+
)
1024+
sdoc_node_grammar_element = sdoc_node_grammar.elements_by_type[
1025+
source_node_type
1026+
]
1027+
if source_node_grammar_element != sdoc_node_grammar_element:
1028+
raise StrictDocException(
1029+
f"Can't merge node {sdoc_node.reserved_uid} with source portion: "
1030+
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
1031+
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
1032+
)
1033+
# Merge strategy: overwrite title if there's a TITLE from custom tags.
1034+
if "TITLE" in source_node.fields:
1035+
sdoc_node.set_field_value(
1036+
field_name="TITLE",
1037+
form_field_index=0,
1038+
value=source_node.fields["TITLE"],
1039+
)
1040+
# Merge strategy: overwrite any field if there's a field with same name from custom tags.
1041+
for node_name_, node_value_ in source_node.fields.items():
1042+
sdoc_node.set_field_value(
1043+
field_name=node_name_,
1044+
form_field_index=0,
1045+
value=node_value_,
1046+
)
1047+
1048+
def connect_sdoc_node_with_file_path(
1049+
self, sdoc_node: SDocNode, path_to_source_file_: str
1050+
) -> None:
1051+
uid = sdoc_node.reserved_uid
1052+
assert uid is not None
1053+
self.map_reqs_uids_to_paths.setdefault(uid, OrderedSet()).add(
1054+
path_to_source_file_
1055+
)
1056+
self.map_paths_to_reqs.setdefault(
1057+
path_to_source_file_, OrderedSet()
1058+
).add(sdoc_node)
1059+
1060+
@staticmethod
1061+
def connect_source_node_requirements(
1062+
source_node: SourceNode,
1063+
sdoc_node: SDocNode,
1064+
traceability_index: "TraceabilityIndex",
1065+
) -> None:
1066+
"""
1067+
Connect auto-generated requirement with function marker and with marker target requirement.
1068+
1069+
If function comment has @relation(REQ, scope=function), connections shall become
1070+
[REQ] <-parent- [auto-generated/merged sdoc_node] -file-> [function marker]
1071+
1072+
Here we link REQ and sdoc_node bidirectional.
1073+
"""
1074+
for marker_ in source_node.markers:
1075+
if not isinstance(marker_, FunctionRangeMarker):
1076+
continue
1077+
for req_ in marker_.reqs:
1078+
node = traceability_index.get_node_by_uid_weak2(req_)
1079+
if (
1080+
node
1081+
not in traceability_index.graph_database.get_link_values(
1082+
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1083+
lhs_node=sdoc_node,
1084+
)
1085+
):
1086+
traceability_index.graph_database.create_link(
1087+
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1088+
lhs_node=sdoc_node,
1089+
rhs_node=node,
1090+
)
1091+
if (
1092+
sdoc_node
1093+
not in traceability_index.graph_database.get_link_values(
1094+
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1095+
lhs_node=node,
1096+
)
1097+
):
1098+
traceability_index.graph_database.create_link(
1099+
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1100+
lhs_node=node,
1101+
rhs_node=sdoc_node,
1102+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[DOCUMENT]
2+
TITLE: Hello world doc
3+
4+
[REQUIREMENT]
5+
UID: REQ-1
6+
TITLE: Requirement Title
7+
STATEMENT: Requirement Statement
8+
9+
[REQUIREMENT]
10+
UID: REQ-2
11+
TITLE: Requirement Title #2
12+
STATEMENT: Requirement Statement #2
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[DOCUMENT]
2+
MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
3+
TITLE: Requirements from Source Nodes
4+
UID: SRC-NODES-BASE
5+
6+
[GRAMMAR]
7+
ELEMENTS:
8+
- TAG: SECTION
9+
PROPERTIES:
10+
IS_COMPOSITE: True
11+
FIELDS:
12+
- TITLE: UID
13+
TYPE: String
14+
REQUIRED: False
15+
- TITLE: TITLE
16+
TYPE: String
17+
REQUIRED: True
18+
- TAG: REQUIREMENT
19+
PROPERTIES:
20+
VIEW_STYLE: Narrative
21+
FIELDS:
22+
- TITLE: UID
23+
TYPE: String
24+
REQUIRED: False
25+
- TITLE: TITLE
26+
TYPE: String
27+
REQUIRED: False
28+
- TITLE: FOO
29+
TYPE: String
30+
REQUIRED: False
31+
- TITLE: BAR
32+
TYPE: String
33+
REQUIRED: False
34+
RELATIONS:
35+
- TYPE: Parent
36+
- TYPE: File
37+
38+
[[SECTION]]
39+
TITLE: Merge example.c into static nodes
40+
41+
[REQUIREMENT]
42+
UID: SRC-NODES-BASE/src/example/example.c/example_1
43+
TITLE: TITLE from sdoc
44+
FOO: FOO text from sdoc
45+
BAR: BAR text from sdoc
46+
RELATIONS:
47+
- TYPE: Parent
48+
VALUE: REQ-1
49+
50+
[[/SECTION]]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <stdio.h>
2+
3+
/**
4+
* Some text.
5+
*
6+
* @relation(REQ-1, scope=function)
7+
*
8+
* FOO: FOO text from example.c
9+
*
10+
* BAR: BAR text from example.c
11+
*/
12+
void example_1(void) {
13+
print("hello world\n");
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[project]
2+
3+
features = [
4+
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
5+
"SOURCE_FILE_LANGUAGE_PARSERS",
6+
]
7+
8+
source_nodes = [
9+
{ "src/" = { uid = "SRC-NODES-BASE", node_type = "REQUIREMENT" } }
10+
]
11+
12+
exclude_source_paths = [
13+
"test.itest"
14+
]

0 commit comments

Comments
 (0)