From f768eba1baffbcd69d301aca05da7813e1f974da Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 20 May 2026 11:27:01 +0200 Subject: [PATCH 1/3] WIP: Adding needextends forbidden options check --- .../score_metamodel/checks/check_options.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/extensions/score_metamodel/checks/check_options.py b/src/extensions/score_metamodel/checks/check_options.py index 1deec28e9..c76ba8556 100644 --- a/src/extensions/score_metamodel/checks/check_options.py +++ b/src/extensions/score_metamodel/checks/check_options.py @@ -22,6 +22,7 @@ from score_metamodel.metamodel_types import AllowedLinksType from sphinx.application import Sphinx from sphinx_needs.need_item import NeedItem +from sphinx_needs.data import SphinxNeedsData def get_need_type(needs_types: list[ScoreNeedType], directive: str) -> ScoreNeedType: @@ -291,3 +292,25 @@ def check_validity_consistency( f"valid_from ({valid_from}) >= valid_until ({valid_until})." ) log.warning_for_need(need, msg) + + +@local_check +def check_needextends_forbidden_options(app: Sphinx, need: NeedItem, log: CheckLogger): + extends_data = list(SphinxNeedsData(app.env).get_or_create_extends().values()) + dissallowed: set[str] = {"status", "safety", "security"} + for needsextends in extends_data: + location = f"{needsextends['docname']}:{needsextends['lineno']}" + modifications = needsextends["modifications"] + for option, _, _ in modifications: + if option in dissallowed: + log.warning( + f"Needextend in document: {needsextends['docname']} modifies {option} which is not allowed", + location, + ) + break + # print(f"{option=}, {extend_type.name=}, {value.value=}") + break + break + # # {'needextend-internals/requirements/requirements-0': {'docname': 'internals/requirements/requirements', 'lineno': 1196, 'target_id': 'needextend-internals/requirements/requirements-0', 'filter': "c.this_doc() and type == 'tool_req'", 'filter_is_id': False, 'modifications': [('security', , FieldLiteralValue(value='NO')), ('safety', , FieldLiteralValue(value='ASIL_B'))], 'list_modifications': [], 'strict': False}, 'needextend-internals/requirements/requirements-1': {'docname': 'internals/requirements/requirements', 'lineno': 1200, 'target_id': 'needextend-internals/requirements/requirements-1', 'filter': "c.this_doc() and type == 'tool_req' and not status", 'filter_is_id': False, 'modifications': [('status', , FieldLiteralValue(value='valid'))], 'list_modifications': [], 'strict': False}} + # print(extends_data) + pass From caf57fd9f4e80f7dcecb7ea553dc5f559f4639db Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 20 May 2026 11:46:24 +0200 Subject: [PATCH 2/3] Feat: Enable unmutable options in needextends check --- src/extensions/score_metamodel/__init__.py | 2 ++ src/extensions/score_metamodel/checks/check_options.py | 9 +-------- src/extensions/score_metamodel/metamodel.yaml | 6 ++++++ src/extensions/score_metamodel/yaml_parser.py | 7 +++++++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 557e7b974..3dd0c7726 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -294,6 +294,7 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]): def setup(app: Sphinx) -> dict[str, str | bool]: app.add_config_value("external_needs_source", "", rebuild="env") app.add_config_value("score_metamodel_yaml", "", rebuild="env") + app.add_config_value("unmutable_options", [], rebuild="env") config_setdefault(app.config, "needs_id_required", True) config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}") @@ -308,6 +309,7 @@ def setup(app: Sphinx) -> dict[str, str | bool]: app.config.needs_fields.update(metamodel.needs_fields) app.config.graph_checks = metamodel.needs_graph_check app.config.prohibited_words_checks = metamodel.prohibited_words_checks + app.config.unmutable_options = metamodel.unmutable_options # app.config.stop_words = metamodel["stop_words"] # app.config.weak_words = metamodel["weak_words"] diff --git a/src/extensions/score_metamodel/checks/check_options.py b/src/extensions/score_metamodel/checks/check_options.py index c76ba8556..c5a4efcd8 100644 --- a/src/extensions/score_metamodel/checks/check_options.py +++ b/src/extensions/score_metamodel/checks/check_options.py @@ -297,7 +297,7 @@ def check_validity_consistency( @local_check def check_needextends_forbidden_options(app: Sphinx, need: NeedItem, log: CheckLogger): extends_data = list(SphinxNeedsData(app.env).get_or_create_extends().values()) - dissallowed: set[str] = {"status", "safety", "security"} + dissallowed: list[str] = app.config.unmutable_options for needsextends in extends_data: location = f"{needsextends['docname']}:{needsextends['lineno']}" modifications = needsextends["modifications"] @@ -307,10 +307,3 @@ def check_needextends_forbidden_options(app: Sphinx, need: NeedItem, log: CheckL f"Needextend in document: {needsextends['docname']} modifies {option} which is not allowed", location, ) - break - # print(f"{option=}, {extend_type.name=}, {value.value=}") - break - break - # # {'needextend-internals/requirements/requirements-0': {'docname': 'internals/requirements/requirements', 'lineno': 1196, 'target_id': 'needextend-internals/requirements/requirements-0', 'filter': "c.this_doc() and type == 'tool_req'", 'filter_is_id': False, 'modifications': [('security', , FieldLiteralValue(value='NO')), ('safety', , FieldLiteralValue(value='ASIL_B'))], 'list_modifications': [], 'strict': False}, 'needextend-internals/requirements/requirements-1': {'docname': 'internals/requirements/requirements', 'lineno': 1200, 'target_id': 'needextend-internals/requirements/requirements-1', 'filter': "c.this_doc() and type == 'tool_req' and not status", 'filter_is_id': False, 'modifications': [('status', , FieldLiteralValue(value='valid'))], 'list_modifications': [], 'strict': False}} - # print(extends_data) - pass diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 7d82bde3b..bdbcc9a4f 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -53,6 +53,12 @@ prohibited_words_checks: - thing - absolutely +unmutable_options: + options: + - safety + - security + - status + needs_types: # See metamodel.md for how to define a new need type diff --git a/src/extensions/score_metamodel/yaml_parser.py b/src/extensions/score_metamodel/yaml_parser.py index 454a502c0..d43ce0b6e 100644 --- a/src/extensions/score_metamodel/yaml_parser.py +++ b/src/extensions/score_metamodel/yaml_parser.py @@ -15,6 +15,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, cast +from itertools import chain from ruamel.yaml import YAML from sphinx_needs import logging @@ -32,6 +33,7 @@ class MetaModelData: needs_types: list[ScoreNeedType] needs_links: dict[str, dict[str, str]] needs_fields: dict[str, dict[str, Any]] + unmutable_options: list[str] prohibited_words_checks: list[ProhibitedWordCheck] needs_graph_check: dict[str, object] @@ -49,6 +51,10 @@ def _parse_prohibited_words( ] +def _parse_unmutable_options(option_dict: dict[str, list[str]]) -> list[str]: + return list(chain(*option_dict.values())) + + def default_options(): """ Helper function to get a list of all default options defined by @@ -216,6 +222,7 @@ def load_metamodel_data(yaml_path: Path | None = None) -> MetaModelData: needs_types=list(needs_types.values()), needs_links=_parse_links(data.get("needs_extra_links", {})), needs_fields=_collect_all_custom_options(needs_types), + unmutable_options=_parse_unmutable_options(data.get("unmutable_options", {})), prohibited_words_checks=prohibited_words_checks, needs_graph_check=data.get("graph_checks", {}), ) From 39dc8b42999ebdd5ffab78a69db4c927af876515 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 20 May 2026 11:49:13 +0200 Subject: [PATCH 3/3] Chore: Formatting --- src/extensions/score_metamodel/checks/check_options.py | 2 +- src/extensions/score_metamodel/yaml_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/score_metamodel/checks/check_options.py b/src/extensions/score_metamodel/checks/check_options.py index c5a4efcd8..685c57a52 100644 --- a/src/extensions/score_metamodel/checks/check_options.py +++ b/src/extensions/score_metamodel/checks/check_options.py @@ -21,8 +21,8 @@ ) from score_metamodel.metamodel_types import AllowedLinksType from sphinx.application import Sphinx -from sphinx_needs.need_item import NeedItem from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.need_item import NeedItem def get_need_type(needs_types: list[ScoreNeedType], directive: str) -> ScoreNeedType: diff --git a/src/extensions/score_metamodel/yaml_parser.py b/src/extensions/score_metamodel/yaml_parser.py index d43ce0b6e..f17db9074 100644 --- a/src/extensions/score_metamodel/yaml_parser.py +++ b/src/extensions/score_metamodel/yaml_parser.py @@ -13,9 +13,9 @@ """Functionality related to reading in the SCORE metamodel.yaml""" from dataclasses import dataclass +from itertools import chain from pathlib import Path from typing import Any, cast -from itertools import chain from ruamel.yaml import YAML from sphinx_needs import logging