diff --git a/loopx/semantics/field_use.py b/loopx/semantics/field_use.py index 2b545757de..01703a8080 100644 --- a/loopx/semantics/field_use.py +++ b/loopx/semantics/field_use.py @@ -263,7 +263,11 @@ def _token_pattern(field: str) -> re.Pattern[str]: def lexical_module_count(field: str, suffix: str, sources: Iterable[SourceFile]) -> int: """The pre-B3 metric: modules whose text contains the standalone token.""" token = _token_pattern(field) - return sum(1 for source in sources if source.suffix == suffix and token.search(source.text)) + # Literal containment is a prerequisite of the same boundary regex. + return sum( + 1 for source in sources + if source.suffix == suffix and field in source.text and token.search(source.text) + ) def scan_field_uses( diff --git a/tests/architecture/test_semantic_field_use.py b/tests/architecture/test_semantic_field_use.py index 4051d26d81..3b3d7eaa17 100644 --- a/tests/architecture/test_semantic_field_use.py +++ b/tests/architecture/test_semantic_field_use.py @@ -16,6 +16,7 @@ FACTS, ROLES, field_use_summary, + lexical_module_count, python_module_scan, scan_field_uses, ) @@ -25,6 +26,20 @@ FIELDS = frozenset({FIELD}) +@pytest.mark.parametrize("suffix, expected", [(".py", 3), (".ts", 1)]) +def test_lexical_modules_preserve_boundaries_and_raw_source_text(suffix: str, expected: int) -> None: + sources = [ + SourceFile("comment.py", ".py", "# goal_boundary goal_boundary"), + SourceFile("prefix.py", ".py", "goal_boundary_repair repair_goal_boundary"), + SourceFile("unicode.py", ".py", "汉goal_boundary字"), + SourceFile("escaped.py", ".py", r'"goal_\u0062oundary"'), + SourceFile("broken.py", ".py", "def broken(:\n# goal_boundary"), + SourceFile("other.ts", ".ts", "// goal_boundary"), + SourceFile("absent.py", ".py", "payload.get(dynamic_key)"), + ] + assert lexical_module_count(FIELD, suffix, iter(sources)) == expected + + def python(text: str) -> dict[str, set[str]]: return python_module_scan(ast.parse(text), FIELDS)[0]