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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# dotnet format style --verify-no-changes --severity=info --verbosity=detailed
# Verify with: dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true
Expand Down
2 changes: 2 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ A string that spans lines carries its state onto the lines it covers, so a marke

A comment sentence also has to start with a capital, which `comment-case` checks. A lowercase opening reads as the continuation of the line above it, so the two rules are read together: a wrapped sentence reports as `comment-wrap`, and a lowercase opening that is not a continuation reports as `comment-case`. Where the first word is a tool whose own casing is lowercase, the fix is to restructure rather than to capitalize the name against CODESTYLE's tooling-casing rule.

**A comment whose whole body is a URI is a reference rather than a sentence**, and neither rule applies to it. It cannot be capitalized or restructured without corrupting the address it exists to carry, so before the exemption every repo carrying a reference block inherited a finding no edit could answer. Consecutive reference lines are separate addresses rather than one sentence wrapping, which is why the exemption also stops the line below a URI from reading as its continuation. A URI inside a sentence is still prose, so the exemption requires the whole body to be the address and nothing else.

`charset` and `dupword` are clean tree-wide and gate CI. `charset-unknown`, `semicolon`, `dash`, `comment-wrap`, and `comment-case` run as one warn-only CI step, so the backlog is visible without blocking and is corrected as each file is next edited.

## `repo_gate.py`
Expand Down
10 changes: 9 additions & 1 deletion scripts/prose_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ def resume_at(carry: Carried, line: str) -> tuple[Carried, int | None]:
r'|cSpell|markdownlint|omit from toc|prettier|eslint|SPDX|Copyright'
r'|^v\d+(\.\d+)*$')

# A comment that is only a URI is a reference, not a sentence, so neither case nor wrap applies.
# It cannot be capitalized or restructured without corrupting the address it exists to carry.
# A URI inside a sentence is still prose, so the whole body has to be the address and nothing else.
# The angle brackets are matched as a pair or not at all.
# One bracket alone is a typo, and exempting it would hide the typo rather than report it.
# The scheme is case-insensitive per RFC 3986, so an uppercase one is the same reference.
BARE_URI = re.compile(r'^(?:<(?:https?|ftp)://[^>\s]+>|(?:https?|ftp)://[^>\s]+)$', re.IGNORECASE)

# Two sentences on one line, guarded against an abbreviation, an initial, or a dotted identifier.
# The initial guard anchors on a word boundary, so `J. Smith` reads as one name.
# A sentence ending in an acronym such as CI is two sentences and has to be caught.
Expand Down Expand Up @@ -755,7 +763,7 @@ def comment_wrap_findings(path: Path, raw: str, lines: list[str]) -> list[tuple[
prev_body = ''
prev_no = 0
for n, body, leading in comments:
if not body or NOT_PROSE.search(body):
if not body or NOT_PROSE.search(body) or BARE_URI.match(body.strip()):
prev_body = ''
continue
if RUN_ON.search(strip_inline_code(body)):
Expand Down
44 changes: 44 additions & 0 deletions scripts/test_prose_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,50 @@ def test_a_marker_inside_a_string_is_not_a_comment(self) -> None:
with self.subTest(file=name):
self.assertEqual([], self.flag(name, text))

def test_a_comment_that_is_only_a_uri_is_a_reference_not_a_sentence(self) -> None:
"""It cannot be capitalized or restructured without corrupting the address it carries.

A reference block opening a config file is the ordinary shape, so before this exemption
every repo carrying one inherited a finding no edit could answer.
"""
for name, text in (('a.yml', '# https://docs.github.com/en/code-security/dependabot\n'),
('.editorconfig', '; https://editorconfig.org\n'),
('a.cs', '// http://example.com/a_b.c\n'),
('a.xml', '<!-- https://example.com/schema -->\n'),
('a.yml', '# <https://example.com/bracketed>\n'),
('a.sh', '# ftp://example.com/pub\n')):
with self.subTest(file=name, comment=text.strip()):
self.assertEqual([], self.flag(name, text))

def test_a_uri_block_does_not_make_the_next_line_a_continuation(self) -> None:
"""Consecutive reference lines are separate addresses, not one sentence wrapping."""
self.assertEqual([], self.flag('a.yml', '# https://example.com/one\n'
'# https://example.com/two\n'))

def test_the_scheme_is_case_insensitive(self) -> None:
"""RFC 3986 makes the scheme case-insensitive, so an uppercase one is the same reference.

The continuation case is the one that matters: a scheme the exemption misses puts the
reference line back in the wrap logic, which is the false positive this exemption removes.
"""
self.assertEqual([], self.flag('a.yml', '# HTTPS://example.com/one\n'
'# Describes the format.\n'))
self.assertEqual([], self.flag('a.yml', '# Https://example.com/one\n'))

def test_an_unbalanced_angle_bracket_is_not_a_delimited_uri(self) -> None:
"""One bracket is a typo rather than a delimiter, so it is reported instead of exempted."""
for body in ('<https://example.com/one', 'https://example.com/one>'):
with self.subTest(body=body):
self.assertFalse(prose_lint.BARE_URI.match(body))
self.assertTrue(prose_lint.BARE_URI.match('<https://example.com/one>'))

def test_a_uri_inside_a_sentence_is_still_prose(self) -> None:
"""The whole body has to be the address, or the exemption would swallow real prose."""
self.assertEqual(['comment-wrap'],
self.flag('a.yml', f'# See https://example.com. {self.RUN_ON}\n'))
self.assertEqual(['comment-case'],
self.flag('a.yml', '# see https://example.com for the options\n'))

def test_a_documentation_comment_is_left_to_codestyle(self) -> None:
"""An XML doc comment and a docstring may run to paragraphs, which CODESTYLE governs."""
self.assertEqual([], self.flag('a.cs', f'/// <summary>{self.RUN_ON}</summary>\n'))
Expand Down
Loading