Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/sdoc_project_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
)
from strictdoc.core.document_iterator import SDocDocumentIterator
from strictdoc.core.project_config import ProjectConfig
from strictdoc.core.statistics.metric import Metric, MetricSection
from strictdoc.core.traceability_index import TraceabilityIndex
from strictdoc.export.html.generators.view_objects.project_statistics_view_object import (
ProjectStatisticsViewObject,
)
from strictdoc.export.html.html_templates import HTMLTemplates
from strictdoc.export.html.renderers.link_renderer import LinkRenderer
from strictdoc.features.project_statistics.metric import Metric, MetricSection
from strictdoc.features.project_statistics.view_object import (
ProjectStatisticsViewObject,
)
from strictdoc.helpers.cast import assert_cast
from strictdoc.helpers.git_client import GitClient

Expand Down Expand Up @@ -50,6 +50,7 @@ class DocumentTreeStats:
requirements_no_uid: int = 0
requirements_no_links: int = 0
requirements_root_no_links: int = 0
requirements_missing_relations: int = 0
requirements_no_rationale: int = 0

# STATUS.
Expand Down Expand Up @@ -125,6 +126,11 @@ def export(
):
document_tree_stats.requirements_no_links += 1

if traceability_index.has_missing_relations_for_requirement(
requirement
):
document_tree_stats.requirements_missing_relations += 1

# RATIONALE.
if (
requirement.ordered_fields_lookup.get("RATIONALE")
Expand Down Expand Up @@ -254,6 +260,16 @@ def export(
link='search?q=(node.is_requirement() and not node.is_root and node["STATUS"] != "Backlog" and not node.has_parent_requirements)',
)
)
if project_config.allow_missing_relation_requirements:
section.metrics.append(
Metric(
name="Requirements with missing relations",
value=str(
document_tree_stats.requirements_missing_relations
),
link="search?q=(node.is_requirement() and node.has_missing_relations)",
)
)
section.metrics.append(
Metric(
name="Requirements with no RATIONALE",
Expand Down
18 changes: 13 additions & 5 deletions strictdoc/backend/rst/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,23 @@ def _print_node_field(self, object_with_parts: SDocNodeField) -> str:
else:
output += part
elif isinstance(part, InlineLink):
node_or_none = self.index.get_linkable_node_by_uid(part.link)
node_or_none = self.index.get_linkable_node_by_uid_weak(
part.link
)
# Labels that aren't placed before a section title can still be
# referenced, but you must give the link an explicit title,
# using this syntax: :ref:`Link title <label-name>`.
# https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html
node_display_title = node_or_none.get_display_title(
include_toc_number=False
)
output += f":ref:`{node_display_title} <{part.link}>`"
if node_or_none is not None:
node_display_title = node_or_none.get_display_title(
include_toc_number=False
)
output += f":ref:`{node_display_title} <{part.link}>`"
else:
# Relaxed mode (allow_missing_relation_requirements): the
# inline link target does not exist. Render the target as
# plain text to avoid producing a broken :ref: reference.
output += part.link
elif isinstance(part, Anchor):
output += f".. _{part.value}:\n"
else:
Expand Down
10 changes: 10 additions & 0 deletions strictdoc/commands/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None:
"If not given, chromedriver is downloaded and saved to "
"strictdoc cache.",
)
command_parser_export.add_argument(
"--allow-missing-relation-requirements",
dest="allow_missing_relation_requirements",
action="store_true",
default=False,
help=(
"Allow exporting even when some requirement relations "
"(parent/child) cannot be resolved."
),
)
command_parser_export.add_argument(
"--config",
type=str,
Expand Down
4 changes: 4 additions & 0 deletions strictdoc/commands/export_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(
generate_diff_git: Optional[str],
generate_diff_dirs: Optional[Tuple[str, str]],
chromedriver: Optional[str],
allow_missing_relation_requirements: bool = False,
):
assert isinstance(input_paths, list), f"{input_paths}"
self.debug: bool = debug
Expand All @@ -52,6 +53,9 @@ def __init__(
self.generate_diff_git: Optional[str] = generate_diff_git
self.generate_diff_dirs: Optional[Tuple[str, str]] = generate_diff_dirs
self.chromedriver: Optional[str] = chromedriver
self.allow_missing_relation_requirements: bool = (
allow_missing_relation_requirements
)

def get_path_to_config(self) -> str:
# FIXME: The control flow can be improved.
Expand Down
10 changes: 10 additions & 0 deletions strictdoc/commands/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None:
"documents change on disk."
),
)
command_parser_server.add_argument(
"--allow-missing-relation-requirements",
default=False,
action="store_true",
dest="allow_missing_relation_requirements",
help=(
"Allow starting the server even when some requirement "
"relations (parent/child) cannot be resolved."
),
)
command_parser_server.add_argument(
"--config",
type=str,
Expand Down
4 changes: 4 additions & 0 deletions strictdoc/commands/server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __init__(
host: Optional[str],
port: Optional[int],
watch: bool = False,
allow_missing_relation_requirements: bool = False,
):
self.debug: bool = debug
self.command: str = command
Expand All @@ -30,6 +31,9 @@ def __init__(
self.host: Optional[str] = host
self.port: Optional[int] = port
self.watch: bool = watch
self.allow_missing_relation_requirements: bool = (
allow_missing_relation_requirements
)

def get_full_input_path(self) -> str:
return os.path.abspath(self._input_path)
Expand Down
8 changes: 8 additions & 0 deletions strictdoc/core/file_traceability_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,14 @@ def validate_and_resolve(
file_posix_path
)
if source_file_traceability_info is None:
if project_config.allow_missing_relation_requirements:
traceability_index.validation_index.add_issue(
forward_requirement_,
issue=f"Missing file relation: {file_posix_path}",
field="RELATIONS (File)",
subject=f"Node: {forward_requirement_.reserved_title}",
)
continue
raise StrictDocException(
f"Requirement {forward_requirement_.reserved_uid} "
"references a file that does not exist: "
Expand Down
21 changes: 21 additions & 0 deletions strictdoc/core/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def __init__(
] = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR,
statistics_generator: Optional[str] = None,
document_line_width: Optional[int] = None,
allow_missing_relation_requirements: bool = False,
# Logo path can be set in the project config to customize the launcher's appearance for a specific project.
launcher_logo_path: Optional[str] = None,
user_plugin: Optional[StrictDocPlugin] = None,
Expand Down Expand Up @@ -394,6 +395,13 @@ def __init__(
)
self.document_line_width: Optional[int] = document_line_width

assert isinstance(allow_missing_relation_requirements, bool), (
allow_missing_relation_requirements
)
self.allow_missing_relation_requirements: bool = (
allow_missing_relation_requirements
)

self.user_plugin: Optional[StrictDocPlugin] = user_plugin

# Optional launcher logo path (absolute or workspace-relative).
Expand Down Expand Up @@ -452,6 +460,9 @@ def integrate_server_config(
self.generate_bundle_document = False
self.export_included_documents = True

if server_config.allow_missing_relation_requirements:
self.allow_missing_relation_requirements = True

def integrate_export_config(
self, export_config: ExportCommandConfig
) -> None:
Expand Down Expand Up @@ -520,6 +531,9 @@ def integrate_export_config(
if not self.reqif_enable_mid:
self.reqif_enable_mid = export_config.reqif_enable_mid

if export_config.allow_missing_relation_requirements:
self.allow_missing_relation_requirements = True

def validate_and_finalize(self) -> None:
project_path = self.get_project_root_path()

Expand Down Expand Up @@ -1016,6 +1030,7 @@ def _load_from_dictionary(
section_behavior: str = ProjectConfigDefault.DEFAULT_SECTION_BEHAVIOR
statistics_generator: Optional[str] = None
document_line_width: Optional[int] = None
allow_missing_relation_requirements: bool = False

if "project" in config_dict:
project_content = config_dict["project"]
Expand Down Expand Up @@ -1111,6 +1126,11 @@ def _load_from_dictionary(
"document_line_width", document_line_width
)

allow_missing_relation_requirements = project_content.get(
"allow_missing_relation_requirements",
allow_missing_relation_requirements,
)

if "source_nodes" in project_content:
source_nodes_config = project_content["source_nodes"]
assert isinstance(source_nodes_config, list)
Expand Down Expand Up @@ -1168,5 +1188,6 @@ def _load_from_dictionary(
section_behavior=section_behavior,
statistics_generator=statistics_generator,
document_line_width=document_line_width,
allow_missing_relation_requirements=allow_missing_relation_requirements,
_config_last_update=config_last_update,
)
6 changes: 6 additions & 0 deletions strictdoc/core/query_engine/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
|
NodeHasChildRequirementsExpression
|
NodeHasMissingRelationsExpression
|
InExpression
|
NotInExpression
Expand Down Expand Up @@ -92,6 +94,10 @@
_ = 'node.has_child_requirements'
;

NodeHasMissingRelationsExpression:
_ = 'node.has_missing_relations'
;

NodeIsRequirementExpression:
_ = 'node.is_requirement' '()'?
;
Expand Down
20 changes: 20 additions & 0 deletions strictdoc/core/query_engine/query_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def __init__(self, parent: Any, _: Any):
self.parent: Any = parent


class NodeHasMissingRelationsExpression:
def __init__(self, parent: Any, _: Any):
self.parent: Any = parent


class NodeIsRequirementExpression:
def __init__(self, parent: Any, _: Any):
self.parent: Any = parent
Expand Down Expand Up @@ -176,6 +181,8 @@ def _evaluate(self, node: SDocExtendedElementIF, expression: Any) -> bool:
return self._evaluate_node_has_parent_requirements(node)
if isinstance(expression, NodeHasChildRequirementsExpression):
return self._evaluate_node_has_child_requirements(node)
if isinstance(expression, NodeHasMissingRelationsExpression):
return self._evaluate_node_has_missing_relations(node)
if isinstance(expression, NodeIsRequirementExpression):
return (
isinstance(node, SDocNode) and node.node_type == "REQUIREMENT"
Expand Down Expand Up @@ -312,6 +319,19 @@ def _evaluate_node_has_child_requirements(
)
return self.traceability_index.has_children_requirements(node)

def _evaluate_node_has_missing_relations(
self, node: SDocExtendedElementIF
) -> bool:
if not isinstance(node, SDocNode):
raise TypeError(
f"node.has_missing_relations can be only called on "
f"Requirement objects, got: {node.__class__.__name__}. To fix "
f"the error, prepend your query with node.is_requirement."
)
return self.traceability_index.has_missing_relations_for_requirement(
node
)

def _evaluate_node_contains(
self,
node: SDocExtendedElementIF,
Expand Down
2 changes: 2 additions & 0 deletions strictdoc/core/query_engine/query_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
NodeContainsExpression,
NodeFieldExpression,
NodeHasChildRequirementsExpression,
NodeHasMissingRelationsExpression,
NodeHasParentRequirementsExpression,
NodeIsRequirementExpression,
NodeIsRootExpression,
Expand All @@ -40,6 +41,7 @@
NodeContainsAnyFreeTextExpression,
NodeFieldExpression,
NodeHasChildRequirementsExpression,
NodeHasMissingRelationsExpression,
NodeHasParentRequirementsExpression,
NodeIsRequirementExpression,
NodeIsRootExpression,
Expand Down
Loading
Loading