Skip to content

Commit 4ae86d3

Browse files
authored
Merge pull request #2539 from haxtibal/tdmg/merge_source_nodes
feat(backend/sdoc_source_code): Support merging sdoc nodes with soure nodes
2 parents d48030e + 86a4951 commit 4ae86d3

File tree

15 files changed

+469
-143
lines changed

15 files changed

+469
-143
lines changed

strictdoc/core/file_traceability_index.py

Lines changed: 206 additions & 92 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,9 +40,10 @@
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
42-
from strictdoc.core.project_config import ProjectConfig
46+
from strictdoc.core.project_config import ProjectConfig, SourceNodesEntry
4347
from strictdoc.core.source_tree import SourceFile
4448
from strictdoc.helpers.cast import assert_cast
4549
from strictdoc.helpers.exception import StrictDocException
@@ -592,9 +596,11 @@ def create_folder_section(
592596
documents_with_generated_content = set()
593597

594598
section_cache = {}
595-
source_nodes_config: List[Dict[str, str]] = project_config.source_nodes
599+
source_nodes_config: List[SourceNodesEntry] = (
600+
project_config.source_nodes
601+
)
596602
unused_source_node_paths = {
597-
config_entry_["path"] for config_entry_ in source_nodes_config
603+
config_entry_.path for config_entry_ in source_nodes_config
598604
}
599605
for (
600606
path_to_source_file_,
@@ -606,16 +612,19 @@ def create_folder_section(
606612
if len(source_nodes_config) == 0:
607613
continue
608614

609-
for config_entry_ in source_nodes_config:
610-
config_entry_path = config_entry_["path"]
611-
if path_to_source_file_.startswith(config_entry_path):
612-
relevant_source_node_entry = config_entry_
613-
unused_source_node_paths.discard(config_entry_path)
614-
break
615+
relevant_source_node_entry = (
616+
project_config.get_relevant_source_nodes_entry(
617+
path_to_source_file_
618+
)
619+
)
620+
if relevant_source_node_entry is not None:
621+
unused_source_node_paths.discard(
622+
relevant_source_node_entry.path
623+
)
615624
else:
616625
continue
617626

618-
document_uid = relevant_source_node_entry["uid"]
627+
document_uid = relevant_source_node_entry.uid
619628
document = traceability_index.get_node_by_uid(document_uid)
620629
documents_with_generated_content.add(document)
621630

@@ -638,95 +647,43 @@ def create_folder_section(
638647
current_top_node = section_cache[path_component_]
639648

640649
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-
)
649650
assert source_node_.entity_name is not None
650651
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-
)
666652

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_,
653+
source_sdoc_node = traceability_index.get_node_by_uid_weak(
654+
source_sdoc_node_uid
655+
)
656+
if source_sdoc_node is not None:
657+
source_sdoc_node = assert_cast(source_sdoc_node, SDocNode)
658+
self.merge_sdoc_node_with_source_node(
659+
source_sdoc_node,
660+
source_node_,
661+
document,
662+
relevant_source_node_entry,
663+
)
664+
else:
665+
source_sdoc_node = self.create_sdoc_node_from_source_node(
666+
source_node_,
667+
source_sdoc_node_uid,
668+
document,
669+
relevant_source_node_entry,
670+
)
671+
current_top_node.section_contents.append(source_sdoc_node)
672+
traceability_index.graph_database.create_link(
673+
link_type=GraphLinkType.UID_TO_NODE,
674+
lhs_node=source_sdoc_node_uid,
675+
rhs_node=source_sdoc_node,
672676
)
673-
current_top_node.section_contents.append(source_sdoc_node)
674677

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,
678+
self.connect_source_node_function(
679+
source_node_, source_sdoc_node_uid, traceability_info_
679680
)
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}()",
681+
self.connect_sdoc_node_with_file_path(
682+
source_sdoc_node, path_to_source_file_
683+
)
684+
self.connect_source_node_requirements(
685+
source_node_, source_sdoc_node, traceability_index
690686
)
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-
)
730687

731688
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration
732689
for unused_source_node_path in unused_source_node_paths:
@@ -990,3 +947,160 @@ def compare_sdocnode_by_uid(node_: SDocNode) -> str:
990947
return assert_cast(node_.reserved_uid, str)
991948

992949
path_requirements_.sort(key=compare_sdocnode_by_uid)
950+
951+
def connect_source_node_function(
952+
self,
953+
source_node: SourceNode,
954+
source_sdoc_node_uid: str,
955+
traceability_info: SourceFileTraceabilityInfo,
956+
) -> None:
957+
source_node_function = source_node.function
958+
assert source_node_function is not None
959+
960+
function_marker = self.forward_function_marker_from_function(
961+
function=source_node_function,
962+
marker_type=RangeMarkerType.FUNCTION,
963+
reqs=[Req(None, source_sdoc_node_uid)],
964+
role=None,
965+
description=f"function {source_node_function.display_name}()",
966+
)
967+
968+
traceability_info.ng_map_reqs_to_markers.setdefault(
969+
source_sdoc_node_uid, []
970+
).append(function_marker)
971+
function_marker_copy = function_marker.create_end_marker()
972+
traceability_info.markers.append(function_marker)
973+
traceability_info.markers.append(function_marker_copy)
974+
975+
@staticmethod
976+
def create_sdoc_node_from_source_node(
977+
source_node: SourceNode,
978+
uid: str,
979+
parent_document: SDocDocumentIF,
980+
relevant_source_node_entry: SourceNodesEntry,
981+
) -> SDocNode:
982+
source_sdoc_node = SDocNode(
983+
parent=parent_document,
984+
node_type=relevant_source_node_entry.node_type,
985+
fields=[],
986+
relations=[],
987+
# It is important that this autogenerated node is marked as such.
988+
autogen=True,
989+
)
990+
source_sdoc_node.ng_document_reference = DocumentReference()
991+
source_sdoc_node.ng_document_reference.set_document(parent_document)
992+
source_sdoc_node.ng_including_document_reference = DocumentReference()
993+
source_sdoc_node.set_field_value(
994+
field_name="UID",
995+
form_field_index=0,
996+
value=uid,
997+
)
998+
source_sdoc_node.set_field_value(
999+
field_name="TITLE",
1000+
form_field_index=0,
1001+
value=source_node.entity_name,
1002+
)
1003+
for node_name_, node_value_ in source_node.fields.items():
1004+
source_sdoc_node.set_field_value(
1005+
field_name=node_name_,
1006+
form_field_index=0,
1007+
value=node_value_,
1008+
)
1009+
return source_sdoc_node
1010+
1011+
@staticmethod
1012+
def merge_sdoc_node_with_source_node(
1013+
sdoc_node: SDocNode,
1014+
source_node: SourceNode,
1015+
parent_document: SDocDocumentIF,
1016+
source_node_config_entry: SourceNodesEntry,
1017+
) -> None:
1018+
# First check if grammar element definitions are compatible.
1019+
source_node_type = source_node_config_entry.node_type
1020+
source_node_grammar = assert_cast(
1021+
parent_document.grammar, DocumentGrammar
1022+
)
1023+
source_node_grammar_element = source_node_grammar.elements_by_type[
1024+
source_node_type
1025+
]
1026+
sdoc_node_document = assert_cast(
1027+
sdoc_node.get_document(), SDocDocumentIF
1028+
)
1029+
sdoc_node_grammar = assert_cast(
1030+
sdoc_node_document.grammar, DocumentGrammar
1031+
)
1032+
sdoc_node_grammar_element = sdoc_node_grammar.elements_by_type[
1033+
source_node_type
1034+
]
1035+
if source_node_grammar_element != sdoc_node_grammar_element:
1036+
raise StrictDocException(
1037+
f"Can't merge node {sdoc_node.reserved_uid} with source portion: "
1038+
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
1039+
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
1040+
)
1041+
# Merge strategy: overwrite title if there's a TITLE from custom tags.
1042+
if "TITLE" in source_node.fields:
1043+
sdoc_node.set_field_value(
1044+
field_name="TITLE",
1045+
form_field_index=0,
1046+
value=source_node.fields["TITLE"],
1047+
)
1048+
# Merge strategy: overwrite any field if there's a field with same name from custom tags.
1049+
for node_name_, node_value_ in source_node.fields.items():
1050+
sdoc_node.set_field_value(
1051+
field_name=node_name_,
1052+
form_field_index=0,
1053+
value=node_value_,
1054+
)
1055+
1056+
def connect_sdoc_node_with_file_path(
1057+
self, sdoc_node: SDocNode, path_to_source_file_: str
1058+
) -> None:
1059+
uid = sdoc_node.reserved_uid
1060+
assert uid is not None
1061+
self.map_reqs_uids_to_paths.setdefault(uid, OrderedSet()).add(
1062+
path_to_source_file_
1063+
)
1064+
self.map_paths_to_reqs.setdefault(
1065+
path_to_source_file_, OrderedSet()
1066+
).add(sdoc_node)
1067+
1068+
@staticmethod
1069+
def connect_source_node_requirements(
1070+
source_node: SourceNode,
1071+
sdoc_node: SDocNode,
1072+
traceability_index: "TraceabilityIndex",
1073+
) -> None:
1074+
"""
1075+
Connect auto-generated requirement with function marker and with marker target requirement.
1076+
1077+
If function comment has @relation(REQ, scope=function), connections shall become
1078+
[REQ] <-parent- [auto-generated/merged sdoc_node] -file-> [function marker]
1079+
1080+
Here we link REQ and sdoc_node bidirectional.
1081+
"""
1082+
for marker_ in source_node.markers:
1083+
if not isinstance(marker_, FunctionRangeMarker):
1084+
continue
1085+
for req_ in marker_.reqs:
1086+
node = traceability_index.get_node_by_uid_weak2(req_)
1087+
if not traceability_index.graph_database.has_link(
1088+
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1089+
lhs_node=sdoc_node,
1090+
rhs_node=node,
1091+
):
1092+
traceability_index.graph_database.create_link(
1093+
link_type=GraphLinkType.NODE_TO_PARENT_NODES,
1094+
lhs_node=sdoc_node,
1095+
rhs_node=node,
1096+
)
1097+
if not traceability_index.graph_database.has_link(
1098+
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1099+
lhs_node=node,
1100+
rhs_node=sdoc_node,
1101+
):
1102+
traceability_index.graph_database.create_link(
1103+
link_type=GraphLinkType.NODE_TO_CHILD_NODES,
1104+
lhs_node=node,
1105+
rhs_node=sdoc_node,
1106+
)

strictdoc/core/graph/abstract_bucket.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
class AbstractBucket(ABC):
1010
@abstractmethod
11-
def has_link(self, *, lhs_node: Any) -> bool:
11+
def has_any_link(self, *, lhs_node: Any) -> bool:
12+
raise NotImplementedError
13+
14+
@abstractmethod
15+
def has_link(
16+
self, *, lhs_node: Any, rhs_node: Any, edge: Optional[str] = None
17+
) -> bool:
1218
raise NotImplementedError
1319

1420
@abstractmethod

strictdoc/core/graph/many_to_many_set.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ def __init__(self, lhs_type: Type[Any], rhs_type: Type[Any]) -> None:
1414
self._lhs_type: type = lhs_type
1515
self._rhs_type: type = rhs_type
1616

17-
def has_link(self, *, lhs_node: Any) -> bool:
17+
def has_any_link(self, *, lhs_node: Any) -> bool:
1818
assert isinstance(lhs_node, self._lhs_type), lhs_node
1919
return lhs_node in self._links and len(self._links[lhs_node]) > 0
2020

21+
def has_link(
22+
self, *, lhs_node: Any, rhs_node: Any, edge: Optional[str] = None
23+
) -> bool:
24+
assert isinstance(rhs_node, self._rhs_type), rhs_node
25+
return rhs_node in self.get_link_values(lhs_node=lhs_node, edge=edge)
26+
2127
def get_count(self, edge: Optional[str] = None) -> int:
2228
total_count = 0
2329
for _, lhs_node_links in self._links.items():

0 commit comments

Comments
 (0)