Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/dayamlchecker/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class FindingClass(StrEnum):

class MessageId(StrEnum):
YAML_DUPLICATE_KEY = "yaml_duplicate_key"
YAML_DUPLICATE_BLOCK_ID = "yaml_duplicate_block_id"
YAML_PARSE_ERROR = "yaml_parse_error"
YAML_STRING_REQUIRED = "yaml_string_required"

Expand Down Expand Up @@ -190,6 +191,13 @@ class MessageDefinition:
summary="YAML parsing error",
template="{error}",
),
MessageId.YAML_DUPLICATE_BLOCK_ID: MessageDefinition(
code="EG104",
severity=Severity.ERROR,
finding_class=FindingClass.GENERAL,
summary="Duplicate block id",
template='Duplicate block id "{block_id}" - first used at line {first_line}. Docassemble will silently use the later block, which is almost certainly a mistake',
),
MessageId.YAML_STRING_REQUIRED: MessageDefinition(
code="EG103",
severity=Severity.ERROR,
Expand Down
18 changes: 18 additions & 0 deletions src/dayamlchecker/yaml_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@ def find_errors_from_string(
]
yaml_parser = _make_yaml_parser()
prior_conditional_fields: list[dict[str, Any]] = []
seen_ids: dict[str, int] = {}
parsed_docs: list[ParsedInterviewDocument] = []
has_yaml_parse_errors = False
line_number = 1
Expand Down Expand Up @@ -1704,6 +1705,23 @@ def find_errors_from_string(
)
)

block_id = _get_case_insensitive(doc, "id")
if isinstance(block_id, str) and block_id.strip():
block_id_clean = block_id.strip()
block_start = line_number + 1 if line_number > 1 else line_number
if block_id_clean in seen_ids:
all_errors.append(
make_finding(
MessageId.YAML_DUPLICATE_BLOCK_ID,
file_name=input_file,
line_number=block_start,
block_id=block_id_clean,
first_line=seen_ids[block_id_clean],
)
)
else:
seen_ids[block_id_clean] = block_start

unmatched_refs = _find_unmatched_interview_order_references(
doc, prior_conditional_fields
)
Expand Down
74 changes: 74 additions & 0 deletions tests/test_yaml_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,80 @@ def test_accessibility_html_rules_are_reported(self):
f"Expected HTML accessibility parity findings, got: {errs}",
)

def test_duplicate_block_id_errors(self):
"""Error: two blocks with the same id should be flagged"""
invalid = """
id: intro
question: |
What is your name?
field: user_name
---
id: intro
mandatory: True
question: |
Hello
"""
errs = find_errors_from_string(invalid, input_file="<string_invalid>")
self.assertTrue(
any(e.message_id == "yaml_duplicate_block_id" for e in errs),
f"Expected duplicate id error, got: {errs}",
)

def test_unique_block_ids_valid(self):
"""Valid: blocks with different ids should pass"""
valid = """
id: intro
question: |
What is your name?
field: user_name
---
id: summary
mandatory: True
question: |
Hello
"""
errs = find_errors_from_string(valid, input_file="<string_valid>")
self.assertFalse(
any(e.message_id == "yaml_duplicate_block_id" for e in errs),
f"Did not expect duplicate id error, got: {errs}",
)

def test_duplicate_id_case_sensitive(self):
"""Valid: id matching is case sensitive"""
valid = """
id: intro
question: |
What is your name?
field: user_name
---
id: Intro
mandatory: True
question: |
Hello
"""
errs = find_errors_from_string(valid, input_file="<string_valid>")
self.assertFalse(
any(e.message_id == "yaml_duplicate_block_id" for e in errs),
f"Did not expect duplicate id error for different cases, got: {errs}",
)

def test_no_ids_valid(self):
"""Valid: blocks with no ids should pass clean"""
valid = """
question: |
What is your name?
field: user_name
---
mandatory: True
question: |
Hello
"""
errs = find_errors_from_string(valid, input_file="<string_valid>")
self.assertFalse(
any(e.message_id == "yaml_duplicate_block_id" for e in errs),
f"Did not expect duplicate id error with no ids, got: {errs}",
)


if __name__ == "__main__":
unittest.main()
Loading