Repository navigation
refactor(diag): unify warning prefixes onto the warn() severity ladder #1199
Description
Status: fixed on
fix/1199-unify-warning-prefixes(315efcbb),
PR pending. All nine plan steps done. Two corrections to the analysis
below, recorded here so the body is not read as current:
- The library has five capitalised sites, not three. The sweep
also foundsrc/output/code_climate.rs:94(empty repo-relative
path) andsrc/concurrent_files.rs:227(not a regular file).- Step 3 is answered by a library-side
src/diag.rscarrying a
singlepub(crate) fn warn, the counterpart to the CLI ladder,
in addition to bringing CSV ontowarn_non_utf8_path. The helper
alone covers only the non-UTF-8-path warnings.Steps 1-2 are byte-for-byte a no-op as predicted. The one deliberate
output change is theIncludeCycletrailing newline, dropped per
step 5. See the closing comment for the evidence.
Problem
Diagnostic severity prefixes are produced in three different places
with two different conventions, and the library has no access to the
CLI helper that defines the house style.
big-code-analysis-cli/src/diag.rs:24 is the intended shape — the
three-severity ladder added in #609:
pub(crate) fn warn(msg: impl Display) {
eprintln!("warning: {msg}");
}Two library surfaces bypass it and bake a capitalised prefix into the
message instead:
src/output/csv.rs:194and:229—"Warning: skipping non-UTF-8 source path in CSV output: {}"src/output/offenders.rs:35—"Warning: skipping non-UTF-8 path in {format} output: {}"
A third, PreprocDiagnostic's Display impl (src/preproc.rs:76-108),
also self-prefixes. Its five variants were internally inconsistent —
three capitalised, two not — which #1198 fixed by lowercasing all five.
That was chosen deliberately as a stepping stone to this issue rather
than as the end state (see below).
bca preproc surfaces the diagnostics with a bare eprintln!, not
through warn():
// big-code-analysis-cli/src/commands/preproc.rs:82-84
for diagnostic in fix_includes(&mut data.files, &all_files) {
eprintln!("{diagnostic}");
}Proposed change
Move the severity prefix out of the message producers and onto the
presentation layer, so warn() is the single place warning: is
written.
- Drop the
warning:prefix from all fivePreprocDiagnostic
variants, leavingDisplayas the bare message. - Route
commands/preproc.rs:83throughdiag::warn. - Decide the same question for
output/csv.rsand
output/offenders.rs. These live in the library and cannot call the
CLI'spub(crate)helper, so they need either their own convention
or a library-side equivalent — worth settling explicitly rather than
leaving two capitalised stragglers.
Why step 1 and 2 are a no-op on output
warn(msg) expands to eprintln!("warning: {msg}"). Because #1198
lowercased the prefix first, eprintln!("{diagnostic}") on a
Display that yields warning: possible self inclusion foo.h and
warn("possible self inclusion foo.h") emit the same bytes. So steps
1-2 should be verifiable as a pure refactor with no user-visible
diff — which is the whole reason the flip went downward.
preproc_diagnostic_display_renders_each_single_line_variant and
preproc_diagnostic_display_lists_every_cycle_member
(src/preproc_tests.rs) pin the current text and are the check that it
stayed a no-op; both will need their expectations moved to the CLI
layer as part of the change.
Wrinkles
IncludeCycleis multi-line.warn()prefixes only the first
line, which happens to be the shape wanted here (header prefixed,
member lines indented under it) — but confirm rather than assume.IncludeCycleends with a trailing newline andeprintln!adds
another, so each cycle block currently prints followed by a blank
line. Pre-existing and now pinned by the test above. This is the
natural place to decide whether that blank line is wanted.
Not a breaking change
STABILITY.md:158 — "The Display impls are stable; the exact wording
of Display output is not." PreprocDiagnostic is a public export
(src/lib.rs:340), so this clause is what clears the change.
Resolution Plan
Confirmed against main (39674df6). All five PreprocDiagnostic
variants are lowercase (src/preproc.rs:80, :83, :95, :100,
:104) per #1198; the two capitalised stragglers are live at
src/output/csv.rs:194, :229 and src/output/offenders.rs:35; and
big-code-analysis-cli/src/commands/preproc.rs:83 still prints with a
bare eprintln!.
Step 3 already has an answer in the tree
The issue leaves the library-side convention open. It is largely settled
already: offenders.rs:30 defines warn_non_utf8_path(format, path),
and five output formats route through it — SARIF (sarif.rs:230),
Code Climate (code_climate.rs:89), Checkstyle (checkstyle.rs:52) and
both warning-line flavours (warning_line.rs:58, :91). CSV is the only
format that does not; it open-codes the same warning twice with a
different wording. So the library does not need a new convention, it
needs CSV brought onto the existing one.
That also settles the more principled alternative — propagating the
diagnostic to the caller so the CLI can route it through warn().
write_csv and write_csv_aggregate (src/output/csv.rs:185, :217)
are pub, so changing their signatures is a SemVer break and cannot
land in the 2.x line (STABILITY.md, AGENTS.md). Not worth deferring the
consistency fix to a major bump for; note it as the 3.0 option and move on.
- Drop the
warning:prefix from all fivePreprocDiagnostic
variants, leavingDisplayas the bare message. - Route
commands/preproc.rs:83throughdiag::warn. - Move the two
PreprocDiagnosticDisplaytests
(preproc_diagnostic_display_renders_each_single_line_variant,
preproc_diagnostic_display_lists_every_cycle_member, in
src/preproc_tests.rs) to assert the bare message, and add a CLI-side
test asserting the rendered stderr still readswarning: …. Both
halves are needed: the library test alone stops guarding the prefix,
and the CLI test alone stops guarding the message. - Prove steps 1-3 are a no-op on output.
warn(msg)expands to
eprintln!("warning: {msg}"), so post-fix: batch of fifteen issues (#1180-#1182, #1184-#1192, #1194-#1196) #1198 the bytes should be
identical. Capturebca preprocstderr on a fixture producing each of
the five variants before and after and diff them. This is the whole
justification for fix: batch of fifteen issues (#1180-#1182, #1184-#1192, #1194-#1196) #1198 having flipped downward first; if the diff is
non-empty, something in the wrinkles below is wrong. - Settle the two
IncludeCyclewrinkles the issue flags, rather than
inheriting them.warn()prefixes only the first line, which is the
shape wanted (header prefixed, members indented under it) — confirm on
real output. The trailing newline pluseprintln!'s own produces a
blank line after each cycle block; that is pre-existing and now pinned
by the test above, so this is the moment to decide whether it stays.
Recommend dropping it: it is an artifact ofwriteln!in aDisplay
impl, not a deliberate separator, andDisplayimpls should not end in
a newline. If it stays, say why in a comment so the next reader does
not "fix" it. - Bring CSV onto
warn_non_utf8_path. Replace bothcsv.rssites
withwarn_non_utf8_path("CSV", source_path), which lowercases the
prefix and removes the duplicated wording as a side effect. Check the
wording change is acceptable: the shared helper says "skipping
non-UTF-8 path in CSV output", the current CSV text says "non-UTF-8
source path". If that distinction matters, widen the helper rather
than keeping a second copy. - Lowercase the remaining prefix in
warn_non_utf8_pathitself
(offenders.rs:35) — after step 6 this is the library's single site,
and it fixes all six formats at once. - Add a guard so a seventh site cannot reappear. The failure mode
here is that a neweprintln!("Warning: …")reads as correct in
review. Arg-based check inutils/wired intomake lint
(asserting noWarning:/Error:capitalised literal outside test
modules) is a few lines and follows the existing gate conventions,
including a*-test.pyself-test. Without it this issue is a cleanup
that will need doing again. - Changelog.
## [Unreleased]→Changed, noting the CSV
non-UTF-8 warning's wording and casing changed. Not breaking:
STABILITY.md:158— "TheDisplayimpls are stable; the exact
wording ofDisplayoutput is not" — coversPreprocDiagnostic,
which is a public export (src/lib.rs:340).
Assessment
| Dimension | Rating |
|---|---|
| Difficulty | Low |
| Complexity | Low |
| Priority | Low |
Difficulty — Low. Five string edits, one call-site swap, two CSV
sites redirected to an existing helper. The optional lint gate in step 8
is the only piece with any substance, and it is a few lines of rg.
Complexity — Low. Two crates, but the seam between them is one
function call. No public-API change, no metric computation, no snapshots.
The one thing to get right is that the test expectations move across the
crate boundary rather than being deleted on one side.
Priority — Low. Purely presentational consistency in diagnostic
output. #1198 already removed the user-visible inconsistency within
PreprocDiagnostic; what remains is two capitalised stragglers and a
structural tidy-up so the prefix is written in one place per crate. No
correctness impact and no workaround needed.
Labelled low-priority.
Activity
dekobon commentedon Aug 4, 2026
Added resolution plan and assessment ratings (difficulty Low / complexity Low / priority Low), and applied
low-priority.Confirmed all five sites on
main(39674df6).The open question in step 3 turns out to be mostly answered in the tree already.
src/output/offenders.rs:30defineswarn_non_utf8_path(format, path), and five output formats route through it — SARIF, Code Climate, Checkstyle, and both warning-line flavours. CSV is the only one that does not; it open-codes the same warning twice with slightly different wording. So the library does not need a new convention, it needs CSV brought onto the existing one, after which lowercasing the single site inwarn_non_utf8_pathfixes all six formats at once.That also rules out the more principled alternative for now:
write_csvandwrite_csv_aggregatearepub, so propagating the diagnostic to the caller instead of printing is a SemVer break and cannot land in 2.x.One addition — step 8 proposes an
rg-basedmake lintcheck for capitalisedWarning:/Error:literals. Without it, a seventh site reads as correct in review and this becomes a cleanup that needs doing again.dekobon commentedon Aug 6, 2026
Fixed on
fix/1199-unify-warning-prefixes(315efcbb), PR to follow.make pre-commitgreen.What the plan got right, and the one thing it missed
Step 3's answer held: CSV moved onto
warn_non_utf8_path, andlowercasing that one site fixed all six formats.
write_csv/write_csv_aggregatestayedpuband unchanged, so nothing wasdeferred to 3.0.
The count was wrong, though. The issue names three library sites; a
workspace sweep for capitalised severity literals found five, in
two spellings the issue does not mention:
src/output/code_climate.rs:94—"Warning: skipping empty repo-relative path in code-climate output"src/concurrent_files.rs:227—"Warning: not a regular file, skipping"Both are lowercase now. This is exactly the failure mode step 8
predicted, already realised, which is a good argument for the gate.
Library-side convention
The library got its own
src/diag.rswith a singlepub(crate) fn warn, the counterpart to the CLI ladder from #609. Every librarystderr warning routes through it — including the two that were already
lowercase (
src/spaces/compute.rs,src/vcs/git/cached.rs), whichwere correct by coincidence rather than by construction. No
die/error:counterpart: the library never exits, so fatal conditions stayMetricsError/io::Errorand the CLI decides how to present them.Step 4: the no-op check, and where it is not a no-op
Captured
bca preprocstderr on a fixture producingSelfInclusionand
IncludeCycle, before and after. The diff is exactly one line —the blank line after the cycle block — and nothing else. So the prefix
move is byte-identical, as #1198 intended.
Step 5: both
IncludeCyclewrinkleswarnprefixes the header line only, members indented beneath.Confirmed on real output, not assumed.
was an artifact of
writeln!in aDisplayimpl, and it stacked withthe caller's own newline. The member separator now leads
(
write!(f, "\n - \"{member}\"")) so the block carries no trailingnewline of its own.
Step 8: the gate
utils/check-diagnostic-prefix.py+-test.py, wired intomake lint/pre-commit/ci, the pre-commit hooks, and anexplicit CI step. It scans every tracked
.rsfor a string literalstarting with
Warning:/Error:/Note:(plus the all-capsspellings), skipping whole-line comments.
It found a legitimate hit immediately:
check_thresholds.rs:372asserts the capitalised spelling is absentfrom
bca checkstderr — the #609 regression guard. That is data, nota diagnostic, so the gate carries a
// diag-prefix-okopt-out markerhonoured on the line itself or anywhere in the comment block above it.
One site uses it today.
Two things the self-tests pin that a naive gate gets wrong:
offender here was wrapped that way. A same-line matcher would have
reported the whole tree clean.
str.splitlines()breaks on U+2028 and the vertical-tab family,which rustc does not, so one inside a literal shifts every reported
line number after it. The gate uses
split("\n"); the test failsagainst
splitlines()(line 3 vs 2).Tests
The two
Displaytests moved to the bare message, and a CLI-sidepreproc_diagnostics_render_under_the_warning_prefixpins the renderedstderr. Both halves verified by perturbing production, not by reading:
reverting the CLI call site to
eprintln!trips the prefix assertion,restoring the
writeln!trips the blank-line assertion, independently.Also added
aggregate_skips_only_the_non_utf8_file— the aggregateCSV skip branch had no test, and a single-file document that skips its
only file is byte-identical to one that bails outright, so the existing
test could not have covered it. Turning the
continueinto areturnfails the new one.
non_utf8_path_skips_data_rowshad its#[cfg(unix)]moved from aninner block onto the
fn: the old form compiled to an empty — andtherefore passing — test body on Windows
(
.claude/rules/testing.md).Baseline
write_code_climate'shalstead.effortmoved +0.2% (60471.78 →60596.14) past its recorded value, so
.bca-baseline.tomlis refreshedin the same commit. The refresh also dropped three entries that had
gone stale on
main.refactor(diag): unify severity prefixes on one helper per crate
fix(diag): harden the prefix gate and finish the migration