diff --git a/PythonScripts/audit_translations/README.md b/PythonScripts/audit_translations/README.md index 8aedf1265..241a500d2 100644 --- a/PythonScripts/audit_translations/README.md +++ b/PythonScripts/audit_translations/README.md @@ -12,7 +12,8 @@ The tool analyzes rule files to detect the following issues: * **Rule Differences:** Structural changes (match expressions, conditions, variables, or test/replace layout) between the source and target translation. * **Definition Coverage:** Compares literal `definitions.yaml` entries by name and collection kind (`vector`, `set`, or `map`). -Add `# audit-ignore` to a rule block to suppress auditing that rule. +After a blank line, add `# audit-ignore` immediately before a rule to suppress +auditing that rule. Existing markers inside a rule block are also supported. --- diff --git a/PythonScripts/audit_translations/parsers.py b/PythonScripts/audit_translations/parsers.py index 11dfc5292..bcbdd0027 100644 --- a/PythonScripts/audit_translations/parsers.py +++ b/PythonScripts/audit_translations/parsers.py @@ -149,11 +149,39 @@ def format_tag(tag_value: Any) -> str | None: def build_raw_blocks(lines: list[str], starts: list[int]) -> list[str]: - blocks = [] if not starts: - return blocks + return [] + + block_starts = starts.copy() for idx, start in enumerate(starts): - end = starts[idx + 1] if idx + 1 < len(starts) else len(lines) + if idx == 0: + while start > 0 and (not lines[start - 1].strip() or lines[start - 1].lstrip().startswith("#")): + start -= 1 + block_starts[idx] = start + continue + + lower_bound = starts[idx - 1] + 1 + dash_line = re.fullmatch(r"\s*-\s*(?:#.*)?", lines[start - 1]) if start > lower_bound else None + if dash_line: + start -= 1 + block_starts[idx] = start + continue + + comment_start = start + while comment_start > lower_bound and lines[comment_start - 1].lstrip().startswith("#"): + comment_start -= 1 + + # A separating blank makes the comment run introductory text for this + # item. Without one, leave the run with the preceding rule. + if comment_start > lower_bound and not lines[comment_start - 1].strip(): + start = comment_start - 1 + while start > lower_bound and not lines[start - 1].strip(): + start -= 1 + block_starts[idx] = start + + blocks = [] + for idx, start in enumerate(block_starts): + end = block_starts[idx + 1] if idx + 1 < len(block_starts) else len(lines) blocks.append("\n".join(lines[start:end])) return blocks diff --git a/PythonScripts/audit_translations/tests/test_parsers.py b/PythonScripts/audit_translations/tests/test_parsers.py index db7f31e1f..6714f3581 100644 --- a/PythonScripts/audit_translations/tests/test_parsers.py +++ b/PythonScripts/audit_translations/tests/test_parsers.py @@ -174,6 +174,91 @@ def test_detects_audit_ignore(self): rules = parse_rules_file(content, data) assert rules[0].audit_ignore + def test_assigns_leading_audit_ignore_to_following_rule(self): + """A marker directly above a rule suppresses that rule, not its neighbour.""" + content = """ +- name: first + tag: mo + match: "." + +# audit-ignore: the following placeholder intentionally differs +# from the source-language rule. +- name: second + tag: mi + match: "false()" +""" + yaml = YAML() + data = yaml.load(content) + rules = parse_rules_file(content, data) + assert not rules[0].audit_ignore + assert rules[1].audit_ignore + + def test_keeps_audit_ignore_inside_rule(self): + """Existing markers within a rule block remain supported.""" + content = """ +- name: first + tag: mo + # audit-ignore + match: "." + +- name: second + tag: mi + match: "x" +""" + yaml = YAML() + data = yaml.load(content) + rules = parse_rules_file(content, data) + assert rules[0].audit_ignore + assert not rules[1].audit_ignore + + def test_keeps_touching_comment_with_preceding_rule(self): + """A comment without a separating blank remains part of the preceding rule.""" + content = """ +- name: first + tag: mo + match: "." +# audit-ignore: this comment documents the first rule +- name: second + tag: mi + match: "x" +""" + yaml = YAML() + data = yaml.load(content) + rules = parse_rules_file(content, data) + assert rules[0].audit_ignore + assert not rules[1].audit_ignore + + def test_includes_comment_on_item_dash_line(self): + """A comment attached to an item's dash belongs to that item.""" + content = """ +- name: first + tag: mo + match: "." + +- # second rule introduction + name: second + tag: mi + match: "x" +""" + yaml = YAML() + data = yaml.load(content) + rules = parse_rules_file(content, data) + assert "second rule introduction" not in rules[0].raw_content + assert "second rule introduction" in rules[1].raw_content + + def test_assigns_file_header_to_first_rule(self): + """A leading marker before the first item is included in its raw block.""" + content = """ +# audit-ignore +- name: first + tag: mo + match: "." +""" + yaml = YAML() + data = yaml.load(content) + rules = parse_rules_file(content, data) + assert rules[0].audit_ignore + def test_handles_array_tag(self): """Ensure handles array tag.""" content = """- name: multi-tag