diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 183d790..3b5132a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,10 +18,10 @@ jobs: distribution: 'adopt' cache: maven - - name: Set up Node.js 14 + - name: Set up Node.js 18 uses: actions/setup-node@v2 with: - node-version: '14.x' # TODO: what version do we want? + node-version: '18.x' # TODO: what version do we want? # cache: npm # TODO: requires package-lock.json - name: Build with Maven diff --git a/pom.xml b/pom.xml index af2ab2e..2d14591 100644 --- a/pom.xml +++ b/pom.xml @@ -4,21 +4,28 @@ 4.0.0 goblint gobpie - 0.0.2-SNAPSHOT + 0.0.3-SNAPSHOT com.github.magpiebridge magpiebridge - 0.1.3 + 0.1.5 + + + + + guru.nidi + graphviz-java + 0.18.1 com.google.code.gson gson - 2.8.6 + 2.9.0 diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 752cea8..ebcd147 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.nio.file.*; +import analysis.ShowCFGCommand; import magpiebridge.core.MagpieServer; import magpiebridge.core.ServerAnalysis; import magpiebridge.core.ServerConfiguration; @@ -27,6 +28,7 @@ public class Main { private static final String gobPieConfFileName = "gobpie.json"; + private static final String cfgHttpServer = "http://localhost:8080/"; private static final Logger log = LogManager.getLogger(Main.class); private static MagpieServer magpieServer; @@ -35,6 +37,9 @@ public static void main(String... args) { try { createMagpieServer(); addAnalysis(); + // launch magpieServer + magpieServer.launchOnStdio(); + log.info("MagpieBridge server launched."); magpieServer.doAnalysis("c", true); } catch (GobPieException e) { String message = e.getMessage(); @@ -60,22 +65,15 @@ public static void main(String... args) { */ private static void createMagpieServer() { - // set up configuration for MagpieServer ServerConfiguration serverConfig = new ServerConfiguration(); serverConfig.setDoAnalysisByFirstOpen(false); magpieServer = new MagpieServer(serverConfig); - - // launch magpieServer - magpieServer.launchOnStdio(); - log.info("MagpieBridge server launched."); - } /** - * Method for creating and adding GoblintAnalysis to MagpieBridge server - * and doing the initial analysis. + * Method for creating and adding Goblint analysis to MagpieBridge server. *

* Creates GoblintServer, GoblintClient and the GoblintAnalysis classes. * @@ -105,6 +103,10 @@ private static void addAnalysis() { ServerAnalysis serverAnalysis = new GoblintAnalysis(magpieServer, goblintServer, goblintClient, gobpieConfiguration); Either analysis = Either.forLeft(serverAnalysis); magpieServer.addAnalysis(analysis, language); + + // add HTTP server for showing CFGs + magpieServer.addHttpServer(cfgHttpServer); + magpieServer.addCommand("showcfg", new ShowCFGCommand(goblintClient)); } diff --git a/src/main/java/analysis/GoblintAnalysis.java b/src/main/java/analysis/GoblintAnalysis.java index 58e2b9f..bce4f95 100644 --- a/src/main/java/analysis/GoblintAnalysis.java +++ b/src/main/java/analysis/GoblintAnalysis.java @@ -2,15 +2,15 @@ import com.ibm.wala.classLoader.Module; import goblintclient.GoblintClient; -import goblintclient.communication.AnalyzeResponse; -import goblintclient.communication.MessagesResponse; -import goblintclient.communication.Request; +import goblintclient.communication.*; +import goblintclient.messages.GoblintFunctions; import goblintclient.messages.GoblintMessages; import goblintserver.GoblintServer; import gobpie.GobPieConfiguration; import gobpie.GobPieException; import gobpie.GobPieExceptionType; import magpiebridge.core.AnalysisConsumer; +import magpiebridge.core.AnalysisResult; import magpiebridge.core.MagpieServer; import magpiebridge.core.ServerAnalysis; import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; @@ -27,7 +27,6 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; -import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -37,7 +36,20 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; - +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The Class GoblintAnalysis. + *

+ * Implementation of the ServerAnalysis interface. + * The class that is responsible for analyzing when an analysis event is triggered. + * Sends the requests to Goblint server, reads the corresponding responses, + * converts the results and passes them to MagpieBridge server. + * + * @author Karoliine Holter + * @since 0.0.1 + */ public class GoblintAnalysis implements ServerAnalysis { @@ -79,7 +91,7 @@ public String source() { * * @param files the files that have been opened in the editor (not using due to using the compilation database). * @param consumer the server which consumes the analysis results. - * @param rerun tells if the analysis should be reran. + * @param rerun tells if the analysis should be rerun. */ @Override @@ -92,7 +104,7 @@ public void analyze(Collection files, AnalysisConsumer consume preAnalyse(); log.info("---------------------- Analysis started ----------------------"); Runnable analysisTask = () -> { - Collection response = reanalyse(); + Collection response = reanalyse(); if (response != null) { server.consume(new ArrayList<>(response), source()); log.info("--------------------- Analysis finished ----------------------"); @@ -142,35 +154,56 @@ private void abortAnalysis() { /** - * Sends the request to Goblint server to reanalyse and reads the result. + * Sends the requests to Goblint server and reads their results. * - * @return returns a collection of analysis results if the request was sucessful, null otherwise + * @return a collection of warning messages and cfg code lenses if request was successful, null otherwise. */ - private Collection reanalyse() { + private Collection reanalyse() { Request analyzeRequest = new Request("analyze"); Request messagesRequest = new Request("messages"); + Request functionsRequest = new Request("functions"); try { + // Analyze analysisRunning.set(true); - goblintClient.writeRequestToSocket(analyzeRequest); - AnalyzeResponse analyzeResponse = goblintClient.readAnalyzeResponseFromSocket(); + AnalyzeResponse analyzeResponse = (AnalyzeResponse) getResponse(analyzeRequest); analysisRunning.set(false); - if (!analyzeRequest.getId().equals(analyzeResponse.getId())) - throw new GobPieException("Response ID does not match request ID.", GobPieExceptionType.GOBLINT_EXCEPTION); if (analyzeResponse.getResult().getStatus().contains("Aborted")) return null; - goblintClient.writeRequestToSocket(messagesRequest); - MessagesResponse messagesResponse = goblintClient.readMessagesResponseFromSocket(); - if (!messagesRequest.getId().equals(messagesResponse.getId())) - throw new GobPieException("Response ID does not match request ID.", GobPieExceptionType.GOBLINT_EXCEPTION); - return convertResultsFromJson(messagesResponse); + // Get warning messages + MessagesResponse messagesResponse = (MessagesResponse) getResponse(messagesRequest); + // Get list of functions + FunctionsResponse functionsResponse = (FunctionsResponse) getResponse(functionsRequest); + return Stream.concat(convertResultsFromJson(messagesResponse).stream(), convertResultsFromJson(functionsResponse).stream()).collect(Collectors.toList()); } catch (IOException e) { throw new GobPieException("Sending the request to or receiving result from the server failed.", e, GobPieExceptionType.GOBLINT_EXCEPTION); } } + /** + * Writes the request to the socket and reads its response according to the request that was sent. + * + * @param request The request to be written into socket. + * @return the response to the request that was sent. + * @throws GobPieException if the request and response ID do not match. + */ + + private Response getResponse(Request request) throws IOException { + goblintClient.writeRequestToSocket(request); + Response response; + if (request.getMethod().equals("analyze")) + response = goblintClient.readAnalyzeResponseFromSocket(); + else if (request.getMethod().equals("messages")) + response = goblintClient.readMessagesResponseFromSocket(); + else + response = goblintClient.readFunctionsResponseFromSocket(); + if (!request.getId().equals(response.getId())) + throw new GobPieException("Response ID does not match request ID.", GobPieExceptionType.GOBLINT_EXCEPTION); + return response; + } + /** * Method for running a command. @@ -192,31 +225,26 @@ public ProcessResult runCommand(File dirPath, String[] command) throws IOExcepti /** - * Deserializes json to GoblintResult objects and then converts the information - * into GoblintAnalysisResult objects, which Magpie uses to generate IDE - * messages. + * Deserializes json from the response and converts the information + * into AnalysisResult objects, which Magpie uses to generate IDE messages. * - * @return A collection of GoblintAnalysisResult objects. + * @param response that was read from the socket and needs to be converted to AnalysisResults. + * @return A collection of AnalysisResult objects. */ - private Collection convertResultsFromJson(MessagesResponse messagesResponse) { + private Collection convertResultsFromJson(MessagesResponse response) { + return response.getResult().stream().map(GoblintMessages::convert).flatMap(List::stream).collect(Collectors.toList()); + } - Collection results = new ArrayList<>(); - try { - List messagesArray = messagesResponse.getResult(); - for (GoblintMessages msg : messagesArray) { - results.addAll(msg.convert()); - } - return results; - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } + private Collection convertResultsFromJson(FunctionsResponse response) { + return response.getResult().stream().map(GoblintFunctions::convert).collect(Collectors.toList()); } /** * Method for creating an observer for Goblint configuration file. * So that the server could be restarted when the configuration file is changed. + * TODO: instead of restarting the server, send new configuration with a request to the server #32 * * @return The FileAlterationObserver of project root directory. */ diff --git a/src/main/java/analysis/GoblintCFGAnalysisResult.java b/src/main/java/analysis/GoblintCFGAnalysisResult.java new file mode 100644 index 0000000..fb05d5d --- /dev/null +++ b/src/main/java/analysis/GoblintCFGAnalysisResult.java @@ -0,0 +1,75 @@ +package analysis; + +import com.ibm.wala.cast.tree.CAstSourcePositionMap; +import com.ibm.wala.util.collections.Pair; +import magpiebridge.core.AnalysisResult; +import magpiebridge.core.Kind; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.DiagnosticSeverity; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * The Class GoblintCFGAnalysisResult. + *

+ * Implementation of the GoblintAnalysisResult class that extends MagpieBridge AnalysisResult class. + * The class that corresponds to the CFG code lenses. + * + * @author Karoliine Holter + * @since 0.0.3 + */ + +public class GoblintCFGAnalysisResult implements AnalysisResult { + private final CAstSourcePositionMap.Position pos; + private final String funName; + private final String fileName; + private final Iterable> related = new ArrayList<>(); + + public GoblintCFGAnalysisResult(CAstSourcePositionMap.Position pos, String funName, String fileName) { + this.pos = pos; + this.funName = funName; + this.fileName = fileName; + } + + @Override + public Iterable command() { + Command command = new Command("show cfg", "showcfg", Collections.singletonList(funName)); + return Collections.singleton(command); + } + + @Override + public Kind kind() { + return Kind.CodeLens; + } + + @Override + public String toString(boolean useMarkdown) { + return "cfg"; + } + + @Override + public CAstSourcePositionMap.Position position() { + return pos; + } + + @Override + public Iterable> related() { + return related; + } + + @Override + public DiagnosticSeverity severity() { + return DiagnosticSeverity.Information; + } + + @Override + public Pair repair() { + return null; + } + + @Override + public String code() { + return null; + } +} diff --git a/src/main/java/analysis/GoblintAnalysisResult.java b/src/main/java/analysis/GoblintMessagesAnalysisResult.java similarity index 77% rename from src/main/java/analysis/GoblintAnalysisResult.java rename to src/main/java/analysis/GoblintMessagesAnalysisResult.java index f93e1e4..bc477f0 100644 --- a/src/main/java/analysis/GoblintAnalysisResult.java +++ b/src/main/java/analysis/GoblintMessagesAnalysisResult.java @@ -2,27 +2,23 @@ import com.ibm.wala.cast.tree.CAstSourcePositionMap.Position; import com.ibm.wala.util.collections.Pair; - -import java.util.ArrayList; - +import goblintclient.messages.GoblintPosition; import magpiebridge.core.AnalysisResult; import magpiebridge.core.Kind; -// import magpiebridge.util.SourceCodeReader; - import org.eclipse.lsp4j.DiagnosticSeverity; -import goblintclient.messages.GoblintPosition; +import java.util.ArrayList; /** - * The Class GoblintAnalysisResult. + * The Class GoblintMessagesAnalysisResult. *

- * Implementation of the MagpieBridge AnalysisResult class. - * Customizes it for the needs of Goblint. + * Implementation of the GoblintAnalysisResult class that extends MagpieBridge AnalysisResult class. + * The class that corresponds to the Goblint warnings that are shown in the IDE. * - * @author Julian Dolby, Linghui Luo and Karoliine Holter + * @author Karoliine Holter */ -public class GoblintAnalysisResult implements AnalysisResult { +public class GoblintMessagesAnalysisResult implements AnalysisResult { private String group_text = ""; private final String text; @@ -30,13 +26,13 @@ public class GoblintAnalysisResult implements AnalysisResult { private final String severity; private Iterable> related = new ArrayList<>(); - public GoblintAnalysisResult(GoblintPosition pos, String text, String severity) { + public GoblintMessagesAnalysisResult(GoblintPosition pos, String text, String severity) { this.text = text; this.pos = pos; this.severity = severity; } - public GoblintAnalysisResult(GoblintPosition pos, String group_text, String text, String severity) { + public GoblintMessagesAnalysisResult(GoblintPosition pos, String group_text, String text, String severity) { this.group_text = group_text; this.text = text; this.pos = pos; @@ -44,7 +40,7 @@ public GoblintAnalysisResult(GoblintPosition pos, String group_text, String text } - public GoblintAnalysisResult(Position pos, String text, String severity, Iterable> related) { + public GoblintMessagesAnalysisResult(Position pos, String text, String severity, Iterable> related) { this.text = text; this.pos = pos; this.severity = severity; diff --git a/src/main/java/analysis/ShowCFGCommand.java b/src/main/java/analysis/ShowCFGCommand.java new file mode 100644 index 0000000..ba8ccf0 --- /dev/null +++ b/src/main/java/analysis/ShowCFGCommand.java @@ -0,0 +1,117 @@ +package analysis; + +import com.google.gson.JsonPrimitive; +import goblintclient.GoblintClient; +import goblintclient.communication.CFGResponse; +import goblintclient.communication.Request; +import gobpie.GobPieException; +import gobpie.GobPieExceptionType; +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import magpiebridge.core.MagpieClient; +import magpiebridge.core.MagpieServer; +import magpiebridge.core.WorkspaceCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.services.LanguageClient; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URISyntaxException; + +public class ShowCFGCommand implements WorkspaceCommand { + + private final GoblintClient goblintClient; + private final Logger log = LogManager.getLogger(ShowCFGCommand.class); + + public ShowCFGCommand(GoblintClient goblintClient) { + this.goblintClient = goblintClient; + } + + @Override + public void execute(ExecuteCommandParams params, MagpieServer server, LanguageClient client) { + try { + String funName; + Object uriJson = params.getArguments().get(0); + if (uriJson instanceof JsonPrimitive) { + funName = ((JsonPrimitive) uriJson).getAsString(); + } else { + funName = (String) uriJson; + } + log.info("Showing CFG for function: " + funName); + String cfg = getCFG(funName); + showHTMLinClientOrBrowser(server, client, cfg); + } catch (IOException | URISyntaxException e) { + MagpieServer.ExceptionLogger.log(e); + e.printStackTrace(); + } + } + + /** + * Writes the request to the socket to get the cfg for the given function. + * + * @param funName The function name for which the CFG was requested. + * @return the CFG of the given function as a dot language string. + * @throws GobPieException if the request and response ID do not match. + */ + + public String getCFG(String funName) { + Request cfgRequest = new Request("cfg", funName); + try { + goblintClient.writeRequestToSocket(cfgRequest); + CFGResponse cfgResponse = goblintClient.readCFGResponseFromSocket(); + if (!cfgRequest.getId().equals(cfgResponse.getId())) + throw new GobPieException("Response ID does not match request ID.", GobPieExceptionType.GOBLINT_EXCEPTION); + return cfgResponse.getResult().getCfg(); + } catch (IOException e) { + throw new GobPieException("Sending the request to or receiving result from the server failed.", e, GobPieExceptionType.GOBLINT_EXCEPTION); + } + } + + + /** + * Show A HTML page with the given CFG in the client, or in a browser if the client doesn't support this. + * + * @param server The MagpieServer + * @param client The IDE/Editor + * @param cfg The CFG which should be shown + * @throws IOException IO exception + * @throws URISyntaxException URI exception + */ + + public static void showHTMLinClientOrBrowser(MagpieServer server, LanguageClient client, String cfg) throws IOException, URISyntaxException { + if (server.clientSupportShowHTML()) { + if (client instanceof MagpieClient) { + // Generate svg from dot using graphviz-java + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Graphviz.fromString(cfg).render(Format.SVG).toOutputStream(output); + String svg = output.toString(); + // TODO: improve this HTML horror? + String content = + "\n" + + "\n" + + " \n" + + " \n" + + " Preview\n" + + " \n" + + " \n" + + " \n" + + /*" " +*/ + " " + svg + "" + + " \n" + + ""; + ((MagpieClient) client).showHTML(content); + } + } /*else { + // TODO: Not tested if this works, probably not? + if (Desktop.isDesktopSupported()) + Desktop.getDesktop().browse(new URI(URIUtils.checkURI(cfg))); + }*/ + } +} + diff --git a/src/main/java/goblintclient/GoblintClient.java b/src/main/java/goblintclient/GoblintClient.java index 96ebfa4..0c1e0ba 100644 --- a/src/main/java/goblintclient/GoblintClient.java +++ b/src/main/java/goblintclient/GoblintClient.java @@ -1,32 +1,25 @@ package goblintclient; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.StandardProtocolFamily; -import java.net.UnixDomainSocketAddress; -import java.nio.channels.Channels; -import java.nio.channels.SocketChannel; -import java.nio.file.Path; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; - -import goblintclient.communication.Request; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import goblintclient.communication.AnalyzeResponse; -import goblintclient.communication.MessagesResponse; +import goblintclient.communication.*; import goblintclient.messages.GoblintMessages; import goblintclient.messages.GoblintTagInterfaceAdapter; import gobpie.GobPieException; import gobpie.GobPieExceptionType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.*; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; /** + * s * The Class GoblintClient. *

* Handles the communication with Goblint Server through unix socket. @@ -85,7 +78,7 @@ public void writeRequestToSocket(Request request) throws IOException { /** - * Method for reading the response from Goblint server. + * Methods for reading the responses from Goblint server. * * @return JsonObject of the results read from Goblint socket. */ @@ -98,12 +91,6 @@ public AnalyzeResponse readAnalyzeResponseFromSocket() throws IOException { } - /** - * Method for reading the response from Goblint server. - * - * @return JsonObject of the results read from Goblint socket. - */ - public MessagesResponse readMessagesResponseFromSocket() throws IOException { String response = inputReader.readLine(); GsonBuilder builder = new GsonBuilder(); @@ -116,4 +103,20 @@ public MessagesResponse readMessagesResponseFromSocket() throws IOException { } + public FunctionsResponse readFunctionsResponseFromSocket() throws IOException { + String response = inputReader.readLine(); + FunctionsResponse functionsResponse = new Gson().fromJson(response, FunctionsResponse.class); + log.info("Response " + functionsResponse.getId() + " read from socket."); + return functionsResponse; + } + + + public CFGResponse readCFGResponseFromSocket() throws IOException { + String response = inputReader.readLine(); + CFGResponse cfgResponse = new Gson().fromJson(response, CFGResponse.class); + log.info("Response " + cfgResponse.getId() + " read from socket."); + return cfgResponse; + } + + } diff --git a/src/main/java/goblintclient/communication/CFGResponse.java b/src/main/java/goblintclient/communication/CFGResponse.java new file mode 100644 index 0000000..fc6edb1 --- /dev/null +++ b/src/main/java/goblintclient/communication/CFGResponse.java @@ -0,0 +1,24 @@ +package goblintclient.communication; + +import goblintclient.messages.GoblintCFG; + +/** + * The Class CFGResponse. + *

+ * Corresponding object to the jsonrpc response JSON for cfg request. + * + * @author Karoliine Holter + * @since 0.0.3 + */ + +public class CFGResponse extends Response { + // method: "cfg" response: + // {"id":0,"jsonrpc":"2.0","result":{"cfg":"digraph cfg {\n\tnode [id=\"\\N\", ... }} + + private GoblintCFG result; + + public GoblintCFG getResult() { + return result; + } +} + diff --git a/src/main/java/goblintclient/communication/FunctionsResponse.java b/src/main/java/goblintclient/communication/FunctionsResponse.java new file mode 100644 index 0000000..0aa2840 --- /dev/null +++ b/src/main/java/goblintclient/communication/FunctionsResponse.java @@ -0,0 +1,26 @@ +package goblintclient.communication; + +import goblintclient.messages.GoblintFunctions; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Class FunctionsResponse. + *

+ * Corresponding object to the jsonrpc response JSON for functions request. + * + * @author Karoliine Holter + * @since 0.0.3 + */ + +public class FunctionsResponse extends Response { + // method: "functions" response: + // {"id":0,"jsonrpc":"2.0","result":[{"funName":"qsort","location":{"file":"/home/ ... }]} + + private final List result = new ArrayList<>(); + + public List getResult() { + return result; + } +} diff --git a/src/main/java/goblintclient/communication/MessagesResponse.java b/src/main/java/goblintclient/communication/MessagesResponse.java index 6a119f6..9b29684 100644 --- a/src/main/java/goblintclient/communication/MessagesResponse.java +++ b/src/main/java/goblintclient/communication/MessagesResponse.java @@ -1,10 +1,10 @@ package goblintclient.communication; +import goblintclient.messages.GoblintMessages; + import java.util.ArrayList; import java.util.List; -import goblintclient.messages.GoblintMessages; - /** * The Class MessagesResponse. *

diff --git a/src/main/java/goblintclient/communication/Request.java b/src/main/java/goblintclient/communication/Request.java index 68825a3..77639d2 100644 --- a/src/main/java/goblintclient/communication/Request.java +++ b/src/main/java/goblintclient/communication/Request.java @@ -12,7 +12,11 @@ */ public class Request { + // Examples of requests used in this project: // {"jsonrpc":"2.0","id":0,"method":"analyze","params":{}} + // {"jsonrpc":"2.0","id":0,"method":"messages"} + // {"jsonrpc":"2.0","id":0,"method":"functions"} + // {"jsonrpc":"2.0","id":0,"method":"cfg", "params":{"fname":"main"}} private final String jsonrpc = "2.0"; private final UUID id; @@ -20,6 +24,7 @@ public class Request { private params params; static class params { + private String fname; } public Request(String method) { @@ -30,6 +35,14 @@ public Request(String method) { this.id = UUID.randomUUID(); } + public Request(String method, String fname) { + this.method = method; + this.params = new params(); + this.params.fname = fname; + this.id = UUID.randomUUID(); + } + + public String getJsonrpc() { return jsonrpc; } diff --git a/src/main/java/goblintclient/communication/Response.java b/src/main/java/goblintclient/communication/Response.java index e71eb7b..9cea179 100644 --- a/src/main/java/goblintclient/communication/Response.java +++ b/src/main/java/goblintclient/communication/Response.java @@ -16,6 +16,10 @@ public abstract class Response { // {"id":0,"jsonrpc":"2.0","result":{"status":["Success"]}} // method: "messages" response: // {"id":0,"jsonrpc":"2.0","result":[{"tags":[{"Category":["Race"]}], ... }]} + // method: "functions" response: + // {"id":0,"jsonrpc":"2.0","result":[{"funName":"qsort","location":{"file":"/home/ ... }]} + // method: "cfg" response: + // {"id":0,"jsonrpc":"2.0","result":{"cfg":"digraph cfg {\n\tnode [id=\"\\N\", ... }} private UUID id; private String jsonrpc = "2.0"; diff --git a/src/main/java/goblintclient/messages/GoblintCFG.java b/src/main/java/goblintclient/messages/GoblintCFG.java new file mode 100644 index 0000000..a80ecb5 --- /dev/null +++ b/src/main/java/goblintclient/messages/GoblintCFG.java @@ -0,0 +1,18 @@ +package goblintclient.messages; + +/** + * The Class GoblintCFG. + *

+ * Corresponding object to the Goblint cfg request response results in JSON. + * + * @author Karoliine Holter + * @since 0.0.3 + */ + +public class GoblintCFG { + private String cfg; + + public String getCfg() { + return cfg; + } +} diff --git a/src/main/java/goblintclient/messages/GoblintFunctions.java b/src/main/java/goblintclient/messages/GoblintFunctions.java new file mode 100644 index 0000000..26a2bf4 --- /dev/null +++ b/src/main/java/goblintclient/messages/GoblintFunctions.java @@ -0,0 +1,51 @@ +package goblintclient.messages; + +import analysis.GoblintCFGAnalysisResult; +import magpiebridge.core.AnalysisResult; + +import java.io.File; +import java.net.MalformedURLException; + +/** + * The Class GoblintFunctions. + *

+ * Corresponding object to the Goblint functions request response results in JSON. + * Converts the results from JSON to AnalysisResult requested by MagpieBridge. + * + * @author Karoliine Holter + * @since 0.0.3 + */ + +public class GoblintFunctions { + + String type = getClass().getName(); + + private String funName; + private location location; + + static class location { + private String file; + private int line; + private int column; + private int endLine; + private int endColumn; + } + + public String getType() { + return type; + } + + public AnalysisResult convert() { + try { + return new GoblintCFGAnalysisResult( + new GoblintPosition(location.line, location.endLine, location.column, location.endColumn, new File(location.file).toURI().toURL()), + funName, + new File(location.file).getName() + ); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + +} + diff --git a/src/main/java/goblintclient/messages/GoblintMessages.java b/src/main/java/goblintclient/messages/GoblintMessages.java index c633168..e6929ec 100644 --- a/src/main/java/goblintclient/messages/GoblintMessages.java +++ b/src/main/java/goblintclient/messages/GoblintMessages.java @@ -1,9 +1,9 @@ package goblintclient.messages; +import analysis.GoblintMessagesAnalysisResult; import com.ibm.wala.cast.tree.CAstSourcePositionMap.Position; import com.ibm.wala.util.collections.Pair; - -import analysis.GoblintAnalysisResult; +import magpiebridge.core.AnalysisResult; import java.io.File; import java.net.MalformedURLException; @@ -14,7 +14,7 @@ /** * The Class GoblintMessages. *

- * Corresponding object to the Goblint results in JSON. + * Corresponding object to the Goblint messages request response results in JSON. * Converts the results from JSON to AnalysisResult requested by MagpieBridge. * * @author Karoliine Holter @@ -23,6 +23,8 @@ public class GoblintMessages { + String type = getClass().getName(); + private final List tags = new ArrayList<>(); private String severity; private multipiece multipiece; @@ -84,30 +86,33 @@ static class loc { } } + public String getType() { + return type; + } - public List convert() throws MalformedURLException { - List results = new ArrayList<>(); + public List convert() { + List results = new ArrayList<>(); if (multipiece.group_text == null) { - GoblintAnalysisResult result = createGoblintAnalysisResult(); + GoblintMessagesAnalysisResult result = createGoblintAnalysisResult(); results.add(result); } else { - List intermresults = new ArrayList<>(); + List intermresults = new ArrayList<>(); List pieces = multipiece.pieces; for (multipiece.pieces piece : pieces) { - GoblintAnalysisResult result = createGoblintAnalysisResult(piece); + GoblintMessagesAnalysisResult result = createGoblintAnalysisResult(piece); intermresults.add(result); } // Add related warnings to all the group elements - List addedRelated = new ArrayList<>(); - for (GoblintAnalysisResult res1 : intermresults) { + List addedRelated = new ArrayList<>(); + for (GoblintMessagesAnalysisResult res1 : intermresults) { List> related = new ArrayList<>(); - for (GoblintAnalysisResult res2 : intermresults) { + for (GoblintMessagesAnalysisResult res2 : intermresults) { if (res1 != res2) { related.add(Pair.make(res2.position(), res2.text())); } } - addedRelated.add(new GoblintAnalysisResult(res1.position(), res1.group_text() + "\n" + res1.text(), + addedRelated.add(new GoblintMessagesAnalysisResult(res1.position(), res1.group_text() + "\n" + res1.text(), res1.severityStr(), related)); } results.addAll(addedRelated); @@ -117,32 +122,40 @@ public List convert() throws MalformedURLException { } - public GoblintAnalysisResult createGoblintAnalysisResult() throws MalformedURLException { - GoblintPosition pos = multipiece.loc == null - ? new GoblintPosition(1, 1, 1, new File("").toURI().toURL()) - : new GoblintPosition( - multipiece.loc.line, - multipiece.loc.endLine, - multipiece.loc.column < 0 ? 0 : multipiece.loc.column - 1, - multipiece.loc.endColumn < 0 ? 10000 : multipiece.loc.endColumn - 1, - new File(multipiece.loc.file).toURI().toURL()); - String msg = tags.stream().map(tag::toString).collect(Collectors.joining("")) + " " + multipiece.text; - return new GoblintAnalysisResult(pos, msg, severity); + public GoblintMessagesAnalysisResult createGoblintAnalysisResult() { + try { + GoblintPosition pos = multipiece.loc == null + ? new GoblintPosition(1, 1, 1, new File("").toURI().toURL()) + : new GoblintPosition( + multipiece.loc.line, + multipiece.loc.endLine, + multipiece.loc.column < 0 ? 0 : multipiece.loc.column - 1, + multipiece.loc.endColumn < 0 ? 10000 : multipiece.loc.endColumn - 1, + new File(multipiece.loc.file).toURI().toURL()); + String msg = tags.stream().map(tag::toString).collect(Collectors.joining("")) + " " + multipiece.text; + return new GoblintMessagesAnalysisResult(pos, msg, severity); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } - public GoblintAnalysisResult createGoblintAnalysisResult(multipiece.pieces piece) throws MalformedURLException { - GoblintPosition pos = piece.loc == null - ? new GoblintPosition(1, 1, 1, new File("").toURI().toURL()) - : new GoblintPosition( - piece.loc.line, - piece.loc.endLine, - piece.loc.column < 0 ? 0 : piece.loc.column - 1, - piece.loc.endColumn < 0 ? 10000 : piece.loc.endColumn - 1, - new File(piece.loc.file).toURI().toURL()); - return new GoblintAnalysisResult(pos, - tags.stream().map(tag::toString).collect(Collectors.joining("")) + " Group: " + multipiece.group_text, - piece.text, severity); + public GoblintMessagesAnalysisResult createGoblintAnalysisResult(multipiece.pieces piece) { + try { + GoblintPosition pos = piece.loc == null + ? new GoblintPosition(1, 1, 1, new File("").toURI().toURL()) + : new GoblintPosition( + piece.loc.line, + piece.loc.endLine, + piece.loc.column < 0 ? 0 : piece.loc.column - 1, + piece.loc.endColumn < 0 ? 10000 : piece.loc.endColumn - 1, + new File(piece.loc.file).toURI().toURL()); + return new GoblintMessagesAnalysisResult(pos, + tags.stream().map(tag::toString).collect(Collectors.joining("")) + " Group: " + multipiece.group_text, + piece.text, severity); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } diff --git a/vscode/installGobPie.sh b/vscode/installGobPie.sh index 4cc200d..395ea38 100755 --- a/vscode/installGobPie.sh +++ b/vscode/installGobPie.sh @@ -1,4 +1,4 @@ mvn clean -f "../../GobPie/pom.xml" mvn install -f "../../GobPie/pom.xml" echo y | vsce package -code --install-extension gobpie-0.0.2.vsix \ No newline at end of file +code --install-extension gobpie-0.0.3.vsix \ No newline at end of file diff --git a/vscode/package.json b/vscode/package.json index e9df7cf..ac1200c 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1,71 +1,72 @@ { - "name": "gobpie", - "displayName": "GobPie", - "description": "The interactive Goblint analyzer extension", - "author": "Karoliine Holter", - "license": "EPL-2.0", - "version": "0.0.2", - "repository": { - "type": "git", - "url": "https://github.com/karoliineh/MagpieBridge-Goblint.git" - }, - "publisher": "karoliineh", - "categories": [], - "keywords": [ - "multi-root ready" - ], - "engines": { - "vscode": "^1.30.0" - }, - "activationEvents": [ - "onLanguage:c" - ], - "main": "./out/extension", - "contributes": { - "configuration": { - "type": "object", - "title": "GobPie", - "properties": { - "gobpie.trace.server": { - "scope": "window", - "type": "string", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "description": "Traces the communication between VS Code and the language server." - } - } - }, - "languages": [ - { - "id": "c", - "aliases": [ - "C", - "c" - ], - "extensions": [ - ".c" - ] - } - ] - }, - "scripts": { - "vscode:prepublish": "cp ../target/gobpie-0.0.2-SNAPSHOT.jar gobpie-0.0.2-SNAPSHOT.jar && npm run compile", - "compile": "tsc -b", - "watch": "tsc -b -w", - "postinstall": "node ./node_modules/vscode/bin/install" - }, - "dependencies": { - "vscode-languageclient": "^5.2.1" - }, - "devDependencies": { - "vscode": "^1.1.33", - "@types/mocha": "^5.2.6", - "@types/node": "^11.13.0", - "tslint": "^5.15.0", - "typescript": "^3.4.2" - } + "name": "gobpie", + "displayName": "GobPie", + "description": "The interactive Goblint analyzer extension", + "author": "Karoliine Holter", + "license": "EPL-2.0", + "version": "0.0.3", + "repository": { + "type": "git", + "url": "https://github.com/karoliineh/MagpieBridge-Goblint.git" + }, + "publisher": "karoliineh", + "categories": [], + "keywords": [ + "multi-root ready" + ], + "engines": { + "vscode": "^1.30.0" + }, + "activationEvents": [ + "onLanguage:c" + ], + "main": "./out/extension", + "contributes": { + "configuration": { + "type": "object", + "title": "GobPie", + "properties": { + "gobpie.trace.server": { + "scope": "window", + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server." + } + } + }, + "languages": [ + { + "id": "c", + "aliases": [ + "C", + "c" + ], + "extensions": [ + ".c" + ] + } + ] + }, + "scripts": { + "vscode:prepublish": "cp ../target/gobpie-0.0.3-SNAPSHOT.jar gobpie-0.0.3-SNAPSHOT.jar && npm run compile", + "compile": "tsc -b", + "watch": "tsc -b -w" + }, + "dependencies": { + "vscode-languageclient": "^5.2.1", + "xmlhttprequest-ts": "^1.0.1" + }, + "devDependencies": { + "@vscode/test-electron": "^2.1.4", + "@types/vscode": "^1.1.37", + "@types/mocha": "^9.1.1", + "@types/node": "^18.6.1", + "tslint": "^6.1.3", + "typescript": "^4.7.4" + } } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 7e705e8..046f632 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -1,12 +1,28 @@ 'use strict'; -import {ExtensionContext, workspace} from 'vscode'; -import {LanguageClient, LanguageClientOptions, ServerOptions} from 'vscode-languageclient'; +import * as vscode from 'vscode'; +import {ExtensionContext, ViewColumn, window, workspace} from 'vscode'; +import { + ClientCapabilities, + DocumentSelector, + DynamicFeature, + InitializeParams, + LanguageClient, + LanguageClientOptions, + RegistrationData, + RPCMessageType, + ServerCapabilities, + ServerOptions +} from 'vscode-languageclient'; +import {XMLHttpRequest} from 'xmlhttprequest-ts'; + +// Track currently webview panel +let panel: vscode.WebviewPanel | undefined = undefined; export function activate(context: ExtensionContext) { let script = 'java'; - let args = ['-jar', context.asAbsolutePath('gobpie-0.0.2-SNAPSHOT.jar')]; + let args = ['-jar', context.asAbsolutePath('gobpie-0.0.3-SNAPSHOT.jar')]; - // Use this for communicating on stdio + // Use this for communicating on stdio let serverOptions: ServerOptions = { run: {command: script, args: args}, debug: {command: script, args: args}, @@ -39,6 +55,76 @@ export function activate(context: ExtensionContext) { // Create the language client and start the client. let lc: LanguageClient = new LanguageClient('GobPie', 'GobPie', serverOptions, clientOptions); + lc.registerFeature(new MagpieBridgeSupport(lc)); lc.start(); } + +export class MagpieBridgeSupport implements DynamicFeature { + constructor(private _client: LanguageClient) { + } + + messages: RPCMessageType | RPCMessageType[]; + fillInitializeParams?: (params: InitializeParams) => void; + + fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.experimental = { + supportsShowHTML: true + } + } + + initialize(capabilities: ServerCapabilities, documentSelector: DocumentSelector): void { + this._client.onNotification("magpiebridge/showHTML", (content: string) => { + this.createWebView(content); + }); + } + + createWebView(content: string) { + const columnToShowIn = ViewColumn.Beside; + + if (panel) { + // If we already have a panel, show it in the target column + panel.reveal(columnToShowIn); + } else { + // Otherwise, create a new panel + panel = window.createWebviewPanel("Customized Web View", "GobPie", columnToShowIn, { + retainContextWhenHidden: true, + enableScripts: true + }); + + // Reset when the current panel is closed + panel.onDidDispose( + () => { + panel = undefined; + }, + null, + ); + } + + panel.webview.html = content; + panel.webview.onDidReceiveMessage( + message => { + switch (message.command) { + case 'action': + var httpRequest = new XMLHttpRequest(); + var url = message.text; + httpRequest.open('GET', url); + httpRequest.send(); + return; + } + + } + ) + } + + register(message: RPCMessageType, data: RegistrationData): void { + } + + unregister(id: string): void { + } + + dispose(): void { + } + +} +