Skip to content

Commit 6aabd5e

Browse files
committed
feat(backend/sdoc_source_code): Support remapping custom tags to sdoc fields
This introduces a config option for source_nodes to describe how tags from an annotated source node map to *different* SDocNode fields. It's most useful in cases where one can't control the custom tag scheme at source code side, but still wants to reuse some of the custom fields for reserved StrictDoc fields with special meaning like UID or TITLE. For example, one can now use the value from "THEIR-PROPRIETARY-ID" as UID within StrictDoc, with all the special meanings of UID.
1 parent e93c3a5 commit 6aabd5e

File tree

14 files changed

+272
-117
lines changed

14 files changed

+272
-117
lines changed

strictdoc/backend/sdoc_source_code/caching_reader.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from typing import Optional, Union
66

7-
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
87
from strictdoc.backend.sdoc.pickle_cache import PickleCache
98
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
109
SourceFileTraceabilityInfo,
@@ -29,7 +28,7 @@ class SourceFileTraceabilityCachingReader:
2928
def read_from_file(
3029
path_to_file: str,
3130
project_config: ProjectConfig,
32-
source_node_grammar_element: Optional[GrammarElement],
31+
source_node_tags: Optional[set[str]] = None,
3332
) -> Optional[SourceFileTraceabilityInfo]:
3433
unpickled_content = PickleCache.read_from_cache(
3534
path_to_file, project_config, "source_file"
@@ -42,7 +41,9 @@ def read_from_file(
4241
return unpickled_content
4342

4443
reader = SourceFileTraceabilityCachingReader._get_reader(
45-
path_to_file, project_config, source_node_grammar_element
44+
path_to_file,
45+
project_config,
46+
source_node_tags,
4647
)
4748
try:
4849
traceability_info = reader.read_from_file(path_to_file)
@@ -64,7 +65,7 @@ def read_from_file(
6465
def _get_reader(
6566
path_to_file: str,
6667
project_config: ProjectConfig,
67-
source_node_grammar_element: Optional[GrammarElement],
68+
source_node_tags: Optional[set[str]] = None,
6869
) -> Union[
6970
SourceFileTraceabilityReader,
7071
SourceFileTraceabilityReader_Python,
@@ -82,12 +83,9 @@ def _get_reader(
8283
or path_to_file.endswith(".hpp")
8384
or path_to_file.endswith(".cpp")
8485
):
85-
custom_tags = (
86-
source_node_grammar_element.get_field_titles()
87-
if source_node_grammar_element is not None
88-
else None
86+
return SourceFileTraceabilityReader_C(
87+
custom_tags=source_node_tags
8988
)
90-
return SourceFileTraceabilityReader_C(custom_tags=custom_tags)
9189
if path_to_file.endswith(".robot"):
9290
return SourceFileTraceabilityReader_Robot()
9391
return SourceFileTraceabilityReader()

strictdoc/backend/sdoc_source_code/comment_parser/marker_lexer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class GrammarTemplate(Template):
6767
class MarkerLexer:
6868
@staticmethod
6969
def parse(
70-
source_input: str, custom_tags: Optional[list[str]] = None
70+
source_input: str, custom_tags: Optional[set[str]] = None
7171
) -> ParseTree:
7272
if custom_tags is not None:
7373
grammar_extension = NODE_GRAMMAR_EXTENSION.substitute(

strictdoc/backend/sdoc_source_code/marker_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def parse(
3232
comment_line_start: int,
3333
entity_name: Optional[str] = None,
3434
col_offset: int = 0,
35-
custom_tags: Optional[list[str]] = None,
35+
custom_tags: Optional[set[str]] = None,
3636
) -> SourceNode:
3737
"""
3838
Parse relation markers from source file comments.

strictdoc/backend/sdoc_source_code/models/source_node.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from dataclasses import dataclass, field
6-
from typing import Dict, List, Optional, Union
6+
from typing import List, Optional, Union
77

88
from strictdoc.backend.sdoc_source_code.models.function import Function
99
from strictdoc.backend.sdoc_source_code.models.function_range_marker import (
@@ -13,6 +13,7 @@
1313
from strictdoc.backend.sdoc_source_code.models.range_marker import (
1414
RangeMarker,
1515
)
16+
from strictdoc.core.project_config import SourceNodesEntry
1617

1718

1819
@dataclass
@@ -21,5 +22,33 @@ class SourceNode:
2122
markers: List[Union[FunctionRangeMarker, RangeMarker, LineMarker]] = field(
2223
default_factory=list
2324
)
24-
fields: Dict[str, str] = field(default_factory=dict)
25+
fields: dict[str, str] = field(default_factory=dict)
2526
function: Optional[Function] = None
27+
28+
def get_sdoc_field(
29+
self,
30+
field_name: str,
31+
cfg_entry: SourceNodesEntry,
32+
) -> Optional[str]:
33+
if field_name in cfg_entry.sdoc_to_source_map:
34+
field_name = cfg_entry.sdoc_to_source_map[field_name]
35+
return self.fields.get(field_name)
36+
37+
def get_sdoc_fields(self, cfg_entry: SourceNodesEntry) -> dict[str, str]:
38+
"""
39+
Get SDoc representation of all available fields.
40+
41+
Returns a dict equivalent to self.fields unless config option 'sdoc_to_source_map' is set.
42+
"""
43+
fields = {
44+
field_name: self.fields[field_name]
45+
for field_name in self.fields
46+
if field_name not in cfg_entry.sdoc_to_source_map.values()
47+
}
48+
for (
49+
field_name,
50+
mapped_field_name,
51+
) in cfg_entry.sdoc_to_source_map.items():
52+
if mapped_field_name in self.fields:
53+
fields[field_name] = self.fields[mapped_field_name]
54+
return fields

strictdoc/backend/sdoc_source_code/reader_c.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040

4141

4242
class SourceFileTraceabilityReader_C:
43-
def __init__(self, custom_tags: Optional[list[str]] = None) -> None:
44-
self.custom_tags: Optional[list[str]] = custom_tags
43+
def __init__(self, custom_tags: Optional[set[str]] = None) -> None:
44+
self.custom_tags: Optional[set[str]] = custom_tags
4545

4646
def read(
4747
self,

strictdoc/core/file_traceability_index.py

Lines changed: 50 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -648,41 +648,46 @@ def create_folder_section(
648648

649649
for source_node_ in traceability_info_.source_nodes:
650650
assert source_node_.entity_name is not None
651-
source_sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
652-
653-
source_sdoc_node = traceability_index.get_node_by_uid_weak(
654-
source_sdoc_node_uid
651+
sdoc_node_uid = source_node_.get_sdoc_field(
652+
"UID", relevant_source_node_entry
653+
)
654+
if sdoc_node_uid is None:
655+
sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
656+
sdoc_node = traceability_index.get_node_by_uid_weak(
657+
sdoc_node_uid
655658
)
656-
if source_sdoc_node is not None:
657-
source_sdoc_node = assert_cast(source_sdoc_node, SDocNode)
659+
660+
if sdoc_node is not None:
661+
sdoc_node = assert_cast(sdoc_node, SDocNode)
658662
self.merge_sdoc_node_with_source_node(
659-
source_sdoc_node,
663+
relevant_source_node_entry,
660664
source_node_,
665+
sdoc_node,
661666
document,
662-
relevant_source_node_entry,
663667
)
664668
else:
665-
source_sdoc_node = self.create_sdoc_node_from_source_node(
669+
sdoc_node = self.create_sdoc_node_from_source_node(
666670
source_node_,
667-
source_sdoc_node_uid,
668-
document,
669671
relevant_source_node_entry,
672+
sdoc_node_uid,
673+
document,
670674
)
671-
current_top_node.section_contents.append(source_sdoc_node)
675+
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
676+
current_top_node.section_contents.append(sdoc_node)
672677
traceability_index.graph_database.create_link(
673678
link_type=GraphLinkType.UID_TO_NODE,
674-
lhs_node=source_sdoc_node_uid,
675-
rhs_node=source_sdoc_node,
679+
lhs_node=sdoc_node_uid,
680+
rhs_node=sdoc_node,
676681
)
677682

678683
self.connect_source_node_function(
679-
source_node_, source_sdoc_node_uid, traceability_info_
684+
source_node_, sdoc_node_uid, traceability_info_
680685
)
681686
self.connect_sdoc_node_with_file_path(
682-
source_sdoc_node, path_to_source_file_
687+
sdoc_node, path_to_source_file_
683688
)
684689
self.connect_source_node_requirements(
685-
source_node_, source_sdoc_node, traceability_index
690+
source_node_, sdoc_node, traceability_index
686691
)
687692

688693
# Warn if source_node was not matched by any include_source_paths, it indicates misconfiguration
@@ -975,45 +980,37 @@ def connect_source_node_function(
975980
@staticmethod
976981
def create_sdoc_node_from_source_node(
977982
source_node: SourceNode,
978-
uid: str,
983+
source_node_config_entry: SourceNodesEntry,
984+
sdoc_node_uid: str,
979985
parent_document: SDocDocumentIF,
980-
relevant_source_node_entry: SourceNodesEntry,
981986
) -> SDocNode:
982-
source_sdoc_node = SDocNode(
987+
sdoc_node = SDocNode(
983988
parent=parent_document,
984-
node_type=relevant_source_node_entry.node_type,
989+
node_type=source_node_config_entry.node_type,
985990
fields=[],
986991
relations=[],
987992
# It is important that this autogenerated node is marked as such.
988993
autogen=True,
989994
)
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
995+
sdoc_node.ng_document_reference = DocumentReference()
996+
sdoc_node.ng_document_reference.set_document(parent_document)
997+
sdoc_node.ng_including_document_reference = DocumentReference()
998+
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
999+
sdoc_node_fields["UID"] = sdoc_node_uid
1000+
if (
1001+
"TITLE" not in sdoc_node_fields
1002+
and source_node.entity_name is not None
1003+
):
1004+
sdoc_node_fields["TITLE"] = source_node.entity_name
1005+
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1006+
return sdoc_node
10101007

10111008
@staticmethod
10121009
def merge_sdoc_node_with_source_node(
1013-
sdoc_node: SDocNode,
1010+
source_node_config_entry: SourceNodesEntry,
10141011
source_node: SourceNode,
1012+
sdoc_node: SDocNode,
10151013
parent_document: SDocDocumentIF,
1016-
source_node_config_entry: SourceNodesEntry,
10171014
) -> None:
10181015
# First check if grammar element definitions are compatible.
10191016
source_node_type = source_node_config_entry.node_type
@@ -1038,19 +1035,19 @@ def merge_sdoc_node_with_source_node(
10381035
f"Grammar element {sdoc_node_document.reserved_uid}::{source_node_type} "
10391036
f"incompatible with {parent_document.reserved_uid}::{source_node_type}"
10401037
)
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-
)
10481038
# 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():
1039+
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
1040+
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
1041+
1042+
@staticmethod
1043+
def set_sdoc_node_fields(
1044+
sdoc_node: SDocNode, sdoc_node_fields: dict[str, str]
1045+
) -> None:
1046+
for field_name, field_value in sdoc_node_fields.items():
10501047
sdoc_node.set_field_value(
1051-
field_name=node_name_,
1048+
field_name=field_name,
10521049
form_field_index=0,
1053-
value=node_value_,
1050+
value=field_value,
10541051
)
10551052

10561053
def connect_sdoc_node_with_file_path(

strictdoc/core/project_config.py

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import sys
99
import tempfile
1010
import types
11-
from dataclasses import dataclass
11+
from dataclasses import dataclass, field
1212
from enum import Enum
1313
from pathlib import Path
1414
from typing import Any, Dict, List, Optional, Tuple
@@ -47,12 +47,8 @@ class SourceNodesEntry:
4747
path: str
4848
uid: str
4949
node_type: str
50-
full_path: Optional[Path]
51-
52-
@classmethod
53-
def from_cfg_data(cls, data: dict[str, str]) -> "SourceNodesEntry":
54-
full_path = Path(data["full_path"]) if "full_path" in data else None
55-
return cls(data["path"], data["uid"], data["node_type"], full_path)
50+
sdoc_to_source_map: Dict[str, str] = field(default_factory=dict)
51+
full_path: Optional[Path] = None
5652

5753

5854
class ProjectFeature(str, Enum):
@@ -124,7 +120,7 @@ def __init__(
124120
include_source_paths: Optional[List[str]] = None,
125121
exclude_source_paths: Optional[List[str]] = None,
126122
test_report_root_dict: Optional[Dict[str, str]] = None,
127-
source_nodes: Optional[List[Dict[str, str]]] = None,
123+
source_nodes: Optional[List[SourceNodesEntry]] = None,
128124
html2pdf_strict: bool = False,
129125
html2pdf_template: Optional[str] = None,
130126
bundle_document_version: Optional[
@@ -209,12 +205,7 @@ def __init__(
209205
test_report_root_dict if test_report_root_dict is not None else {}
210206
)
211207
self.source_nodes: List[SourceNodesEntry] = (
212-
[
213-
SourceNodesEntry.from_cfg_data(source_node)
214-
for source_node in source_nodes
215-
]
216-
if source_nodes is not None
217-
else []
208+
source_nodes if source_nodes is not None else []
218209
)
219210

220211
# Settings derived from the command-line parameters.
@@ -585,7 +576,7 @@ def _load_from_dictionary(
585576
include_source_paths: List[str] = []
586577
exclude_source_paths: List[str] = []
587578
test_report_root_dict: Dict[str, str] = {}
588-
source_nodes: List[Dict[str, str]] = []
579+
source_nodes: List[SourceNodesEntry] = []
589580
html2pdf_strict: bool = False
590581
html2pdf_template: Optional[str] = None
591582
bundle_document_version = (
@@ -801,14 +792,16 @@ def _load_from_dictionary(
801792
assert isinstance(source_nodes_config, list)
802793
for item_ in source_nodes_config:
803794
source_node_path = next(iter(item_))
804-
source_node_uid = item_[source_node_path]["uid"]
805-
source_node_node_type = item_[source_node_path]["node_type"]
795+
source_node_item = item_[source_node_path]
806796
source_nodes.append(
807-
{
808-
"path": source_node_path,
809-
"uid": source_node_uid,
810-
"node_type": source_node_node_type,
811-
}
797+
SourceNodesEntry(
798+
path=source_node_path,
799+
uid=source_node_item["uid"],
800+
node_type=source_node_item["node_type"],
801+
sdoc_to_source_map=source_node_item["map"]
802+
if "map" in source_node_item
803+
else {},
804+
)
812805
)
813806

814807
if "server" in config_dict:

strictdoc/core/traceability_index.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from strictdoc.backend.sdoc.models.anchor import Anchor
1111
from strictdoc.backend.sdoc.models.document import SDocDocument
1212
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
13+
from strictdoc.backend.sdoc.models.grammar_element import GrammarElement
1314
from strictdoc.backend.sdoc.models.inline_link import InlineLink
1415
from strictdoc.backend.sdoc.models.node import SDocNode
1516
from strictdoc.backend.sdoc_source_code.models.source_file_info import (
@@ -353,6 +354,14 @@ def get_incoming_links(
353354
# FIXME: Should the graph database return OrderedSet or a copied list()?
354355
return list(incoming_links)
355356

357+
def get_grammar_element(
358+
self, document_uid: str, node_type: str
359+
) -> Optional[GrammarElement]:
360+
document = self.get_node_by_uid_weak2(document_uid)
361+
if isinstance(document, SDocDocument) and document.grammar is not None:
362+
return document.grammar.elements_by_type.get(node_type)
363+
return None
364+
356365
def create_traceability_info(
357366
self,
358367
source_file: SourceFile,

0 commit comments

Comments
 (0)