Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
37 changes: 30 additions & 7 deletions docuchango/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,13 +870,36 @@
else:
# Closing fence
if remainder:
# Closing fence has extra text - INVALID
# Per CommonMark: closing fence should have no info string
error = f"Line {line_num}: Closing code fence has extra text (```{remainder}), should be just ```"
doc.errors.append(error)
self.log(f" ✗ {doc.file_path.name}:{line_num} - Closing fence with text '```{remainder}'")
doc_invalid_blocks += 1
total_invalid += 1
# Closing fence has extra text - this might indicate an unclosed block earlier
# If the remainder looks like a language specifier (single word, lowercase/numbers),
# this is likely an opening fence that's being misinterpreted as closing
looks_like_language = remainder and " " not in remainder and len(remainder) <= 20

if looks_like_language and opening_line:
# Strong signal: unclosed block earlier
error = f"Unclosed code block starting at line {opening_line} (```{opening_language})"
doc.errors.append(error)
self.log(f" ✗ {doc.file_path.name} - Unclosed block at line {opening_line}")
error = f"Line {line_num}: This appears to be a new opening fence (```{remainder}), but was interpreted as a closing fence due to the unclosed block above"
doc.errors.append(error)
self.log(f" ℹ {doc.file_path.name}:{line_num} - Cascading error from unclosed block")
doc_invalid_blocks += 2 # Count both errors
total_invalid += 2
# Reset state and treat this as opening fence
in_code_block = True
opening_line = line_num
opening_language = remainder
previous_line_blank = False
continue
else:

Check failure on line 894 in docuchango/validator.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (RET507)

docuchango/validator.py:894:29: RET507 Unnecessary `else` after `continue` statement
# Actual closing fence with extra text
error = f"Line {line_num}: Closing code fence has extra text (```{remainder}), should be just ```"
doc.errors.append(error)
self.log(
f" ✗ {doc.file_path.name}:{line_num} - Closing fence with text '```{remainder}'"
)
doc_invalid_blocks += 1
total_invalid += 1
else:
# Valid closing fence
doc_valid_blocks += 1
Expand Down
78 changes: 78 additions & 0 deletions tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,81 @@
all_errors.extend(doc.errors)

assert len(all_errors) > 0, f"Fixture {fixture_file.name} should fail but passed validation"

def test_unclosed_code_fence_error_message(self, tmp_path):
"""Test that unclosed code fence errors are clear and point to the root cause (Issue #31)."""
docs_root = tmp_path / "test_issue31"
target_dir = docs_root / "docs-cms" / "memos"
target_dir.mkdir(parents=True, exist_ok=True)

# Create a document with an unclosed code fence that causes cascading errors
content = """---
id: "memo-999"
slug: memo-999-test
title: "Test Unclosed Fence"
date: "2025-11-16"
author: "Test Author"
created: "2025-11-16"
updated: "2025-11-16"
tags: ["test"]
project_id: "test-project"
doc_uuid: "12345678-1234-4000-8000-123456789012"
---

## Section 1

Some content here.

```markdown
This is markdown content.
More markdown content here.

## Section 2

This should trigger confusion because the markdown fence above is unclosed.

```text
This looks like a new code block.
```

## Section 3

More content.
"""
target_file = target_dir / "memo-999-test-unclosed.md"
target_file.write_text(content)

validator = DocValidator(repo_root=docs_root, verbose=False)
validator.scan_documents()
validator.check_code_blocks()

# Collect all errors
all_errors = []
for doc in validator.documents:
all_errors.extend(doc.errors)

# Should have at least 2 errors: unclosed block + cascading error
assert len(all_errors) >= 2, f"Expected at least 2 errors, got {len(all_errors)}: {all_errors}"

# Line 18 is where the ```markdown code block starts in the test content above (after frontmatter)
UNCLOSED_BLOCK_START_LINE = 18

Check failure on line 162 in tests/test_validator.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (N806)

tests/test_validator.py:162:9: N806 Variable `UNCLOSED_BLOCK_START_LINE` in function should be lowercase

# First error should mention the unclosed block at the original location
unclosed_errors = [
e for e in all_errors if f"Unclosed code block starting at line {UNCLOSED_BLOCK_START_LINE}" in e
]
assert len(unclosed_errors) > 0, (
f"Should detect unclosed block at line {UNCLOSED_BLOCK_START_LINE}. Errors: {all_errors}"
)

# Should have a cascading error explanation
cascading_errors = [
e for e in all_errors if "appears to be a new opening fence" in e and "interpreted as a closing fence" in e
]
assert len(cascading_errors) > 0, f"Should explain cascading error. Errors: {all_errors}"

# Should NOT have confusing "Closing fence has extra text" errors without context
confusing_errors = [
e for e in all_errors if "Closing code fence has extra text" in e and "appears to be" not in e
]
assert len(confusing_errors) == 0, f"Should not have confusing closing fence errors. Found: {confusing_errors}"
Loading