Refactor method names and add nullness annotations#84
Merged
Conversation
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.
|
Codecov Report❌ Patch coverage is
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
getBufferSize()→getBufferElementCount()andblockDataAvailable()→waitForAnyData()for clarity; deprecate old namescorrectOffsetAndLength*()→validateOffsetAndLength*()to better reflect their purpose@NullMarkedmodule-level nullness annotations and Checker Framework qualifiers; configure NullAway and Checker Framework as compile-time enforcementLinkedListwithArrayDequefor better performance (O(1) vs O(n) operations)getAvailableBytesExact()accessor for fulllongcount (vsavailable()clamped toInteger.MAX_VALUE)1.8to8for claritymodule-info.java) for Java 9+ module-path consumersSystem.exit(),Random, andThread.sleep()Test plan
StreamBufferTestandStreamBufferArchitectureTesttests updated and passing)Related issues / PRs
None
Checklist
CONTRIBUTING.mdandCODE_OF_CONDUCT.mdhttps://claude.ai/code/session_01CfWQZHtpJZgMv3C5tJBqxx