diff --git a/qc_otx/checks/core_checker/__init__.py b/qc_otx/checks/core_checker/__init__.py index 28b8a69..5fa137b 100644 --- a/qc_otx/checks/core_checker/__init__.py +++ b/qc_otx/checks/core_checker/__init__.py @@ -10,3 +10,4 @@ from . import public_main_procedure as public_main_procedure from . import mandatory_constant_initialization as mandatory_constant_initialization from . import unique_node_names as unique_node_names +from . import no_use_of_undefined_import_prefixes as no_use_of_undefined_import_prefixes diff --git a/qc_otx/checks/core_checker/core_checker.py b/qc_otx/checks/core_checker/core_checker.py index 768b5cd..7668060 100644 --- a/qc_otx/checks/core_checker/core_checker.py +++ b/qc_otx/checks/core_checker/core_checker.py @@ -17,6 +17,7 @@ public_main_procedure, mandatory_constant_initialization, unique_node_names, + no_use_of_undefined_import_prefixes, ) @@ -35,6 +36,7 @@ def run_checks(checker_data: models.CheckerData) -> None: document_name_package_uniqueness.check_rule, # Chk002 no_dead_import_links.check_rule, # Chk003 no_unused_imports.check_rule, # Chk004 + no_use_of_undefined_import_prefixes.check_rule, have_specification_if_no_realisation_exists.check_rule, # Chk007 public_main_procedure.check_rule, # Chk008 mandatory_constant_initialization.check_rule, # Chk009 diff --git a/qc_otx/checks/core_checker/no_dead_import_links.py b/qc_otx/checks/core_checker/no_dead_import_links.py index a5b8297..119ad7b 100644 --- a/qc_otx/checks/core_checker/no_dead_import_links.py +++ b/qc_otx/checks/core_checker/no_dead_import_links.py @@ -45,14 +45,16 @@ def check_rule(checker_data: models.CheckerData) -> None: import_prefix = import_node.get("prefix") import_package = import_node.get("package") import_document = import_node.get("document") + logging.debug( + f"import_prefix: {import_prefix} - import_package {import_package} - import_document {import_document}" + ) import_xpath = tree.getpath(import_node) - # Convert package name first.second.file.otx to filesystem path first/second/file.otx - import_package_splits = import_package.split(".") - import_path = "" - for import_package_element in import_package_splits: - import_path = os.path.join(import_path, import_package_element) - full_imported_path = os.path.join(import_path, import_document + ".otx") + # Import path checked in the same input file directory following + # Recommendation: Use only references inside the same package. + full_imported_path = os.path.join(import_document + ".otx") + logging.debug(f"cwd: {os.getcwd()}") + logging.debug(f"full_imported_path: {full_imported_path}") # Check if file exists import_file_exists = os.path.exists(full_imported_path) diff --git a/qc_otx/checks/core_checker/no_use_of_undefined_import_prefixes.py b/qc_otx/checks/core_checker/no_use_of_undefined_import_prefixes.py new file mode 100644 index 0000000..fbc7ed1 --- /dev/null +++ b/qc_otx/checks/core_checker/no_use_of_undefined_import_prefixes.py @@ -0,0 +1,88 @@ +import logging, os + +from typing import List + +from lxml import etree + +from qc_baselib import Result, IssueSeverity + +from qc_otx import constants +from qc_otx.checks import models, utils + +from qc_otx.checks.core_checker import core_constants + +logging.basicConfig(level=logging.DEBUG) + +RULE_SEVERITY = IssueSeverity.ERROR + +OTX_LINK_ATTRIBUTES = set() +OTX_LINK_ATTRIBUTES.add("implements") +OTX_LINK_ATTRIBUTES.add("validFor") +OTX_LINK_ATTRIBUTES.add("procedure") +OTX_LINK_ATTRIBUTES.add("valueOf") +OTX_LINK_ATTRIBUTES.add("mutexLock") + + +def check_rule(checker_data: models.CheckerData) -> None: + """ + Implements core checker rule Core_Chk005 + Criterion: If an imported name is accessed by prefix in an OtxLink type attribute, + the corresponding prefix definition shall exist in an element. + Severity: Critical + + """ + logging.info("Executing no_use_of_undefined_import_prefixes check") + + rule_uid = checker_data.result.register_rule( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=core_constants.CHECKER_ID, + emanating_entity="asam.net", + standard="otx", + definition_setting="1.0.0", + rule_full_name="core.chk_005.no_use_of_undefined_import_prefixes", + ) + + tree = checker_data.input_file_xml_root + root = tree.getroot() + + import_nodes = root.findall(".//import", namespaces=root.nsmap) + + if import_nodes is None: + logging.error("No import nodes found. Skipping check..") + return + + import_prefixes = [x.get("prefix") for x in import_nodes] + + attributes = utils.get_all_attributes(tree, root) + otx_link_attributes = [x for x in attributes if x.name in OTX_LINK_ATTRIBUTES] + + logging.debug(f"attributes: {attributes}") + logging.debug(f"import_prefixes: {import_prefixes}") + logging.debug(f"otx_link_attributes: {otx_link_attributes}") + + for otx_link in otx_link_attributes: + if ":" not in otx_link.value: + continue + current_value_split = otx_link.value.split(":") + if len(current_value_split) == 0: + continue + current_prefix = current_value_split[0] + + has_issue = current_prefix not in import_prefixes + + if has_issue: + issue_id = checker_data.result.register_issue( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=core_constants.CHECKER_ID, + description="Issue flagging when prefix definition does not exists in an import element", + level=RULE_SEVERITY, + rule_uid=rule_uid, + ) + + checker_data.result.add_xml_location( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=core_constants.CHECKER_ID, + issue_id=issue_id, + xpath=otx_link.xpath, + description=f"Imported prefix {current_prefix} not found across import elements", + ) diff --git a/qc_otx/checks/models.py b/qc_otx/checks/models.py index aadc41a..b8f6786 100644 --- a/qc_otx/checks/models.py +++ b/qc_otx/checks/models.py @@ -1,9 +1,23 @@ from dataclasses import dataclass from lxml import etree +from typing import Union from qc_baselib import Configuration, Result +@dataclass +class QueueNode: + element: etree._ElementTree + xpath: Union[str, None] + + +@dataclass +class AttributeInfo: + name: str + value: str + xpath: str + + @dataclass class CheckerData: input_file_xml_root: etree._ElementTree diff --git a/qc_otx/checks/utils.py b/qc_otx/checks/utils.py index 898a547..f7862cd 100644 --- a/qc_otx/checks/utils.py +++ b/qc_otx/checks/utils.py @@ -1,5 +1,42 @@ from lxml import etree -from typing import Union +from typing import Union, List +from qc_otx.checks.models import QueueNode, AttributeInfo + + +def get_all_attributes( + tree: etree._ElementTree, root: etree._Element +) -> List[AttributeInfo]: + """Function to get all attributes in input xml document + + Args: + tree (etree._ElementTree): the xml tree to analyse + root (etree._Element): the root node of the xml to analyse + + Returns: + _type_: _description_ + """ + attributes = [] + stack = [ + QueueNode(root, tree.getpath(root)) + ] # Initialize stack with the root element + + while stack: + current_node = stack.pop() + current_element = current_node.element + current_xpath = current_node.xpath + + # Process attributes of the current element + for attr, value in current_element.attrib.items(): + attributes.append(AttributeInfo(attr, value, current_xpath)) + + # Push children to the stack for further processing + stack.extend( + reversed( + [QueueNode(x, tree.getpath(x)) for x in current_element.getchildren()] + ) + ) + + return attributes def get_standard_schema_version(root: etree._ElementTree) -> Union[str, None]: diff --git a/tests/data/Core_Chk003/org/iso/otx/examples/ImportExample.otx b/tests/data/Core_Chk003/ImportExample.otx similarity index 100% rename from tests/data/Core_Chk003/org/iso/otx/examples/ImportExample.otx rename to tests/data/Core_Chk003/ImportExample.otx diff --git a/tests/data/Core_Chk005/Core_Chk005_negative.otx b/tests/data/Core_Chk005/Core_Chk005_negative.otx new file mode 100644 index 0000000..9ec4acf --- /dev/null +++ b/tests/data/Core_Chk005/Core_Chk005_negative.otx @@ -0,0 +1,49 @@ + + + + + + + + + + This defines global constant + + + + + + + + + + + Valid if executed in a workshop environment + + + + + + + + Valid if executed at an assembly line + + + + + + + + + + + + + + + diff --git a/tests/data/Core_Chk005/Core_Chk005_positive.otx b/tests/data/Core_Chk005/Core_Chk005_positive.otx new file mode 100644 index 0000000..ead91c7 --- /dev/null +++ b/tests/data/Core_Chk005/Core_Chk005_positive.otx @@ -0,0 +1,48 @@ + + + + + + + + + + This defines global constant + + + + + + + + + + + Valid if executed in a workshop environment + + + + + + + Valid if executed at an assembly line + + + + + + + + + + + + + + + diff --git a/tests/data/Core_Chk005/contexts.otx b/tests/data/Core_Chk005/contexts.otx new file mode 100644 index 0000000..60bfbae --- /dev/null +++ b/tests/data/Core_Chk005/contexts.otx @@ -0,0 +1,9 @@ + + + + Example for showing the OTX document root structure + diff --git a/tests/test_core_checks.py b/tests/test_core_checks.py index 03ca512..00d13f8 100644 --- a/tests/test_core_checks.py +++ b/tests/test_core_checks.py @@ -187,6 +187,51 @@ def test_chk004_negative( test_utils.cleanup_files() +def test_chk005_positive( + monkeypatch, +) -> None: + base_path = "tests/data/Core_Chk005" + target_file_name = f"Core_Chk005_positive.otx" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + core_issues = result.get_issues_by_rule_uid( + "asam.net:otx:1.0.0:core.chk_005.no_use_of_undefined_import_prefixes" + ) + assert len(core_issues) == 0 + test_utils.cleanup_files() + + +def test_chk005_negative( + monkeypatch, +) -> None: + base_path = "tests/data/Core_Chk005" + target_file_name = f"Core_Chk005_negative.otx" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + core_issues = result.get_issues_by_rule_uid( + "asam.net:otx:1.0.0:core.chk_005.no_use_of_undefined_import_prefixes" + ) + assert len(core_issues) == 1 + assert core_issues[0].level == IssueSeverity.ERROR + assert "foo" in core_issues[0].locations[0].description + + test_utils.cleanup_files() + + def test_chk007_positive( monkeypatch, ) -> None: