I was writing an @InvocableMethod, hadn't typed the parameter's element type yet, and member completion stopped working in the whole class. Typing r. on a variable gave me nothing, in every method. The Problems panel showed a syntax error and nothing else.
The cause is a crash inside the Apex language server, and the server never tells you about it. This post explains what happens, how I measured which signatures trigger it, and what my VS Code extension Virga now does about it.
Reproducing it
Put this in a class in a project with the Salesforce Extension Pack installed:
public with sharing class OrderIntake {
@InvocableMethod(label='Take orders' description='Accepts or rejects each order.')
public static List<Result> take(List requests) {
List<Result> results = new List<Result>();
return results;
}
public static void probe() {
Request r = new Request();
r.
}
public class Request {
@InvocableVariable(required=true) public String orderNumber;
}
public class Result {
@InvocableVariable public Boolean accepted;
}
}
Ask for completion after r. in probe(). With the raw List in take, the language server returns no members. With List<Request> requests they come back. Completion after r. on a local variable whose type is an inner class is also the test I used for every measured shape below. Note that the broken signature and the method where completion fails don't have to be the same method.
What happens inside the language server
The Apex language server in the Extension Pack is called jorje. It runs extra semantic checks on methods marked @InvocableMethod, static @AuraEnabled, @HttpGet / @HttpPost and the other @Http* annotations, webservice and global. Those checks unwrap the element types of collection parameters, and which collections they unwrap depends on the method: List (two levels deep) in an @InvocableMethod, List in a webservice method, List and Map in static @AuraEnabled and @Http* methods, and List, Set and Map in global methods.
A raw List has no type argument, so the unwrap gets an empty list where it expects exactly one element. In the server's log it looks like this:
java.lang.IllegalArgumentException: Expected list of size 1 but was: []
at apex.common.collect.MoreIterables.getOnlyElement(MoreIterables.java:186)
at apex.jorje.semantic.symbol.type.common.CollectionTypeInfoUtil.getElementType(CollectionTypeInfoUtil.java:44)
at apex.jorje.semantic.ast.modifier.rule.InvocableContextRule.isAtMostTwoNestedLists(InvocableContextRule.java:134)
The server logs the exception and goes on. The file, though, is left without resolved types, and completion after . returns nothing anywhere in it until the signature is fixed. The same happens with List<>, with an unfinished List<Req directly followed by ), and with the wrong number of type arguments, such as List<String, String>.
There is a second, separate mechanism in the parser. A raw collection as a return type (List check(...), List<>, List<List>) makes the parser drop the whole method. So does a parameter cut off right after < or a comma, like List<). That one is local: only completion inside that method is gone.
Measuring instead of guessing
The obvious rule would be to flag every raw collection in a signature. That rule would be wrong in a lot of places. A raw List in a plain public static method is just a syntax error: the server reports it and completion keeps working. A check that fires on harmless code teaches people to ignore it.
So before writing any rule, I measured. I took the language server jar from the extension (salesforcedx-vscode-apex 67.26), started it headless with the same flags VS Code uses, and talked to it over the language server protocol. For each signature I sent the class with that signature as a change to the document, then asked for completion after r. on a local variable whose type is an inner class. Two things were recorded per shape: whether the server logged an exception, and whether that completion came back empty.
That ran over 274 signatures. Some results surprised me:
| Signature shape | Where | Result |
|---|---|---|
List requests |
@InvocableMethod |
crash, whole file |
List<List> requests |
@InvocableMethod |
crash, whole file |
List<List<List>> requests |
@InvocableMethod |
clean |
Set requests, Map requests
|
@InvocableMethod |
clean |
List<Request requests (no >) |
@InvocableMethod |
clean, the parser inserts the >
|
List<Request) { |
@InvocableMethod |
crash, whole file |
Map requests, Map<String> requests
|
static @AuraEnabled
|
crash, whole file |
Set requests |
global method |
crash, whole file |
List< > requests (with a space) |
any method | crash, whole file |
List requests |
plain method | clean, only a syntax error |
List check(...) as return type |
any method | method dropped |
The List<List<List>> row fits the stack trace: the rule is called isAtMostTwoNestedLists, and in the measurement it unwrapped two levels and no more. And List< > with a space is different from List<> written together, because jorje reads <> as a single token, while < > leaves an empty argument slot, which breaks type resolution in any method at all.
What Virga does with it
Virga flags exactly the shapes that broke in the measurement and stays quiet about the ones that measured clean, even when they are syntax errors. The diagnostic has source virga and one of two codes: jorje-crash when completion is gone in the whole file, jorje-parse when only that method is affected. The message names the cause:
Raw List in an @InvocableMethod parameter. The Apex language server crashes on it
and stops suggesting members after '.' in this file. Use List<Type>.
Diagnostics appear 400 ms after you stop typing, cover .cls files only, and skip anonymous Apex and the generated copies under .sfdx/.
The quick fix (Ctrl+.) inserts the type and picks it from the file:
- in an
@InvocableMethodparameter, an inner class with@InvocableVariablefields, Request- or Input-style names first; - in an
@AuraEnabledparameter, a class with@AuraEnabledfields; - in a return type, the output class (Response, Result or Output), even an empty one;
- otherwise
List<String>orMap<String, Object>, and the title says why.
It completes an unfinished List<Req, names a parameter that has no name, and adds a missing ):
// before
@InvocableMethod
public static void check(List) {
// after the quick fix
@InvocableMethod
public static void check(List<Request> requests) {
Only the type is replaced, so comments and line breaks inside the signature stay where they are. A fix is marked preferred only when the choice is unambiguous. When more than one class fits, you get more than one fix:
Virga's checks agree with the measurement in all 274 signatures. Then I applied the quick fixes and ran the breaking cases again: the server threw in none of the 184, and completion after . came back in 180 of the 181 where it had been empty. The one left over is @InvocableMethod on an interface method, which the parser rejects anyway.
What I took from it
When a language server goes silent, its log usually knows why. The syntax error in Problems was true but beside the point. The stack trace named the exact rule.
The other lesson was to measure before writing a lint rule. A rule that looked reasonable on paper would have flagged Set and Map in invocable methods, which are fine, and missed List< > in ordinary methods, which isn't.
Virga is free and MIT licensed: VS Code Marketplace, Open VSX, source on GitHub. The measured shapes are listed in the README, and the unit tests in test/local-types.test.js pin both the breaking and the clean ones.
Salesforce, Apex and related marks are trademarks of Salesforce, Inc. Virga is an independent project and is not affiliated with or endorsed by Salesforce.

Top comments (0)