Releases: aviadshiber/java-functional-lsp
v0.7.7
v0.7.6
fix: skip non-Java URIs in jdtls diagnostics callback
jdtls publishes diagnostics for directory URIs and build files during workspace initialization. The callback tried to read these as text documents, causing [Errno 21] Is a directory errors. Only run custom analysis on .java URIs.
v0.7.5 — restore 120s initialize timeout
Fix: restore 120s timeout for jdtls initialize handshake.
Even module-scoped init can take >30s when the module has heavy Maven dependencies that need classpath resolution on first cold start. _INITIALIZE_TIMEOUT = 120s applies only to the initialize request; normal request timeout stays at 30s.
brew upgrade java-functional-lspv0.7.4 — lazy module-scoped jdtls with adaptive waiting
Features
Lazy module-scoped jdtls initialization
jdtls no longer blocks on startup. First didOpen → finds nearest pom.xml/build.gradle → starts jdtls scoped to that module (~2-3s vs 30-120s). Custom diagnostics publish immediately.
Incremental module loading
Each new file open adds its module via workspace/didChangeWorkspaceFolders. Full workspace expands in the background.
Demand-driven module prioritization (ModuleRegistry)
When hover/definition/references targets an unloaded module:
- READY → forward instantly (zero overhead)
- UNKNOWN → add module, try, wait adaptively via
asyncio.Event, retry - ADDED → try, wait, retry
First success fires Event.set() — all waiting coroutines wake instantly. No fixed sleep.
Dynamic jdtls capability registration
hover/definition/references/completion/documentSymbol registered only after jdtls starts. IDE diagnostic tooltips never suppressed.
Numbers
- 361 tests, 84% coverage
- Custom diagnostics: immediate (< 1s)
- jdtls features: ~2-3s (was 30-120s)
Upgrade
brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lspv0.7.3 — dynamic jdtls capability registration
Fix
Diagnostic tooltips no longer suppressed while jdtls is starting.
Previously, hover/definition/references/completion/documentSymbol were advertised in the static InitializeResult even before jdtls started. When the IDE sent a hover request and we returned null (jdtls not ready), the IDE suppressed its built-in diagnostic tooltips.
Now these capabilities are registered dynamically via client/registerCapability only after jdtls initializes successfully. Diagnostic tooltips from our custom rules show immediately; jdtls features activate when ready.
Details
- Idempotent registration with guard flag (safe on proxy restart)
- Handler + client notification in same try/except (no capability mismatch on failure)
- 336 tests, 84% coverage
Upgrade
brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lspv0.7.2 — fix jdtls request forwarding + e2e test suite
Bug Fix
Fixed broken jdtls request forwarding — every go-to-definition, find-references, hover, document symbol, and completion request was failing with NullPointerException.
Root cause: cattrs.Converter() emitted snake_case field names (text_document) instead of LSP-required camelCase (textDocument). jdtls saw null for TextDocumentPositionParams.getTextDocument() and NPE'd on every forwarded request.
Fix: One-line change: cattrs.Converter() → lsprotocol.converters.get_converter().
Test Architecture (the real payload)
Added a comprehensive 4-tier test suite that makes such regressions impossible:
| Tier | Tests | Mocks | What it catches |
|---|---|---|---|
| Unit | 292 | Minimal | Analyzer logic, fix generators, proxy helpers |
| Server internals | 16 | 1 (transport) | Config, diagnostics, code actions, camelCase |
| LSP lifecycle | 9 | Zero | Real server subprocess via pygls LanguageClient |
| jdtls e2e | 7 | Zero | Real jdtls: definition, references, hover, completion, documentSymbol |
Numbers
- 331 tests (was 292)
- 83% coverage (was 77%), threshold raised to 80%
- LSP lifecycle tests: ~3s (real subprocess)
- jdtls e2e tests: ~10s (class-scoped fixtures, single cold-start)
CI
- Unit matrix: 8 combos (Ubuntu + macOS × 4 Python versions),
-m "not e2e", 80% coverage - Integration job: Ubuntu + macOS, Python 3.12, Java 21 + jdtls 1.57.0 (sha256-verified), full suite
Upgrade
brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lspv0.7.1 — jdtls Java 21+ detection fix
Highlights
Patch release fixing a jdtls startup regression. If you use java-functional-lsp with IntelliJ (or any IDE) on a project with a Java SDK older than Java 21, this release makes jdtls features (completions, hover, go-to-def) work again — the custom functional rules were unaffected.
Bug Fixes
jdtls subprocess fails when IDE project SDK is older than Java 21 (#42)
Symptom:
java_functional_lsp.proxy ERROR: jdtls stderr: raise Exception("jdtls requires at least Java 21")
java_functional_lsp.proxy WARNING: jdtls request initialize timed out after 30.0s
java_functional_lsp.proxy ERROR: jdtls initialize request failed or timed out
java_functional_lsp.server INFO: jdtls proxy unavailable — running with custom rules only
Root cause: jdtls 1.57's Python launcher checks $JAVA_HOME first and only falls back to its hardcoded default if the variable is unset. When IntelliJ launches the LSP with JAVA_HOME inherited from a project SDK (commonly Java 8/11/17), that value is forwarded to the jdtls subprocess and the version check rejects it.
Fix: Before launching jdtls, detect a Java 21+ installation and set JAVA_HOME explicitly in the subprocess environment. Resolution order:
JDTLS_JAVA_HOMEvariable (explicit user override)- Existing
JAVA_HOMEif it already points at Java 21+ - macOS:
/usr/libexec/java_home -v 21+ javaonPATHif it reports version >= 21
If none are found, JAVA_HOME is stripped so the jdtls launcher can use its bundled default.
Security hardening (defense in depth)
- Environment allow-list — jdtls now receives only a filtered set of variables (
PATH,HOME,USER,LANG,LC_*,XDG_*,JDTLS_*,JAVA_HOME,JAVA_TOOL_OPTIONS, etc.) instead of the full parent environment. Prevents secrets likeAWS_ACCESS_KEY_ID,GITHUB_TOKEN, andANTHROPIC_API_KEYfrom leaking into a third-party subprocess. - Log redaction — path values in log messages are now redacted to just their basename (
.../jdk21) instead of the full filesystem path, avoiding username and directory-layout disclosure (CWE-532). - System-prefix rejection — PATH-based Java discovery rejects paths whose
parent.parentresolves to/usr,/usr/local,/bin, or similar system roots that would not be validJAVA_HOMEvalues.
Performance
- Java detection now runs via
loop.run_in_executor(...), so the blocking subprocess calls (java -version,/usr/libexec/java_home) no longer block the asyncio event loop during LSP startup. The IDE's initial LSP handshake proceeds without stalling. /usr/libexec/java_home -v 21+output is now trusted directly — the redundantjava -versionre-check on its result is skipped, saving 200-500 ms on macOS startup._JAVA_VERSION_CHECK_TIMEOUT_SECreduced from 5 s to 2 s.
Quality & Tests
- Extracted
_is_in_bean_method,IGNORED_CHILDREN, andreferences_vartoanalyzers/base.pyfor sharing between the analyzer and fix generator (previously duplicated from #40). - 292 tests pass (up from 273), 77% coverage.
- 18 new proxy tests covering:
subprocess.TimeoutExpiredpath,JDTLS_JAVA_HOMEfall-through, empty-override handling, nonexistent-path short-circuit spy, macOS-trust behavior, PATH-fallback happy/reject/system-prefix paths,shutil.whichpath=forwarding, allow-list secret filtering, LC_/XDG_/JDTLS_ prefix forwarding, mutation isolation, path redaction, symlink-following, and an integration test verifyingJdtlsProxy.start()passesenv=tocreate_subprocess_exec.
Included PRs
- #42 — fix: detect Java 21+ for jdtls subprocess, override inherited JAVA_HOME
Full Changelog: v0.7.0...v0.7.1
v0.7.0 — try-catch-to-monadic code action
Highlights
A new functional-refactoring rule: try-catch-to-monadic that detects imperative try/catch blocks and offers a QuickFix that rewrites them to Vavr Try.of(...) monadic chains.
New Rules
try-catch-to-monadic (HINT, opt-in severity)
Detects rewritable try/catch shapes and offers a QuickFix for three patterns:
Pattern 1 — Simple default
try { return risky(); }
catch (IOException e) { return "default"; }→ return Try.of(() -> risky()).getOrElse("default");
(eager vs lazy .getOrElse(...) based on whether the default is a literal/identifier or a method call)
Pattern 2 — Logging + default
try { return risky(); }
catch (IOException e) {
logger.warn("failed", e);
return "default";
}→ return Try.of(() -> risky()).onFailure(e -> logger.warn("failed", e)).getOrElse("default");
Pattern 3 — Exception-dependent recovery
try { return risky(); }
catch (IOException e) { return fallback(e); }→ return Try.of(() -> risky()).recover(IOException.class, e -> fallback(e)).get();
The rule is conservative: it skips try-with-resources, finally clauses, multi-catch (A | B e), multi-statement try bodies, and the Pattern 2+3 hybrid. Diagnostic is suppressed inside @Bean methods (same as throw-statement/catch-rethrow).
Bug Fixes
- jdtls — Increase jdtls heap to 4 GB and log stderr for debugging (#39)
Internal improvements
- Extracted
IGNORED_CHILDREN,references_var, andhas_error_or_missingtoanalyzers/base.pyas shared helpers - Consolidated
ExceptionChecker.analyzeto a single tree walk viacollect_nodes_by_type(3× reduction in traversal cost) - Defensive
has_error_or_missinggate refuses to rewrite subtrees with tree-sitter ERROR/MISSING nodes (prevents broken edits during incremental typing) - Added
_strip_trailing_semicolonhelper that usesremovesuffixinstead of the fragilerstrip(";")character-set strip
Testing
- 259 tests passing (up from 242), 76% coverage
- 17 new tests covering union-catch, try-with-resources, nested try/catch, Pattern 2+3 hybrid rejection, ancestor-walk lookup, exact-equality assertions, and more
- Review ensemble (5 agents) verdict: APPROVE after fixes
Included PRs
- #39 — fix: increase jdtls heap to 4GB and log stderr for debugging
- #40 — feat: add try-catch-to-monadic code action with Vavr Try.of() rewrites
- #41 — chore: bump version to 0.7.0
Full Changelog: v0.6.4...v0.7.0
v0.6.4
Bug Fixes
- Fix jdtls subprocess crash — jdtls requires a
-datadirectory for workspace metadata (index, classpath, build state). Now uses~/.cache/jdtls-data/<hash>so it persists across reboots - Fix
rootUri: nullcrash — LSP spec allows null rootUri for single-file mode. Now falls back torootPaththencwd()instead of crashing with AttributeError - Fix pre-commit version hook — extracts actual version value instead of comparing full lines, skips check when version is unresolvable (new projects), works correctly with
--amend
Install / Upgrade
brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lspFull Changelog: v0.6.3...v0.6.4
v0.6.3
New Feature
- Chained null-check code action — detects and rewrites chained identity null-checks into
Option.of().orElse()chains:Supports N levels of chaining, lazy// BEFORE Integer val = map1.get(key); if (val != null) { return val; } else { val = map2.get(key); if (val != null) { return val; } } return defaultVal; // AFTER (code action) return Option.of(map1.get(key)) .orElse(() -> Option.of(map2.get(key))) .getOrElse(defaultVal);
.getOrElse(() -> compute())for method call defaults, and parenthesizes complex fallback expressions (ternary, lambda).
Improvements
- Suppress duplicate diagnostics — inner if-statements in a chain no longer produce separate
null-check-to-monadicdiagnostics - Safety guards — iterative collection (no recursion), max 10 chain depth, declaration adjacency validation, complex expression parenthesization
Bug Fix
- Diagnostic message — updated
null-check-to-monadicmessage from.getOrNull()to.getOrElse()to align with the code action output
Install / Upgrade
brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lspFull Changelog: v0.6.1...v0.6.3