Fix all open issues: option-map-nullable rule (#69), PostToolUse lint hook (#70), AI-UX closure (#74) - #94
Merged
Conversation
Detect Vavr Option chains where .map() can wrap a null mapper result as Some(null) — unlike java.util.Optional — and a chained .filter()/.map()/ .forEach()/etc. then NPEs on the value. Conservative three-gate detection to avoid false positives: 1. a value-consuming follower is chained after .map(...) 2. the receiver chain is rooted in a literal Option.of()/Option.ofOptional() 3. the lambda body matches a known possibly-null shape (x.get(key) — Map.get / JsonNode.get; zero-arg get() and integer-index get(0) excluded) The data payload carries a paste-able .flatMap(x -> Option.of(...)) snippet built from the real lambda parameter and body text. https://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
Add hooks/post_tool_lint.py: runs java-functional-lsp on the edited file after Edit/MultiEdit/Write and surfaces violations to Claude as hookSpecificOutput.additionalContext, so Vavr-specific rules are checked in the editing flow instead of only via manual /lint-java runs. - only fires on .java files; lints just the changed file (in-process import with CLI fallback when the package isn't importable under the hook's python3) - silent on clean files; diagnostics capped at 25 to bound context - failure-safe: every path exits 0, SIGALRM + subprocess timeout cap runtime, so a linter problem can never break the editing session The existing reminder hook is narrowed to Read — the new hook embeds its own fix instruction for edits, so keeping the reminder on Edit|Write would duplicate context. https://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
Complete issue #74 (PR #88 shipped the core — variant-aware impure-method, recommendedApi/suggestedSnippet payloads, and two new quick fixes): - imperative-option-unwrap's suggested_snippet now derives the lambda body from the real then-branch expression (return opt.get().toUpperCase() becomes .map(value -> value.toUpperCase())) instead of the placeholder value -> value, mirroring the quick-fix rewrite in fixes.py - README/SKILL.md: flip Quick Fix column for mutable-dto and imperative-option-unwrap, document the two code actions, the recommendedApi/suggestedSnippet data fields, and autoImportLombok - README/SKILL.md/editor READMEs: rule count 16 -> 17 for the new option-map-nullable rule (vscode README still said 12) - uv.lock: refresh stale package version (0.10.0 -> 0.11.1) https://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
aviadshiber
marked this pull request as ready for review
June 12, 2026 14:17
…use IGNORED_CHILDREN Ensemble review findings on PR #94: - xs.get(010) / xs.get(0b101) falsely fired option-map-nullable because tree-sitter emits octal_integer_literal / binary_integer_literal node types that were missing from _INTEGER_LITERAL_TYPES; index access throws rather than returning null regardless of radix. Verified empirically. - mutation_checker's new helpers re-declared the comment-filter tuple locally; use the shared IGNORED_CHILDREN constant from base instead. https://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
…se.py Majority-agreement finding from the ensemble review (Reuse + Altitude agents independently flagged it): three copies of single-return-statement extraction lived in functional_checker, mutation_checker, and fixes.py, and the var.get()->param regex rewrite was duplicated between mutation_checker and fixes.py. All now live in base.py (the leaf module, so no import cycle): single_return_stmt, return_expr_node, return_expr_text, single_return_expr_text, rewrite_var_get_call, rewrite_var_references. The shared rewrite also fixes two confirmed defects in the old copies: - regex \b never matches before $-prefixed Java identifiers (and falsely matches inside them), silently dropping the rewrite - no tolerance for whitespace around '.'/parens (opt .get () is valid Java) Gate order in option-map-nullable kept as-is: benchmarked the suggested lambda-shape-first reorder and the chain walk is ~3x cheaper per .map() on fluent-heavy code (4.4ms vs 12.2ms per 4000-call pass), so the claim was refuted by measurement; documented in the gate comment. https://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
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
One PR addressing all three open issues, planned via parallel design passes per issue.
#69 — New rule:
option-map-nullableDetects Vavr
Optionchains where.map()can wrap a null mapper result asSome(null)(unlikejava.util.Optional) and a chained.filter()/.map()/.forEach()/etc. then NPEs on the value — the exact bug from the DEV-219412 incident:Detection is deliberately conservative (three gates) to keep false positives at zero:
filter,map,flatMap,forEach,peek,exists,forAll) is chained after.map(...)— terminal extractors likegetOrElseare excludedOption.of()/Option.ofOptional()(qualifiedio.vavr.control.Option.ofalso matched); bare variables andjava.util.Optionalnever firex.get(key)(Map.get / JsonNode.get). Zero-argget()(Vavr/Supplier) and integer-indexList.get(0)are excluded — all four Java integer-literal radixes (decimal/hex/octal/binary)Severity: Warning. The
datapayload carriesfixType: USE_FLATMAP_OPTION_OFand a paste-able snippet built from the real AST names, e.g..flatMap(m -> Option.of(m.get("author"))). Broader heuristics from the issue (getters without@NonNull,@Nonnullanalysis) are left as future work — tree-sitter has no type/annotation resolution across files.#70 — PostToolUse hook: real-time linting on Edit/Write
New
hooks/post_tool_lint.py, wired inhooks.jsonwith matcherEdit|MultiEdit|Write:.javafile (in-process import, single-file analysis ~ms; CLI fallback when the package isn't importable under the hook'spython3)hookSpecificOutput.additionalContext, so the agent actually sees them in context (plain stdout on PostToolUse is transcript-only).java-functional-lsp.jsonexcludesThe existing reminder hook is narrowed from
Read|Edit|WritetoRead: the new hook embeds its own fix instruction for edits, so keeping the reminder there would duplicate context. Flagging this as the one intentional behavior change — happy to revert the narrowing if you prefer zero change to the reminder.Manual (non-plugin) install instructions added to README. A future strict mode could use
decision: block/ exit 2; left out as it would fight multi-step edits and the repo's non-blocking philosophy.#74 — AI-agent UX (closure)
PR #88 already shipped the core of #74 (variant-aware
impure-methodmessages + offending-line ranges,recommendedApi/suggestedSnippetpayloads,imperative-option-unwrapandmutable-dtoquick fixes — the tests cite the issue). This PR closes the remaining gaps:imperative-option-unwrap'ssuggestedSnippetnow derives the lambda body from the real then-branch expression —return opt.get().toUpperCase();→.map(value -> value.toUpperCase())instead of thevalue -> valueplaceholder, sharing the rewrite helper with the quick fixmutable-dtoandimperative-option-unwrap, document both code actions, therecommendedApi/suggestedSnippetdata fields, and the previously-undocumentedautoImportLombokconfig keyNot included (follow-up candidates): a
forEachquick fix for statement-shape unwraps, and afield-injectionquick fix (intentionally unregistered infixes.py— constructor synthesis risks clobbering user code).Post-review hardening (ensemble review, commits 4–5)
A 7-angle ensemble review of this PR produced findings that were verified empirically before acting:
xs.get(010),xs.get(0b101)) falsely firedoption-map-nullable— tree-sitter emits per-radix literal node typesvar.get()regex rewrite into sharedbase.pyhelpers (single_return_stmt,return_expr_node/text,single_return_expr_text,rewrite_var_get_call,rewrite_var_references);functional_checker,mutation_checker, andfixes.pynow share one implementation. The shared rewrite also fixes two confirmed defects of the old copies:\bregex boundaries silently failing on$-prefixed Java identifiers, and no tolerance for whitespace around.get ()option-map-nullable(lambda-shape check before chain walk) — benchmarked at ~3× slower (chain walks terminate after one step at a bare identifier in fluent code); the measured-cheapest order is documented in the gate commentscollect_nodes_by_typeforFunctionalChecker(the new rule costs ~64ms only on a pathological 307KB/2000-method file; negligible on real files), acli.lint_file_for_hook()entry point, and a_NULLABLE_GETTER_METHODSconfig setCloses #69, closes #70, closes #74.
Testing
$-identifier and whitespace regressions); full suite 716 passed, 7 skipped, coverage 84.58% (≥80% gate)ruff check,ruff format --check,mypyall cleanoption-map-nullableat the.maptoken; the flatMap fix pattern stays silenthttps://claude.ai/code/session_018SmBpTQU3hqtw8EbWMcZ5M
Generated by Claude Code