From 14c4edb007cd1a452dddcd09b4ad1d23259d1e53 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 30 Jul 2026 14:41:50 -0700 Subject: [PATCH 1/3] Bound block-comment scanning by the earliest line-comment marker A line comment runs to end of line, so a block opener after one is text. The extractor searched openers first against an unbounded ceiling, so a `/*` inside a `//` comment opened a real block: the line's own comment was truncated at the opener, the closer carried into the lines below, and the code there was handed to the comment rules as prose. PowerShell fails the same way with `<#` inside a `#` comment. Those are the two fleet syntaxes carrying both marker kinds; CSS, XML, and the hash-only syntaxes carry one each and never reach the case. `min(cut, line_at)` keeps the reverse intact, so a `//` inside a block still belongs to the block, and the doc-marker skip is applied when locating the line marker so `///` does not become the ceiling. Tree-wide counts are unchanged - dash 963, comment-wrap 454, semicolon 388, comment-case 56 - because nothing here nests the markers that way. The exposure is downstream, in the C# and PowerShell repositories this extractor was written for, and it would surface the moment the comment rules gate rather than warn. The case is watched failing against the old extractor first, where it reports the swallowed code line as a second finding. Reported by Copilot on the promotion PR (#460). Co-Authored-By: Claude Opus 5 (1M context) --- scripts/prose_lint.py | 9 ++++++++- scripts/test_prose_lint.py | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index 97b26121..7c462c9a 100644 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -383,11 +383,18 @@ def extracted_comments(path: Path, lines: list[str]) -> list[tuple[int, str, boo closing = '' if end >= 0 else closing continue masked = strip_strings(line, spec['quotes']) + # A line comment runs to end of line, so a block opener after one is text. + # Left unbounded it opens a block that swallows the code lines below. + line_at = len(line) + for marker in spec['line']: + at = masked.find(marker) + if 0 <= at < line_at and not any(line[at:].startswith(d) for d in spec['doc']): + line_at = at cut = len(line) leading = True for opener, closer in spec['block']: at = masked.find(opener) - if 0 <= at < cut: + if 0 <= at < min(cut, line_at): if any(line[at:].startswith(d) for d in spec['doc']): continue cut, leading = at, not line[:at].strip() diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index 46288291..a2e9030d 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -368,6 +368,13 @@ def test_a_comment_inside_a_fenced_block_is_skipped(self) -> None: self.assertEqual(['comment-wrap'], self.flag('a.md', 'Prose.\n\n\n')) + def test_a_block_opener_inside_a_line_comment_is_text(self) -> None: + """Read as a real opener it opens a block, and the code lines below are linted as prose.""" + for name, text in (('a.cs', '// Match a /* opener here\nvar x = 1; // Two things. Here.\n'), + ('a.ps1', '# Match a <# opener here\n$x = 1 # Two things. Here.\n')): + with self.subTest(file=name): + self.assertEqual(['comment-wrap'], self.flag(name, text)) + def test_css_has_block_comments_only(self) -> None: """A `//` in CSS is the scheme separator of a URL, not a comment marker.""" self.assertEqual([], self.flag('a.css', 'a { background: url(http://x/y. Z); }\n')) From 64285b8c0ec6f4c27de0f99a96444914409d7b7d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 30 Jul 2026 14:48:28 -0700 Subject: [PATCH 2/3] Let a documentation comment bound the block-opener ceiling too Excluding doc markers from the ceiling reopened the defect for the syntax whose doc marker is itself a line comment. `/// See a /* opener here` left the ceiling unbounded, so the opener started a block and the code below it was linted as prose again - the same three-line failure, one marker along. The doc check belongs to what gets emitted, not to what counts as a comment: a `///` line runs to end of line like any other. Dropping it from the ceiling loop leaves the emit-side skip in place, so a doc comment still yields no findings. The case is watched failing against the previous commit before the change. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/prose_lint.py | 3 ++- scripts/test_prose_lint.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index 7c462c9a..ea2d5c53 100644 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -385,10 +385,11 @@ def extracted_comments(path: Path, lines: list[str]) -> list[tuple[int, str, boo masked = strip_strings(line, spec['quotes']) # A line comment runs to end of line, so a block opener after one is text. # Left unbounded it opens a block that swallows the code lines below. + # A documentation comment bounds it too, being skipped as prose rather than as a comment. line_at = len(line) for marker in spec['line']: at = masked.find(marker) - if 0 <= at < line_at and not any(line[at:].startswith(d) for d in spec['doc']): + if 0 <= at < line_at: line_at = at cut = len(line) leading = True diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index a2e9030d..24bce1b7 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -369,10 +369,15 @@ def test_a_comment_inside_a_fenced_block_is_skipped(self) -> None: self.flag('a.md', 'Prose.\n\n\n')) def test_a_block_opener_inside_a_line_comment_is_text(self) -> None: - """Read as a real opener it opens a block, and the code lines below are linted as prose.""" + """Read as a real opener it opens a block, and the code lines below are linted as prose. + + The documentation form is the same case: skipping it as prose must not unbound the + ceiling, or the syntax whose doc marker is a line comment reopens the defect. + """ for name, text in (('a.cs', '// Match a /* opener here\nvar x = 1; // Two things. Here.\n'), + ('a.cs', '/// See a /* opener here\nvar x = 1; // Two things. Here.\n'), ('a.ps1', '# Match a <# opener here\n$x = 1 # Two things. Here.\n')): - with self.subTest(file=name): + with self.subTest(file=name, line=text.split('\n')[0]): self.assertEqual(['comment-wrap'], self.flag(name, text)) def test_css_has_block_comments_only(self) -> None: From f8037abd28c998a17afd0c28acc3ad613d67aa7c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 30 Jul 2026 14:52:03 -0700 Subject: [PATCH 3/3] Say what the doc-comment exemption covers, and fix a verb "Skipped as prose" reads as though a doc comment were prose, when the point is the opposite: it is a comment here like any other, and only the linting exempts it. The ceiling loop is the place that distinction matters, so the comment on it should not invite the reading the code just stopped taking. The test docstring said "must not unbound the ceiling", which is not a verb. Both from the low-confidence block of the round-two review (#461). Co-Authored-By: Claude Opus 5 (1M context) --- scripts/prose_lint.py | 2 +- scripts/test_prose_lint.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/prose_lint.py b/scripts/prose_lint.py index ea2d5c53..31867bb7 100644 --- a/scripts/prose_lint.py +++ b/scripts/prose_lint.py @@ -385,7 +385,7 @@ def extracted_comments(path: Path, lines: list[str]) -> list[tuple[int, str, boo masked = strip_strings(line, spec['quotes']) # A line comment runs to end of line, so a block opener after one is text. # Left unbounded it opens a block that swallows the code lines below. - # A documentation comment bounds it too, being skipped as prose rather than as a comment. + # A documentation comment bounds it too, being exempt from linting rather than from here. line_at = len(line) for marker in spec['line']: at = masked.find(marker) diff --git a/scripts/test_prose_lint.py b/scripts/test_prose_lint.py index 24bce1b7..0384ad9d 100644 --- a/scripts/test_prose_lint.py +++ b/scripts/test_prose_lint.py @@ -371,8 +371,8 @@ def test_a_comment_inside_a_fenced_block_is_skipped(self) -> None: def test_a_block_opener_inside_a_line_comment_is_text(self) -> None: """Read as a real opener it opens a block, and the code lines below are linted as prose. - The documentation form is the same case: skipping it as prose must not unbound the - ceiling, or the syntax whose doc marker is a line comment reopens the defect. + The documentation form is the same case: exempting it from linting must not leave the + ceiling unbounded, or the syntax whose doc marker is a line comment reopens the defect. """ for name, text in (('a.cs', '// Match a /* opener here\nvar x = 1; // Two things. Here.\n'), ('a.cs', '/// See a /* opener here\nvar x = 1; // Two things. Here.\n'),