Skip to content

Commit cb73c5a

Browse files
committed
DOC: add code-review-lessons from PR comment analysis
New file Documentation/AI/code-review-lessons.md: 13 rules distilled from 8,484 inline review comments across 1,457 PRs (2017–2026), counted per-PR to avoid inflation. Methodology in the file. Also strengthens git-commits.md: 78-char subject limit is a hard constraint; all pre-commit hooks must pass before submitting a PR. Assisted-by: Claude Code — aggregated and classified PR review comments
1 parent 3e87231 commit cb73c5a

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

Documentation/AI/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Add file to table in AGENTS.md for routing:
2424
| File | When to load |
2525
|------|-------------|
2626
| `building.md` | Configuring or building ITK with Pixi or CMake |
27+
| `code-review-lessons.md` | Writing or reviewing C++ code — recurring reviewer concerns |
2728
| `testing.md` | Writing, running, or converting tests |
2829

2930
## Adding a New Context File
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# ITK Code Review Lessons — Recurring Reviewer Concerns
2+
3+
Distilled from 8,484 inline review comments across 1,457 pull requests
4+
spanning 2017–2026. These are patterns that ITK's core reviewers flag
5+
repeatedly; an AI assistant that avoids them will produce PRs that pass
6+
review with fewer round-trips.
7+
8+
---
9+
10+
## 1. Remove All Orphaned Artifacts After Refactoring
11+
12+
**Flagged on 31% of reviewed PRs (453/1,457). Active 2018–2026.**
13+
14+
When you remove the last user of a helper function, a local variable,
15+
an `#include`, or a type alias — also remove the definition itself.
16+
Reviewers catch orphaned code more than any other single issue.
17+
18+
```cpp
19+
// BAD — removed the std::cout print that used chBuffer,
20+
// but left the clGetPlatformInfo() call that populated it.
21+
clGetPlatformInfo(platform, CL_PLATFORM_NAME, sizeof(chBuffer), chBuffer, nullptr);
22+
23+
// GOOD — removed both the consumer AND the producer.
24+
```
25+
26+
**Checklist before submitting a refactoring PR:**
27+
- Grep for every symbol you modified or deleted.
28+
- If a helper, typedef, or include has zero remaining callers, remove it.
29+
- If a `using` alias was only public for one consumer, move it to `private`
30+
or remove it entirely.
31+
32+
---
33+
34+
## 2. Test Quality and GTest Conventions
35+
36+
**Flagged on 28% of reviewed PRs (411/1,457). Active 2017–2026.**
37+
38+
### Unique suite names per `.cxx` file
39+
40+
```cpp
41+
// BAD — "HeavisideStepFunction" reused across multiple test files:
42+
TEST(HeavisideStepFunction, ConvertedLegacyTest) // in file A
43+
TEST(HeavisideStepFunction, AnotherTest) // in file B
44+
45+
// GOOD — one unique suite name per .cxx file:
46+
TEST(SinRegularizedHeavisideStepFunction, ConvertedLegacyTest)
47+
```
48+
49+
### Use `ConvertedLegacyTest` for migrated CTest tests
50+
51+
When converting a legacy `itkFooTest.cxx` to GTest, name the test
52+
`TEST(Foo, ConvertedLegacyTest)` unless the test has a more specific
53+
purpose worth naming.
54+
55+
### Non-fatal assertions need null guards
56+
57+
`ITK_TEST_EXPECT_TRUE` is non-fatal — it records failure but continues.
58+
If a `dynamic_cast` might return null, guard before dereferencing:
59+
60+
```cpp
61+
// BAD — continues past null and crashes:
62+
auto * p = dynamic_cast<Derived *>(base.GetPointer());
63+
ITK_TEST_EXPECT_TRUE(p != nullptr);
64+
p->DoSomething(); // CRASH if dynamic_cast failed
65+
66+
// GOOD — bail immediately on null:
67+
auto * p = dynamic_cast<Derived *>(base.GetPointer());
68+
if (p == nullptr)
69+
{
70+
std::cerr << "dynamic_cast failed" << std::endl;
71+
return EXIT_FAILURE;
72+
}
73+
```
74+
75+
---
76+
77+
## 3. Include and Header Hygiene
78+
79+
**Flagged on 21% of reviewed PRs (313/1,457). Active 2018–2026.**
80+
81+
- Include only what you use. Do not leave includes for removed code.
82+
- Prefer forward declarations in headers when only a pointer or
83+
reference is needed.
84+
- After removing code, check whether any `#include` directives became
85+
orphaned.
86+
87+
---
88+
89+
## 4. Naming Clarity
90+
91+
**Flagged on 17% of reviewed PRs (254/1,457). Active 2018–2026.**
92+
93+
- Variable names should describe what they hold, not how they were computed.
94+
- After applying a limit or filter, rename the variable to reflect its
95+
new meaning (e.g., `nodes` → `displayed_nodes` after truncation).
96+
- Magic numbers should be named constants or use ITK's existing named
97+
constants (e.g., `itk::Statistics::MersenneTwisterRandomVariateGenerator::DefaultSeed`
98+
instead of `121212`).
99+
100+
---
101+
102+
## 5. Style Consistency Within a Function
103+
104+
**Flagged on 15% of reviewed PRs (214/1,457). Active 2018–2026.**
105+
106+
After a partial fix, check that the modified code is consistent with the
107+
rest of the function and file:
108+
109+
- If a function parameter type was updated, check that the function body
110+
uses the same qualified form.
111+
- If a naming pattern was corrected, apply the correction to all similar
112+
instances in the same scope — do not leave a mix.
113+
- Sub-section numbering, comment formatting, and JSON keys should be
114+
consistent within and across files.
115+
116+
---
117+
118+
## 6. Error Handling and Exception Safety
119+
120+
**Flagged on 14% of reviewed PRs (201/1,457). Active 2018–2026.**
121+
122+
- Check return values from functions that can fail (`dynamic_cast`,
123+
Python C API calls, file I/O). A `nullptr` or error return passed
124+
silently to the next line is a crash.
125+
- After removing cleanup code (e.g., `delete m_Writer; m_Writer = nullptr;`),
126+
verify the replacement (`unique_ptr`, RAII) provides equivalent
127+
exception-safety guarantees.
128+
- `EXPECT_NO_THROW` in GTest should wrap only the call being tested,
129+
not surrounding side-effects like `std::cout`.
130+
131+
---
132+
133+
## 7. Signed/Unsigned Conversions and `size_t`
134+
135+
**Flagged on 8% of reviewed PRs (120/1,457). Active 2018–2026.**
136+
137+
- Avoid narrowing conversions between signed and unsigned types.
138+
Prefer ITK's `SizeValueType` or `unsigned int` consistently.
139+
- Do not add `static_cast` just to silence a warning — ask whether the
140+
conversion is actually safe. Unnecessary casts obscure real bugs.
141+
- For template parameters, prefer `unsigned{VRows}` over
142+
`static_cast<unsigned int>(VRows)` — it is type-safe and rejects
143+
narrowing.
144+
145+
---
146+
147+
## 8. Locale-Safe Numeric Parsing
148+
149+
**Flagged on 8% of reviewed PRs (113/1,457). Active 2018–2026.**
150+
151+
`std::stod`, `std::stof`, `atof`, and `std::to_string` are
152+
locale-dependent. Under European locales they produce/consume `,`
153+
instead of `.` as the decimal separator, silently corrupting
154+
medical image metadata.
155+
156+
```cpp
157+
// BAD — locale-dependent:
158+
double value = std::stod(str);
159+
buffer << std::fixed << value;
160+
161+
// GOOD — locale-safe:
162+
buffer << itk::ConvertNumberToString(value);
163+
```
164+
165+
Use `itk::ConvertNumberToString()` for serialization.
166+
See `itk-locale-safe-migration` for the full set of affected functions.
167+
168+
---
169+
170+
## 9. When to Use `auto`
171+
172+
**Flagged on 7% of reviewed PRs (105/1,457). Active 2018–2026.**
173+
174+
ITK is not anti-`auto`, but reviewers reject it when the deduced type is
175+
not obvious from the initializer.
176+
177+
```cpp
178+
// GOOD — type is obvious from the RHS:
179+
const auto size = image->GetLargestPossibleRegion().GetSize();
180+
auto filter = MedianImageFilter::New();
181+
const auto it = container.begin();
182+
183+
// BAD — reader cannot guess the deduced type:
184+
const auto value = interp->EvaluateDerivativeAtContinuousIndex(index);
185+
// (value is a CovariantVector — not obvious)
186+
187+
// BETTER — spell out non-obvious types:
188+
const CovariantVectorType value = interp->EvaluateDerivativeAtContinuousIndex(index);
189+
```
190+
191+
**Rule of thumb:** use `auto` when the type name appears on the same line
192+
(factory methods, iterators, casts) or is unambiguous from the method name
193+
(`GetSize()`, `GetSpacing()`). Spell out the type when the return type
194+
requires knowledge of the class's internal typedefs.
195+
196+
---
197+
198+
## 10. ITK Initializer Patterns
199+
200+
**Flagged on 4% of reviewed PRs (57/1,457). Active 2018–2026.**
201+
202+
Use the single-expression forms for FixedArray-based types:
203+
204+
```cpp
205+
// BAD — two-line declare-then-fill:
206+
SizeType size;
207+
size.Fill(2);
208+
209+
// GOOD — single expression:
210+
constexpr auto size = SizeType::Filled(2);
211+
212+
// BAD:
213+
image->Allocate(true);
214+
215+
// GOOD:
216+
image->AllocateInitialized();
217+
218+
// BAD — zero-initialize in two lines:
219+
IndexType start;
220+
start.Fill(0);
221+
222+
// GOOD — brace initialization is zero-fill:
223+
constexpr IndexType start{};
224+
```
225+
226+
---
227+
228+
## 11. No C-Style Casts; Prefer Local Fixes Over Cascading Changes
229+
230+
**Flagged on 10 PRs (0.7%). Active 2019–2026. Every instance was a correctness concern.**
231+
232+
```cpp
233+
// BAD:
234+
unsigned int rows = (unsigned int)VRows;
235+
236+
// GOOD — prefer no cast; if needed:
237+
unsigned int rows = unsigned{VRows}; // type-safe, rejects narrowing
238+
unsigned int rows = static_cast<unsigned int>(VRows);
239+
```
240+
241+
Before adding any cast, ask: "Do I actually get a compiler warning without
242+
this?" If not, the cast is unnecessary and obscures the code.
243+
244+
When a cast exists because a local variable has a mismatched type, it is
245+
often better to change the local variable's type to eliminate the cast
246+
entirely. However, **do not cascade type changes into function signatures,
247+
template parameters, or public API boundaries** to avoid a cast. A small
248+
local `static_cast` or `T{x}` conversion is preferable to changing an
249+
API that downstream consumers depend on. The rule: fix the narrowest
250+
scope that removes the cast without altering any interface.
251+
252+
---
253+
254+
## 12. Redundant Namespace Qualifiers
255+
256+
**Flagged on 11 PRs (0.8%). Active 2021–2026.**
257+
258+
Code inside `namespace itk { ... }` should not prefix ITK symbols with
259+
`itk::`.
260+
261+
```cpp
262+
// BAD — inside a .cxx file that is already in namespace itk:
263+
itk::ConvertNumberToString(value);
264+
265+
// GOOD:
266+
ConvertNumberToString(value);
267+
```
268+
269+
---
270+
271+
## 13. AI-Generated Descriptions Must Be Factually Verified
272+
273+
**Flagged on 1 PR (2026). Severity: high — incorrect claims erode reviewer trust.**
274+
275+
AI-generated PR descriptions and review summaries have been observed
276+
claiming incorrect counts (e.g., "three temporary variables eliminated"
277+
when only one existed). This has been compared to known LLM counting
278+
errors.
279+
280+
**Rule:** Before submitting an AI-generated description, manually verify
281+
every concrete claim — counts, variable names, file paths, and behavioral
282+
assertions. If the AI says "N items were changed," count them yourself.
283+
284+
### Keep commit messages and PR descriptions in sync with scope
285+
286+
Refactoring, squashing, addressing reviewer comments, and adding fixup
287+
commits frequently change the scope of a PR. After any such change, AI
288+
tools must re-read all commit messages and the PR title/body and verify
289+
they still accurately describe what the PR does. Stale descriptions
290+
that reference removed work, omit added work, or overstate the change
291+
are a common source of reviewer confusion and erode trust in
292+
AI-assisted PRs.
293+
294+
**Checklist after every scope change:**
295+
- Does the PR title still describe the current change set?
296+
- Does each commit message accurately reflect its diff?
297+
- Were claims about "N files changed" or "M patterns fixed" invalidated
298+
by the scope change?
299+
- If commits were squashed, does the squashed message cover everything
300+
that was folded in?
301+
302+
---
303+
304+
## Methodology
305+
306+
Generated 2026-04-12 by analyzing 8,484 inline review comments across
307+
1,457 PRs (2017–2026) from the ITK GitHub repository. Topics counted
308+
per distinct PR, not per comment. See the PR description for details.

Documentation/AI/git-commits.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ Longer explanation if needed. Describe *what* changed and *why*,
1212
not just that a tool made the change.
1313
```
1414

15+
**The 78-character subject-line limit is a hard constraint, not a guideline.**
16+
The `kw-commit-msg.py` hook rejects commits that exceed it, and CI will
17+
reject PRs containing such commits. Count characters before committing.
18+
The prefix (e.g., `DOC: `) counts toward the 78. When a descriptive
19+
subject would exceed the limit, move detail to the commit body — the
20+
subject should be scannable in `git log --oneline`.
21+
22+
**Prefer ≤72 characters when practical.** GitHub's web UI, `git log`
23+
default terminal width, mail clients, and most other git tooling render
24+
subject lines optimally at 72 chars or fewer. 78 is the hard ceiling
25+
that avoids hook rejection; 72 is the soft target that avoids visual
26+
truncation and line-wrapping in downstream tools.
27+
1528
## Prefixes
1629

1730
| Prefix | Use for |

0 commit comments

Comments
 (0)