Skip to content

Commit 86a4951

Browse files
committed
fix(backend/sdoc_source_code): Fix source node parsing on Windows
For config entries like 'source_nodes = [{ "src/" = ... }]' on Windows, StrictDoc did not find matches since it compared slash path-strings with backslash path-strings. This effectively disabled the Source Nodes feature. We can use pathlib.Path, which automatically creates a WindowsPath or a PosixPath and supports matching amongst both. The fix comes with minor refactoring, most notably representing the config entries as dataclass.
1 parent cd7e61b commit 86a4951

File tree

3 files changed

+82
-33
lines changed

3 files changed

+82
-33
lines changed

strictdoc/core/file_traceability_index.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from strictdoc.backend.sdoc_source_code.models.source_node import SourceNode
4444
from strictdoc.core.constants import GraphLinkType
4545
from strictdoc.core.document_iterator import SDocDocumentIterator
46-
from strictdoc.core.project_config import ProjectConfig
46+
from strictdoc.core.project_config import ProjectConfig, SourceNodesEntry
4747
from strictdoc.core.source_tree import SourceFile
4848
from strictdoc.helpers.cast import assert_cast
4949
from strictdoc.helpers.exception import StrictDocException
@@ -596,9 +596,11 @@ def create_folder_section(
596596
documents_with_generated_content = set()
597597

598598
section_cache = {}
599-
source_nodes_config: List[Dict[str, str]] = project_config.source_nodes
599+
source_nodes_config: List[SourceNodesEntry] = (
600+
project_config.source_nodes
601+
)
600602
unused_source_node_paths = {
601-
config_entry_["path"] for config_entry_ in source_nodes_config
603+
config_entry_.path for config_entry_ in source_nodes_config
602604
}
603605
for (
604606
path_to_source_file_,
@@ -610,16 +612,19 @@ def create_folder_section(
610612
if len(source_nodes_config) == 0:
611613
continue
612614

613-
for config_entry_ in source_nodes_config:
614-
config_entry_path = config_entry_["path"]
615-
if path_to_source_file_.startswith(config_entry_path):
616-
relevant_source_node_entry = config_entry_
617-
unused_source_node_paths.discard(config_entry_path)
618-
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+
)
619624
else:
620625
continue
621626

622-
document_uid = relevant_source_node_entry["uid"]
627+
document_uid = relevant_source_node_entry.uid
623628
document = traceability_index.get_node_by_uid(document_uid)
624629
documents_with_generated_content.add(document)
625630

@@ -651,7 +656,10 @@ def create_folder_section(
651656
if source_sdoc_node is not None:
652657
source_sdoc_node = assert_cast(source_sdoc_node, SDocNode)
653658
self.merge_sdoc_node_with_source_node(
654-
source_sdoc_node, source_node_, document, config_entry_
659+
source_sdoc_node,
660+
source_node_,
661+
document,
662+
relevant_source_node_entry,
655663
)
656664
else:
657665
source_sdoc_node = self.create_sdoc_node_from_source_node(
@@ -969,11 +977,11 @@ def create_sdoc_node_from_source_node(
969977
source_node: SourceNode,
970978
uid: str,
971979
parent_document: SDocDocumentIF,
972-
relevant_source_node_entry: dict[str, str],
980+
relevant_source_node_entry: SourceNodesEntry,
973981
) -> SDocNode:
974982
source_sdoc_node = SDocNode(
975983
parent=parent_document,
976-
node_type=relevant_source_node_entry["node_type"],
984+
node_type=relevant_source_node_entry.node_type,
977985
fields=[],
978986
relations=[],
979987
# It is important that this autogenerated node is marked as such.
@@ -1005,10 +1013,10 @@ def merge_sdoc_node_with_source_node(
10051013
sdoc_node: SDocNode,
10061014
source_node: SourceNode,
10071015
parent_document: SDocDocumentIF,
1008-
source_node_config_entry: dict[str, str],
1016+
source_node_config_entry: SourceNodesEntry,
10091017
) -> None:
10101018
# First check if grammar element definitions are compatible.
1011-
source_node_type = source_node_config_entry["node_type"]
1019+
source_node_type = source_node_config_entry.node_type
10121020
source_node_grammar = assert_cast(
10131021
parent_document.grammar, DocumentGrammar
10141022
)

strictdoc/core/project_config.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import sys
99
import tempfile
1010
import types
11+
from dataclasses import dataclass
1112
from enum import Enum
13+
from pathlib import Path
1214
from typing import Any, Dict, List, Optional, Tuple
1315

1416
import toml
@@ -40,6 +42,19 @@ def parse_relation_tuple(column_name: str) -> Optional[Tuple[str, str]]:
4042
return match_result.group(1), match_result.group(4)
4143

4244

45+
@dataclass
46+
class SourceNodesEntry:
47+
path: str
48+
uid: str
49+
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)
56+
57+
4358
class ProjectFeature(str, Enum):
4459
# Stable features.
4560
TABLE_SCREEN = "TABLE_SCREEN"
@@ -193,8 +208,13 @@ def __init__(
193208
self.test_report_root_dict: Dict[str, str] = (
194209
test_report_root_dict if test_report_root_dict is not None else {}
195210
)
196-
self.source_nodes: List[Dict[str, str]] = (
197-
source_nodes if source_nodes is not None else []
211+
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 []
198218
)
199219

200220
# Settings derived from the command-line parameters.
@@ -443,23 +463,39 @@ def get_project_hash(self) -> str:
443463
assert self.input_paths is not None and len(self.input_paths) > 0
444464
return get_md5(self.input_paths[0])
445465

446-
def parse_nodes_type(self, path_to_file: str) -> Optional[tuple[str, str]]:
466+
def get_relevant_source_nodes_entry(
467+
self, path_to_file: str
468+
) -> Optional[SourceNodesEntry]:
469+
"""
470+
Get relevant source_nodes config item for a given source code file.
471+
472+
Returns data for the first entry from source_nodes that is a parent path of path_to_file.
473+
If path_to_file is absolute, source node config entries are assumed to be in the source_root_path.
474+
"""
447475
if self.source_root_path is None:
448476
return None
449477

478+
source_file_path = Path(path_to_file)
450479
for sdoc_source_config_entry_ in self.source_nodes:
451480
# FIXME: Move the setting of full paths to .finalize() of this config
452481
# class when it is implemented.
453-
full_path = sdoc_source_config_entry_.setdefault(
454-
"full_path",
455-
os.path.join(
456-
self.source_root_path, sdoc_source_config_entry_["path"]
457-
),
458-
)
459-
if path_to_file.startswith(full_path):
460-
return sdoc_source_config_entry_[
461-
"uid"
462-
], sdoc_source_config_entry_["node_type"]
482+
if sdoc_source_config_entry_.full_path is None:
483+
sdoc_source_config_entry_.full_path = Path(
484+
self.source_root_path
485+
) / Path(sdoc_source_config_entry_.path)
486+
487+
if source_file_path.is_absolute():
488+
if (
489+
sdoc_source_config_entry_.full_path
490+
in source_file_path.parents
491+
):
492+
return sdoc_source_config_entry_
493+
else:
494+
if (
495+
Path(sdoc_source_config_entry_.path)
496+
in source_file_path.parents
497+
):
498+
return sdoc_source_config_entry_
463499

464500
return None
465501

strictdoc/core/traceability_index_builder.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -885,13 +885,18 @@ def source_node_grammar_element(
885885
project_config: ProjectConfig,
886886
traceability_index: TraceabilityIndex,
887887
) -> Optional[GrammarElement]:
888-
maybe_parse_nodes_type = project_config.parse_nodes_type(path_to_file)
889-
if maybe_parse_nodes_type is None:
888+
source_nodes_cfg_entry = project_config.get_relevant_source_nodes_entry(
889+
path_to_file
890+
)
891+
if source_nodes_cfg_entry is None:
890892
return None
891-
parse_nodes_uid, parse_nodes_type = maybe_parse_nodes_type
892893
sdoc_document = assert_cast(
893-
traceability_index.get_node_by_uid_weak2(parse_nodes_uid),
894+
traceability_index.get_node_by_uid_weak2(
895+
source_nodes_cfg_entry.uid
896+
),
894897
SDocDocument,
895898
)
896899
assert sdoc_document.grammar is not None
897-
return sdoc_document.grammar.elements_by_type.get(parse_nodes_type)
900+
return sdoc_document.grammar.elements_by_type.get(
901+
source_nodes_cfg_entry.node_type
902+
)

0 commit comments

Comments
 (0)