diff --git a/src/main/java/org/checkerframework/languageserver/CFTextDocumentService.java b/src/main/java/org/checkerframework/languageserver/CFTextDocumentService.java index e276ec6..b328f0f 100644 --- a/src/main/java/org/checkerframework/languageserver/CFTextDocumentService.java +++ b/src/main/java/org/checkerframework/languageserver/CFTextDocumentService.java @@ -202,20 +202,154 @@ public void didSave(DidSaveTextDocumentParams params) { public void publish(Map>> result) { for (Map.Entry>> entry : result.entrySet()) { List diagnostics = new ArrayList<>(); + Map> 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> processDiagnosticString( + Map.Entry>> entry, + List diagnostics) { + Map> 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 the position is not in the map, add a new entry. + List checkerTypeKinds = + TypeMessage.computeIfAbsent(positionInfo, k -> new ArrayList<>()); + // If the checker is not in the list, add a new entry. + CheckerTypeKind checkerTypeKind = + checkerTypeKinds.stream() + .filter(c -> c.getCheckername().equals(checker)) + .findFirst() + .orElseGet( + () -> { + CheckerTypeKind newCheckerTypeKind = + new CheckerTypeKind(checker, new HashMap<>()); + checkerTypeKinds.add(newCheckerTypeKind); + return newCheckerTypeKind; + }); + // If the type is not in the map, add a new entry. + checkerTypeKind.getTypeToKindMap().putIfAbsent(type, kind); + } else { + // If is not type message, add to diagnostics. + 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); - } else { - diagnostics.add(convertToLSPDiagnostic(diagnostic)); + /** + * 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>> entry, + Map> diagnosticString) { + for (Map.Entry> typeMessage : diagnosticString.entrySet()) { + String location = typeMessage.getKey(); + for (CheckerTypeKind checkerTypeKind : typeMessage.getValue()) { + Map typeKinds = checkerTypeKind.getTypeToKindMap(); + for (Map.Entry typeKind : typeKinds.entrySet()) { + StringBuilder typeMessageBuilder = + new StringBuilder(checkerTypeKind.getCheckername()); + // If there are more than one type message from a checker, add the kind of type + // e.g. use/declared. If not, collapse the message. + if (typeKinds.size() > 1) { + typeMessageBuilder.append(" ").append(typeKind.getValue()); + } + typeMessageBuilder.append(": ").append(typeKind.getKey()).append(location); + 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(';'); + return typeMessage.substring(lastDelimiter + 1).trim(); } /** diff --git a/src/main/java/org/checkerframework/languageserver/CheckerTypeKind.java b/src/main/java/org/checkerframework/languageserver/CheckerTypeKind.java new file mode 100644 index 0000000..5b9bef4 --- /dev/null +++ b/src/main/java/org/checkerframework/languageserver/CheckerTypeKind.java @@ -0,0 +1,44 @@ +package org.checkerframework.languageserver; + +import java.util.Map; + +/** This class is for storing the type information from Checker Framework. */ +public class CheckerTypeKind { + /** The string stores checker name e.g. Nullness, KeyFor. */ + private final String checkerName; + + /** + * 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. + */ + private final Map typeToKindMap; + + /** + * Constructor for creating the CheckerTypeKind class + * + * @param checkerName checker's name + * @param typeToKindMap type and kind pair + */ + CheckerTypeKind(String checkerName, Map typeToKindMap) { + this.checkerName = checkerName; + this.typeToKindMap = typeToKindMap; + } + + /** + * Return the name of the checker + * + * @return name of the checker + */ + /*package-private*/ String getCheckername() { + return this.checkerName; + } + + /** + * Return the type and kind map + * + * @return type and kind map + */ + Map getTypeToKindMap() { + return this.typeToKindMap; + } +}