Skip to content

Use LSP-over-legacy for hover#278

Draft
helin24 wants to merge 3 commits intoflutter:mainfrom
helin24:lsp-over-legacy2
Draft

Use LSP-over-legacy for hover#278
helin24 wants to merge 3 commits intoflutter:mainfrom
helin24:lsp-over-legacy2

Conversation

@helin24
Copy link
Member

@helin24 helin24 commented Mar 5, 2026

Follow up to #166 to show that this can be a path for incremental changeover to LSP instead of the legacy protocol.

This is intercepting messages over streams instead of using MessageProducer etc as mentioned in #166 (comment). The agent tells me that MessageProducer is more performant (vs this is more a proof of concept), so I will try to set up that way in the next iteration.

This is the hover content generated from lsp4ij:
image
(ignore the strange coloring. Something I did here broke the editor colors)

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an integration with "lsp4ij" to proxy hover requests to the Dart Analysis Server, serving as a proof-of-concept for incrementally migrating to LSP. However, it contains several security and robustness issues, including a JSON injection vulnerability when faking the "initialize" response, and sensitive information being logged at default levels. The custom JSON-RPC message parsing logic is also fragile. Additionally, the "DartLanguageServerFactory" requires improvements in robustness, adherence to platform best practices, and code maintainability, such as refactoring the long "start()" method, using managed thread pools, and improving exception handling.

Comment on lines +114 to +128
// A very naive detection of the end of a JSON-RPC message.
// A robust implementation should parse Content-Length properly.
String content = buffer.toString();
if (content.endsWith("}")) {
int braceCount = 0;
int startIdx = content.indexOf("{");
if (startIdx != -1) {
for (int i = startIdx; i < content.length(); i++) {
if (content.charAt(i) == '{')
braceCount++;
else if (content.charAt(i) == '}')
braceCount--;
}
if (braceCount == 0 && content.substring(0, startIdx).contains("Content-Length")) {
String jsonPart = content.substring(startIdx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

[MUST-FIX] The current implementation for parsing LSP messages is not robust. As noted in the code comment, it's a naive implementation that relies on checking for a closing brace and counting braces. This approach is brittle and can easily fail if:

  • The JSON payload contains a } character within a string value.
  • Multiple messages arrive in a single read, or a single message arrives in multiple reads.

A correct implementation must adhere to the Language Server Protocol specification by:

  1. Reading the Content-Length: NNN\r\n\r\n header.
  2. Reading exactly NNN bytes for the JSON content.

While this is a proof-of-concept, this is a fundamental correctness issue that should be addressed before this approach is built upon further.

References
  1. This is a logical bug that can lead to incorrect behavior and instability, which falls under the [MUST-FIX] severity category. (link)

Comment on lines +136 to +137
String fakeResponse = "{\"jsonrpc\":\"2.0\",\"id\":" + id
+ ",\"result\":{\"capabilities\":{\"hoverProvider\":true,\"completionProvider\":{\"resolveProvider\":false}}}}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The id field from an incoming JSON-RPC request is concatenated directly into a JSON response string without proper escaping or quoting, leading to a JSON injection vulnerability. An attacker could inject arbitrary keys into the JSON response, causing unexpected behavior. This issue is located within the start() method, which is also over 100 lines long, violating the repository style guide and making it difficult to read and maintain. Consider refactoring this method into smaller, focused private methods to improve readability and address the injection vulnerability.

LOG.info("Ignored lsp4ij request: " + method);
}
} catch (Exception e) {
LOG.warn("Failed to parse lsp4ij message: " + jsonPart, e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The application logs raw LSP payloads and messages at INFO and WARN levels, which can expose sensitive information like source code or file paths, leading to intellectual property leakage if logs are shared. This logging occurs within a section where a raw new Thread() is created. It's a best practice in IntelliJ Platform development to use managed thread pools via ApplicationManager.getApplication().executeOnPooledThread() for proper thread lifecycle and resource management, especially when handling potentially sensitive data streams.


if (lspPayload != null) {
String lspMessageString = lspPayload.toString();
LOG.info("Dart server sent lsp payload: " + lspMessageString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The application logs raw LSP payloads and messages at INFO and WARN levels. LSP messages frequently contain sensitive information, including source code snippets, file paths, and symbol names from the project being analyzed. Logging this data can lead to the exposure of intellectual property or other sensitive information if the IDE logs are shared for debugging or support purposes.

Comment on lines +99 to +102
} catch (Exception e) {
LOG.warn("Failed to parse lspToDartStream", e);
// Ignore parse errors from non-json or unrelated traffic
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] Catching the generic Exception is overly broad and can hide unexpected runtime errors. It's better to catch more specific exceptions that you expect to handle, such as com.google.gson.JsonParseException, to make the error handling more explicit and robust. The log message is also a bit misleading, as it's parsing a response from the Dart Analysis Server, not reading from lspToDartStream.

Suggested change
} catch (Exception e) {
LOG.warn("Failed to parse lspToDartStream", e);
// Ignore parse errors from non-json or unrelated traffic
}
} catch (com.google.gson.JsonParseException e) {
LOG.warn("Failed to parse message from Dart Analysis Server", e);
// Ignore parse errors from non-json or unrelated traffic
}
References
  1. This change improves maintainability by making error handling more specific, which aligns with the goal of reducing "'clever' code that is hard to read" and improving overall code quality. (link)

Comment on lines +161 to +163
} catch (Exception e) {
LOG.warn("Failed to parse lsp4ij message: " + jsonPart, e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] Similar to the other comment, catching the generic Exception is too broad. Please catch a more specific exception, like com.google.gson.JsonParseException, to make the error handling more precise and avoid masking other potential issues.

Suggested change
} catch (Exception e) {
LOG.warn("Failed to parse lsp4ij message: " + jsonPart, e);
}
} catch (com.google.gson.JsonParseException e) {
LOG.warn("Failed to parse lsp4ij message: " + jsonPart, e);
}
References
  1. This change improves maintainability by making error handling more specific, which aligns with the goal of reducing "'clever' code that is hard to read" and improving overall code quality. (link)

@helin24 helin24 mentioned this pull request Mar 9, 2026
Closed
import java.io.PipedOutputStream;
import java.nio.charset.StandardCharsets;

public class DartLanguageServerDefinition extends LanguageServerDefinition {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should never create your own LanguageServerDefinition. You should use https://github.com/redhat-developer/lsp4ij/blob/main/docs/DeveloperGuide.md#languageserverfactory

class DartStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
val definition = DartLanguageServerDefinition(project)
val mapping = ServerLanguageMapping(Language.findLanguageByID("Dart")!!, "Dart", "dart", object : com.redhat.devtools.lsp4ij.DocumentMatcher {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override fun match(vFile: VirtualFile, project: Project): Boolean = true
})
PluginLogger.createLogger(DartStartupActivity::class.java).info("Registering Dart lsp4ij ServerLanguageMapping!")
LanguageServersRegistry.getInstance().registerAssociation(definition, mapping)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this line code and register your language server factory in plugin.xml, see https://github.com/redhat-developer/lsp4ij/blob/main/docs/DeveloperGuide.md#extension-point-declaration

Copy link

@DanTup DanTup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this codebase, but from the changes here this sounds promising to me :-)

JsonObject lspMessage = JsonParser.parseString(jsonPayload).getAsJsonObject();
String method = lspMessage.has("method") ? lspMessage.get("method").getAsString() : null;

if ("initialize".equals(method)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how hard it would be, but it could be good if we could capture the clientCapabilities here, and maybe use them when initializing the legacy server (see lspClientCapabilities on setClientCapabilities). That way, the server will respond in ways that better match what lsp4ij expects (for example it will honour the supported completion kinds, symbol kinds, etc.).

It may mean stalling the legacy server initialization until this happens (and handling it not happening gracefully) though.

Comment on lines +204 to +209
while ((c = in.read()) != -1) {
headers.append((char) c);
if (headers.toString().endsWith("\r\n\r\n")) {
break;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have already tried this, but could we avoid doing this ourselves and use existing code in the library?

I haven't tried to use it (and I don't know java), but Gemini had previously mentioned the consumer/producer classes, and a quick search turned up this which I think is the lsp4j version of this that might be reusable?

https://github.com/eclipse-lsp4j/lsp4j/blob/5486d6fc35c87b22afd0a241154ca27f204dd9da/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants