From d0367d474cbdc4776906883a3b678627511e58ff Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 13:58:36 -0400 Subject: [PATCH 1/6] Add no-dayc suppression comments --- src/dayamlchecker/yaml_structure.py | 142 +++++++++++++++++++++++++++- tests/test_yaml_structure.py | 44 +++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) diff --git a/src/dayamlchecker/yaml_structure.py b/src/dayamlchecker/yaml_structure.py index 382526f..73e9532 100644 --- a/src/dayamlchecker/yaml_structure.py +++ b/src/dayamlchecker/yaml_structure.py @@ -51,6 +51,145 @@ DEFAULT_LINT_MODE = "default" ACCESSIBILITY_LINT_MODE = "accessibility" +_SUPPRESSION_RE = re.compile( + r"#\s*no-dayc(?P-block)?\s*:\s*(?P[^#]+)", + re.IGNORECASE, +) +_SUPPRESS_ALL_CODES = frozenset({"*", "ALL"}) + + +def _parse_suppression_codes(raw_codes: str) -> frozenset[str]: + return frozenset( + code.strip().upper() + for code in re.split(r"[\s,]+", raw_codes) + if code.strip() + ) + + +def _finding_matches_suppression(finding: Finding, codes: frozenset[str]) -> bool: + if not codes: + return False + if codes & _SUPPRESS_ALL_CODES: + return True + return finding.code.upper() in codes or str(finding.message_id).upper() in codes + + +def _document_line_ranges(full_content: str) -> list[tuple[int, int]]: + lines = full_content.splitlines() + ranges: list[tuple[int, int]] = [] + start_line = 1 + + for line_number, line in enumerate(lines, start=1): + if document_match.match(line): + if start_line <= line_number - 1: + ranges.append((start_line, line_number - 1)) + start_line = line_number + 1 + + if start_line <= len(lines): + ranges.append((start_line, len(lines))) + + return ranges + + +def _parse_dayc_suppressions( + full_content: str, +) -> tuple[dict[int, frozenset[str]], list[tuple[int, int, frozenset[str]]]]: + inline_by_line: dict[int, frozenset[str]] = {} + block_directives: list[tuple[int, frozenset[str]]] = [] + + for line_number, line in enumerate(full_content.splitlines(), start=1): + match = _SUPPRESSION_RE.search(line) + if not match: + continue + + codes = _parse_suppression_codes(match.group("codes")) + if not codes: + continue + + if match.group("scope"): + block_directives.append((line_number, codes)) + else: + inline_by_line[line_number] = inline_by_line.get( + line_number, frozenset() + ) | codes + + block_ranges: list[tuple[int, int, frozenset[str]]] = [] + document_ranges = _document_line_ranges(full_content) + + for directive_line, codes in block_directives: + for start_line, end_line in document_ranges: + if start_line <= directive_line <= end_line: + block_ranges.append((start_line, end_line, codes)) + break + + return inline_by_line, block_ranges + + +def _finding_is_suppressed( + finding: Finding, + suppressions: tuple[ + dict[int, frozenset[str]], list[tuple[int, int, frozenset[str]]] + ], +) -> bool: + if finding.line_number is None: + return False + + inline_by_line, block_ranges = suppressions + + inline_codes = inline_by_line.get(finding.line_number) + if inline_codes and _finding_matches_suppression(finding, inline_codes): + return True + + return any( + start_line <= finding.line_number <= end_line + and _finding_matches_suppression(finding, codes) + for start_line, end_line, codes in block_ranges + ) + + +def _apply_dayc_suppressions( + findings: list[Finding], full_content: str +) -> list[Finding]: + suppressions = _parse_dayc_suppressions(full_content) + return [ + finding + for finding in findings + if not _finding_is_suppressed(finding, suppressions) + ] + + +def _apply_dayc_suppressions_from_files(findings: list[Finding]) -> list[Finding]: + suppressions_by_file: dict[ + str, + tuple[ + dict[int, frozenset[str]], + list[tuple[int, int, frozenset[str]]], + ] + | None, + ] = {} + filtered: list[Finding] = [] + + for finding in findings: + file_name = finding.file_name + if not file_name or file_name.startswith("<"): + filtered.append(finding) + continue + + if file_name not in suppressions_by_file: + try: + suppressions_by_file[file_name] = _parse_dayc_suppressions( + Path(file_name).read_text() + ) + except OSError: + suppressions_by_file[file_name] = None + + suppressions = suppressions_by_file[file_name] + if suppressions is not None and _finding_is_suppressed(finding, suppressions): + continue + + filtered.append(finding) + + return filtered @dataclass(frozen=True) @@ -1937,7 +2076,7 @@ def find_errors_from_string( options=style_options, ) ) - return all_errors + return _apply_dayc_suppressions(all_errors, full_content) _COMMON_COURTFORMSONLINE_METADATA_FIELDS = ( @@ -2365,6 +2504,7 @@ def main(argv: Optional[list[str]] = None) -> int: ) all_findings.extend(url_check_result.issues) + all_findings = _apply_dayc_suppressions_from_files(all_findings) had_error = False warning_count = sum(1 for f in all_findings if f.severity == "warning") diff --git a/tests/test_yaml_structure.py b/tests/test_yaml_structure.py index de4cf92..dc4d47b 100644 --- a/tests/test_yaml_structure.py +++ b/tests/test_yaml_structure.py @@ -2171,6 +2171,50 @@ def test_mandatory_null_errors(self): f"Expected mandatory null error, got: {errs}", ) + def test_inline_suppression_hides_matching_code(self): + yaml_content = """question: First +question: Second # no-dayc: EG101 +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertFalse(_has_code(errs, "EG101"), f"Expected EG101 suppressed: {errs}") + + def test_inline_suppression_does_not_hide_other_codes(self): + yaml_content = """question: First +question: Second # no-dayc: WG999 +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertTrue(_has_code(errs, "EG101"), f"Expected EG101 to remain: {errs}") + + def test_block_suppression_hides_matching_code_in_document(self): + yaml_content = """--- +# no-dayc-block: WG123 +code: | + answer = 1 + def helper(): + return answer +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertFalse(_has_code(errs, "WG123"), f"Expected WG123 suppressed: {errs}") + + def test_block_suppression_does_not_leak_to_next_document(self): + yaml_content = """# no-dayc-block: WG123 +code: | + answer = 1 + def helper(): + return answer +--- +code: | + answer = 1 + def helper(): + return answer +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertEqual( + sum(err.code == "WG123" for err in errs), + 1, + f"Expected only second WG123 to remain: {errs}", + ) + class TestALLinterParityRules(unittest.TestCase): def test_missing_question_id_is_reported(self): From 6baaa9dd4ffb8f65b1c2964f83611a0fc27c6717 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 14:04:37 -0400 Subject: [PATCH 2/6] Add --suppress CLI argument and support filtering by finding class --- src/dayamlchecker/yaml_structure.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/dayamlchecker/yaml_structure.py b/src/dayamlchecker/yaml_structure.py index 73e9532..8ffa9e6 100644 --- a/src/dayamlchecker/yaml_structure.py +++ b/src/dayamlchecker/yaml_structure.py @@ -71,7 +71,11 @@ def _finding_matches_suppression(finding: Finding, codes: frozenset[str]) -> boo return False if codes & _SUPPRESS_ALL_CODES: return True - return finding.code.upper() in codes or str(finding.message_id).upper() in codes + return ( + finding.code.upper() in codes + or str(finding.message_id).upper() in codes + or str(finding.finding_class).upper() in codes + ) def _document_line_ranges(full_content: str) -> list[tuple[int, int]]: @@ -2314,6 +2318,12 @@ def main(argv: Optional[list[str]] = None) -> int: type=Path, help="YAML files or directories to validate (directories are searched recursively)", ) + parser.add_argument( + "--suppress", + action="append", + default=[], + help="Suppress a finding by ID or class. Can be a comma-separated list or passed multiple times.", + ) parser.add_argument( "--check-all", action="store_true", @@ -2505,6 +2515,13 @@ def main(argv: Optional[list[str]] = None) -> int: all_findings.extend(url_check_result.issues) all_findings = _apply_dayc_suppressions_from_files(all_findings) + if args.suppress: + cli_suppressed_codes = _parse_suppression_codes(",".join(args.suppress)) + all_findings = [ + f for f in all_findings + if not _finding_matches_suppression(f, cli_suppressed_codes) + ] + had_error = False warning_count = sum(1 for f in all_findings if f.severity == "warning") From 2c08215a49cf6482db792f5135e458cb5544612c Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 14:06:36 -0400 Subject: [PATCH 3/6] Update README with documentation for suppression checks --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 38dd8cd..d3392b8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,30 @@ pip install . python3 -m dayamlchecker `find . -name "*.yml" -path "*/questions/*" snot -path "*/.venv/*" -not -path "*/build/*"` # i.e. a space separated list of files ``` +## Suppressing checks + +You can suppress specific errors or warnings by their ID or finding class (`accessibility`, `style`, `translatability`, `general`). + +**Inline and block comments in YAML:** +To suppress a finding on a specific line, use a `# no-dayc: ` comment: +```yaml +question: Second # no-dayc: EG101 +``` +To suppress findings for an entire document or block, use `# no-dayc-block: ` before the block: +```yaml +--- +# no-dayc-block: style, WG123 +code: | + answer = 1 +``` +You can use `ALL` or `*` to suppress all findings on a line/block (`# no-dayc: ALL`). Multiple codes can be separated by spaces or commas. + +**Command-line argument:** +To globally suppress findings across all files being checked, pass a comma-separated list of IDs or classes to the `--suppress` parameter: +```bash +python3 -m dayamlchecker --suppress accessibility,EG101 path/to/interview.yml +``` + ## WCAG checks The checker includes WCAG-style checks for clear static accessibility failures in interview source. These checks run by default; use `--no-wcag` to disable them. From ce56da200ca6a0aa0b3db385d638370265a3a5d6 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 14:10:19 -0400 Subject: [PATCH 4/6] Format with black --- src/dayamlchecker/yaml_structure.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/dayamlchecker/yaml_structure.py b/src/dayamlchecker/yaml_structure.py index 8ffa9e6..af1cdcf 100644 --- a/src/dayamlchecker/yaml_structure.py +++ b/src/dayamlchecker/yaml_structure.py @@ -60,9 +60,7 @@ def _parse_suppression_codes(raw_codes: str) -> frozenset[str]: return frozenset( - code.strip().upper() - for code in re.split(r"[\s,]+", raw_codes) - if code.strip() + code.strip().upper() for code in re.split(r"[\s,]+", raw_codes) if code.strip() ) @@ -113,9 +111,9 @@ def _parse_dayc_suppressions( if match.group("scope"): block_directives.append((line_number, codes)) else: - inline_by_line[line_number] = inline_by_line.get( - line_number, frozenset() - ) | codes + inline_by_line[line_number] = ( + inline_by_line.get(line_number, frozenset()) | codes + ) block_ranges: list[tuple[int, int, frozenset[str]]] = [] document_ranges = _document_line_ranges(full_content) @@ -2518,7 +2516,8 @@ def main(argv: Optional[list[str]] = None) -> int: if args.suppress: cli_suppressed_codes = _parse_suppression_codes(",".join(args.suppress)) all_findings = [ - f for f in all_findings + f + for f in all_findings if not _finding_matches_suppression(f, cli_suppressed_codes) ] From 85ca2d34f3460c6a8158585f1750db95447aa10e Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 15:06:43 -0400 Subject: [PATCH 5/6] Address PR feedback --- README.md | 2 +- src/dayamlchecker/yaml_structure.py | 12 +++++++----- tests/test_yaml_structure.py | 18 ++++++++++++++++++ tests/test_yaml_structure_cli.py | 24 ++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d3392b8..f88a13d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To suppress a finding on a specific line, use a `# no-dayc: ` comment: ```yaml question: Second # no-dayc: EG101 ``` -To suppress findings for an entire document or block, use `# no-dayc-block: ` before the block: +To suppress findings for an entire document or block, use `# no-dayc-block: ` inside the block (e.g., after the `---` document marker): ```yaml --- # no-dayc-block: style, WG123 diff --git a/src/dayamlchecker/yaml_structure.py b/src/dayamlchecker/yaml_structure.py index af1cdcf..fe0ae57 100644 --- a/src/dayamlchecker/yaml_structure.py +++ b/src/dayamlchecker/yaml_structure.py @@ -133,11 +133,13 @@ def _finding_is_suppressed( dict[int, frozenset[str]], list[tuple[int, int, frozenset[str]]] ], ) -> bool: - if finding.line_number is None: - return False - inline_by_line, block_ranges = suppressions + if finding.line_number is None: + return any( + _finding_matches_suppression(finding, codes) for _, _, codes in block_ranges + ) + inline_codes = inline_by_line.get(finding.line_number) if inline_codes and _finding_matches_suppression(finding, inline_codes): return True @@ -180,9 +182,9 @@ def _apply_dayc_suppressions_from_files(findings: list[Finding]) -> list[Finding if file_name not in suppressions_by_file: try: suppressions_by_file[file_name] = _parse_dayc_suppressions( - Path(file_name).read_text() + Path(file_name).read_text(encoding="utf-8") ) - except OSError: + except (OSError, UnicodeDecodeError): suppressions_by_file[file_name] = None suppressions = suppressions_by_file[file_name] diff --git a/tests/test_yaml_structure.py b/tests/test_yaml_structure.py index dc4d47b..c63a659 100644 --- a/tests/test_yaml_structure.py +++ b/tests/test_yaml_structure.py @@ -2215,6 +2215,24 @@ def helper(): f"Expected only second WG123 to remain: {errs}", ) + def test_inline_suppression_hides_matching_class(self): + yaml_content = """question: First +question: Second # no-dayc: general +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertFalse(_has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}") + + def test_block_suppression_hides_matching_class_in_document(self): + yaml_content = """--- +# no-dayc-block: general +question: First +question: Second +""" + errs = find_errors_from_string(yaml_content, input_file="") + self.assertFalse(_has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}") + + + class TestALLinterParityRules(unittest.TestCase): def test_missing_question_id_is_reported(self): diff --git a/tests/test_yaml_structure_cli.py b/tests/test_yaml_structure_cli.py index fc27314..6c7c011 100644 --- a/tests/test_yaml_structure_cli.py +++ b/tests/test_yaml_structure_cli.py @@ -428,3 +428,27 @@ def fake_run_url_check(**kwargs): assert captured["yaml_severity"] == "error" assert captured["document_severity"] == "ignore" assert captured["unreachable_severity"] == "error" + + +def test_main_cli_suppress_suppresses_findings(monkeypatch, tmp_path): + interview = tmp_path / "interview.yml" + interview.write_text("question: First\nquestion: Second\n") + + monkeypatch.setattr( + sys, + "argv", + ["dayamlchecker", "--suppress", "EG101", str(interview)], + ) + assert yaml_structure.main() == 0 + + +def test_main_cli_suppress_does_not_suppress_unmatched_findings(monkeypatch, tmp_path): + interview = tmp_path / "interview.yml" + interview.write_text("question: First\nquestion: Second\n") + + monkeypatch.setattr( + sys, + "argv", + ["dayamlchecker", "--suppress", "WG999", str(interview)], + ) + assert yaml_structure.main() == 1 From a4c6957d837e4c239cced3c2c4a4d2d8660e6310 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 15:09:45 -0400 Subject: [PATCH 6/6] Format w/ black --- tests/test_yaml_structure.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_yaml_structure.py b/tests/test_yaml_structure.py index c63a659..14a9a2c 100644 --- a/tests/test_yaml_structure.py +++ b/tests/test_yaml_structure.py @@ -2220,7 +2220,9 @@ def test_inline_suppression_hides_matching_class(self): question: Second # no-dayc: general """ errs = find_errors_from_string(yaml_content, input_file="") - self.assertFalse(_has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}") + self.assertFalse( + _has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}" + ) def test_block_suppression_hides_matching_class_in_document(self): yaml_content = """--- @@ -2229,9 +2231,9 @@ def test_block_suppression_hides_matching_class_in_document(self): question: Second """ errs = find_errors_from_string(yaml_content, input_file="") - self.assertFalse(_has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}") - - + self.assertFalse( + _has_code(errs, "EG101"), f"Expected EG101 suppressed by class: {errs}" + ) class TestALLinterParityRules(unittest.TestCase):