Skip to content

Commit de4f3fc

Browse files
committed
fix: report syntax error line numbers from the original .plain file
1 parent 15961c3 commit de4f3fc

2 files changed

Lines changed: 59 additions & 5 deletions

File tree

plain_file.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,19 @@ def process_imports(
455455
return required_concepts
456456

457457

458+
def normalize_line_endings(plain_source_text: str) -> str:
459+
return plain_source_text.replace("\r\n", "\n").replace("\r", "\n")
460+
461+
462+
def restore_stripped_lines(plain_source_text: str, content: str) -> str:
463+
stripped_source = plain_source_text.rstrip()
464+
if not content or not stripped_source.endswith(content):
465+
return content
466+
467+
stripped_line_count = stripped_source[: len(stripped_source) - len(content)].count("\n")
468+
return "\n" * stripped_line_count + content
469+
470+
458471
def read_plain_source_metadata(plain_source_text):
459472
try:
460473
plain_source_obj = frontmatter.loads(plain_source_text)
@@ -490,6 +503,8 @@ def parse_plain_source( # noqa: C901
490503
imported_modules: list[str],
491504
modules_trace: list[str],
492505
) -> PlainFileParseResult:
506+
plain_source_text = normalize_line_endings(plain_source_text)
507+
493508
plain_source_obj = read_plain_source_metadata(plain_source_text)
494509

495510
plain_source = PLAIN_SOURCE_TEMPLATE.copy()
@@ -508,7 +523,9 @@ def parse_plain_source( # noqa: C901
508523

509524
[_, loaded_templates] = file_utils.get_loaded_templates(template_dirs, plain_source_text)
510525

511-
plain_source_full_text = render_plain_source(plain_source_obj.content, loaded_templates, code_variables)
526+
plain_source_content = restore_stripped_lines(plain_source_text, plain_source_obj.content)
527+
528+
plain_source_full_text = render_plain_source(plain_source_content, loaded_templates, code_variables)
512529

513530
plain_file = mistletoe.Document(io.StringIO(plain_source_full_text))
514531

tests/test_plainfileparser.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_unknown_section():
3636
with pytest.raises(
3737
Exception,
3838
match=re.escape(
39-
"Syntax error at line 3: Invalid specification heading (`Unknown Section:`). Allowed headings: definitions, implementation reqs, test reqs, functional specs, acceptance tests"
39+
"Syntax error at line 4: Invalid specification heading (`Unknown Section:`). Allowed headings: definitions, implementation reqs, test reqs, functional specs, acceptance tests"
4040
),
4141
):
4242
plain_file.parse_plain_source(plain_source, {}, [], [], [])
@@ -50,7 +50,7 @@ def test_duplicate_section():
5050
"""
5151
with pytest.raises(
5252
Exception,
53-
match=re.escape("Syntax error at line 3: Duplicate specification heading (`definitions`)"),
53+
match=re.escape("Syntax error at line 4: Duplicate specification heading (`definitions`)"),
5454
):
5555
plain_file.parse_plain_source(plain_source, {}, [], [], [])
5656

@@ -64,11 +64,48 @@ def test_invalid_top_level_element():
6464
"""
6565
with pytest.raises(
6666
Exception,
67-
match=re.escape("Syntax error at line 2: Invalid source structure (`code block`)"),
67+
match=re.escape("Syntax error at line 3: Invalid source structure (`code block`)"),
6868
):
6969
plain_file.parse_plain_source(plain_source, {}, [], [], [])
7070

7171

72+
def test_syntax_error_line_number_accounts_for_frontmatter():
73+
plain_source = """---
74+
description: 'Plain file with frontmatter'
75+
---
76+
77+
***definitions***
78+
79+
***Unknown Section:***
80+
"""
81+
with pytest.raises(
82+
Exception,
83+
match=re.escape("Syntax error at line 7: Invalid specification heading (`Unknown Section:`)"),
84+
):
85+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
86+
87+
88+
def test_syntax_error_line_number_with_windows_line_endings():
89+
plain_source = (
90+
"---\r\n"
91+
"description: 'Plain file with frontmatter'\r\n"
92+
"---\r\n"
93+
"\r\n"
94+
"***definitions***\r\n"
95+
"\r\n"
96+
"***Unknown Section:***\r\n"
97+
)
98+
with pytest.raises(
99+
Exception,
100+
match=re.escape("Syntax error at line 7: Invalid specification heading (`Unknown Section:`)"),
101+
):
102+
plain_file.parse_plain_source(plain_source, {}, [], [], [])
103+
104+
105+
def test_normalize_line_endings_does_not_duplicate_newlines():
106+
assert plain_file.normalize_line_endings("a\r\nb\rc\nd") == "a\nb\nc\nd"
107+
108+
72109
def test_plain_file_parser_with_comments(get_test_data_path):
73110
_, plain_sections, _ = plain_file.plain_file_parser(
74111
"plain_file_parser_with_comments.plain",
@@ -423,7 +460,7 @@ def test_acceptance_tests_top_level_rejected():
423460
with pytest.raises(
424461
PlainSyntaxError,
425462
match=re.escape(
426-
"Syntax error at line 1: acceptance tests heading should be nested under specific functional spec."
463+
"Syntax error at line 2: acceptance tests heading should be nested under specific functional spec."
427464
),
428465
):
429466
plain_file.parse_plain_source(plain_source, {}, [], [], [])

0 commit comments

Comments
 (0)