diff --git a/graphify/affected.py b/graphify/affected.py index d73c2c0c6..e52845c23 100644 --- a/graphify/affected.py +++ b/graphify/affected.py @@ -1,5 +1,6 @@ from __future__ import annotations +import unicodedata from collections import deque from dataclasses import dataclass from pathlib import Path @@ -8,6 +9,16 @@ import networkx as nx +def _normalize(text: str) -> str: + """Return *text* in a canonical form so NFD and NFC compare equal. + + macOS stores filenames in NFD; Python string literals and most input + methods produce NFC. Comparing the two without normalization silently + fails for accented Latin, Japanese with dakuten, and Korean Hangul. + """ + return unicodedata.normalize("NFC", text) + + DEFAULT_AFFECTED_RELATIONS = ( "calls", "references", @@ -52,11 +63,11 @@ def _bare_name(label: str) -> str: def resolve_seed(graph: nx.Graph, query: str) -> str | None: if query in graph: return query - query_lower = query.lower() + query_lower = _normalize(query).lower() exact_label_matches = [ str(node_id) for node_id, data in graph.nodes(data=True) - if str(data.get("label", "")).lower() == query_lower + if _normalize(str(data.get("label", ""))).lower() == query_lower ] if len(exact_label_matches) == 1: return exact_label_matches[0] @@ -67,21 +78,21 @@ def resolve_seed(graph: nx.Graph, query: str) -> str | None: bare_name_matches = [ str(node_id) for node_id, data in graph.nodes(data=True) - if _bare_name(str(data.get("label", ""))) == query_bare + if _bare_name(_normalize(str(data.get("label", "")))) == query_bare ] if len(bare_name_matches) == 1: return bare_name_matches[0] exact_source_matches = [ str(node_id) for node_id, data in graph.nodes(data=True) - if str(data.get("source_file", "")).lower() == query_lower + if _normalize(str(data.get("source_file", ""))).lower() == query_lower ] if len(exact_source_matches) == 1: return exact_source_matches[0] contains_matches = [ str(node_id) for node_id, data in graph.nodes(data=True) - if query_lower in str(data.get("label", "")).lower() + if query_lower in _normalize(str(data.get("label", ""))).lower() ] if len(contains_matches) == 1: return contains_matches[0] diff --git a/tests/test_affected.py b/tests/test_affected.py new file mode 100644 index 000000000..0d96eb72b --- /dev/null +++ b/tests/test_affected.py @@ -0,0 +1,27 @@ +"""Unit tests for graphify.affected.resolve_seed. + +Covers Unicode normalization (NFC) so accented and CJK labels resolve +regardless of whether the query comes in NFC (Python source, most input +methods) or NFD (macOS filenames, some IMEs). +""" +from __future__ import annotations + +import unicodedata + +import networkx as nx + +from graphify.affected import resolve_seed + + +def _graph_with(label: str) -> nx.Graph: + """Build a one-node graph whose label is stored exactly as given.""" + g = nx.DiGraph() + g.add_node("n1", label=label, source_file="pkg/foo.py", source_location="L1") + return g + + +def test_resolve_seed_matches_nfd_query_against_nfc_label() -> None: + label_nfc = "Auditoría" + query_nfd = unicodedata.normalize("NFD", label_nfc) + graph = _graph_with(label_nfc) + assert resolve_seed(graph, query_nfd) == "n1"