Skip to content

Commit 043df2a

Browse files
authored
Json rpc diagnostics (#124)
* Remove populateCacheFromDisplay config * [POC] Switch to json rpc diagnostics * Only use json rpc if available * Cleanup ReplaceableCode * [Diagnostics] Run json rpc diagnostics for all (Haxe) open files
1 parent 62040da commit 043df2a

12 files changed

+148
-130
lines changed

.haxerc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"version": "569e52e",
2+
"version": "15abdcc",
33
"resolveLibs": "scoped"
4-
}
4+
}

src/haxeLanguageServer/Configuration.hx

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ typedef UserConfig = {
8888
var enableDiagnostics:Bool;
8989
var enableServerView:Bool;
9090
var enableSignatureHelpDocumentation:Bool;
91+
var diagnosticsForAllOpenFiles:Bool;
9192
var diagnosticsPathFilter:String;
9293
var displayHost:String;
9394
var displayPort:EitherType<Int, String>;
9495
var buildCompletionCache:Bool;
9596
var enableCompletionCacheWarning:Bool;
9697
var useLegacyCompletion:Bool;
97-
var populateCacheFromDisplay:Bool;
9898
var codeGeneration:CodeGenerationConfig;
9999
var exclude:Array<String>;
100100
var postfixCompletion:PostfixCompletionConfig;
@@ -157,12 +157,12 @@ class Configuration {
157157
enableDiagnostics: true,
158158
enableServerView: false,
159159
enableSignatureHelpDocumentation: true,
160+
diagnosticsForAllOpenFiles: true,
160161
diagnosticsPathFilter: "${workspaceRoot}",
161162
displayHost: null,
162163
displayPort: null,
163164
buildCompletionCache: true,
164165
enableCompletionCacheWarning: true,
165-
populateCacheFromDisplay: true,
166166
useLegacyCompletion: false,
167167
codeGeneration: {
168168
functions: {

src/haxeLanguageServer/features/haxe/DiagnosticsFeature.hx

+120-111
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package haxeLanguageServer.features.haxe;
22

33
import haxe.Json;
4+
import haxe.display.Diagnostic;
5+
import haxe.display.Display.DiagnosticsParams;
6+
import haxe.display.Display.DisplayMethods;
47
import haxe.display.JsonModuleTypes;
58
import haxe.ds.BalancedTree;
69
import haxe.io.Path;
@@ -13,10 +16,10 @@ import js.Node.setImmediate;
1316
import js.node.ChildProcess;
1417
import jsonrpc.CancellationToken;
1518
import languageServerProtocol.Types.Diagnostic;
16-
import languageServerProtocol.Types.DiagnosticSeverity;
1719
import languageServerProtocol.Types.Location;
1820

1921
using Lambda;
22+
using haxeLanguageServer.features.haxe.DiagnosticsFeature;
2023

2124
class DiagnosticsFeature {
2225
public static inline final SortImportsUsingsTitle = "Sort imports/usings";
@@ -30,6 +33,9 @@ class DiagnosticsFeature {
3033
final pendingRequests:Map<DocumentUri, CancellationTokenSource>;
3134
final errorUri:DocumentUri;
3235

36+
final useJsonRpc:Bool;
37+
final timerName:String;
38+
3339
var haxelibPath:Null<FsPath>;
3440

3541
public function new(context:Context) {
@@ -38,23 +44,56 @@ class DiagnosticsFeature {
3844
pendingRequests = new Map();
3945
errorUri = new FsPath(Path.join([context.workspacePath.toString(), "Error"])).toUri();
4046

47+
useJsonRpc = context.haxeServer.supports(DisplayMethods.Diagnostics);
48+
timerName = useJsonRpc ? DisplayMethods.Diagnostics : "@diagnostics";
49+
4150
ChildProcess.exec(context.config.haxelib.executable + " config", (error, stdout, stderr) -> haxelibPath = new FsPath(stdout.trim()));
4251

4352
context.languageServerProtocol.onNotification(LanguageServerMethods.RunGlobalDiagnostics, onRunGlobalDiagnostics);
4453
}
4554

4655
function onRunGlobalDiagnostics(_) {
4756
final stopProgress = context.startProgress("Collecting Diagnostics");
48-
final onResolve = context.startTimer("@diagnostics");
49-
50-
context.callDisplay("global diagnostics", ["diagnostics"], null, null, function(result) {
51-
processDiagnosticsReply(null, onResolve, result);
52-
context.languageServerProtocol.sendNotification(LanguageServerMethods.DidRunRunGlobalDiagnostics);
53-
stopProgress();
54-
}, function(error) {
55-
processErrorReply(null, error);
56-
stopProgress();
57-
});
57+
final onResolve = context.startTimer(timerName);
58+
59+
if (useJsonRpc) {
60+
context.callHaxeMethod(DisplayMethods.Diagnostics, {}, null, result -> {
61+
processDiagnosticsReply(null, onResolve, result);
62+
context.languageServerProtocol.sendNotification(LanguageServerMethods.DidRunRunGlobalDiagnostics);
63+
stopProgress();
64+
return null;
65+
}, function(error) {
66+
processErrorReply(null, error);
67+
stopProgress();
68+
});
69+
} else {
70+
context.callDisplay("global diagnostics", ["diagnostics"], null, null, function(result) {
71+
final data = parseLegacyDiagnostics(result);
72+
if (data == null) {
73+
clearDiagnosticsOnClient(errorUri);
74+
} else {
75+
processDiagnosticsReply(null, onResolve, data);
76+
}
77+
context.languageServerProtocol.sendNotification(LanguageServerMethods.DidRunRunGlobalDiagnostics);
78+
stopProgress();
79+
}, function(error) {
80+
processErrorReply(null, error);
81+
stopProgress();
82+
});
83+
}
84+
}
85+
86+
function parseLegacyDiagnostics(result:DisplayResult):Null<ReadOnlyArray<{file:haxe.display.FsPath, diagnostics:ReadOnlyArray<haxe.display.Diagnostic<Any>>}>> {
87+
return switch result {
88+
case DResult(s):
89+
try {
90+
Json.parse(s);
91+
} catch (e) {
92+
trace("Error parsing diagnostics response: " + e);
93+
null;
94+
}
95+
case DCancelled: null;
96+
};
5897
}
5998

6099
function processErrorReply(uri:Null<DocumentUri>, error:String) {
@@ -108,7 +147,7 @@ class DiagnosticsFeature {
108147

109148
final diag = {
110149
range: {start: position, end: endPosition},
111-
severity: DiagnosticSeverity.Error,
150+
severity: languageServerProtocol.Types.DiagnosticSeverity.Error,
112151
message: problemMatcher.matched(7)
113152
};
114153
publishDiagnostic(targetUri, diag, error);
@@ -122,7 +161,7 @@ class DiagnosticsFeature {
122161
}
123162
final diag = {
124163
range: {start: {line: 0, character: 0}, end: {line: 0, character: 0}},
125-
severity: DiagnosticSeverity.Error,
164+
severity: languageServerProtocol.Types.DiagnosticSeverity.Error,
126165
message: problemMatcher.matched(2)
127166
};
128167
publishDiagnostic(errorUri, diag, error);
@@ -132,22 +171,12 @@ class DiagnosticsFeature {
132171
function publishDiagnostic(uri:DocumentUri, diag:Diagnostic, error:String) {
133172
context.languageServerProtocol.sendNotification(PublishDiagnosticsNotification.type, {uri: uri, diagnostics: [diag]});
134173
final argumentsMap = diagnosticsArguments[uri] = new DiagnosticsMap();
135-
argumentsMap.set({code: CompilerError, range: diag.range}, error);
174+
argumentsMap.set({code: DKCompilerError, range: diag.range}, error);
136175
}
137176

138-
function processDiagnosticsReply(uri:Null<DocumentUri>, onResolve:(result:Dynamic, ?debugInfo:String) -> Void, result:DisplayResult) {
177+
function processDiagnosticsReply(uri:Null<DocumentUri>, onResolve:(result:Dynamic, ?debugInfo:String) -> Void,
178+
data:ReadOnlyArray<{file:haxe.display.FsPath, diagnostics:ReadOnlyArray<haxe.display.Diagnostic<Any>>}>) {
139179
clearDiagnosticsOnClient(errorUri);
140-
final data:Array<HaxeDiagnosticResponse<Any>> = switch result {
141-
case DResult(s):
142-
try {
143-
Json.parse(s);
144-
} catch (e) {
145-
trace("Error parsing diagnostics response: " + e);
146-
return;
147-
}
148-
case DCancelled:
149-
return;
150-
}
151180
var count = 0;
152181
final sent = new Map<DocumentUri, Bool>();
153182
for (data in data) {
@@ -181,7 +210,7 @@ class DiagnosticsFeature {
181210
final diag:Diagnostic = {
182211
range: range,
183212
code: hxDiag.code,
184-
severity: hxDiag.severity,
213+
severity: cast hxDiag.severity,
185214
message: hxDiag.kind.getMessage(doc, hxDiag.args, range),
186215
data: {kind: hxDiag.kind},
187216
relatedInformation: hxDiag.relatedInformation?.map(rel -> {
@@ -192,7 +221,7 @@ class DiagnosticsFeature {
192221
message: convertIndentation(rel.message, rel.depth)
193222
})
194223
}
195-
if (kind == ReplaceableCode || kind == UnusedImport || diag.message.contains("has no effect") || kind == InactiveBlock) {
224+
if (kind == ReplaceableCode || kind == DKUnusedImport || diag.message.contains("has no effect") || kind == InactiveBlock) {
196225
diag.severity = Hint;
197226
diag.tags = [Unnecessary];
198227
}
@@ -229,23 +258,23 @@ class DiagnosticsFeature {
229258
return !PathHelper.matches(path, pathFilter);
230259
}
231260

232-
function filterRelevantDiagnostics(diagnostics:Array<HaxeDiagnostic<Any>>):Array<HaxeDiagnostic<Any>> {
261+
function filterRelevantDiagnostics(diagnostics:ReadOnlyArray<HaxeDiagnostic<Any>>):ReadOnlyArray<HaxeDiagnostic<Any>> {
233262
// hide regular compiler errors while there's parser errors, they can be misleading
234263
final hasProblematicParserErrors = diagnostics.find(d -> switch (d.kind : Int) {
235-
case ParserError: d.args != "Missing ;"; // don't be too strict
264+
case DKParserError: d.args != "Missing ;"; // don't be too strict
236265
case _: false;
237266
}) != null;
238267
if (hasProblematicParserErrors) {
239268
diagnostics = diagnostics.filter(d -> switch (d.kind : Int) {
240-
case CompilerError, UnresolvedIdentifier: false;
269+
case DKCompilerError, DKUnresolvedIdentifier: false;
241270
case _: true;
242271
});
243272
}
244273

245274
// hide unused import warnings while there's compiler errors (to avoid false positives)
246-
final hasCompilerErrors = diagnostics.find(d -> d.kind == cast CompilerError) != null;
275+
final hasCompilerErrors = diagnostics.find(d -> d.kind == cast DKCompilerError) != null;
247276
if (hasCompilerErrors) {
248-
diagnostics = diagnostics.filter(d -> d.kind != cast UnusedImport);
277+
diagnostics = diagnostics.filter(d -> d.kind != cast DKUnusedImport);
249278
}
250279

251280
// hide inactive blocks that are contained within other inactive blocks
@@ -300,25 +329,62 @@ class DiagnosticsFeature {
300329

301330
function invokePendingRequest(uri:DocumentUri, token:CancellationToken) {
302331
final doc:Null<HaxeDocument> = context.documents.getHaxe(uri);
332+
303333
if (doc != null) {
304-
final onResolve = context.startTimer("@diagnostics");
305-
context.callDisplay("@diagnostics", [doc.uri.toFsPath() + "@0@diagnostics"], null, token, result -> {
306-
pendingRequests.remove(uri);
307-
processDiagnosticsReply(uri, onResolve, result);
308-
}, error -> {
309-
pendingRequests.remove(uri);
310-
processErrorReply(uri, error);
311-
});
334+
final onResolve = context.startTimer(timerName);
335+
if (useJsonRpc) {
336+
var params:DiagnosticsParams = {fileContents: []};
337+
338+
if (context.config.user.diagnosticsForAllOpenFiles) {
339+
context.documents.iter(function(doc) {
340+
final path = doc.uri.toFsPath();
341+
if (doc.languageId == "haxe" && !isPathFiltered(path)) {
342+
params.fileContents.sure().push({file: path, contents: null});
343+
}
344+
});
345+
} else {
346+
params.file = doc.uri.toFsPath();
347+
}
348+
349+
context.callHaxeMethod(DisplayMethods.Diagnostics, params, token, result -> {
350+
pendingRequests.remove(uri);
351+
processDiagnosticsReply(uri, onResolve, result);
352+
return null;
353+
}, error -> {
354+
pendingRequests.remove(uri);
355+
processErrorReply(uri, error);
356+
});
357+
} else {
358+
context.callDisplay("@diagnostics", [doc.uri.toFsPath() + "@0@diagnostics"], null, token, result -> {
359+
pendingRequests.remove(uri);
360+
final data = parseLegacyDiagnostics(result);
361+
if (data == null) {
362+
clearDiagnosticsOnClient(errorUri);
363+
} else {
364+
processDiagnosticsReply(null, onResolve, data);
365+
}
366+
}, error -> {
367+
pendingRequests.remove(uri);
368+
processErrorReply(uri, error);
369+
});
370+
}
312371
} else {
313372
pendingRequests.remove(uri);
314373
}
315374
}
316375

317376
function cancelPendingRequest(uri:DocumentUri) {
318-
var tokenSource = pendingRequests[uri];
319-
if (tokenSource != null) {
320-
pendingRequests.remove(uri);
321-
tokenSource.cancel();
377+
if (useJsonRpc && context.config.user.diagnosticsForAllOpenFiles) {
378+
for (tokenSource in pendingRequests) {
379+
tokenSource.cancel();
380+
}
381+
pendingRequests.clear();
382+
} else {
383+
var tokenSource = pendingRequests[uri];
384+
if (tokenSource != null) {
385+
pendingRequests.remove(uri);
386+
tokenSource.cancel();
387+
}
322388
}
323389
}
324390

@@ -333,79 +399,22 @@ class DiagnosticsFeature {
333399
}
334400
}
335401

336-
enum abstract UnresolvedIdentifierSuggestion(Int) {
337-
final Import;
338-
final Typo;
339-
}
340-
341-
enum abstract MissingFieldCauseKind<T>(String) {
342-
final AbstractParent:MissingFieldCauseKind<{parent:JsonTypePathWithParams}>;
343-
final ImplementedInterface:MissingFieldCauseKind<{parent:JsonTypePathWithParams}>;
344-
final PropertyAccessor:MissingFieldCauseKind<{property:JsonClassField, isGetter:Bool}>;
345-
final FieldAccess:MissingFieldCauseKind<{}>;
346-
final FinalFields:MissingFieldCauseKind<{fields:Array<JsonClassField>}>;
347-
}
348-
349-
typedef MissingFieldCause<T> = {
350-
var kind:MissingFieldCauseKind<T>;
351-
var args:T;
352-
}
353-
354-
typedef MissingField = {
355-
var field:JsonClassField;
356-
var type:JsonType<Dynamic>;
357-
358-
/**
359-
When implementing multiple interfaces, there can be field duplicates among them. This flag is only
360-
true for the first such occurrence of a field, so that the "Implement all" code action doesn't end
361-
up implementing the same field multiple times.
362-
**/
363-
var unique:Bool;
364-
}
365-
366-
typedef MissingFieldDiagnostic = {
367-
var fields:Array<MissingField>;
368-
var cause:MissingFieldCause<Dynamic>;
369-
}
370-
371-
typedef MissingFieldDiagnostics = {
372-
var moduleType:JsonModuleType<Dynamic>;
373-
var moduleFile:String;
374-
var entries:Array<MissingFieldDiagnostic>;
375-
}
376-
377-
typedef ReplaceableCode = {
378-
var description:String;
379-
var range:Range;
380-
var ?newCode:String;
381-
}
382-
383-
enum abstract DiagnosticKind<T>(Int) from Int to Int {
384-
final UnusedImport:DiagnosticKind<Void>;
385-
final UnresolvedIdentifier:DiagnosticKind<Array<{kind:UnresolvedIdentifierSuggestion, name:String}>>;
386-
final CompilerError:DiagnosticKind<String>;
387-
final ReplaceableCode:DiagnosticKind<ReplaceableCode>;
388-
final ParserError:DiagnosticKind<String>;
389-
final DeprecationWarning:DiagnosticKind<String>;
390-
final InactiveBlock:DiagnosticKind<Void>;
391-
final MissingFields:DiagnosticKind<MissingFieldDiagnostics>;
392-
393-
public inline function new(i:Int) {
394-
this = i;
395-
}
402+
class DiagnosticKindHelper {
403+
public static function make<T>(code:Int)
404+
return (code : DiagnosticKind<T>);
396405

397-
public function getMessage(doc:Null<HaxeDocument>, args:T, range:Range) {
398-
return switch (this : DiagnosticKind<T>) {
399-
case UnusedImport: "Unused import/using";
400-
case UnresolvedIdentifier:
406+
public static function getMessage<T>(dk:DiagnosticKind<T>, doc:Null<HaxeDocument>, args:T, range:Range) {
407+
return switch dk {
408+
case DKUnusedImport: "Unused import/using";
409+
case DKUnresolvedIdentifier:
401410
var message = 'Unknown identifier';
402411
if (doc != null) {
403412
message += ' : ${doc.getText(range)}';
404413
}
405414
message;
406-
case CompilerError: args.trim();
415+
case DKCompilerError: args.trim();
407416
case ReplaceableCode: args.description;
408-
case ParserError: args;
417+
case DKParserError: args;
409418
case DeprecationWarning: args;
410419
case InactiveBlock: "Inactive conditional compilation block";
411420
case MissingFields:

0 commit comments

Comments
 (0)