diff --git a/lib/extension.ts b/lib/extension.ts index 2399f95dd..c73c44d27 100644 --- a/lib/extension.ts +++ b/lib/extension.ts @@ -26,14 +26,6 @@ export async function activate(context: ExtensionContext) { // Synchronize the setting section 'java' to the server // NOTE: this currently doesn't do anything configurationSection: 'java', - // Notify the server about file changes to 'javaconfig.json' files contain in the workspace - fileEvents: [ - workspace.createFileSystemWatcher('**/javaconfig.json'), - workspace.createFileSystemWatcher('**/pom.xml'), - workspace.createFileSystemWatcher('**/WORKSPACE'), - workspace.createFileSystemWatcher('**/BUILD'), - workspace.createFileSystemWatcher('**/*.java') - ] }, outputChannelName: 'Java', revealOutputChannelOn: 4 // never diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index cfe727830..71def8992 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -65,7 +65,7 @@ private void loadCompile(Collection sources) { cachedCompile.borrow.close(); } cachedCompile = doCompile(sources); - cachedModified.clear(); + clearCachedModified(); for (var f : sources) { cachedModified.put(f, f.getLastModified()); } @@ -350,5 +350,9 @@ public CompileTask compile(Collection sources) { return new CompileTask(compile.task, compile.roots, diags, compile::close); } + void clearCachedModified() { + cachedModified.clear(); + } + private static final Logger LOG = Logger.getLogger("main"); } diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index aa96ebd09..f10cedd73 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -4,6 +4,7 @@ import com.google.gson.*; import com.sun.source.util.Trees; +import com.sun.tools.javac.tree.JCTree; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; @@ -142,6 +143,7 @@ private Set docPath() { } return paths; } + private Set addExports() { if (!settings.has("addExports")) return Set.of(); var array = settings.getAsJsonArray("addExports"); @@ -189,7 +191,7 @@ public InitializeResult initialize(InitializeParams params) { } private static final String[] watchFiles = { - "**/*.java", "**/pom.xml", "**/BUILD", + "**/*.java", "**/pom.xml", "**/BUILD", "**/javaconfig.json", "**/WORKSPACE" }; @Override @@ -241,10 +243,10 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { FileStore.externalChange(file); break; case FileChangeType.Deleted: - FileStore.externalDelete(file); + removeClass(file); break; } - return; + continue; } var name = file.getFileName().toString(); switch (name) { @@ -480,6 +482,24 @@ private RenameVariable renameVariable(CompileTask task, VariableElement variable return new RenameVariable(file, (int) position, newName); } + private void removeClass(Path file) { + var className = cacheCompiler.fileManager.getClassName(file); + FileStore.externalDelete(file); + var compiler = compiler(); + var referencePaths = + Arrays.stream(compiler.findTypeReferences(className)).filter(ref -> !ref.equals(file)).toList(); + if (referencePaths.isEmpty()) { + return; + } + for (var referencePath : referencePaths) { + try (var task = compiler.compile(referencePath)) { + compiler.compiler.removeClass((JCTree.JCCompilationUnit) task.root(), className); + } + } + compiler.clearCachedModified(); + lint(referencePaths); + } + private boolean uncheckedChanges = false; private Path lastEdited = Paths.get(""); diff --git a/src/main/java/org/javacs/ReusableCompiler.java b/src/main/java/org/javacs/ReusableCompiler.java index 2d50f74c7..37669cdaa 100644 --- a/src/main/java/org/javacs/ReusableCompiler.java +++ b/src/main/java/org/javacs/ReusableCompiler.java @@ -31,14 +31,11 @@ import com.sun.source.util.TaskListener; import com.sun.tools.javac.api.*; import com.sun.tools.javac.code.Types; -import com.sun.tools.javac.comp.Annotate; -import com.sun.tools.javac.comp.Check; -import com.sun.tools.javac.comp.CompileStates; -import com.sun.tools.javac.comp.Enter; -import com.sun.tools.javac.comp.Modules; +import com.sun.tools.javac.comp.*; import com.sun.tools.javac.main.Arguments; import com.sun.tools.javac.main.JavaCompiler; import com.sun.tools.javac.model.JavacElements; +import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.DefinedBy; import com.sun.tools.javac.util.DefinedBy.Api; @@ -128,6 +125,10 @@ Borrow getTask( return new Borrow(task, currentContext); } + public void removeClass(JCTree.JCCompilationUnit root, String className) { + currentContext.removeClass(root, className); + } + class Borrow implements AutoCloseable { final JavacTask task; boolean closed; @@ -209,6 +210,10 @@ void drop(Class c) { ht.remove(key(c)); } + void removeClass(JCTree.JCCompilationUnit root, String className) { + ((ReusableJavaCompiler) get(JavaCompiler.compilerKey)).removeClass(root, className); + } + /** * Reusable JavaCompiler; exposes a method to clean up the component from leftovers associated with previous * compilations. @@ -226,6 +231,13 @@ public void close() { // do nothing } + void removeClass(JCTree.JCCompilationUnit root, String className) { + for (var classSymbol : syms.getClassesForName(names.fromString(className))) { + syms.removeClass(root.modle, classSymbol.flatname); + chk.removeCompiled(classSymbol); + } + } + void clear() { newRound(); } diff --git a/src/main/java/org/javacs/SourceFileManager.java b/src/main/java/org/javacs/SourceFileManager.java index 6d063c535..ec228d38b 100644 --- a/src/main/java/org/javacs/SourceFileManager.java +++ b/src/main/java/org/javacs/SourceFileManager.java @@ -42,15 +42,19 @@ private JavaFileObject asJavaFileObject(Path file) { public String inferBinaryName(Location location, JavaFileObject file) { if (location == StandardLocation.SOURCE_PATH) { var source = (SourceFileObject) file; - var packageName = FileStore.packageName(source.path); - var className = removeExtension(source.path.getFileName().toString()); - if (!packageName.isEmpty()) className = packageName + "." + className; - return className; + return getClassName(source.path); } else { return super.inferBinaryName(location, file); } } + String getClassName(Path path) { + var packageName = FileStore.packageName(path); + var className = removeExtension(path.getFileName().toString()); + if (!packageName.isEmpty()) className = packageName + "." + className; + return className; + } + private String removeExtension(String fileName) { var lastDot = fileName.lastIndexOf("."); return (lastDot == -1 ? fileName : fileName.substring(0, lastDot)); diff --git a/src/main/java/org/javacs/lsp/LSP.java b/src/main/java/org/javacs/lsp/LSP.java index 27cf6c55c..2654b622f 100644 --- a/src/main/java/org/javacs/lsp/LSP.java +++ b/src/main/java/org/javacs/lsp/LSP.java @@ -6,6 +6,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.Random; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; @@ -150,11 +151,20 @@ public void showMessage(ShowMessageParams params) { @Override public void registerCapability(String method, JsonElement options) { var params = new RegistrationParams(); - params.id = UUID.randomUUID().toString(); - params.method = method; - params.registerOptions = options; - - notifyClient(send, "client/registerCapability", params); + var registration = new RegistrationParams.Registration(); + registration.id = UUID.randomUUID().toString(); + registration.method = method; + registration.registerOptions = options; + params.registrations.add(registration); + var jsonText = toJson(params); + var requestMethod = "client/registerCapability"; + // The request should contain the id param. Otherwise, it will be considered a notification. + var id = new Random().nextInt(); + var messageText = + String.format( + "{\"jsonrpc\":\"2.0\",\"id\":\"%d\",\"method\":\"%s\",\"params\":%s}", + id, requestMethod, jsonText); + writeClient(send, messageText); } @Override @@ -172,7 +182,7 @@ public static void connect( // Read messages and process cancellations on a separate thread class MessageReader implements Runnable { void peek(Message message) { - if (message.method.equals("$/cancelRequest")) { + if ("$/cancelRequest".equals(message.method)) { var params = gson.fromJson(message.params, CancelParams.class); var removed = pending.removeIf(r -> r.id != null && r.id.equals(params.id)); if (removed) LOG.info(String.format("Cancelled request %d, which had not yet started", params.id)); diff --git a/src/main/java/org/javacs/lsp/RegistrationParams.java b/src/main/java/org/javacs/lsp/RegistrationParams.java index 9030aa045..e6a12a54a 100644 --- a/src/main/java/org/javacs/lsp/RegistrationParams.java +++ b/src/main/java/org/javacs/lsp/RegistrationParams.java @@ -1,8 +1,14 @@ package org.javacs.lsp; import com.google.gson.JsonElement; +import java.util.ArrayList; +import java.util.List; public class RegistrationParams { - public String id, method; - public JsonElement registerOptions; + List registrations = new ArrayList<>(); + + public static class Registration { + public String id, method; + public JsonElement registerOptions; + } } diff --git a/src/test/java/org/javacs/CompletionsTest.java b/src/test/java/org/javacs/CompletionsTest.java index bb7d9f5b7..ff0d9db5c 100644 --- a/src/test/java/org/javacs/CompletionsTest.java +++ b/src/test/java/org/javacs/CompletionsTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.javacs.completion.CompletionProvider; @@ -752,4 +753,40 @@ public void multilineChain() { var inserts = filterText("/org/javacs/example/MultilineChain.java", 6, 14); assertThat(inserts, hasItem("concat")); } + + @Test + public void removeCompiledClassShouldPublishCorrectDiagnostic() throws IOException { + var xClass = FindResource.path("/org/javacs/example/X.java"); + var yClass = FindResource.path("/org/javacs/example/Y.java"); + try { + try (var writer = Files.newBufferedWriter(xClass, StandardOpenOption.CREATE_NEW)) { + writer.write( + "package org.javacs.example;\n" + + "class X {\n " + + "static void test() {\n" + + "Y.test();\n" + + "}}"); + } + try (var writer = Files.newBufferedWriter(yClass, StandardOpenOption.CREATE_NEW)) { + writer.write( + "package org.javacs.example;\nclass Y {\n" + "static int test() {\n" + "return 1;\n" + "}}"); + } + List lintErrors = new ArrayList<>(); + var server = LanguageServerFixture.getJavaLanguageServer(diagnostic -> lintErrors.add(diagnostic.message)); + server.compiler().compile(xClass, yClass).close(); + + var deleteEvent = new FileEvent(); + deleteEvent.uri = yClass.toUri(); + deleteEvent.type = FileChangeType.Deleted; + var deleteFileParams = new DidChangeWatchedFilesParams(); + deleteFileParams.changes = List.of(deleteEvent); + server.didChangeWatchedFiles(deleteFileParams); + + assertEquals(1, lintErrors.size()); + assertTrue(lintErrors.stream().anyMatch(e -> e.contains("cannot find symbol"))); + } finally { + Files.delete(xClass); + Files.delete(yClass); + } + } }