Skip to content

Commit f11cdf8

Browse files
committed
Fix script and add new testcases
1 parent 3c4a08c commit f11cdf8

File tree

2 files changed

+140
-82
lines changed

2 files changed

+140
-82
lines changed

clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py

Lines changed: 82 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import os
3030
import re
3131
import sys
32-
from typing import Dict, List, Optional, Sequence, Tuple, Union, overload
32+
from typing import Dict, List, Optional, Sequence, Tuple, NamedTuple
3333
from operator import itemgetter
3434

3535
# Matches a :doc:`label <path>` or :doc:`label` reference anywhere in text and
@@ -50,6 +50,63 @@
5050
RELEASE_NOTES_DOC = os.path.join(DOCS_DIR, "ReleaseNotes.rst")
5151

5252

53+
CheckLabel = str
54+
Lines = List[str]
55+
BulletBlock = List[str]
56+
BulletItem = Tuple[CheckLabel, BulletBlock]
57+
BulletStart = int
58+
59+
60+
class BulletBlocks(NamedTuple):
61+
"""Structured result of parsing a bullet-list section.
62+
63+
- prefix: lines before the first bullet within the section range.
64+
- blocks: list of (label, block-lines) pairs for each bullet block.
65+
- suffix: lines after the last bullet within the section range.
66+
"""
67+
prefix: Lines
68+
blocks: List[BulletItem]
69+
suffix: Lines
70+
71+
class ScannedBlocks(NamedTuple):
72+
"""Result of scanning bullet blocks within a section range.
73+
74+
- blocks_with_pos: list of (start_index, block_lines) for each bullet block.
75+
- next_index: index where scanning stopped; start of the suffix region.
76+
"""
77+
blocks_with_pos: List[Tuple[BulletStart, BulletBlock]]
78+
next_index: int
79+
80+
81+
def _scan_bullet_blocks(
82+
lines: Sequence[str], start: int, end: int
83+
) -> ScannedBlocks:
84+
"""Scan consecutive bullet blocks and return (blocks_with_pos, next_index).
85+
86+
Each entry in blocks_with_pos is a tuple of (start_index, block_lines).
87+
next_index is the index where scanning stopped (start of suffix).
88+
"""
89+
i = start
90+
n = end
91+
blocks_with_pos: List[Tuple[BulletStart, BulletBlock]] = []
92+
while i < n:
93+
if not _is_bullet_start(lines[i]):
94+
break
95+
bstart = i
96+
i += 1
97+
while i < n and not _is_bullet_start(lines[i]):
98+
if (
99+
i + 1 < n
100+
and set(lines[i + 1].rstrip("\n")) == {"^"}
101+
and lines[i].strip()
102+
):
103+
break
104+
i += 1
105+
block: BulletBlock = list(lines[bstart:i])
106+
blocks_with_pos.append((bstart, block))
107+
return ScannedBlocks(blocks_with_pos, i)
108+
109+
53110
def read_text(path: str) -> List[str]:
54111
with io.open(path, "r", encoding="utf-8") as f:
55112
return f.read().splitlines(True)
@@ -66,7 +123,7 @@ def _normalize_list_rst_lines(lines: Sequence[str]) -> List[str]:
66123
i = 0
67124
n = len(lines)
68125

69-
def key_for(line: str):
126+
def check_name(line: str):
70127
m = DOC_LINE_RE.match(line)
71128
if not m:
72129
return (1, "")
@@ -89,7 +146,7 @@ def key_for(line: str):
89146
entries.append(lines[i])
90147
i += 1
91148

92-
entries_sorted = sorted(entries, key=key_for)
149+
entries_sorted = sorted(entries, key=check_name)
93150
out.extend(entries_sorted)
94151
continue
95152

@@ -99,27 +156,10 @@ def key_for(line: str):
99156
return out
100157

101158

102-
@overload
103159
def normalize_list_rst(data: str) -> str:
104-
...
105-
106-
107-
@overload
108-
def normalize_list_rst(data: List[str]) -> List[str]:
109-
...
110-
111-
112-
def normalize_list_rst(data: Union[str, List[str]]) -> Union[str, List[str]]:
113-
"""Normalize list.rst; returns same type as input (str or list).
114-
115-
- If given a string, returns a single normalized string.
116-
- If given a sequence of lines, returns a list of lines.
117-
"""
118-
if isinstance(data, str):
119-
lines = data.splitlines(True)
120-
return "".join(_normalize_list_rst_lines(lines))
121-
else:
122-
return _normalize_list_rst_lines(data)
160+
"""Normalize list.rst content and return a string."""
161+
lines = data.splitlines(True)
162+
return "".join(_normalize_list_rst_lines(lines))
123163

124164

125165
def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
@@ -134,7 +174,7 @@ def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
134174
for i in range(len(lines) - 1):
135175
if lines[i].rstrip("\n") == title:
136176
underline = lines[i + 1].rstrip("\n")
137-
if underline and set(underline) == {"^"} and len(underline) >= len(title):
177+
if underline and set(underline) == {"^"} and len(underline) == len(title):
138178
return i
139179
return None
140180

@@ -144,44 +184,31 @@ def extract_label(text: str) -> str:
144184
return m.group("label") if m else text
145185

146186

147-
def is_bullet_start(line: str) -> bool:
187+
def _is_bullet_start(line: str) -> bool:
148188
return line.startswith("- ")
149189

150190

151-
def parse_bullet_blocks(
191+
def _parse_bullet_blocks(
152192
lines: Sequence[str], start: int, end: int
153-
) -> Tuple[List[str], List[Tuple[str, List[str]]], List[str]]:
193+
) -> BulletBlocks:
154194
i = start
155195
n = end
156196
first_bullet = i
157-
while first_bullet < n and not is_bullet_start(lines[first_bullet]):
197+
while first_bullet < n and not _is_bullet_start(lines[first_bullet]):
158198
first_bullet += 1
159-
prefix = list(lines[i:first_bullet])
199+
prefix: Lines = list(lines[i:first_bullet])
160200

161-
blocks: List[Tuple[str, List[str]]] = []
162-
i = first_bullet
163-
while i < n:
164-
if not is_bullet_start(lines[i]):
165-
break
166-
bstart = i
167-
i += 1
168-
while i < n and not is_bullet_start(lines[i]):
169-
if (
170-
i + 1 < n
171-
and set(lines[i + 1].rstrip("\n")) == {"^"}
172-
and lines[i].strip()
173-
):
174-
break
175-
i += 1
176-
block = list(lines[bstart:i])
177-
key = extract_label(block[0])
201+
blocks: List[BulletItem] = []
202+
res = _scan_bullet_blocks(lines, first_bullet, n)
203+
for _, block in res.blocks_with_pos:
204+
key: CheckLabel = extract_label(block[0])
178205
blocks.append((key, block))
179206

180-
suffix = list(lines[i:n])
181-
return prefix, blocks, suffix
207+
suffix: Lines = list(lines[res.next_index:n])
208+
return BulletBlocks(prefix, blocks, suffix)
182209

183210

184-
def sort_blocks(blocks: List[Tuple[str, List[str]]]) -> List[List[str]]:
211+
def sort_blocks(blocks: List[BulletItem]) -> List[BulletBlock]:
185212
"""Return blocks sorted deterministically by their extracted label.
186213
187214
Duplicates are preserved; merging is left to authors to handle manually.
@@ -206,24 +233,12 @@ def find_duplicate_entries(
206233
i = sec_start
207234
n = sec_end
208235

209-
while i < n and not is_bullet_start(lines[i]):
236+
while i < n and not _is_bullet_start(lines[i]):
210237
i += 1
211238

212239
blocks_with_pos: List[Tuple[str, int, List[str]]] = []
213-
while i < n:
214-
if not is_bullet_start(lines[i]):
215-
break
216-
bstart = i
217-
i += 1
218-
while i < n and not is_bullet_start(lines[i]):
219-
if (
220-
i + 1 < n
221-
and set(lines[i + 1].rstrip("\n")) == {"^"}
222-
and lines[i].strip()
223-
):
224-
break
225-
i += 1
226-
block = list(lines[bstart:i])
240+
res = _scan_bullet_blocks(lines, i, n)
241+
for bstart, block in res.blocks_with_pos:
227242
key = extract_label(block[0])
228243
blocks_with_pos.append((key, bstart, block))
229244

@@ -287,7 +302,7 @@ def _normalize_release_notes_section(
287302
return list(lines)
288303
_, sec_start, sec_end = bounds
289304

290-
prefix, blocks, suffix = parse_bullet_blocks(lines, sec_start, sec_end)
305+
prefix, blocks, suffix = _parse_bullet_blocks(lines, sec_start, sec_end)
291306
sorted_blocks = sort_blocks(blocks)
292307

293308
new_section: List[str] = []
@@ -374,20 +389,8 @@ def main(argv: Sequence[str]) -> int:
374389
else:
375390
return process_checks_list(out_path, list_doc)
376391

377-
list_lines = read_text(list_doc)
378-
rn_lines = read_text(rn_doc)
379-
list_norm = normalize_list_rst("".join(list_lines))
380-
rn_norm = normalize_release_notes(rn_lines)
381-
if "".join(list_lines) != list_norm:
382-
write_text(list_doc, list_norm)
383-
if "".join(rn_lines) != rn_norm:
384-
write_text(rn_doc, rn_norm)
385-
386-
report = _emit_duplicate_report(rn_lines, "Changes in existing checks")
387-
if report:
388-
sys.stderr.write(report)
389-
return 3
390-
return 0
392+
process_checks_list(list_doc, list_doc)
393+
return process_release_notes(rn_doc, rn_doc)
391394

392395

393396
if __name__ == "__main__":

clang-tools-extra/clang-tidy/tool/check_alphabetical_order_test.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ def test_normalize_list_rst_sorts_rows(self):
4141
out_str = _mod.normalize_list_rst("".join(input_lines))
4242
self.assertEqual(out_str, "".join(expected_lines))
4343

44-
out_lines = _mod.normalize_list_rst(input_lines)
45-
self.assertEqual(out_lines, expected_lines)
46-
4744
def test_find_heading(self):
4845
lines = [
4946
"- Deprecated the :program:`clang-tidy` ``zircon`` module. All checks have been\n",
@@ -281,6 +278,64 @@ def test_process_release_notes_with_duplicates_fails(self):
281278
out = f.read()
282279
self.assertEqual(out, "".join(rn_lines))
283280

281+
def test_release_notes_handles_nested_sub_bullets(self):
282+
rn_lines = [
283+
"Changes in existing checks\n",
284+
"^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
285+
"\n",
286+
"- Improved :doc:`bugprone-easily-swappable-parameters\n",
287+
" <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by\n",
288+
" correcting a spelling mistake on its option\n",
289+
" ``NamePrefixSuffixSilenceDissimilarityTreshold``.\n",
290+
"\n",
291+
"- Improved :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals\n",
292+
" <clang-tidy/checks/llvm/prefer-isa-or-dyn-cast-in-conditionals>` check:\n",
293+
"\n",
294+
" - Fix-it handles callees with nested-name-specifier correctly.\n",
295+
"\n",
296+
" - ``if`` statements with init-statement (``if (auto X = ...; ...)``) are\n",
297+
" handled correctly.\n",
298+
"\n",
299+
" - ``for`` loops are supported.\n",
300+
"\n",
301+
"- Improved :doc:`bugprone-exception-escape\n",
302+
" <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:\n",
303+
" exceptions from captures are now diagnosed, exceptions in the bodies of\n",
304+
" lambdas that aren't actually invoked are not.\n",
305+
"\n",
306+
]
307+
308+
out = _mod.normalize_release_notes(rn_lines)
309+
310+
expected_out = "".join(
311+
[
312+
"Changes in existing checks\n",
313+
"^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
314+
"\n",
315+
"- Improved :doc:`bugprone-easily-swappable-parameters\n",
316+
" <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by\n",
317+
" correcting a spelling mistake on its option\n",
318+
" ``NamePrefixSuffixSilenceDissimilarityTreshold``.\n",
319+
"\n",
320+
"- Improved :doc:`bugprone-exception-escape\n",
321+
" <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:\n",
322+
" exceptions from captures are now diagnosed, exceptions in the bodies of\n",
323+
" lambdas that aren't actually invoked are not.\n",
324+
"\n",
325+
"- Improved :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals\n",
326+
" <clang-tidy/checks/llvm/prefer-isa-or-dyn-cast-in-conditionals>` check:\n",
327+
"\n",
328+
" - Fix-it handles callees with nested-name-specifier correctly.\n",
329+
"\n",
330+
" - ``if`` statements with init-statement (``if (auto X = ...; ...)``) are\n",
331+
" handled correctly.\n",
332+
"\n",
333+
" - ``for`` loops are supported.\n",
334+
"\n\n",
335+
]
336+
)
337+
self.assertEqual(out, expected_out)
338+
284339
def test_process_checks_list_normalizes_output(self):
285340
list_lines = [
286341
".. csv-table:: List\n",

0 commit comments

Comments
 (0)