Skip to content

Releases: aviadshiber/java-functional-lsp

v0.7.7

11 Apr 20:24
9f67d6c

Choose a tag to compare

fix: remove eager workspace expansion that negated module-scoping

  • Remove eager workspace expansion that undid module-scoping optimization
  • Gate READY signal on publishDiagnostics (reliable indexing-complete signal)
  • Add startup retry with 5-minute cooldown for transient failures

v0.7.6

11 Apr 19:05
9235820

Choose a tag to compare

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

11 Apr 18:06
ff0f3f2

Choose a tag to compare

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-lsp

v0.7.4 — lazy module-scoped jdtls with adaptive waiting

11 Apr 16:48
a239874

Choose a tag to compare

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-lsp

v0.7.3 — dynamic jdtls capability registration

10 Apr 07:14
a88a9f1

Choose a tag to compare

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-lsp

v0.7.2 — fix jdtls request forwarding + e2e test suite

10 Apr 05:51
150d553

Choose a tag to compare

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-lsp

v0.7.1 — jdtls Java 21+ detection fix

09 Apr 14:30
03edf9c

Choose a tag to compare

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:

  1. JDTLS_JAVA_HOME variable (explicit user override)
  2. Existing JAVA_HOME if it already points at Java 21+
  3. macOS: /usr/libexec/java_home -v 21+
  4. java on PATH if 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 like AWS_ACCESS_KEY_ID, GITHUB_TOKEN, and ANTHROPIC_API_KEY from 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.parent resolves to /usr, /usr/local, /bin, or similar system roots that would not be valid JAVA_HOME values.

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 redundant java -version re-check on its result is skipped, saving 200-500 ms on macOS startup.
  • _JAVA_VERSION_CHECK_TIMEOUT_SEC reduced from 5 s to 2 s.

Quality & Tests

  • Extracted _is_in_bean_method, IGNORED_CHILDREN, and references_var to analyzers/base.py for 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.TimeoutExpired path, JDTLS_JAVA_HOME fall-through, empty-override handling, nonexistent-path short-circuit spy, macOS-trust behavior, PATH-fallback happy/reject/system-prefix paths, shutil.which path= forwarding, allow-list secret filtering, LC_/XDG_/JDTLS_ prefix forwarding, mutation isolation, path redaction, symlink-following, and an integration test verifying JdtlsProxy.start() passes env= to create_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

09 Apr 10:52
cb4d073

Choose a tag to compare

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, and has_error_or_missing to analyzers/base.py as shared helpers
  • Consolidated ExceptionChecker.analyze to a single tree walk via collect_nodes_by_type (3× reduction in traversal cost)
  • Defensive has_error_or_missing gate refuses to rewrite subtrees with tree-sitter ERROR/MISSING nodes (prevents broken edits during incremental typing)
  • Added _strip_trailing_semicolon helper that uses removesuffix instead of the fragile rstrip(";") 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

08 Apr 08:51
55002b6

Choose a tag to compare

Bug Fixes

  • Fix jdtls subprocess crash — jdtls requires a -data directory for workspace metadata (index, classpath, build state). Now uses ~/.cache/jdtls-data/<hash> so it persists across reboots
  • Fix rootUri: null crash — LSP spec allows null rootUri for single-file mode. Now falls back to rootPath then cwd() 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-lsp

Full Changelog: v0.6.3...v0.6.4

v0.6.3

08 Apr 08:15
a7041f1

Choose a tag to compare

New Feature

  • Chained null-check code action — detects and rewrites chained identity null-checks into Option.of().orElse() chains:
    // 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);
    Supports N levels of chaining, lazy .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-monadic diagnostics
  • Safety guards — iterative collection (no recursion), max 10 chain depth, declaration adjacency validation, complex expression parenthesization

Bug Fix

  • Diagnostic message — updated null-check-to-monadic message from .getOrNull() to .getOrElse() to align with the code action output

Install / Upgrade

brew upgrade java-functional-lsp
# or
pip install --upgrade java-functional-lsp

Full Changelog: v0.6.1...v0.6.3