Skip to content

Commit 901ab59

Browse files
committed
Fix pony-lint blank-lines rule false positives on multi-line docstrings
_MaxLineVisitor only checked node.line() for AST nodes, which reports only the start line of string literals. For multi-line docstrings, this meant blank lines inside the docstring body were miscounted as blank lines between members. The fix uses end_pos() to get the actual end line for multi-line TK_STRING nodes, scoped to strings whose token_value contains a newline (single-line and empty strings skip end_pos() due to a pre-existing parsing bug in that method for short strings). Also restores multi-paragraph docstrings in pony-lsp that were collapsed in #5098 as a workaround for this bug. Closes #5099
1 parent 0e6215e commit 901ab59

File tree

6 files changed

+232
-5
lines changed

6 files changed

+232
-5
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Fix pony-lint blank-lines rule false positives on multi-line docstrings
2+
3+
The `style/blank-lines` rule incorrectly counted blank lines inside multi-line docstrings as blank lines between members. A method or field whose docstring contained blank lines (e.g., between paragraphs) would be flagged for having too many blank lines before the next member. The rule now correctly identifies where a docstring ends rather than using only its start line.

tools/pony-lint/_ast_dispatcher.pony

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,33 @@ class ref _MaxLineVisitor is ast.ASTVisitor
2222
then
2323
return ast.Continue
2424
end
25-
let l = node.line()
25+
// Multi-line string literals (docstrings) can span multiple
26+
// lines but node.line() only reports the opening """. Use
27+
// end_pos() to get the actual end line so blank lines inside
28+
// the docstring aren't miscounted as blank lines between
29+
// members. Only do this for strings whose value contains a
30+
// newline — single-line and empty strings use node.line()
31+
// because end_pos() has a parsing bug with short strings.
32+
let l =
33+
if node.id() == ast.TokenIds.tk_string() then
34+
let is_multiline =
35+
match node.token_value()
36+
| let s: String val => s.contains("\n")
37+
else
38+
false
39+
end
40+
if is_multiline then
41+
match node.end_pos()
42+
| let p: ast.Position => p.line()
43+
else
44+
node.line()
45+
end
46+
else
47+
node.line()
48+
end
49+
else
50+
node.line()
51+
end
2652
if l > max_line then max_line = l end
2753
ast.Continue
2854

tools/pony-lint/test/_test_blank_lines.pony

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,26 @@ class \nodoc\ _TestMaxLineVisitor is ast.ASTVisitor
457457
then
458458
return ast.Continue
459459
end
460-
let l = node.line()
460+
let l =
461+
if node.id() == ast.TokenIds.tk_string() then
462+
let is_multiline =
463+
match node.token_value()
464+
| let s: String val => s.contains("\n")
465+
else
466+
false
467+
end
468+
if is_multiline then
469+
match node.end_pos()
470+
| let p: ast.Position => p.line()
471+
else
472+
node.line()
473+
end
474+
else
475+
node.line()
476+
end
477+
else
478+
node.line()
479+
end
461480
if l > max_line then max_line = l end
462481
ast.Continue
463482

@@ -526,3 +545,174 @@ class \nodoc\ _TestBlankLinesBetweenDocstringEntities is UnitTest
526545
else
527546
h.fail("compilation failed")
528547
end
548+
549+
class \nodoc\ _TestBlankLinesMultiLineDocMethodClean is UnitTest
550+
"""
551+
Abstract method with multi-line docstring containing blank
552+
lines, followed by another method with 1 blank line — should
553+
be clean. Uses a trait so the docstring is the last descendant
554+
(no body expression to extend past it). Regression test for
555+
#5099.
556+
"""
557+
fun name(): String =>
558+
"BlankLines: multi-line doc method 1 blank is clean"
559+
560+
fun exclusion_group(): String => "ast-compile"
561+
562+
fun apply(h: TestHelper) =>
563+
let source: String val =
564+
"trait Foo\n" +
565+
" fun foo(): None\n" +
566+
" \"\"\"\n" +
567+
" First paragraph.\n" +
568+
"\n" +
569+
" Second paragraph.\n" +
570+
" \"\"\"\n" +
571+
"\n" +
572+
" fun bar(): None => None\n"
573+
try
574+
(let program, let sf) = _ASTTestHelper.compile(h, source)?
575+
match program.package()
576+
| let pkg: ast.Package val =>
577+
match pkg.module()
578+
| let mod: ast.Module val =>
579+
let diags = _CollectRuleDiags(mod, sf, lint.BlankLines)
580+
h.assert_eq[USize](0, diags.size())
581+
else
582+
h.fail("no module")
583+
end
584+
else
585+
h.fail("no package")
586+
end
587+
else
588+
h.fail("compilation failed")
589+
end
590+
591+
class \nodoc\ _TestBlankLinesMultiLineDocMethodNoBlank is UnitTest
592+
"""
593+
Abstract method with multi-line docstring containing blank
594+
lines, followed by another method with 0 blank lines — should
595+
be flagged. Verifies that end_pos() reports the correct end
596+
line and doesn't overshoot. Regression test for #5099.
597+
"""
598+
fun name(): String =>
599+
"BlankLines: multi-line doc method 0 blanks flagged"
600+
601+
fun exclusion_group(): String => "ast-compile"
602+
603+
fun apply(h: TestHelper) =>
604+
let source: String val =
605+
"trait Foo\n" +
606+
" fun foo(): None\n" +
607+
" \"\"\"\n" +
608+
" First paragraph.\n" +
609+
"\n" +
610+
" Second paragraph.\n" +
611+
" \"\"\"\n" +
612+
" fun bar(): None => None\n"
613+
try
614+
(let program, let sf) = _ASTTestHelper.compile(h, source)?
615+
match program.package()
616+
| let pkg: ast.Package val =>
617+
match pkg.module()
618+
| let mod: ast.Module val =>
619+
let diags = _CollectRuleDiags(mod, sf, lint.BlankLines)
620+
h.assert_true(diags.size() > 0)
621+
try
622+
h.assert_true(
623+
diags(0)?.message.contains("1 blank line"))
624+
else
625+
h.fail("could not access diagnostic")
626+
end
627+
else
628+
h.fail("no module")
629+
end
630+
else
631+
h.fail("no package")
632+
end
633+
else
634+
h.fail("compilation failed")
635+
end
636+
637+
class \nodoc\ _TestBlankLinesMultiLineDocFieldToMethod is UnitTest
638+
"""
639+
Field with multi-line docstring containing blank lines,
640+
followed by a method with 1 blank line — should be clean.
641+
Regression test for #5099.
642+
"""
643+
fun name(): String =>
644+
"BlankLines: multi-line doc field->method 1 blank is clean"
645+
646+
fun exclusion_group(): String => "ast-compile"
647+
648+
fun apply(h: TestHelper) =>
649+
let source: String val =
650+
"class Foo\n" +
651+
" let x: U32 = 0\n" +
652+
" \"\"\"\n" +
653+
" First paragraph.\n" +
654+
"\n" +
655+
" Second paragraph.\n" +
656+
" \"\"\"\n" +
657+
"\n" +
658+
" fun apply(): U32 => x\n"
659+
try
660+
(let program, let sf) = _ASTTestHelper.compile(h, source)?
661+
match program.package()
662+
| let pkg: ast.Package val =>
663+
match pkg.module()
664+
| let mod: ast.Module val =>
665+
let diags = _CollectRuleDiags(mod, sf, lint.BlankLines)
666+
h.assert_eq[USize](0, diags.size())
667+
else
668+
h.fail("no module")
669+
end
670+
else
671+
h.fail("no package")
672+
end
673+
else
674+
h.fail("compilation failed")
675+
end
676+
677+
class \nodoc\ _TestBlankLinesMultiLineDocEntities is UnitTest
678+
"""
679+
Entity with multi-line docstring containing blank lines,
680+
followed by another entity with 1 blank line — should be
681+
clean (between-entity check). Regression test for #5099.
682+
"""
683+
fun name(): String =>
684+
"BlankLines: multi-line doc entities 1 blank (AST pipeline)"
685+
686+
fun exclusion_group(): String => "ast-compile"
687+
688+
fun apply(h: TestHelper) =>
689+
let source: String val =
690+
"primitive Foo\n" +
691+
" \"\"\"\n" +
692+
" First paragraph.\n" +
693+
"\n" +
694+
" Second paragraph.\n" +
695+
" \"\"\"\n" +
696+
"\n" +
697+
"primitive Bar\n" +
698+
" \"\"\"Bar docs.\"\"\"\n"
699+
try
700+
(let program, let sf) = _ASTTestHelper.compile(h, source)?
701+
match program.package()
702+
| let pkg: ast.Package val =>
703+
match pkg.module()
704+
| let mod: ast.Module val =>
705+
let collector = _TestEntityCollector
706+
mod.ast.visit(collector)
707+
let entities = collector.entities()
708+
let diags = lint.BlankLines.check_module(entities, sf)
709+
h.assert_eq[USize](0, diags.size())
710+
else
711+
h.fail("no module")
712+
end
713+
else
714+
h.fail("no package")
715+
end
716+
else
717+
h.fail("compilation failed")
718+
end

tools/pony-lint/test/main.pony

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,17 @@ actor \nodoc\ Main is TestList
274274
test(_TestBlankLinesMultiLineMethodsOneBlank)
275275
test(_TestBlankLinesMultiLineMethodsNoBlank)
276276
test(_TestBlankLinesOneLineMethodsNoBlank)
277+
test(_TestBlankLinesMultiLineDocMethodClean)
278+
test(_TestBlankLinesMultiLineDocMethodNoBlank)
279+
test(_TestBlankLinesMultiLineDocFieldToMethod)
277280

278281
// BlankLines tests (between-entity)
279282
test(_TestBlankLinesBetweenEntitiesOneBlank)
280283
test(_TestBlankLinesBetweenEntitiesNoBlank)
281284
test(_TestBlankLinesBetweenEntitiesTooMany)
282285
test(_TestBlankLinesOneLinerEntitiesNoBlank)
283286
test(_TestBlankLinesBetweenDocstringEntities)
287+
test(_TestBlankLinesMultiLineDocEntities)
284288

285289
// IndentationSize tests
286290
test(_TestIndentationSizeClean)

tools/pony-lsp/compiler_notify.pony

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ trait tag LspCompiler
179179
be apply_settings(settings: (Settings | None))
180180
"""
181181
Provide settings to initialize or reconfigure the
182-
compiler. `None` can be provided when no new
182+
compiler.
183+
184+
`None` can be provided when no new
183185
settings should be applied, but the initialization
184186
step should be completed.
185187
"""

tools/pony-lsp/settings.pony

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ class val Settings
2222
"""
2323
Additional ponypath entries, that will be added
2424
to the entries that pony-lsp adds on its own:
25-
the stdlib path and path list extracted from the
26-
`PONYPATH` environment variable.
25+
26+
1. the stdlib path
27+
2. path list extracted from the `PONYPATH`
28+
environment variable
2729
"""
2830

2931
new val from_json(data: JsonObject) =>

0 commit comments

Comments
 (0)