feat: enrich diagnostics for AI-agent autofix (Closes #74) - #88
Conversation
Adds two optional fields to DiagnosticData — recommended_api and suggested_snippet — and propagates them on the LSP wire as recommendedApi / suggestedSnippet. Per-rule builders synthesise concrete fix snippets using real variable names from the AST, so agents can paste fixes without re-deriving them. - impure-method now distinguishes IO from throw side-effects (different target_library, message, and recommended_api) and points the diagnostic at the offending statement instead of the method declaration. - Quick-fix coverage extended: imperative-option-unwrap, mutable-dto, field-injection. https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
CI runs `ruff format --check` which fails on local changes not formatted to match. Apply the formatter on the touched files. https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
|
@claude review with ensemble (with majority vote) |
Ensemble review of #88 (majority vote, 3 independent reviewers)Blockers (single reviewer, but type/correctness-fatal)
Major (≥2 reviewers agreed)
Minor (≥2 reviewers agreed)
Minor (single-reviewer, worth flagging)
What did NOT come up as a concern
Suggested triageI'd treat #1, #2, #4, #5 as ship-blockers (one produces broken Java, one is misleading enough to mislead the agents this PR aims to help) and address them in follow-up commits on this branch. #3, #6 are easy AST lookups. The rest is fair game for follow-up issues. Let me know which subset to fix here vs split into follow-ups. Generated by Claude Code |
Blockers: - fix_imperative_option_unwrap now bails when there's no else branch — the previous behavior produced `return opt.map(it -> it);` returning Option<T> instead of the method's declared T (type mismatch). - field-injection is no longer in _FIX_REGISTRY: the previous WorkspaceEdit removed @Autowired and added `final` but never synthesised a constructor, leaving an uninitialised final field that wouldn't compile. The diagnostic + suggested_snippet remain available for AI agents to apply themselves. Majors: - Snippets for field-injection, mutable-dto, and component-annotation now use the real enclosing class name from the AST rather than the placeholder `Foo`. - null-check-to-monadic snippet builds the lambda body from the real return expression (rewriting the checked variable as `it`) and pulls the else-fallback from either the nested else-branch or the fallthrough return — replacing the degenerate `Option.of(x).map(v -> v).getOrElse(null)`. - fix_mutable_dto restricted to @DaTa only (@Setter rewrites silently broke callers by making the class final + stripping setters); also bails on @ConfigurationProperties (use @ConstructorBinding instead) and verifies the annotation is on a class_declaration. - Placeholder strings (`{ ... }`, `(...)`) removed from static snippets. Minors: - autoImportLombok flag added (was incorrectly gated on autoImportVavr). - _to_lsp_diagnostic data typed as dict[str, Any] | None for future non-string fields. - field-injection snippet now bails for multi-declarator fields. - _classify_side_effect kind typed as Literal["throw", "io"]. - null_checker assignment/field-assignment snippets pull real variable name and declared type from the AST. Docs + tests: - CONTRIBUTING.md "Adding a New Rule" updated with recommended_api and suggested_snippet guidance. - New e2e test asserts recommendedApi + suggestedSnippet appear on the LSP wire. - 13 regression tests covering each fix above. https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
…d variants, snippet quality)
Material items from the three individual reviewer reports:
1. frozen-mutation snippet now bails on chained LHS (e.g. `foo.getList()`,
`this.items`) and explicitly spells out the io.vavr.collection.List type
migration — pasting `items = items.append(...)` alone wouldn't compile when
`items` is still a java.util.List.
2. impure-method io vs throw variants surface via distinct fix_type values
(`EXTRACT_PURE_LOGIC_IO` / `EXTRACT_PURE_LOGIC_THROW`), so agents can filter
on the variant without parsing target_library or message text. The wire-
visible rule code stays `impure-method` to preserve user severity-overrides.
3. mutable-variable recommended_api drops `final +` (it's a language modifier,
not an API). The rationale field still mentions final.
4. throw-statement snippet picks a single alternative (`return Either.left(e);`)
instead of mixing two with a `//` comment — agents pasting verbatim would
otherwise get the comment too.
5. KNOWN_RULES set added in analyzers/__init__.py; server import-time assertion
catches typos in _FIX_TITLES that reference rules no analyzer emits.
6. _build_throw_statement_data filters named_children for the expression
(tree-sitter-java doesn't field-name throw's expression).
7. All _build_*_data builders refactored to use dataclasses.replace instead of
hand-copying every DiagnosticData field — adding a new field to
DiagnosticData no longer requires touching every builder.
8. _VAVR_MUTATION_METHODS lifted to module scope (was re-allocated per
diagnostic).
9. New `_apply_edits` test helper applies TextEdits to source and lets tests
assert on resulting source — catches off-by-one range bugs.
10. _classify_side_effect docstring acknowledges DFS-first semantics for the
rare case of a single statement containing both throw and IO.
11 regression tests covering each of the above. 679 tests pass, ruff format +
check + mypy strict all clean.
https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
Minor bump for the AI-agent diagnostic enrichment feature (PR #88, Closes #74): DiagnosticData adds optional recommendedApi + suggestedSnippet fields, three new quick-fix generators, refined impure-method discrimination. https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
Suggested release notes for v0.11.0The release-drafter created a draft labeled v0.10.1 (default patch bump) because no Tag: Title: Body: ## What's Changed
`DiagnosticData` now carries two optional fields — `recommendedApi` and `suggestedSnippet` — that flow through to the LSP wire so AI coding agents (e.g. Claude Code) can produce correct fixes on the first try without re-deriving Vavr/Lombok APIs or variable names.
### Highlights
- **AST-aware snippets**: per-rule builders pull real variable names, types, and return expressions from the AST so snippets are paste-able. Example for `if (myOpt.isDefined()) return myOpt.get(); else return "fallback";` produces `return myOpt.map(it -> it).getOrElse("fallback");`.
- **`impure-method` discriminates IO vs throw**: distinct messages, distinct `target_library` (Try vs Either), distinct `fix_type` (`EXTRACT_PURE_LOGIC_IO` / `_THROW`); diagnostic now points at the offending statement, not the method declaration.
- **New quick-fixes**: `imperative-option-unwrap`, `mutable-dto`. `field-injection` ships richer diagnostic + snippet but no quick-fix (constructor synthesis would risk clobbering existing user code).
- **`KNOWN_RULES` symmetry check** at server import time catches typos in `_FIX_TITLES` that reference non-existent rule codes.
- **New `autoImportLombok` config flag** (defaults true) so users can disable Vavr auto-import without losing Lombok auto-import (or vice versa).
### Test plan
- 90 new tests including end-to-end wire assertion that `recommendedApi`/`suggestedSnippet` reach the client.
- `ruff format/check` + `mypy --strict` clean.
### Commits
- feat: enrich diagnostics for AI-agent autofix (Closes #74) (#88)
**Full Changelog**: https://github.com/aviadshiber/java-functional-lsp/compare/v0.10.0...v0.11.0For future PRs: adding the Generated by Claude Code |
PR #88 bumped pyproject.toml to 0.11.0 but left src/java_functional_lsp/__init__.py at 0.10.0, so the installed 0.11.0 package reported "java-functional-lsp 0.10.0" via --version. Bump both files to 0.11.1 so the published artifact reports the correct version. No functional change. Co-authored-by: Aviad Shiber <aviad.s@taboola.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
PR #88 (v0.11.0) bumped pyproject.toml but left src/java_functional_lsp/__init__.py at 0.10.0, shipping a package whose --version disagreed with its installed metadata. v0.11.1 synced them. This test reads both values and asserts they match, so the next bump that touches only one side fails CI before it ships. Co-authored-by: Aviad Shiber <aviad.s@taboola.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
… hook (#70), AI-UX closure (#74) (#94) * feat: add option-map-nullable rule for Vavr Some(null) hazard (#69) 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 * feat: lint edited .java files via PostToolUse hook (#70) 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 * fix: derive real lambda body in unwrap snippet; sync docs (#74) 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 * fix: exclude octal/binary index literals from option-map-nullable; reuse 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 * refactor: consolidate return-extraction and var.get() rewrite into base.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 --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Addresses all 5 suggestions in #74 to make
java-functional-lspdiagnostics self-describing enough for an AI coding agent to autofix on the first try.DiagnosticDatagets two new optional fields:recommended_api(library-agnostic API hint, e.g."forEach (NOT ifPresent — Vavr Option)") andsuggested_snippet(concrete fix using real AST variable names). Serialised to the LSP wire asrecommendedApi/suggestedSnippet. Existing payloads stay backward-compatible.if (myOpt.isDefined()) return myOpt.get(); else return "fallback";now produces a snippet that referencesmyOptand"fallback"literally.impure-methodrefined: distinguishes IO side-effects (Try) from throw side-effects (Either) with different messages, differenttarget_library, and differentrecommended_api. Diagnostic range now points at the offending statement instead of the method declaration (issue AI agent UX: richer diagnostic data for automated fix generation #74 fix: release-drafter permissions and bump to v7 #5).imperative-option-unwrap,mutable-dto,field-injectionnow have registered fix generators. Each bails safely when the AST shape isn't trivially rewritable.Files touched
analyzers/base.py,server.pyanalyzers/{mutation,functional,exception,spring,null}_checker.pyfixes.pytests/test_{fixes,mutation_checker,functional_checker,exception_checker,spring_checker,server}.pyTest plan
uv run pytest tests/— 659 passed (17 new), 7 skipped (jdtls-only).uv run ruff check src/ tests/— clean.uv run mypy src/— clean.recommendedApi/suggestedSnippetappear inDiagnostic.dataJSON for at leastimperative-option-unwrapandimpure-method.if (opt.isDefined()) return opt.get(); else return "x";— confirm the Convert-to-Option-map code action applies cleanly.impure-methodon a throw-only method points at thethrowline and produces the "exceptions" message variant.https://claude.ai/code/session_01Sp1TuWErAKym7CJ8n9pdrq
Generated by Claude Code