diff --git a/codewiki/src/be/dependency_analyzer/leaf_selection.py b/codewiki/src/be/dependency_analyzer/leaf_selection.py index fd37b512..e0c15830 100644 --- a/codewiki/src/be/dependency_analyzer/leaf_selection.py +++ b/codewiki/src/be/dependency_analyzer/leaf_selection.py @@ -1,4 +1,5 @@ from typing import Dict, List, Set +import re from codewiki.src.be.dependency_analyzer.models.core import Node @@ -16,6 +17,11 @@ OOP_TYPES = {"class", "interface", "struct"} +# Error strings that occasionally reach leaf-node selection instead of an +# identifier. Matched on word boundaries and only for entries that are not +# known components, so that names like `handleInvalidInput` survive. +ERROR_MESSAGE_RE = re.compile(r"\b(error|exception|failed|invalid)\b", re.IGNORECASE) + def compute_valid_leaf_types(components: Dict[str, Node]) -> Set[str]: """ @@ -60,8 +66,13 @@ def filter_leaf_nodes( """Keep leaf nodes that are known components of a valid type.""" keep_leaf_nodes = [] for leaf_node in leaf_nodes: - # Skip any leaf nodes that are clearly error strings or invalid identifiers - if not isinstance(leaf_node, str) or leaf_node.strip() == "" or any(err_keyword in leaf_node.lower() for err_keyword in ['error', 'exception', 'failed', 'invalid']): + if not isinstance(leaf_node, str) or leaf_node.strip() == "": + logger.debug(f"Skipping invalid leaf node identifier: '{leaf_node}'") + continue + + # Only reject strings that look like error messages, not identifiers + # that merely contain such a word (handleInvalidInput, ErrorLog, ...). + if leaf_node not in components and ERROR_MESSAGE_RE.search(leaf_node): logger.debug(f"Skipping invalid leaf node identifier: '{leaf_node}'") continue diff --git a/tests/test_leaf_selection.py b/tests/test_leaf_selection.py new file mode 100644 index 00000000..d43f12b5 --- /dev/null +++ b/tests/test_leaf_selection.py @@ -0,0 +1,79 @@ +"""Tests for leaf-node identifier filtering. + +Covers filter_leaf_nodes: identifiers that merely contain a word like +"invalid" must survive, while strings that are actually error messages +reaching leaf-node selection instead of an identifier are dropped. +""" + +from __future__ import annotations + +from codewiki.src.be.dependency_analyzer.leaf_selection import filter_leaf_nodes +from codewiki.src.be.dependency_analyzer.models.core import Node + + +def _component(component_id: str, component_type: str = "function") -> Node: + file_path = component_id.split("::")[0] + return Node( + id=component_id, + name=component_id.split("::")[-1], + component_type=component_type, + file_path=file_path, + relative_path=file_path, + ) + + +def _components(*component_ids: str) -> dict[str, Node]: + return {cid: _component(cid) for cid in component_ids} + + +def test_identifiers_containing_error_words_are_kept() -> None: + components = _components( + "src/input.cpp::handleInvalidInput", + "src/game.cpp::gameFailedCheck", + "src/log.cpp::ErrorLog", + "src/parser.cpp::parseExceptionTable", + ) + + kept = filter_leaf_nodes(list(components), components, {"function"}) + + assert set(kept) == set(components) + + +def test_error_messages_are_dropped() -> None: + components = _components("src/player.cpp::playerMove") + candidates = [ + "src/player.cpp::playerMove", + "Error: could not parse file", + "invalid syntax at line 3", + "Analysis failed for this component", + ] + + kept = filter_leaf_nodes(candidates, components, {"function"}) + + assert kept == ["src/player.cpp::playerMove"] + + +def test_unknown_and_malformed_entries_are_dropped() -> None: + components = _components("src/player.cpp::playerMove") + candidates = [ + "src/player.cpp::playerMove", + "src/player.cpp::doesNotExist", + "", + " ", + None, + ] + + kept = filter_leaf_nodes(candidates, components, {"function"}) + + assert kept == ["src/player.cpp::playerMove"] + + +def test_components_of_other_types_are_dropped() -> None: + components = { + "src/game.cpp::runGame": _component("src/game.cpp::runGame", "function"), + "src/game.h::GameState": _component("src/game.h::GameState", "struct"), + } + + kept = filter_leaf_nodes(list(components), components, {"function"}) + + assert kept == ["src/game.cpp::runGame"]