Skip to content

candor-java v0.40.15

Latest

Choose a tag to compare

@tombaldwin tombaldwin released this 22 Oct 11:54
· 1 commit to main since this release

[0.40.15] — 2026-10-22

  • ⚠ A JDK Throwable constructor's cause that is the body's own JDK-typed parameter is decided by what each caller
    passes
    (R1337). CliException(Throwable c) { super(c); } was charged the toString/getMessage of EVERY project
    throwable (CHA over java/lang/Throwable at the R1305 site), and every caller inherited those charges whatever it
    passed: zookeeper's new CliParseException(parseEx), whose commons-cli cause no project override can run on,
    charged 20 CliCommand.parse overrides Fs, Log, Net. Executed: a java.text.ParseException cause wrote nothing
    and read Fs. Each caller is now resolved by its own argument (one project body charged; a WIDE type disclosed; a
    foreign type nothing). The body itself is disclosed Unknown[dispatch:java.lang.Object.toString] where some
    override carries an effect or an Unknown it lacks. A project-typed cause keeps its precise charge, through every
    hop. Measured against v0.40.14 on the 452-jar census: the lost (unit, effect) set is unchanged (414, the same
    pairs). Against the held stack: 517 concrete charges in 209 rows of 18 jars become Unknown, and none of those
    charges existed in v0.40.14. Chained (304 entries): 5 consumer rows lose a concrete effect only the stack had, and
    each keeps Unknown.
  • Shipping knowingly in this release — named so a pin bump reads them first:
    • A resolution traded for a hedge, chained (SOUNDNESS R1331/R1336): zookeeper C_CliCommand.dispatch.
      deny Fs|Log|Net on that unit goes 1 → 0 against v0.40.14; deny Net Unknown holds 1. 3 cells.
      CliCommand.parse now has 21 overrides that are not pure, over the 12-target bound, so the consumer hedges
      (dispatch:…CliCommand.parse). v0.40.14 counted only the ONE override row that had an effect, and unioned it.
      The consumer's bound counts override ROWS rather than overriding subtypes (R1336), so it flips with producer
      precision. R1337 removes the fabricated concrete charges but not the count: the overrides keep Unknown.
    • A resolution traded for a hedge, standalone: 14 netty-common rows (*ObjectHashMap$KeySet.retainAll ×5,
      DefaultPromise.toStringBuilder, SingleThreadEventExecutor.removeTask, DefaultPriorityQueue/
      PriorityQueue priorityChanged/remove/removeTyped ×7) lose Env/Fs (and Log on retainAll) while
      keeping Unknown with a dispatch reason.
    • A false reason class, chained (R1330): 38 consumer cells gain unresolved, so deny Unknown[unresolved]
      goes 0 → 1, where the consumer reaches a merged default-method row whose Unknown came only from the merge. An
      over-report.
    • A route inequality, present in v0.40.14 too (R1338): an exact I.super.m() caller of a BROAD-merged default
      method reads dispatch on gate --report and not on scan --policy. Fixture: deny Unknown[dispatch] p.Ex.call, scan 0 / report 1 on both jars. An over-report on the report route.
    • R1337's residue: a caller handing a foreign cause through a JDK-typed parameter now reads Unknown, the
      helper's disclosure, where it is really pure (e.g. zookeeper LsCommand.parse). An over-report, in place of the
      fabricated concrete charge.
  • A chained call through an interface member whose dependency row is a merged DEFAULT method carries the
    overrides' reasons
    (R1327, SPEC §6.2). The producer merges an interface's implementers into its default
    method's row, so the row carries the overrides' effects, but its unknownWhy and calls are the default
    body's own. A consumer dispatching on that member took the effects and only the default body's reason classes:
    scala-compiler's TypeClassConstraint.toString gave its chained callers dispatch where an override reads by
    reflection, and deny Unknown[reflect,unresolved] read 1 against v0.40.13 and 0 here. The consumer now walks
    down to the overrides' own rows for that shape, as it already did for invokevirtual. The row itself is left
    unchanged: adding the reasons there gave an exact Shape.super.name() caller the overrides' class on the
    producer's report route only.
  • A chained consumer joining two overloads of one member carries each union entry's own reasons (R1326,
    SPEC §3.1/§6.2). The union entries of an overloaded member share one fn, and the consumer's reason memo was
    keyed by fn but seeded with whichever entry it was asked about first, so every sibling got THAT entry's
    reasons in place of its own. Since R1239 gave union entries their contributors' classes, assertj-core's
    EnumerableAssert.hasSameSizeAs(Iterable) then (Object) kept only the first union's dispatch and dropped the
    second's reflect/callback: deny Unknown[reflect,unresolved] read 1 against v0.40.13 and 0 here. The same
    memo charged a caller of ONE overload with its sibling's class. An entry's own reasons are now added on every
    join; only the closure over calls, a function of fn alone, is memoised.
  • ⚠ A toString/hashCode/equals call on a receiver typed by the package's own abstraction is named in
    dispatchesOn
    (R1304, SPEC §4 ⟨0.40⟩). This resolves an ambiguity AGAINST the reading this engine shipped: the
    README documented the Object protocol as a pure-exempt dispatch set and dispatchesOn never named it, so a
    library formatting a Converter field ran the consumer's Converter and a chained consumer read pure — deny Fs
    0 chained while the same code scanned as one tree read 1. The member is now named under the receiver's declared
    public, non-final project interface or class (both javac spellings: invokevirtual Object.toString from javac ≤17,
    invokeinterface Converter.toString from 18+), and through a JDK string or hashing sink over such a value. A
    producer report grows (scoped measurement: +0.20% rows, +1.0% bytes); a chained consumer gains its own
    implementors' effects. compareTo is unchanged.
  • ⚠ printStackTrace charges its cause chain's toString, and a JDK re-entry held back at the CHA bound is
    disclosed instead of dropped
    (R1306). printStackTrace prints every throwable in the cause and suppressed chain;
    the chain's contents are unknown, so the call is a Throwable.toString dispatch over an unknown throwable. And where
    more than twelve project classes inherit the JDK toString (fifteen exceptions overriding only getMessage), the
    re-entry bodies were held out of the CHA answer and "x" + t, t.toString() and t.printStackTrace() read pure;
    the Object floor now weighs them. Executed: deny Fs Unknown 0 → 1 for both shapes; pure controls stay 0.
  • The self-gate's forbid io.poly.candor.model -> io.poly.candor.Policy rule is green again (R1307). R1239's lazy
    reason-class accumulator was an anonymous AbstractMap whose get ran Policy.scanReasonClassAcc(); the engine's
    CHA over Map.get reached it from model.Effect.fromSpecName (whose receiver is a private static final HashMap), a
    crossing no execution performs. The accumulator is now a private holder that is not a Map. No report changes.
  • ⚠ new RuntimeException(cause) charges the cause's toString (R1305). Throwable(Throwable) sets
    detailMessage = cause.toString(), which for a project exception runs its getMessage (R1235), but nothing in the
    caller's bytecode names toString, so a wrap over an exception whose getMessage wrote a file read absent. The JDK
    constructors that stringify an argument are now a toString sink at exactly that argument — 82 of them, DERIVED from
    the pinned Temurin 17/21/25 images (including AssertionError(Object), i.e. assert c : detail); the
    (String, Throwable) forms do not stringify and stay pure. Executed: five shapes (a parameter, a local allocation, a
    project super(cause), UncheckedIOException, a caught exception) deny Fs 0 → 1.
  • ⚠ The Object floor carries an override's Unknown, not only its concrete effects (R1303). Past the CHA bound an
    equals/hashCode/toString dispatch is decided by the Object floor, which disclosed only where some override it
    could run carried a CONCRETE effect the caller lacked — so an override that is itself Unknown (a reflective call, an
    Object-typed concat) never reached the caller. Hidden while the site CHA'd over java/lang/Object, where some
    unrelated class nearly always carried a concrete effect; R1299 bounded it by the receiver's declared type and 15 units
    read pure over candidates that still carry Unknown (ant MethodHandleCPInfo.toString, commons-io
    NotFileFilter.toString, guice, assertj-db, scala-reflect ×10). The floor is re-decided until nothing new is
    disclosed. Executed: deny Fs Unknown p.H.toString 0 → 1 over a field whose reflective subclass wrote a file.
  • ⚠ e.printStackTrace() charges the exception's own toString/getMessage and getCause (R1300). The JDK body
    prints the receiver (println(this) → toString → getLocalizedMessage → getMessage) and calls the virtual
    getCause(); R1235's table named only toString/getLocalizedMessage. Executed: four shapes (no-arg, PrintStream,
    PrintWriter, an overridden getCause) wrote a file and read absent; deny Fs 0 → 1. A pure toString keeps it pure.
  • ⚠ An Object-owned call (x.toString() where x's type does not declare it) is bounded by x's declared project
    type
    (R1299). javac names java/lang/Object as the owner in that case, so the dispatch fanned to every project
    class, and since R1235 to every unrelated class's hashCode: commons-beanutils ConverterFacade.toString /
    ArrayConverter.toString were charged Clock/Db/Log/Unknown from classes that are not Converters. An ABSTRACT class
    is also no longer a re-entry target of its own (spring MethodOverrideCallbackFilter.accept lost a false Unknown).
    Where the declared type has no candidate at all, the old Object-wide answer and its hedge are kept.
  • ⚠ An interfaceUnion entry carrying Unknown now carries its contributors' reasons (R1239, SPEC §3.1 ⟨0.40⟩).
    A union entry published a reason only on a BROAD hedge (dispatch:), so a chained consumer read every other union's
    Unknown as dep: → unresolved. It now carries one raw reason per class of its contributors' TRANSITIVE class sets,
    as this engine's own gate resolves them, plus its own hedge; R869 bridge rows (also interfaceUnion) carry their
    contributor's the same way. Executed: a consumer whose go(lib.Sink s) reaches an implementer that reads the
    environment by reflection went deny Unknown[reflect] app.Main.go 0 → 1 and deny Unknown[unresolved] 1 → 0,
    on both routes. Spot check (gson, commons-compress, jackson-databind): union rows carrying Unknown with a reason
    53 of 286 → 286 of 286; no unit row and no union inferred changed.
  • ⚠ gate --report: an interfaceUnion entry is never a unit of the verdict, and always a contributor to the join
    (R1223, R1234, SPEC §3.1 ⟨0.40⟩). Three changes, one clause. (1) A union entry is no longer the subject of the
    narrowed-filter answerability test: a reasonless union refused deny Unknown[native] at exit 2 where no unit could
    fail it (now exit 0); a REAL caller whose only reason source is that union is still refused. (2) A rule whose only
    matching entries are union entries now rides zeroMatch (it was neither evaluated nor disclosed). (3) A union
    published BESIDE a real entry under the same hash (a candor-swift/ts-shaped report) is no longer merged into that
    unit: deny Env app.B over a body with no Env goes 1 → 0, while every caller of the key still inherits the union's
    reason classes. The same change stops the two entries' unknownWhy overwriting each other (last-wins): a real
    app.B.m carrying reflect: beside a union carrying native: read deny Unknown[reflect] app.B and … app.C
    (its caller) exit 0; both now exit 1.
  • ⚠ A JDK body that dispatches back into project code now charges the body it runs (R1235). Executed on JDK 21,
    each of these wrote a file and read absent (deny Fs exit 0): Object.toString() runs this.hashCode(), so a class
    overriding an effectful hashCode but not toString was pure under h.toString(), "" + h, String.valueOf(h),
    append(h) and Objects.toString(h); Throwable.toString() runs getLocalizedMessage() → getMessage();
    Objects.hashCode(p)/Objects.equals(p, q) on a parameter charged only the callers; and a JDK collection or map
    stringified (explicitly, by concat, or through a string sink) runs every element's and value's toString. Each is
    now a resolution — the override's own effects, never an Unknown — and its controls stay pure (a pure toString
    over an effectful hashCode, an array stringified). Measured on commons-lang3, joda-time and gson (9,964 analysed
    units): 0 rows gain a concrete effect, 0 lose one, 19 newly carry Unknown (the overrides reached are themselves
    Unknown, e.g. commons-lang3's ContextedException.getMessage), and one interface-union row widens. Not covered:
    the JDK collection SKELETONS (AbstractList.contains → get, AbstractCollection → iterator, AbstractMap →
    entrySet), which run project overrides the same way and still read absent. Known over-charges in this
    release (SOUNDNESS R1299):
    where the bytecode names java/lang/Object as the owner of a call on a receiver
    typed by a project abstraction, the edge reaches unrelated classes' hashCode/toString. On the corpus that is
    7 fabricated charges, e.g. commons-beanutils ArrayConverter.toString gaining Clock/Db/Log. These are
    over-reports, not silences, and are fixed in this release (R1299, below).