Forward-looking work items for java-coding-standards. Items
here are PLANNED but not yet committed — distinct from
CHANGELOG.md's [Unreleased] section, which is reserved for
committed-but-unreleased changes per the Keep a Changelog
convention.
Status: scoped during a planning session after 0.4.3 shipped. Five items targeted for 0.5.0; coordinated batch because the underlying questions overlap (wrap-engine candidate set, spec C6 scope, source-preserve column policy).
| # | Item | Risk | Spec change? |
|---|---|---|---|
| 1 | Control-flow paren-alignment (extend spec C6) | Low | Yes |
| 2a | Label/value-aware binary + chain wrap |
Low | Yes |
| 2b | Same-method greedy method-chain wrap | Medium | Yes |
| 3 | Greedy-P2 binary expression wrap | Low | Minor (new candidate) |
| 4 | Context-aware source-preservation | High | Yes |
Phase 1 — Item 4 spike (throwaway). Empirical
comparison of the two candidate implementation paths for
item 4 (column-remapping vs partial semantic emission).
Run each on senzing-commons-java, measure reformat
surface and LineLength regressions. Don't commit — the
result picks the option for Phase 5.
Phase 2 — Item 1 (control-flow paren-align). Lowest risk, additive. Trial-run on consumer to gauge visual density before tagging.
Phase 3 — Items 2a + 3. Same wrap engine
(_emit_binary_expression), shared cascade positioning.
Implement together.
Phase 4 — Item 2b spike + implementation. Greedy method-chain has medium reformat surface. Spike on a throwaway branch first to gauge impact (same pattern as Phase 1); commit if the diff looks acceptable.
Phase 5 — Item 4 actual implementation. Uses the spike-winner option from Phase 1. Last because dependence on items 2a/2b/3 is now clear and the IOException long-literal back-off needs careful fixture coverage.
Phase 6 — Bulk adoption + tag. Reformat
senzing-commons-java (and ideally sz-sdk-java as a
second adopter), verify mvn -Pcheckstyle validate BUILD
SUCCESS, idempotency holds. Tag 0.5.0.
Item 4's option 2 (partial semantic emission) would cause
_emit_binary_expression, _emit_method_chain_wrapped,
and _emit_block to fire on inputs they don't see in
0.4.3 (inside source-preserved arg lists). Items 2a, 2b,
and 3 would behave differently in those new contexts. If
we land 2a/2b/3 first under today's assumptions and then
land option 2, the interaction surprises are hard to
unwind. If option 1 (column-remapping) wins the spike,
this concern evaporates and items can land in any order.
Item 2b touches every .method().method() chain with the
same method name in adopter code. That's potentially
hundreds of files. A throwaway preview before committing
the implementation lets us see the diff quality and adjust
the gating heuristic (currently "all segments call the
same method name") if it over- or under-triggers.
Each implementation PR carries its own spec-text update to
docs/java-coding-standards.md (no separate spec-first
PR). The user is the primary adopter during 0.5.0
development; pre-circulating spec for external review
isn't necessary.
The combined _emit_binary_expression cascade after
0.5.0:
- P1 (single line)
- 2a
emit_pair_aligned(label/value pattern matches; usesparen_align_colif set, else +4 indent) emit_paren_aligned(0.4.3, only when grouping paren in scope and 2a didn't apply)- P2 (break-at-leftmost-only, rest on one continuation line)
- 3
emit_greedy(pack as many operands per line as fit) - P3 (every operand on its own line)
The _emit_method_chain_wrapped cascade after 0.5.0:
- P1 (single line)
- 2b
emit_p2_greedy(only when all segments call the same method name; pack as many segments per line as fit) - P2 (dot-aligned, one segment per line)
- P3 (continuation-indent, one segment per line)
The items below are the original coordinated gaps / ambiguous-cases list captured at the end of 0.4.3 review. The scoping decisions above (item 2 splitting into 2a/2b, retention of item 3, etc.) refine these into the final 0.5.0 plan.
New emit_p2_pair_aligned candidate in
_emit_binary_expression. Spec extension: when a binary +
chain alternates between string_literal operands and
non-string operands AND each label literal begins with a
delimiter character ( , ,, ;, ], ), }, |, :),
break before each label so each line carries one label/value
pair aligned at the chain's continuation column. Generic
example (the classic toString() builder pattern):
// current 0.4.3 output (P3 — break before every `+`):
return ("{ option=[ "
+ this.getOption()
+ " ], processedValue=[ "
+ this.getProcessedValue()
+ " ], source=[ "
+ this.getSource()
+ " ] }");
// proposed 0.4.4 output (pair-aligned):
return ("{ option=[ " + this.getOption()
+ " ], processedValue=[ " + this.getProcessedValue()
+ " ], source=[ " + this.getSource()
+ " ] }");Detection is purely structural (operand types from AST) plus
a lexical delimiter-prefix check on the string literals.
Falls back to break-at-every-operator (P3) when the
alternation breaks (two consecutive strings or two
consecutive values) or when any pair would overflow 80 chars.
Won't fire on arithmetic chains like a + b + c + d because
the operand types don't alternate.
Requires: spec section in docs/java-coding-standards.md
("Label/value-aware string concatenation"), wrap candidate
implementation, ≥2 fixtures (engagement case +
alternation-broken fall-back), consumer re-verification.
Also in _emit_binary_expression. Generic fallback for
binary chains that don't match the label/value pattern but
where P3's one-per-operator break is wider than necessary.
Tries to fit as many operands as possible on each
continuation line before breaking. Complements the
label/value-aware candidate above by handling the
non-alternating cases. Lower priority — only worth
implementing if real cases surface in consumer adoption that
P3 handles poorly.
_emit_argument_list source-preserve path. Currently the
source-preserve path emits an arg list's bytes verbatim via
write_raw_lines — including the bodies of any contained
multi-row constructs (lambda blocks, multi-row chain
expressions, multi-row binary concatenations). The
continuation columns in those bodies are whatever the source
originally chose, often inherited from an earlier surrounding
context that has since shifted to a different indent. Two
visible flavors of the same underlying issue:
(a) Lambda body inherited from prior context. Example:
// current 0.4.3 output — body at the source's original col:
collection.entrySet().forEach(entry -> {
String key = entry.getKey();
if (key != null) {
key = key.trim().toUpperCase();
}
…
});
// proposed — body re-indented per current indent_level:
collection.entrySet().forEach(entry -> {
String key = entry.getKey();
if (key != null) {
key = key.trim().toUpperCase();
}
…
});(b) Non-lambda multi-row arg inherited from prior context. E.g. a chain expression arg whose continuation column was set by the original author. Generic example:
// current 0.4.3 — continuation at source's original col 16:
if (!canonicalTarget.toPath().startsWith(
canonicalTargetDir.toPath()))
{
// proposed — continuation at current_indent_col + 4 (col 20):
if (!canonicalTarget.toPath().startsWith(
canonicalTargetDir.toPath()))
{The 0.4.3 FormatterWarning advisory channel surfaces flavor
(a) (lambda body below indent), so developers see the issue
in CI logs. Flavor (b) is silent today — the continuation
column happens to be ≥ indent_level * 4, so the advisory
doesn't fire — but the visual quirk is the same family.
The fix path covers both flavors: refactor the
source-preserve emit to either (1) shift continuation columns
by a computed delta relative to the new emission context
(column-remapping source-preserve), or (2) switch to semantic
emission for multi-row inner constructs while keeping the
outer parens verbatim (partial-preserve). Option (1)
preserves the developer's intra-arg break choices but
rewrites the absolute column; option (2) lets _emit_block /
chain / binary engines re-emit cleanly but may pick different
break points than the original. Pick by trial — see how the
consumer reformat reads under each — before committing to one
path.
Constraints: must not regress the IOException("…long literal…" + var) case where a long string literal at a low
column is THE thing that fits in 80 chars. Re-indenting
upward there would push the literal past 80 (verified
empirically during 0.4.3 — the naïve "decline source-preserve
when continuation < indent" caused ~10 LineLength failures
across 5 consumer files). String literals cannot be
auto-split, so the fix must back off in that case (likely via
a width check: only re-indent when the re-indented layout
actually fits 80).
Requires: source-preserve path refactor, fixture coverage for lambda body / non-lambda multi-row arg / long-literal fall-back, and consumer re-verification across adopters.
_emit_parenthesized_expression + _PAREN_NOT_GROUPING_PARENT_TYPES.
0.4.3's paren-alignment applies only to grouping parens
(developer-authored (...) around an expression for
emphasis); the control-flow required parens — if (cond),
while (cond), for (...), catch (...), synchronized (...), switch (...) — use the standard cumulative +4
continuation indent for their binary-operator wraps. Generic
example:
// current 0.4.3 (cumulative +4 continuation):
} else if (owner.fileParts.size()
> this.currentFileIndex)
{
…
}
// proposed (paren-aligned under `(`):
} else if (owner.fileParts.size()
> this.currentFileIndex)
{
…
}The visual case for extending: the continuation operator lines up directly under the column the condition opens at, making the wrap point unambiguous at a glance. The case against: deeper indents per level (more horizontal space consumed in nested control flow), and it breaks a convention many adopters already rely on.
Fallback behavior: when paren-alignment would overflow on the
second line (long condition relative to deep indent), the
wrap engine should fall back to the standard +4 cumulative
continuation. The existing try_priorities cascade handles
this naturally — paren-aligned candidate emits speculatively,
engine checks width, accepts or rolls back and tries the +4
candidate.
Tagged as a candidate for 0.4.4 or 0.5.0 depending on how
invasive the consumer-side reformat ends up being. Worth a
trial run on senzing-commons-java + a second adopter to
gauge the visual impact before committing to a
semantic-version bump.