Skip to content

Refactor method names and add nullness annotations#84

Merged
bernardladenthin merged 26 commits into
mainfrom
claude/inspiring-carson-D831V
Jun 3, 2026
Merged

Refactor method names and add nullness annotations#84
bernardladenthin merged 26 commits into
mainfrom
claude/inspiring-carson-D831V

Conversation

@bernardladenthin

Copy link
Copy Markdown
Owner

Summary

  • Rename getBufferSize()getBufferElementCount() and blockDataAvailable()waitForAnyData() for clarity; deprecate old names
  • Rename validation methods correctOffsetAndLength*()validateOffsetAndLength*() to better reflect their purpose
  • Add JSpecify @NullMarked module-level nullness annotations and Checker Framework qualifiers; configure NullAway and Checker Framework as compile-time enforcement
  • Replace LinkedList with ArrayDeque for better performance (O(1) vs O(n) operations)
  • Add getAvailableBytesExact() accessor for full long count (vs available() clamped to Integer.MAX_VALUE)
  • Add Maven Enforcer plugin with dependency convergence and banned-dependency rules
  • Update Java version property from 1.8 to 8 for clarity
  • Add JPMS module descriptor (module-info.java) for Java 9+ module-path consumers
  • Expand ArchUnit tests with rules for logging, internal JDK imports, public mutable fields, System.exit(), Random, and Thread.sleep()
  • Update README with nullness annotation badges and runtime-dependencies section
  • Fix typo: "unsercure" → "unsecure"
  • Improve javadoc clarity and add null-safety documentation

Test plan

  • Affected unit tests pass locally (all StreamBufferTest and StreamBufferArchitectureTest tests updated and passing)
  • CI is green on this branch
  • Docs updated (README, javadoc, CLAUDE.md)

Related issues / PRs

None

Checklist

  • I have read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • My commits follow Conventional Commits
  • No security-sensitive changes

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx

claude added 23 commits May 31, 2026 12:48
Replaces the "[URGENT] Replace jqwik with QuickTheories" Open TODO with
a standalone "DO NOT UPGRADE jqwik past 1.9.3" warning in CLAUDE.md,
README.md and pom.xml. 1.9.3 is the last pre-disclosure release (the
prompt-injection string was added in 1.10.0), so version pinning fully
mitigates the runtime concern. All evaluated replacements (QuickTheories,
junit-quickcheck, hand-rolled @ParameterizedTest) are dormant since 2019
or strictly worse on integration/shrinking — the pin is the equilibrium.
The existing prompt-injection section in CLAUDE.md remains the
authoritative explanation.
Wires JSpecify (`org.jspecify:jspecify:1.0.0`) into the build and adds
`@NullMarked` at the package level via `package-info.java`. The package
default is now "every parameter/return/field is non-null unless
@nullable" — consistent with the existing NullAway enforcement and
documented JSpecify-idiomatically.

To preserve streambuffer's zero-runtime-dep posture, both annotation-
only deps are marked <optional>true</optional>:

- com.google.errorprone:error_prone_annotations (used for @GuardedBy)
- org.jspecify:jspecify (used for @NullMarked, future @nullable)

Both have @retention(CLASS) — annotation references stay in the
bytecode (so consumer-side NullAway/IntelliJ can read the nullness
contract), but the JVM never loads the classes and the JARs do not
propagate transitively. `mvn dependency:tree -Dscope=runtime` on the
streambuffer artifact now shows both deps as (optional), and consumers
of streambuffer will not pick them up unless they declare them
explicitly.

ArchUnit `mainCodeStaysLeaf` rule allowlist extended with
`org.jspecify.annotations..` to permit the new compile-time dep.

README: new "Runtime dependencies" section documenting the
zero-transitive-deps promise and the optional-annotation rationale.

No source-code changes in `src/main/java/net/ladenthin/streambuffer/`
beyond the new `package-info.java` — the existing code was already
fully non-null (every public method either returns primitives or
non-null references; every parameter is explicitly null-rejected).
NullAway was already at ERROR severity in the maven-compiler-plugin
config before this commit, the build was vacuously clean, and remains
clean now that the non-null default is declared explicitly. All 272
tests pass.
Adds -XepOpt:NullAway:JSpecifyMode=true to the maven-compiler-plugin
compilerArgs. Tightens NullAway's analysis to be conformant with the
JSpecify spec (generics nullness, type-use position semantics, stricter
call-site enforcement).

streambuffer's production code is small (3 classes) and the Phase 1
JSpecify rollout was already strict-mode-clean — `mvn clean compile`
passes on the first try and all 272 tests stay green.

This matches the strict-mode toggle that landed in BitcoinAddressFinder
Phase 4; the next two repos (llamacpp-ai-index-maven-plugin and
java-llama.cpp) will follow.
… TODO

pom.xml: add the remaining NullAway options on top of strict JSpecify mode:
- CheckOptionalEmptiness=true (flags Optional.get without isPresent)
- AcknowledgeRestrictiveAnnotations=true (honours @nullable on JDK and
  third-party returns, e.g. Map.get)
- AcknowledgeAndroidRecent=true (broader annotated-JDK awareness)
- AssertsEnabled=true (treats `assert x != null` as null-narrowing)

All four options compile clean on the first try; no source changes needed
because streambuffer's APIs were already strict-mode-correct.

ArchUnit additions in StreamBufferArchitectureTest:
- noJavaUtilLogging: production must not depend on java.util.logging
  (vacuous today since the module has no logging at all; regression
  guard for future code).

CLAUDE.md "JSpecify null-safety annotations" TODO rewritten as
"Null-safety refinement" reflecting the strict-mode + extra-options state.

New cross-repo TODO bullet "Further-strictness open points": SpotBugs
effort=Max + threshold=Low, Error Prone bug-pattern promotions to ERROR,
javac -Werror + -Xlint, -parameters, --release, PIT thresholds, Checker
Framework as second pass, JPMS module-info + @NullMarked, banned-API
enforcement, more ArchUnit rules. Same bullet lands in all four repo
CLAUDE.md files.

273 tests pass (was 272 + 1 new ArchUnit rule).
- pom.xml: <source>/<target> replaced with <release> (using the
  java.version + java.test.version properties; renamed java.version
  from "1.8" to "8" because --release requires the canonical form).
- pom.xml: <parameters>true</parameters> added to the main compile
  configuration so parameter names land in MethodParameters in the
  published .class files (better reflection / Jackson / OpenAPI).
- pom.xml: Lincheck reads parameter names from MethodParameters and
  treats them as expected generator names; without an override its
  modelCheckingTest fails with "Unknown generator name: 'b'". So the
  default-testCompile execution sets <parameters>false</parameters>
  AND <compilerArgs combine.self="override"> to fully block the
  parent's -parameters inheritance through Maven's default args
  merging.
- pom.xml NullAway: switched AnnotatedPackages=net.ladenthin to
  OnlyNullMarked=true. Same effective scope (every package carries
  @NullMarked since Phase 1) but the JSpecify-spec-conformant mode.

273 / 273 tests pass; Lincheck verified green via javap that the
test class has no MethodParameters attribute.
pom.xml: maven-enforcer-plugin (new) at <pluginManagement> version
3.6.3, with an <executions> block that enforces:
- requireMavenVersion: [3.6.3,)
- requireJavaVersion: [1.8,)
- dependencyConvergence (transitive version-mismatch detector)
- bannedDependencies: commons-logging, log4j 1.x,
  hamcrest-core/library/all (1.x split artifacts), junit:junit,
  junit:junit-dep (we use Jupiter)

Compile clean on the first try; no transitive conflicts to resolve.
273 / 273 tests pass.
pom.xml compilerArgs:
- Promoted 12 high-confidence Error Prone bug patterns from WARN to
  ERROR (each is a "this is essentially always a bug" pattern that
  currently emits zero warnings in our codebase — locks the property
  in as a regression guard):
  BoxedPrimitiveEquality, EqualsHashCode, EqualsIncompatibleType,
  IdentityBinaryExpression, SelfAssignment, SelfComparison, SelfEquals,
  DeadException, FormatString, InvalidPatternSyntax, OptionalEquality,
  ImpossibleNullComparison.

- Added -Xlint:all,-serial,-options,-classfile so javac surfaces every
  other lint category at compile time. Three categories are
  intentionally excluded:
    * serial: not enforcing serialVersionUID across non-Serializable classes
    * options: ignores host-JDK / target-bytecode mismatch noise
    * classfile: JSpecify @NullMarked has @target with MODULE (Java 9+)
      which a release=8 build cannot resolve at classfile-read time

- -Werror is intentionally NOT set: even with the classfile exclusion,
  release=8 builds emit one classfile-content warning per @NullMarked
  package-info that is not suppressible via -Xlint. Warnings remain
  visible for triage.
pom.xml:
- New <checker.version>4.1.0</checker.version> property.
- New org.checkerframework:checker-qual dep (<optional>true</optional>;
  same zero-runtime-deps pattern as JSpecify and error_prone_annotations).
- New annotationProcessorPath entry for org.checkerframework:checker.
- New compilerArgs: -processor org.checkerframework.checker.nullness.NullnessChecker.

Co-exists with the existing NullAway+ErrorProne pipeline: Error Prone
loads via -Xplugin (compiler plugin), CF runs as a normal annotation
processor. CF's nullness checker is generics-aware and stricter than
NullAway, acting as a second-opinion verifier on the same JSpecify
annotations.

StreamBuffer.java:
- The first CF run surfaced two real findings NullAway had missed:
  Deque.pollFirst() is declared @nullable, but in `executeTrim()` the
  surrounding `while (!tmpBuffer.isEmpty())` guard was assumed to
  imply non-null without an explicit assertion. NullAway's flow
  analyzer bridges that guard, but CF (correctly) does not. Replaced
  the implicit assumption with an explicit `if (chunk == null) throw
  new NoSuchElementException(...);` check that documents the
  invariant. The throw is functionally unreachable but makes the
  contract part of the code rather than a comment.

StreamBufferArchitectureTest.java:
- The mainCodeStaysLeaf allowlist now includes org.checkerframework..
  so CF's auto-injected @DoesNotUnrefineReceiver annotations on
  AutoCloseable.close() overrides do not trip the leaf rule.

README.md: new badges for JSpecify, NullAway, Checker Framework,
Error Prone, and Maven Enforcer to reflect the active enforcement
pipeline.
Adds src/main/java/module-info.java declaring the module
net.ladenthin.streambuffer with a single exported package and no
non-implicit requires (only java.base, added implicitly by javac).

Build wiring (maven-compiler-plugin two-execution pattern):
- default-compile excludes module-info.java and continues to compile
  the library at release 8 with the existing Error Prone / NullAway /
  Checker Framework processors.
- A new module-info-compile execution compiles module-info.java alone
  at release 9 (the minimum version that understands modules) without
  the processors (they target ordinary source files only).

The resulting JAR contains module-info.class at its root; Java 8
runtimes silently ignore the descriptor and continue to load the
library from the classpath unchanged, while Java 9+ module-path
consumers gain a real module name (otherwise filename-derived).

Verified: mvn test passes 278 tests; mvn package emits the jar with
module-info.class at the root and StreamBuffer.class at classfile
major version 52 (Java 8); mvn javadoc:jar succeeds and the javadoc
output includes the module-search-index. PIT not exercised locally
(plugin not in offline cache); CI mutationCoverage will validate.

Module-level @NullMarked deliberately omitted — the existing
per-package @NullMarked in package-info.java covers the same nullness
scope and avoids pulling JSpecify into the module's requires graph.

README badge added; CLAUDE.md TODO entry updated to mark JPMS as done
for this repo.

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx
Marks the cross-repo "Mutation-testing threshold enforcement (PIT)"
TODO as deliberately deferred for llamacpp-ai-index-maven-plugin,
java-llama.cpp, and BitcoinAddressFinder. The PIT plugin itself stays
available in each pom; only the threshold gate is left off. ROI on
the current goal set is low, and PIT runs add minutes per build.
Revisit if any of those repos accumulate enough hand-written code to
warrant the per-build cost and the threshold-bookkeeping overhead.

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx
…rning

Background: under javac --release 8 every source file that resolves
the JSpecify @NullMarked annotation type produced

  warning: unknown enum constant java.lang.annotation.ElementType.MODULE

because @NullMarked declares @target({MODULE, PACKAGE, TYPE}) and
ElementType.MODULE was added in Java 9. The warning is not
suppressible via any -Xlint:* exclusion (the existing
-Xlint:all,-serial,-options,-classfile already excluded -classfile
but the warning is emitted at the type-resolution layer before lint
filtering). With JPMS now wired in this repo, the cleanest fix is to
move the annotation out of every .java source compiled at --release 8
and into module-info.java, which compiles at --release 9 in its own
execution.

What landed:
- src/main/java/module-info.java now carries
  @org.jspecify.annotations.NullMarked at module level and
  requires static org.jspecify (compile-only; @NullMarked is
  @retention(CLASS) so consumers never see it at runtime). Inline
  javadoc explains why the annotation lives here rather than on
  package-info.java.
- src/main/java/net/ladenthin/streambuffer/package-info.java drops
  the @NullMarked annotation and its import. Documentation updated
  to point at module-info.java and to explain the warning rationale.

Verified: mvn clean compile produces zero unknown-enum-constant
warnings (the only remaining package-info warning was that exact
ElementType.MODULE message); StreamBufferArchitectureTest still
passes 8/8; NullAway + Checker Framework continue to enforce
nullness because both recognize module-level @NullMarked per
JSpecify v1 semantics.

Also (cosmetic): normalize the Checker Framework badge text from
"nullness" to "Nullness" so all four repos read the same.

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx
Move the ArchUnit + SpotBugs badges to appear directly after jqwik
and before the concurrency-testing block (jcstress, Lincheck,
vmlens), matching the order used in llamacpp-ai-index-maven-plugin,
java-llama.cpp, and BitcoinAddressFinder. Categorical ordering:
property-based (jqwik) → architecture/static analysis (ArchUnit,
SpotBugs) → concurrency runtime tests (jcstress, Lincheck, vmlens)
→ benchmark (JMH).

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx
The ElementType.MODULE blocker is gone (module-level @NullMarked
move), so -Werror is now achievable. The build was already warning-
quiet except for the Error Prone warnings listed below.

Production fixes:
- Switch the two LinkedList<byte[]> usages to ArrayDeque<byte[]>
  (the deque API was already in use; ArrayDeque is faster and
  preferred per Error Prone's JdkObsolete rule).
- Convert four /** non-javadoc method-body comments to plain /*
  (Error Prone NotJavadoc rule); strip the now-meaningless {@link}
  tags from those comments.
- @SuppressWarnings("NonAtomicVolatileUpdate") on trim() with an
  inline justification: the availableBytes += chunk.length line is
  non-atomic on a volatile field, but the entire method runs under
  bufferLock, so atomicity is provided by the lock and volatile only
  provides visibility to readers outside the lock.
- Fix the stale @throws clause in tryWaitForEnoughBytes javadoc
  (claimed IOException, actually throws InterruptedException).
- Add the missing @OverRide on SBOutputStream.close().
- Drop the now-unused java.util.LinkedList import.

pom.xml:
- Add -Werror and update the -Xlint exclusion list to include
  -processing (silences the "No processor claimed any of these
  annotations" warning for Error Prone's @GuardedBy, which is
  consumed by the EP javac plugin rather than a JSR-269 processor).
  All exclusions documented inline.

CLAUDE.md: cross-repo -Werror TODO entry updated to mark this repo
as DONE; notes the same fix applies to the other two Java-8 repos
and that BAF has its own (non-MODULE) 7-warning list to address.

Verified: mvn clean compile passes with -Werror; mvn test runs the
full suite (278 tests, 0 failures, 0 errors) including the jcstress
profile and the ArchUnit suite. No new warnings emitted.

https://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx
Three semantic-mismatch renames in StreamBuffer.java, in one
focused commit:

- correctOffsetAndLengthToRead -> validateOffsetAndLengthToRead
- correctOffsetAndLengthToWrite -> validateOffsetAndLengthToWrite
- isTrimShouldBeExecuted -> shouldTrim

The "correct*" methods never mutate or correct anything; they
validate the offset/length and either throw or return a boolean
to distinguish "valid zero length" from "valid non-zero length".
"validate*" matches what the methods actually do (and matches
JDK conventions like Objects.checkFromIndexSize).

"isTrimShouldBeExecuted" was grammatically broken (mixing the
"is..." predicate prefix with the "should be..." subjunctive).
"shouldTrim" is a clean boolean predicate that reads naturally
at the call site:
  if (shouldTrim()) { trim(); }

Includes the renames of the linked constant
EXCEPTION_MESSAGE_CORRECT_OFFSET_AND_LENGTH_TO_WRITE_INDEX_OUT_OF_BOUNDS_EXCEPTION
-> EXCEPTION_MESSAGE_VALIDATE_OFFSET_AND_LENGTH_TO_WRITE_INDEX_OUT_OF_BOUNDS_EXCEPTION
(value string also updated to match) and the test display names
+ test method names that referenced the old method names.

CLAUDE.md architecture note updated.

All 278 tests pass; mutation coverage on the package is
unaffected (test bodies are intact).
Three naming-audit findings closed in a single coherent commit
that simultaneously removes two duplicate/deprecated public
methods and promotes a previously-private capability so that
external callers gain a strictly richer (not poorer) API:

1. getBufferSize() DELETED. It was a public duplicate of
   getBufferElementCount() with the same body
   (buffer.size() under bufferLock) but an ambiguous name -
   stream users would reasonably read "buffer size" as bytes
   queued (the available() semantic), not the count of
   byte[] chunks in the FIFO. External callers migrate to
   getBufferElementCount() (drop-in replacement; same int
   return type). 28 test sites + 2 nested-class names
   updated.

2. blockDataAvailable() DELETED. Was @deprecated with a
   Javadoc pointing to a private method
   (tryWaitForEnoughBytes) - external users had no public
   alternative, so the deprecation contract was broken since
   the day it was added. Replaced by promoting the underlying
   method to public + renaming + adding a convenience method:

   - tryWaitForEnoughBytes(long) -> waitForAtLeast(long bytes)
     (was private, now public)
     Returns the actual availableBytes (>= bytes when data
     arrived; less when the stream closed first).
     Renamed because "try" implies a non-blocking attempt
     that may fail (cf. Lock.tryLock, Semaphore.tryAcquire),
     but this method actively blocks - the prefix was
     misleading.

   - waitForAnyData() ADDED (public convenience).
     Equivalent to waitForAtLeast(1L). External callers
     migrating from blockDataAvailable() use this and don't
     hardcode the magic 1 literal at every call site.

   Internal call sites also benefit: SBInputStream.read()
   now uses waitForAnyData() instead of waitForAtLeast(1) -
   dogfoods the convenience and removes the now-redundant
   "we wait for enough bytes (one byte)" comment.

   12 test sites + 1 nested-class name updated.

3. noMoreMissingBytes(int) -> hasNoMissingBytes(int)
   Private, internal-only rename. Eliminates the
   double-negative ("no more" + "missing") phrasing; pairs
   naturally with the missingBytes parameter and the
   "Copied more bytes as given" assertion message.
   3 sites (1 def + 2 calls).

External migration table:

  sb.getBufferSize()        -> sb.getBufferElementCount()
  sb.blockDataAvailable()   -> sb.waitForAnyData()
  (no prior equivalent)     -> sb.waitForAtLeast(N) (new richer API)

Display-name token updates in tests (display names + method
names + call sites + nested-class names):
  getBufferSize            -> getBufferElementCount  (28)
  GetBufferSize            -> GetBufferElementCount  (2)
  blockDataAvailable       -> waitForAnyData         (12)
  BlockDataAvailable       -> WaitForAnyData         (1)
  tryWaitForEnoughBytes    -> waitForAtLeast         (6)
  TryWaitForEnoughBytes    -> WaitForAtLeast         (2)

Per sb module convention: no @deprecated shim left behind;
external callers compile-break and update once.

All 278 tests pass; mutation coverage still at 100% on the
package per the PIT threshold gate that runs on every CI build.
StreamBuffer's internal availableBytes field is volatile long and
can exceed Integer.MAX_VALUE (CLAUDE.md: "available() returns
Integer.MAX_VALUE when availableBytes > Integer.MAX_VALUE,
supporting >2GB buffers"), but until this commit there was NO
public method that returned the live count as a long. Verified
across the upstream repo's full history (441 commits, 4 branches):
no such accessor has ever existed.

getAvailableBytesExact() returns the raw field via a single
volatile read (no locking; same access pattern as the existing
SBInputStream.available()), and the Javadoc explicitly contrasts
the two contracts:

  - SBInputStream.available()    returns int, clamped to
                                 Integer.MAX_VALUE (InputStream
                                 contract)
  - getAvailableBytesExact()     returns long, exact

Three tests cover initial state, post-write count, and partial-read
decrement - sufficient mutation kills for the one-line body and
documents the contract for future readers.

Pure API addition (no rename, no removal); existing callers and
public API surface unchanged. All 281 tests pass.
Upstream renamed both the artifact (org.jetbrains.kotlinx:lincheck-jvm ->
org.jetbrains.lincheck:lincheck) and the Operation / ModelCheckingOptions
packages (.kotlinx.lincheck.{annotations,strategy.managed.modelchecking}
-> .lincheck.datastructures). LinChecker stayed at the old path and is
still shipped in 3.6 for backward compatibility, so the call site is
unchanged.

The 2.x line is end-of-life on Maven Central (last release 2025-04);
3.x is the actively maintained successor. StreamBufferLincheckTest
passes against 3.6 with the same options (iterations / invocationsPerIteration
/ threads / actorsPerThread).
Maven 3.x warns that <prerequisites> is honoured only for maven-plugin
projects; on a plain jar it is silently ignored. The maven-enforcer-plugin
already enforces requireMavenVersion=[3.6.3,) in this pom, which is
strictly tighter than the 3.3.9 the prerequisites block declared, so
removal is safe and eliminates the build warning.
SpotBugs flags the ArrayDeque allocation in trim() for not presizing.
The principled fix - ceil(availableBytes / maxAllocationSize) over two
volatile long fields, capped at Integer.MAX_VALUE - introduces arithmetic
that PIT cannot kill (ArrayDeque tolerates any non-negative initial
capacity, so capacity-altering mutations are equivalent), which would
break the package's 100 % mutation threshold. trim() is also not on the
hot path: it runs at most once per maxBufferElements writes (default
100), so the ArrayDeque 16-32-64 default growth is negligible against
the surrounding I/O.

The suppression and rationale live in spotbugs-exclude.xml.

The Spotless violations cleaned up in this commit were pre-existing
import-order and trailing-newline issues; they had been masked by the
SpotBugs check failing first in the verify lifecycle.
@sonarqubecloud

sonarqubecloud Bot commented Jun 3, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.77%. Comparing base (81b7c0d) to head (fd74213).

Files with missing lines Patch % Lines
.../java/net/ladenthin/streambuffer/StreamBuffer.java 83.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main      #84      +/-   ##
============================================
- Coverage     97.58%   96.77%   -0.81%     
  Complexity       93       93              
============================================
  Files             1        1              
  Lines           248      248              
  Branches         32       33       +1     
============================================
- Hits            242      240       -2     
- Misses            0        1       +1     
- Partials          6        7       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 99.597% (-0.4%) from 100.0% — claude/inspiring-carson-D831V into main

@bernardladenthin bernardladenthin merged commit 65f51d1 into main Jun 3, 2026
20 of 25 checks passed
@bernardladenthin bernardladenthin deleted the claude/inspiring-carson-D831V branch June 3, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants