-
Notifications
You must be signed in to change notification settings - Fork 7
Add initial support for simplified hover type information #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
f5c0f8c
d3e8fc4
b24905d
590f5de
4c09a40
497c522
80e227e
fcb8692
3c756c1
6a618c3
81ba914
45f7d64
f29a717
b184575
451231e
5c9eeb3
d851dd7
c84507e
6d52076
7c33d4b
bfaa7de
c4f0143
1a543dc
1dba908
ade6b64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| package org.checkerframework.languageserver; | ||
|
|
||
| import com.google.common.base.Splitter; | ||
| import com.google.common.collect.RangeMap; | ||
| import com.google.common.collect.TreeRangeMap; | ||
|
|
||
| import org.checkerframework.javacutil.BugInCF; | ||
| import org.eclipse.lsp4j.CodeAction; | ||
| import org.eclipse.lsp4j.CodeActionKind; | ||
| import org.eclipse.lsp4j.CodeActionParams; | ||
| import org.eclipse.lsp4j.Command; | ||
| import org.eclipse.lsp4j.Diagnostic; | ||
| import org.eclipse.lsp4j.DiagnosticSeverity; | ||
| import org.eclipse.lsp4j.DidChangeTextDocumentParams; | ||
|
|
@@ -17,10 +22,18 @@ | |
| import org.eclipse.lsp4j.Position; | ||
| import org.eclipse.lsp4j.PublishDiagnosticsParams; | ||
| import org.eclipse.lsp4j.Range; | ||
| import org.eclipse.lsp4j.TextEdit; | ||
| import org.eclipse.lsp4j.WorkspaceEdit; | ||
| import org.eclipse.lsp4j.jsonrpc.messages.Either; | ||
| import org.eclipse.lsp4j.services.TextDocumentService; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.net.URISyntaxException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Paths; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
|
|
@@ -184,20 +197,177 @@ public void didSave(DidSaveTextDocumentParams params) { | |
| public void publish(Map<String, List<javax.tools.Diagnostic<?>>> result) { | ||
| for (Map.Entry<String, List<javax.tools.Diagnostic<?>>> entry : result.entrySet()) { | ||
| List<Diagnostic> diagnostics = new ArrayList<>(); | ||
| Map<String, List<CheckerTypeKind>> uniqueTypeInfo = | ||
| processDiagnosticString(entry, diagnostics); | ||
| publishTypeMessageWithFilter(entry, uniqueTypeInfo); | ||
| server.publishDiagnostics(new PublishDiagnosticsParams(entry.getKey(), diagnostics)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Process Diagnostic strings to separate type message and error message. | ||
| * | ||
| * @param entry The Diagnostic entry for getting diagnostic message | ||
| * @param diagnostics A list for storing checkerframework issued errors | ||
| * @return TypeMessage processed for non-verbose output | ||
| */ | ||
| private Map<String, List<CheckerTypeKind>> processDiagnosticString( | ||
| Map.Entry<String, List<javax.tools.Diagnostic<?>>> entry, | ||
| List<Diagnostic> diagnostics) { | ||
| Map<String, List<CheckerTypeKind>> TypeMessage = new HashMap<>(); | ||
| for (javax.tools.Diagnostic<?> diagnostic : entry.getValue()) { | ||
| String message = diagnostic.getMessage(Locale.getDefault()); | ||
| if (message != null && message.contains("lsp.type.information")) { | ||
| String checker = getChecker(message); | ||
| String kind = getKind(message); | ||
| String type = getType(message); | ||
| String positionInfo = getPosition(message); | ||
| if (positionInfo != null) { | ||
| if (!TypeMessage.containsKey(positionInfo)) { | ||
| List<CheckerTypeKind> checkerTypeKinds = new ArrayList<>(); | ||
| Map<String, String> kindType = new HashMap<>(); | ||
| kindType.put(type, kind); | ||
| checkerTypeKinds.add(new CheckerTypeKind(checker, kindType)); | ||
| TypeMessage.put(positionInfo, checkerTypeKinds); | ||
| } else { | ||
| List<CheckerTypeKind> checkerTypeKinds = TypeMessage.get(positionInfo); | ||
| boolean foundChecker = false; | ||
| for (CheckerTypeKind checkerTypeKind : checkerTypeKinds) { | ||
| if (checkerTypeKind.getCheckername().equals(checker)) { | ||
| foundChecker = true; | ||
| if (!checkerTypeKind.getTypeKind().containsKey(type)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These methods have a very deep nesting level. Can you try to split off smaller helper methods? Or at least add some comments to make the flow understandable?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. already refactored and added comment. |
||
| checkerTypeKind.getTypeKind().put(type, kind); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| if (!foundChecker) { | ||
| Map<String, String> kindType = new HashMap<>(); | ||
| kindType.put(type, kind); | ||
| checkerTypeKinds.add(new CheckerTypeKind(checker, kindType)); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| diagnostics.add(convertToLSPDiagnostic(diagnostic)); | ||
| } | ||
| } | ||
| return TypeMessage; | ||
| } | ||
|
|
||
| for (javax.tools.Diagnostic<?> diagnostic : entry.getValue()) { | ||
| String message = diagnostic.getMessage(Locale.getDefault()); | ||
| if (message != null && message.contains("lsp.type.information")) { | ||
| // this message is for lsp support | ||
| File file = new File(URI.create(entry.getKey())); | ||
| publishTypeMessage(file, message); | ||
| /** | ||
| * Publish the given type message for the given file in non-verbose mode. | ||
| * | ||
| * @param entry The Diagnostic entry for getting file name string | ||
| * @param diagnosticString processed for simplicity output | ||
| */ | ||
| private void publishTypeMessageWithFilter( | ||
| Map.Entry<String, List<javax.tools.Diagnostic<?>>> entry, | ||
| Map<String, List<CheckerTypeKind>> diagnosticString) { | ||
| for (Map.Entry<String, List<CheckerTypeKind>> typeMessage : diagnosticString.entrySet()) { | ||
| String positionInfo = typeMessage.getKey(); | ||
| for (CheckerTypeKind checkerTypeKind : typeMessage.getValue()) { | ||
| if (checkerTypeKind.getTypeKind().entrySet().size() == 1) { | ||
| for (Map.Entry<String, String> typeKind : | ||
| checkerTypeKind.getTypeKind().entrySet()) { | ||
| StringBuilder typeMessageBuilder = | ||
| new StringBuilder(checkerTypeKind.getCheckername()) | ||
| .append(": ") | ||
| .append(typeKind.getKey()) | ||
| .append(positionInfo); | ||
| publishTypeMessage( | ||
| new File(URI.create(entry.getKey())), | ||
| typeMessageBuilder.toString()); | ||
| } | ||
| } else { | ||
| diagnostics.add(convertToLSPDiagnostic(diagnostic)); | ||
| for (Map.Entry<String, String> typeKind : | ||
| checkerTypeKind.getTypeKind().entrySet()) { | ||
| StringBuilder typeMessageBuilder = | ||
| new StringBuilder(checkerTypeKind.getCheckername()) | ||
| .append(" ") | ||
| .append(typeKind.getValue()) | ||
| .append(":") | ||
| .append(typeKind.getKey()) | ||
| .append(positionInfo); | ||
| publishTypeMessage( | ||
| new File(URI.create(entry.getKey())), | ||
| typeMessageBuilder.toString()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| server.publishDiagnostics(new PublishDiagnosticsParams(entry.getKey(), diagnostics)); | ||
| /** | ||
| * Get checker name from type message in checkerframework. | ||
| * | ||
| * @param typeMessage Type message string from checkerframework | ||
| * @return Checker name from checkerframework e.g. Nullness | ||
| */ | ||
| private static String getChecker(String typeMessage) { | ||
| String checkerPrefix = "checker="; | ||
| int checkerStart = typeMessage.indexOf(checkerPrefix); | ||
| if (checkerStart != -1) { | ||
| int checkerEnd = typeMessage.indexOf(";", checkerStart); | ||
| if (checkerEnd != -1) { | ||
| String checker = | ||
| typeMessage | ||
| .substring(checkerStart + checkerPrefix.length(), checkerEnd) | ||
| .trim(); | ||
| checker = checker.replace("Subchecker", "").replace("Checker", "").trim(); | ||
| return checker; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Get message kind from type message in checkerframework. | ||
| * | ||
| * @param typeMessage Type message string from checkerframework | ||
| * @return Lower case message kind from checkerframework e.g. use/declared | ||
| */ | ||
| private static String getKind(String typeMessage) { | ||
| String kindPrefix = "kind="; | ||
| int kindStart = typeMessage.indexOf(kindPrefix); | ||
| if (kindStart != -1) { | ||
| int kindEnd = typeMessage.indexOf(";", kindStart); | ||
| if (kindEnd != -1) { | ||
| String kind = | ||
| typeMessage.substring(kindStart + kindPrefix.length(), kindEnd).trim(); | ||
| kind = kind.toLowerCase(Locale.ROOT).replace("_type", ""); | ||
| return kind; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Get type information from type message in checkerframework. | ||
| * | ||
| * @param typeMessage Type Message String from checkerframework | ||
| * @return Type information from checkerframework e.g. @Nullable Object | ||
| */ | ||
| private static String getType(String typeMessage) { | ||
| String typePrefix = "type="; | ||
| int typeStart = typeMessage.indexOf(typePrefix); | ||
| if (typeStart != -1) { | ||
| int typeEnd = typeMessage.indexOf(";", typeStart); | ||
| return typeMessage.substring(typeStart + typePrefix.length(), typeEnd).trim() + "; "; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Get position range from type message in checkerframework. | ||
| * | ||
| * @param typeMessage Type Message String from checkerframework | ||
| * @return Position range from checkerframework e.g. range=(11, 8, 11, 9) | ||
| */ | ||
| private static String getPosition(String typeMessage) { | ||
| int lastDelimiter = typeMessage.lastIndexOf(';'); | ||
| String positionInfo = typeMessage.substring(lastDelimiter + 1).trim(); | ||
| return positionInfo; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -266,4 +436,97 @@ private void publishTypeMessage(File file, String msg) { | |
| currentTypeInfo.put( | ||
| com.google.common.collect.Range.closed(start, end), typeInfoForPosition); | ||
| } | ||
|
|
||
| @Override | ||
| public CompletableFuture<List<Either<Command, CodeAction>>> codeAction( | ||
|
aosen-xiong marked this conversation as resolved.
Outdated
|
||
| CodeActionParams params) { | ||
| List<Either<Command, CodeAction>> actions = new ArrayList<>(); | ||
| for (Diagnostic diagnostic : params.getContext().getDiagnostics()) { | ||
| if (!diagnostic.getMessage().contains("lsp.type.information")) { | ||
| if (diagnostic.getMessage().contains("dereference.of.nullable")) { | ||
| CodeAction codeAction = new CodeAction("Add nullcheck"); | ||
| codeAction.setKind(CodeActionKind.QuickFix); | ||
| String uri = params.getTextDocument().getUri(); | ||
| Range range = params.getRange(); | ||
| String str = getContentInRange(uri, range); | ||
| List<TextEdit> editList = new ArrayList<>(); | ||
| TextEdit edit1 = | ||
| new TextEdit( | ||
| new Range( | ||
| new Position( | ||
| params.getRange().getStart().getLine(), | ||
| params.getRange().getStart().getCharacter()), | ||
| new Position( | ||
| params.getRange().getEnd().getLine(), | ||
| params.getRange().getEnd().getCharacter())), | ||
| " if (" | ||
| + str | ||
| + " != null) {\n" | ||
| + " " | ||
| + str | ||
| + ".toString();\n"); | ||
| TextEdit edit2 = | ||
| new TextEdit( | ||
| new Range( | ||
| new Position( | ||
| params.getRange().getStart().getLine() + 1, | ||
| params.getRange().getStart().getCharacter()), | ||
| new Position( | ||
| params.getRange().getEnd().getLine(), | ||
| params.getRange().getEnd().getCharacter())), | ||
| " } else {\n" | ||
| + " //TODO: Implement if the variable is null\n" | ||
| + " }\n" | ||
| + " }"); | ||
| editList.add(edit1); | ||
| editList.add(edit2); | ||
| codeAction.setEdit(new WorkspaceEdit(Collections.singletonMap(uri, editList))); | ||
| codeAction.setDiagnostics(Collections.singletonList(diagnostic)); | ||
| actions.add(Either.forRight(codeAction)); | ||
| } | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| return CompletableFuture.completedFuture(actions); | ||
| } | ||
|
|
||
| private static String getContentInRange(String uri, Range range) { | ||
| try { | ||
| URI uriObject = new URI(uri); | ||
| String filePath = Paths.get(uriObject).toFile().getAbsolutePath(); | ||
| String fileContent = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); | ||
| int startLine = range.getStart().getLine(); | ||
| int startCharacter = range.getStart().getCharacter(); | ||
| int endLine = range.getEnd().getLine(); | ||
| int endCharacter = range.getEnd().getCharacter(); | ||
|
|
||
| List<String> lines = Splitter.onPattern(System.lineSeparator()).splitToList(fileContent); | ||
|
|
||
| StringBuilder textInRange = new StringBuilder(); | ||
| for (int i = startLine; i <= endLine; i++) { | ||
| String line = lines.get(i); | ||
| if (i == startLine) { | ||
| if (i == endLine) { | ||
| textInRange.append(line.substring(startCharacter, endCharacter)); | ||
| } else { | ||
| textInRange.append(line.substring(startCharacter)); | ||
| textInRange.append(System.lineSeparator()); | ||
| } | ||
| } else if (i == endLine) { | ||
| textInRange.append(line.substring(0, endCharacter)); | ||
| } else { | ||
| textInRange.append(line); | ||
| textInRange.append(System.lineSeparator()); | ||
| } | ||
| } | ||
|
|
||
| return textInRange.toString(); | ||
| } catch (IOException e) { | ||
| e.printStackTrace(); | ||
| return ""; | ||
| } catch (URISyntaxException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package org.checkerframework.languageserver; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** This class is for processing type message. */ | ||
|
aosen-xiong marked this conversation as resolved.
Outdated
|
||
| public class CheckerTypeKind { | ||
| private String checkername; | ||
|
|
||
| private Map<String, String> TypeKind; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this mapping from/to?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The map stores type information in the key of the map and type kind information e.g. used/declared in the value of the map. |
||
|
|
||
| CheckerTypeKind(String checkername, Map<String, String> kindType) { | ||
| this.checkername = checkername; | ||
| this.TypeKind = kindType; | ||
| } | ||
|
|
||
| String getCheckername() { | ||
|
aosen-xiong marked this conversation as resolved.
Outdated
|
||
| return this.checkername; | ||
| } | ||
|
|
||
| Map<String, String> getTypeKind() { | ||
| return this.TypeKind; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.