diff --git a/docs/conf.py b/docs/conf.py index 56031d451..dcab1e78c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,8 @@ project_url = "https://eclipse-score.github.io/docs-as-code/" version = "0.1" +required_in_id = ["blabasdfasdfsla"] + extensions = [ "score_sphinx_bundle", ] diff --git a/docs/how-to/write_docs.rst b/docs/how-to/write_docs.rst index 68f43676d..3d0f9f0fa 100644 --- a/docs/how-to/write_docs.rst +++ b/docs/how-to/write_docs.rst @@ -70,3 +70,47 @@ For further documentation on needextends please `look here ____``. +The ``<feature>`` part must relate to where the requirement lives in the documentation, +so that IDs stay meaningful and traceable as the project evolves. + +The feature part is validated by checking that at least one of the following is true: + +* A segment of the feature part (split on ``_`` and ``-``) appears in the document's directory path +* The initials of the feature part's segments appear in the document's directory path +* The feature part contains a string explicitly allowed via ``required_in_id`` in ``conf.py`` + +**Examples** — given a requirement in ``internals/safety/fmea/requirements.rst``: + +.. list-table:: + :header-rows: 1 + :widths: 45 10 45 + + * - ID + - + - Reason + * - ``feat_saf__fmea__late_message`` + - ✅ + - ``fmea`` is in the path + * - ``feat_saf__safety_fmea__late_message`` + - ✅ + - ``safety`` and ``fmea`` are in the path + * - ``feat_saf__sf__late_message`` + - ✅ + - ``sf`` are the initials of ``safety_fmea``, which is in the path + * - ``feat_saf__blabla__late_message`` + - ❌ + - ``blabla`` has no relation to the path ``internals/safety/fmea`` + +To explicitly allow a feature part that intentionally doesn't match the path +(e.g. in a single module repository), add a matching string to ``required_in_id`` in ``conf.py``: + +.. code-block:: python + + # conf.py + required_in_id = ["persistenc"] diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 97e4738e3..822d50991 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -235,6 +235,7 @@ def _clear_needs_defaults(app: Sphinx): 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("required_in_id", [], rebuild="env") config_setdefault(app.config, "needs_id_required", True) config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}") diff --git a/src/extensions/score_metamodel/checks/id_contains_feature.py b/src/extensions/score_metamodel/checks/id_contains_feature.py index 035cf18a9..47b486b22 100644 --- a/src/extensions/score_metamodel/checks/id_contains_feature.py +++ b/src/extensions/score_metamodel/checks/id_contains_feature.py @@ -58,19 +58,36 @@ def id_contains_feature(app: Sphinx, need: NeedItem, log: CheckLogger): for featurepart in featureparts if featureparts and featurepart and docname ) + allowed_parts_from_config = app.config.required_in_id + found_part_from_config = any( + part_from_config.lower() in need.get("id") + for part_from_config in allowed_parts_from_config + if allowed_parts_from_config + ) # allow abbreviation of the feature initials = ( "".join(fp[0].lower() for fp in featureparts) if len(featureparts) > 1 else "" ) foundinitials = bool(initials) and docname and initials in docname.lower() + if not (foundfeatpart or foundinitials or found_part_from_config): + parts_display = ", ".join(f"'{p}'" for p in featureparts) + config_display = ( + ", ".join(f"'{p}'" for p in allowed_parts_from_config) + if allowed_parts_from_config + else "[]" + ) + + fix_options = [f"rename the feature part to match a segment of '{docname}'"] + if initials: + fix_options.append(f"use correct abbreviation '{initials}'") + fix_options.append( + f"Add an allowed part to `required_in_id` in conf.py (currently: {config_display})" + ) - if not (foundfeatpart or foundinitials): - log.warning_for_option( - need, - "id", - ( - f"Featurepart '{featureparts}' not in path '{docname}' " - f"or abbreviation not ok, expected: '{initials}'." - ), + combined_msg = ( + f"Feature part {parts_display} not found in path '{docname}'. " + "\nHow can you fix this:\n=>" + f"{'\n=> '.join(fix_options)}.\n" ) + log.warning_for_option(need, "id", combined_msg) diff --git a/src/extensions/score_metamodel/tests/rst/id_contains_feature/test_id_contains_feature.rst b/src/extensions/score_metamodel/tests/rst/id_contains_feature/test_id_contains_feature.rst index 50b07c966..833c7ebfc 100644 --- a/src/extensions/score_metamodel/tests/rst/id_contains_feature/test_id_contains_feature.rst +++ b/src/extensions/score_metamodel/tests/rst/id_contains_feature/test_id_contains_feature.rst @@ -11,8 +11,10 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + #CHECK: id_contains_feature + .. Feature is in the path of the RST file #EXPECT-NOT[+2]: Feature 'id_contains_feature' not in path @@ -34,3 +36,19 @@ .. stkh_req:: This is a test :id: stkh_req__test__abce + + + +.. Check if feature is correctly found to not be in path +#EXPECT[+2]: Feature part 'abcabc' not found in path 'id_contains_feature'. + +.. feat_req:: Testing if warning correctly triggers + :id: feat_req__abcabc__testing + + + +.. Check if feature is correctly found to be in path +#EXPECT-NOT[+2]: Feature part + +.. feat_req:: Testing if warning correctly triggers + :id: feat_req__id_contains__testing