kdiff 0.4.0
What changes here is kdiff-runtime's API. No annotation means anything different, the processor is
untouched, and an annotated class compiles to byte-identical generated code — so upgrading is a
recompile of the code you wrote around the generated code, guided by the breaking items below.
Added
-
A diff is a collection.
DiffimplementsIterable<Change>and gainssize,isNotEmpty(),
plus, avarargconstructor andDiff.EMPTY, so the standard library's operators apply to a diff
directly instead of throughchanges. It is deliberatelyIterablerather thanList: aDiff
equals only anotherDiff, and advertising list-ness while equalling no list would mislead. -
A diff narrows by property reference.
diff.at(Order::billing)keeps the change reported at that
property;diff.under(Order::billing)keeps that plus everything beneath it. Both return aDiff,
so they compose with each other and with+, and neither matches a string — renaming the property
reaches the call site.Name the type —
diff.at<Order>(Order::billing)— to have the property checked against it.Diff
carries no type argument, so an inferred type parameter comes from the property alone and
orderDiff.at(Address::street)compiles and matches nothing.route<Order> { }has no such hole. -
PatchResult.getOrThrow()returns the value when every change applied and raises
PatchFailedException, carrying every failure, when any did not. Applying itself stays partial —
that is what lets a caller salvage what applied — so all-or-nothing is now a choice at the call site. -
A cyclic structure is reported instead of overflowing the stack. Comparing and applying descend
at mostMAX_DESCENT(512) nested levels and then raiseCyclicStructureException, naming the path
they stopped at and saying whether an instance was re-entered — a cycle — or the structure is simply
deeper than kdiff descends. This costs about 9% of comparison throughput on a model with nested
@Diffableproperties and 13% at six levels of nesting; collections and flat types are unaffected. -
Builder blocks are scoped and run exactly once.
@KdiffDslcloses every builder scope, so a
block nested in another can no longer reach the outer builder's members — arouteframe calling the
enclosing routing'sonused to compile and quietly register a handler against the wrong frame. Each
builder function also declarescallsInPlace(block, EXACTLY_ONCE), so avalcan be assigned inside
a block and read after it. -
Build gates.
./gradlew checknow also runs ktlint, detekt,allWarningsAsErrorsand ABI
validation against a checked-in API dump per published module, so removing a public declaration fails
the build rather than reaching a consumer.dokkaGeneratebuilds reference documentation, and is
deliberately not part ofcheck. See CONTRIBUTING.md.
Changed
-
BREAKING —
PatchFailure.reasonis a sealedPatchFailure.Reasoninstead of aString. The
fourteen reasons the runtime reports are now declared cases, each carrying what it knows — the
property, the key, the index — so a caller can branch on why a change did not apply instead of
matching prose.Migration:
failure.reason == "no element with this key to patch"becomes
failure.reason is PatchFailure.Reason.NoElementForKey. A failure logged as text is unchanged:
PatchFailure.toString()renders the same sentence it always did. LikeChange, the vocabulary is
closed, so adding a case is itself breaking. -
BREAKING (source only) —
Diff.isEmptyis now a function,isEmpty(), to match the shape the
standard library uses for every other collection.Migration: add the parentheses. Kotlin compiles both a
val isEmpty: Booleanand a
fun isEmpty(): Booleanto the sameisEmpty()Zsignature, so this is a recompile rather than a
link error — already-compiled callers keep working.DiffNode.isEmptyis unchanged and remains a
property. -
BREAKING — the duplicate-
@DiffKeyrefusal raisesDuplicateDiffKeyException, carrying the list
property, the key property and the duplicated key value as inspectable properties. It is an
IllegalArgumentException, which is what was raised before, so an existingcatchkeeps working and
the message is unchanged. -
BREAKING —
TrackScopeBuilderandTrackerBuilderdeclare their five shared members through one
ScopeDeclaration<T>interface over one implementation, rather than one re-declaring and forwarding
the other's. Source-compatible for every call site inside a builder block; binary-incompatible, which
is what the new API dump exists to make visible. -
BREAKING — a
differ { }naming no property and no subtype is rejected where it is built. Such a
differ reported every pair of instances as equivalent however much they differed, which is the same
mistake the processor already rejects for an annotated class with nothing to compare. -
BREAKING — consumers must compile at JVM target 21. The builder entry points are
inlineso they
can statecallsInPlace, and Kotlin will not inline bytecode built for a higher target than the
module being compiled. Loading these classes already required a JVM 21; compiling against them now
does too. -
The generated API surface is unchanged. No annotation means anything different, the processor is
untouched, and a@Diffableclass compiles to byte-identical generated code — the breaking items
above are all inkdiff-runtime, in what a consumer writes around that code.