Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
ServerCapabilities capabilities = new ServerCapabilities();
capabilities.setTextDocumentSync(TextDocumentSyncKind.Full);
capabilities.setHoverProvider(Boolean.TRUE);
capabilities.setCodeActionProvider(Boolean.TRUE);
return CompletableFuture.completedFuture(new InitializeResult(capabilities));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import com.google.common.collect.TreeRangeMap;

import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.languageserver.quickfix.NullPointerDereferenceQuickFixProvider;
import org.checkerframework.languageserver.quickfix.QuickFixProvider;
import org.checkerframework.languageserver.quickfix.QuickFixRegistry;
import org.eclipse.lsp4j.CodeAction;
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;
Expand All @@ -17,6 +23,7 @@
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.TextDocumentService;

import java.io.File;
Expand Down Expand Up @@ -67,9 +74,14 @@ public class CFTextDocumentService implements TextDocumentService, Publisher {
private final Map<File, RangeMap<ComparablePosition, List<String>>> filesToTypeInfo =
new HashMap<>();

/** The quick fix registry for the Checker Framework document service. */
QuickFixRegistry quickFixRegistry = new QuickFixRegistry();

/** Default constructor for Checker Framework document service. */
CFTextDocumentService(CFLanguageServer server) {
this.server = server;
quickFixRegistry.registerProvider(
"dereference.of.nullable", new NullPointerDereferenceQuickFixProvider());
}

/** Setter for the executor field. */
Expand Down Expand Up @@ -284,4 +296,17 @@ 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(
CodeActionParams params) {
List<Either<Command, CodeAction>> actions = new ArrayList<>();
for (Diagnostic diagnostic : params.getContext().getDiagnostics()) {
QuickFixProvider provider = quickFixRegistry.getProvider(diagnostic);
if (provider != null && provider.canHandle(diagnostic)) {
actions.addAll(provider.getQuickFixes(diagnostic, params));
}
}
return CompletableFuture.completedFuture(actions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package org.checkerframework.languageserver.quickfix;

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.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Quick fix provider for null pointer dereference diagnostics. */
public class NullPointerDereferenceQuickFixProvider implements QuickFixProvider {
/**
* Check if the provider can handle a diagnostic.
*
* @param diagnostic the diagnostic
* @return true if the provider can handle the diagnostic
*/
@Override
public boolean canHandle(Diagnostic diagnostic) {
return diagnostic.getMessage().contains("dereference.of.nullable");
}

/**
* Get the quick fixes for a diagnostic.
*
* @param diagnostic the diagnostic
* @param params the code action parameters
* @return the quick fixes
*/
@Override
public List<Either<Command, CodeAction>> getQuickFixes(
Diagnostic diagnostic, CodeActionParams params) {
CodeAction codeAction = new CodeAction("Add null check");
codeAction.setKind(CodeActionKind.QuickFix);
String uri = params.getTextDocument().getUri();
Range range = params.getRange();
String str = QuickFixProvider.getContentInRange(uri, range);
int startLine = range.getStart().getLine();
int startCharacter = range.getStart().getCharacter();
int endLine = range.getEnd().getLine();
int endCharacter = range.getEnd().getCharacter();
TextEdit editBefore =
new TextEdit(
new Range(
new Position(startLine, startCharacter),
new Position(endLine, endCharacter)),
" if (" + str + " != null) {\n" + " " + str + ".toString();\n");
TextEdit editAfter =
new TextEdit(
new Range(
new Position(startLine + 1, startCharacter),
new Position(endLine, endCharacter)),
" } else {\n"
+ " //TODO: Implement if the variable is null\n"
+ " }\n"
+ " }");
codeAction.setEdit(createWorkspaceEdit(uri, editBefore, editAfter));
codeAction.setDiagnostics(Collections.singletonList(diagnostic));
List<Either<Command, CodeAction>> code = new ArrayList<>();
code.add(Either.forRight(codeAction));
code.add(Either.forRight(addSuppressWarning(diagnostic, params)));
return code;
}

/**
* Add a suppress warning quick fix.
*
* @param diagnostic the diagnostic
* @param params the code action parameters
* @return the code action
*/
public CodeAction addSuppressWarning(Diagnostic diagnostic, CodeActionParams params) {
CodeAction codeAction = new CodeAction("Suppress nullness warning");
codeAction.setKind(CodeActionKind.QuickFix);
String uri = params.getTextDocument().getUri();
Range range = params.getRange();
String str = QuickFixProvider.getContentInRange(uri, range);
TextEdit suppressWarningEdit = createSuppressWarningEdit(str, params.getRange());
codeAction.setEdit(createWorkspaceEdit(uri, suppressWarningEdit));
codeAction.setDiagnostics(Collections.singletonList(diagnostic));
return codeAction;
}

/**
* Create a SuppressWarningEdit.
*
* @param str the string to insert
* @param range the range
* @return the text edit
*/
private TextEdit createSuppressWarningEdit(String str, Range range) {
return new TextEdit(
new Range(
new Position(range.getStart().getLine(), range.getStart().getCharacter()),
new Position(range.getEnd().getLine(), range.getEnd().getCharacter())),
"@SuppressWarnings(\"nullness\")\n" + " " + str);
}

/**
* Create a workspace edit.
*
* @param uri the URI of the file
* @param edits the text edits
* @return the workspace edit
*/
private WorkspaceEdit createWorkspaceEdit(String uri, TextEdit... edits) {
return new WorkspaceEdit(Collections.singletonMap(uri, Arrays.asList(edits)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.checkerframework.languageserver.quickfix;

import com.google.common.base.Splitter;

import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

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.List;

/** Interface for quick fix providers. */
public interface QuickFixProvider {
/**
* Check if the provider can handle a diagnostic.
*
* @param diagnostic the diagnostic
* @return true if the provider can handle the diagnostic
*/
boolean canHandle(Diagnostic diagnostic);

/**
* Get the quick fixes for a diagnostic.
*
* @param diagnostic the diagnostic
* @param params the code action parameters
* @return the quick fixes
*/
List<Either<Command, CodeAction>> getQuickFixes(Diagnostic diagnostic, CodeActionParams params);

/**
* Get the content in a range.
*
* @param uri the URI of the file
* @param range the range
* @return the content in the range
*/
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,52 @@
package org.checkerframework.languageserver.quickfix;

import org.eclipse.lsp4j.Diagnostic;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Registry for quick fix providers. */
public class QuickFixRegistry {
/** Map from diagnostic code to quick fix provider. */
private static final Map<String, QuickFixProvider> providers = new HashMap<>();

/**
* Register a quick fix provider for a diagnostic code.
*
* @param diagnosticCode the diagnostic code
* @param provider the quick fix provider
*/
public void registerProvider(String diagnosticCode, QuickFixProvider provider) {
providers.put(diagnosticCode, provider);
}

/**
* Get the quick fix provider for a diagnostic.
*
* @param diagnostic the diagnostic
* @return the quick fix provider
*/
public QuickFixProvider getProvider(Diagnostic diagnostic) {
return providers.get(extractMessage(diagnostic.getMessage()));
}

/**
* Extract the message from a diagnostic. Example: "[dereference.of.nullable] ..." ->
* "dereference.of.nullable"
*
* @param input the diagnostic message
* @return the extracted message
*/
private static String extractMessage(String input) {
Pattern pattern = Pattern.compile("\\[([^]]+)\\]");
Matcher matcher = pattern.matcher(input);

if (matcher.find()) {
return matcher.group(1); // Returns the first capturing group
}

return "No match found"; // Or any appropriate default/fallback value
}
}