Skip to content

get_diagnostics_for_file returns empty for Java (Eclipse JDT LS): publishDiagnostics fallback timeout (2.5s) shorter than JDT validation #1613

Description

@weiconghe

Summary

get_diagnostics_for_file returns an empty result ({}) for Java files even when the Eclipse JDT Language Server has real diagnostics to report. The bundled JDT LS does not implement textDocument/diagnostic (LSP 3.17 pull diagnostics), so solidlsp falls back to waiting for publishDiagnostics. The default fallback wait is 2.5s, but JDT LS's first-time validation of a file in a real project commonly takes 10–15s+, so serena stops waiting and returns {} before JDT publishes.

Scenario

A single shared serena MCP instance (streamable-http transport on a fixed port), used by multiple AI coding agents concurrently, activated against a large multi-module Maven Java project (~67 modules, Spring Boot). get_diagnostics_for_file was called on a service-implementation .java file.

  • serena 1.5.3 (latest release), Windows 11
  • bundled JDT LS 1.58.0-SNAPSHOT (git 7be965c, 2026-04-15)

Timeline from the serena log (the file was opened for the first time in this session):

16:54:01.575  get_diagnostics_for_file  relative_path='.../DataSupplierServiceImpl.java', min_severity=1
16:54:06.499  [java stderr] java.lang.UnsupportedOperationException
                at org.eclipse.lsp4j.services.TextDocumentService.diagnostic(TextDocumentService.java:691)
16:54:06.932  JDT logMessage: Reconciled 1. Took 34 ms
16:54:09.022  Task Result: {}                  <- tool returns empty (pull failed ~06.5 + 2.5s wait)
16:54:22.071  JDT logMessage: 5 problems reported for /DataSupplierServiceImpl.java
16:54:22.109  JDT logMessage: Validated 1. Took 14572 ms

The tool returned {} at +7.5s; JDT actually computed and published 5 real problems at +15s — ~13s after serena had already given up. The net effect is that the agent receives a false "no diagnostics" signal for a file that has 5 real problems.

Root cause — exact code path

get_diagnostics_for_file reaches SolidLanguageServer.request_text_document_diagnostics in src/solidlsp/ls.py. For a Java file the execution is:

  1. A pull-diagnostics request is sent. The guard is if self._supports_pull_diagnostics():. _supports_pull_diagnostics() defaults to True (ls.py, base class). Only julia_server.py overrides it. EclipseJDTLS (src/solidlsp/language_servers/eclipse_jdtls.py) does not override it, so for Java the pull request textDocument/diagnostic is always sent.

  2. JDT LS cannot handle pull diagnostics — by design, not by accident. In lsp4j, a textDocument/diagnostic request is reflected onto org.eclipse.lsp4j.services.TextDocumentService.diagnostic(...), which is a default interface method whose body throws UnsupportedOperationException. The Eclipse JDT Language Server implementation does not override diagnostic(...) (it uses the classic publishDiagnostics push model), so every pull request hits the default stub and throws. The stack trace in the log — at org.eclipse.lsp4j.services.TextDocumentService.diagnostic(TextDocumentService.java:691) — is precisely that default stub. JDT turns the throw into a JSON-RPC error response ("Internal error: null"). So this is not a transient or version-specific failure; it fails identically on every Java diagnostics call.

  3. solidlsp catches the pull failure and falls back to push. send_request raises SolidLSPException on the error response (src/solidlsp/ls_process.py); request_text_document_diagnostics catches it, sets pull_diagnostics_failed = True, and falls through to _wait_for_relevant_published_diagnostics(...).

  4. The push fallback waits only 2.5s, even right after a pull failure. The wait timeout is self._get_published_diagnostics_wait_timeout(pull_diagnostics_failed). The base implementation returns 2.5 and ignores its pull_diagnostics_failed argument. So although pull just failed and serena now depends entirely on the push channel, the wait is still only 2.5s. (EclipseJDTLS does not override this method either.)

  5. JDT publishes after ~14.5s, well past the 2.5s window. JDT emits textDocument/publishDiagnostics for a file only once its validation pass completes. For a first-opened file in a large multi-module Maven project this pass resolves cross-module type references and takes ~14.5s here (log: Validated 1. Took 14572 ms). Since 2.5s < 14.5s, the wait expires empty and the tool returns {}.

Two things worth noting

  • The pull_diagnostics_failed parameter on _get_published_diagnostics_wait_timeout exists precisely so a server can wait longer when it had to fall back to push. The base class doesn't act on it and Java doesn't override it — so the existing design already anticipates this case, it just isn't wired up for Java.
  • The push channel itself works fine for Java. The same log shows JDT did compute and report the problems (5 problems reported for /DataSupplierServiceImpl.java at +15s; the window/logMessage accompanies a textDocument/publishDiagnostics notification), and test/solidlsp/java/test_java_diagnostics.py passes — proving serena's publishDiagnostics capture works for Java. So the empty result is purely the too-short wait, not a capture/URI bug. A longer window would have received the same 5 problems.

Why the existing test doesn't catch this

test/solidlsp/java/test_java_diagnostics.py exercises a single tiny fixture (DiagnosticsSample.java) where JDT validates in milliseconds, so the 2.5s window is always plenty. The bug only manifests on large-project cold validation, which the fixture can't reproduce.

What I verified

  • Reproduced the mechanism from serena's source against the same project: the pull UnsupportedOperationException fires on every call, and request_text_document_diagnostics returns [] in ~3s (pull fails fast + 2.5s wait), matching the production timeline.
  • I could not reproduce the full "5 problems arrive late" in an isolated run: a fresh JDT workspace on this 67-module Maven project did not finish resolving within my warm-up budget, so it produced no diagnostics in the window I measured. The warm, repeatedly-used workspace in the original session is where the late-but-real publication is observed (log above). I'm flagging this honestly rather than overclaiming a verified fix.

Suggested fix (mirrors Julia — commit da59a19)

Java is the exact same situation as Julia: pull unsupported + a slow language server that publishes late. Julia's merged fix overrides both hooks, and Java can do the same:

# src/solidlsp/language_servers/eclipse_jdtls.py
@override
def _supports_pull_diagnostics(self) -> bool:
    # Eclipse JDT LS does not implement textDocument/diagnostic (lsp4j default
    # stub throws UnsupportedOperationException); force the publishDiagnostics path.
    return False

@override
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
    # JDT's first-time validation of a file in a large project commonly takes
    # 10-15s+; the 2.5s default expires before it publishes.
    return 30.0

Distinction from Rust (#1559 / #1557)

#1559 ("Stabilize Rust diagnostics retrieval") was closed without merge, but that is Rust-specific: rust-analyzer's pull diagnostics work once checkOnSave is enabled (see open #1557), so a wait-timeout bump was the wrong layer for Rust. Java, like Julia, has pull genuinely unsupported — the per-LS override is the established pattern here (already used by julia/typescript/fortran/powershell/solidity).

Open questions

  • Timeout value: 30s (Julia's value)? On a cold large project the very first call after a fresh LS start may still exceed it; subsequent calls work once JDT warms. Is a per-LS override the direction you want, or would you prefer a more general mechanism (e.g., gating pull on the server-advertised diagnosticProvider capability, plus a longer/smarter push wait)? Note the base class currently doesn't store the server's advertised capabilities, so the general route is a larger change.
  • Happy to send a PR (override + a policy-level test asserting the override values) once you confirm the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions