Lazy load recipes. Bug fixes for reconciling

This commit is contained in:
aboyko
2023-08-28 20:24:17 -04:00
parent d97fc827ad
commit 5019e927c0
19 changed files with 344 additions and 602 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2023 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -10,7 +10,9 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.quickfix;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface QuickfixHandler {
QuickfixEdit createEdits(Object params);
Mono<QuickfixEdit> createEdits(Object params);
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2023 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -39,7 +39,7 @@ public class QuickfixRegistry {
return new QuickfixType() {
@Override
public QuickfixEdit createEdits(Object params) {
public Mono<QuickfixEdit> createEdits(Object params) {
return handler.createEdits(params);
}
@@ -55,7 +55,7 @@ public class QuickfixRegistry {
return new QuickfixType() {
@Override
public QuickfixEdit createEdits(Object params) {
public Mono<QuickfixEdit> createEdits(Object params) {
return handler.createEdits(params);
}
@@ -68,9 +68,7 @@ public class QuickfixRegistry {
public Mono<QuickfixEdit> handle(QuickfixResolveParams params) {
QuickfixHandler handler = registry.get(params.getType());
return Mono.fromSupplier(() -> {
return handler.createEdits(params.getParams());
});
return handler.createEdits(params.getParams());
}
public boolean hasFixes() {

View File

@@ -1,9 +1,12 @@
package org.springframework.ide.vscode.commons.languageserver.util;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.WorkspaceEdit;
public interface CodeActionResolver {
void resolve(CodeAction codeAction);
CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction);
}

View File

@@ -15,7 +15,6 @@ import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -550,18 +549,17 @@ public class SimpleTextDocumentService implements TextDocumentService, DocumentE
@Override
public CompletableFuture<CodeAction> resolveCodeAction(CodeAction ca) {
return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelToken -> {
if (appContext!=null) {
Map<String, CodeActionResolver> resolvers = appContext.getBeansOfType(CodeActionResolver.class);
for (CodeActionResolver r : resolvers.values()) {
r.resolve(ca);
if (ca.getEdit() != null) {
return ca;
}
}
}
return ca;
});
return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelToken -> appContext == null ? Collections.<String, CodeActionResolver>emptyMap() : appContext.getBeansOfType(CodeActionResolver.class))
.thenCompose(resolvers -> {
List<CompletableFuture<Void>> resolutions = resolvers.values().stream().map(r -> r.resolve(ca).thenAccept(we -> {
if (ca.getEdit() != null) {
throw new IllegalStateException("More than one CodeActionResolver resolves the code action");
} else {
ca.setEdit(we);
}
})).collect(Collectors.toList());
return CompletableFuture.allOf(resolutions.toArray(new CompletableFuture[resolutions.size()])).thenApply(v -> ca);
});
}
@Override

View File

@@ -39,6 +39,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import reactor.core.publisher.Mono;
public class YamlQuickfixes {
private static final Logger LOG = LoggerFactory.getLogger(YamlQuickfixes.class);
@@ -53,7 +55,7 @@ public class YamlQuickfixes {
private final Gson gson = new Gson();
public YamlQuickfixes(QuickfixRegistry r, SimpleTextDocumentService textDocumentService, YamlStructureProvider structureProvider) {
MISSING_PROP_FIX = r.register("MISSING_PROP_FIX", (Object _params) -> {
MISSING_PROP_FIX = r.register("MISSING_PROP_FIX", (Object _params) -> Mono.fromSupplier(() -> {
MissingPropertiesData params = gson.fromJson((JsonElement)_params, MissingPropertiesData.class);
try {
TextDocument _doc = textDocumentService.getLatestSnapshot(params.getUri());
@@ -91,9 +93,9 @@ public class YamlQuickfixes {
}
//Something went wrong. Return empty edit object.
return NULL_FIX;
});
}));
SIMPLE_TEXT_EDIT = r.register("SIMPLE_TEXT_EDIT", (_params) -> {
SIMPLE_TEXT_EDIT = r.register("SIMPLE_TEXT_EDIT", (_params) -> Mono.fromSupplier(() -> {
try {
ReplaceStringData params = gson.fromJson((JsonElement)_params, ReplaceStringData.class);
TextDocument _doc = textDocumentService.getLatestSnapshot(params.getUri());
@@ -111,7 +113,7 @@ public class YamlQuickfixes {
}
//Something went wrong. Return empty edit object.
return NULL_FIX;
});
}));
}
public static QuickfixEdit createReplacementQuickfic(TextDocument doc, YamlPathEdits edits) throws BadLocationException {

View File

@@ -32,7 +32,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -46,7 +45,6 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc;
import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaProjectReconcilerScheduler;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler;
import org.springframework.ide.vscode.boot.java.links.DefaultJavaElementLocationProvider;
@@ -64,7 +62,6 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDa
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.value.PropertyValueAnnotationDefProvider;
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
@@ -352,8 +349,8 @@ public class BootLanguageServerBootApp {
}
@Bean
BootJavaReconcileEngine getBootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers, SimpleLanguageServer server) {
return new BootJavaReconcileEngine(projectFinder, javaReconcilers, server);
BootJavaReconcileEngine getBootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers) {
return new BootJavaReconcileEngine(projectFinder, javaReconcilers);
}
@Bean
@@ -361,16 +358,6 @@ public class BootLanguageServerBootApp {
return new BootJavaCodeActionProvider(projectFinder, codeActionHandlers);
}
@ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness")
@ConditionalOnProperty(prefix = "languageserver", name = "reconcile-only-opened-docs", havingValue = "false", matchIfMissing = true)
@Bean
BootJavaProjectReconcilerScheduler bootJavaProjectReconcilerScheduler(SimpleLanguageServer server,
BootJavaReconcileEngine bootJavaReconciler, ProjectObserver projectObserver, BootJavaConfig config,
Optional<RewriteRecipeRepository> recipeRepoOpt, JavaProjectFinder projectFinder) {
return new BootJavaProjectReconcilerScheduler(bootJavaReconciler, projectObserver, config,
recipeRepoOpt.orElse(null), projectFinder, server);
}
@Bean
JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) {
return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider()));

View File

@@ -28,9 +28,7 @@ import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents
import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.java.utils.ServerUtils;
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents;
import org.springframework.ide.vscode.boot.xml.SpringXMLLanguageServerComponents;
@@ -73,7 +71,6 @@ public class BootLanguageServerInitializer implements InitializingBean {
@Autowired SpringSymbolIndex springIndexer;
@Autowired(required = false) List<ICompletionEngine> completionEngines;
@Autowired private JavaProjectFinder projectFinder;
@Autowired(required = false) private RewriteRecipeRepository recipesRepo;
@Qualifier("adHocProperties") @Autowired ProjectBasedPropertyIndexProvider adHocProperties;
@@ -148,24 +145,9 @@ public class BootLanguageServerInitializer implements InitializingBean {
components.getCodeActionProvider().ifPresent(documents::onCodeAction);
components.getDocumentSymbolProvider().ifPresent(documents::onDocumentSymbol);
// TODO: seems to hang tests if done on server initialize. Test Harness Server is likely to be initialized already by this point
// server.doOnInitialized(() -> {
if (recipesRepo != null) {
recipesRepo.onRecipesLoaded(v -> {
// Recipes will start loading only after config has been received. Therefore safe to start listening to config changes now
// and launch initial project reconcile since both config and recipes are present
startListeningToPerformReconcile();
reconcile();
});
} else {
// Reconcile would occur as listeners will be receiving events
startListeningToPerformReconcile();
//Uncomment reconcile() call if done within server.doOnInitialized()
// reconcile();
}
// });
startListeningToPerformReconcile();
server.onCommand("sts/show/document", p -> {
ShowDocumentParams showDocParams = new Gson().fromJson((JsonElement)p.getArguments().get(0), ShowDocumentParams.class);
return server.getClient().showDocument(showDocParams).thenApply(r -> {
@@ -193,7 +175,7 @@ public class BootLanguageServerInitializer implements InitializingBean {
server.validateWith(doc.getId(), reconcileEngine);
});
ServerUtils.listenToClassFileChanges(server.getWorkspaceService().getFileObserver(), projectFinder, project -> validateAll(components, server, project));
// ServerUtils.listenToClassFileChanges(server.getWorkspaceService().getFileObserver(), projectFinder, project -> validateAll(components, server, project));
});
config.addListener(evt -> reconcile());
params.projectObserver.addListener(reconcileDocumentsForProjectChange(server, components, params.projectFinder));

View File

@@ -1,89 +0,0 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.handlers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine;
import org.springframework.ide.vscode.boot.common.ProjectReconcileScheduler;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository;
import org.springframework.ide.vscode.boot.java.utils.ServerUtils;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
public class BootJavaProjectReconcilerScheduler extends ProjectReconcileScheduler {
private static final Logger log = LoggerFactory.getLogger(BootJavaProjectReconcilerScheduler.class);
private ProjectObserver projectObserver;
private BootJavaConfig config;
private RewriteRecipeRepository recipesRepo;
public BootJavaProjectReconcilerScheduler(IJavaProjectReconcileEngine reconciler, ProjectObserver projectObserver,
BootJavaConfig config, RewriteRecipeRepository recipesRepo, JavaProjectFinder projectFinder, SimpleLanguageServer server) {
super(server, reconciler, projectFinder);
this.projectObserver = projectObserver;
this.config = config;
this.recipesRepo = recipesRepo;
}
@Override
protected void init() {
log.info("Starting project reconciler for Java sources");
super.init();
if (recipesRepo != null) {
recipesRepo.onRecipesLoaded(v -> {
// Recipes will start loading only after config has been received. Therefore safe to start listening to config changes now
// and launch initial project reconcile since both config and recipes are present
startListeningToPerformReconcile();
scheduleValidationForAllProjects();
log.info("Started project reconciler for Java sources");
});
} else {
startListeningToPerformReconcile();
scheduleValidationForAllProjects();
log.info("Started project reconciler for Java sources");
}
}
private void startListeningToPerformReconcile() {
config.addListener(evt -> scheduleValidationForAllProjects());
projectObserver.addListener(new ProjectObserver.Listener() {
@Override
public void deleted(IJavaProject project) {
unscheduleValidation(project);
clear(project, true);
}
@Override
public void created(IJavaProject project) {
scheduleValidation(project);
}
@Override
public void changed(IJavaProject project) {
scheduleValidation(project);
}
});
ServerUtils.listenToClassFileChanges(getServer().getWorkspaceService().getFileObserver(), getProjectFinder(), this::scheduleValidation);
// TODO: index update even happens on every file save. Very expensive to blindly reconcile all projects.
// Need to figure out a check if spring index has any changes
// springIndexer.onUpdate(v -> reconcile());
}
}

View File

@@ -10,54 +10,32 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.handlers;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.common.IJavaProjectReconcileEngine;
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.PercentageProgressTask;
import org.springframework.ide.vscode.commons.languageserver.ProgressService;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.UriUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.LazyTextDocument;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Martin Lippert
*/
public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectReconcileEngine {
public class BootJavaReconcileEngine implements IReconcileEngine {
private static final Logger log = LoggerFactory.getLogger(BootJavaReconcileEngine.class);
private final JavaProjectFinder projectFinder;
private final JavaReconciler[] javaReconcilers;
private final SimpleLanguageServer server;
public BootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers,
SimpleLanguageServer server) {
public BootJavaReconcileEngine(JavaProjectFinder projectFinder, JavaReconciler[] javaReconcilers) {
this.projectFinder = projectFinder;
this.javaReconcilers = javaReconcilers;
this.server = server;
}
@Override
@@ -116,69 +94,4 @@ public class BootJavaReconcileEngine implements IReconcileEngine, IJavaProjectRe
}
}
@Override
public void reconcile(IJavaProject project, ProgressService progressService) {
// Stream<Path> files = IClasspathUtil.getProjectJavaSourceFolders(project.getClasspath()).flatMap(folder -> {
// try {
// return Files.walk(folder.toPath()).filter(Files::isRegularFile);
// } catch (IOException e) {
// return Stream.empty();
// }
// });
//
// Stream<TextDocumentIdentifier> docIds = files
// .filter(f -> f.getFileName().toString().endsWith(".java"))
// .map(f -> new TextDocumentIdentifier(f.toUri().toASCIIString()));
//
// List<TextDocument> docs = docIds.filter(docId -> server.getTextDocumentService().getLatestSnapshot(docId.getUri()) == null)
// .map(docId -> new LazyTextDocument(docId.getUri(), LanguageId.JAVA)).collect(Collectors.toList());
//
// Map<IDocument, IProblemCollector> problemCollectors = docs.stream()
// .collect(Collectors.toMap(doc -> doc, doc -> server.createProblemCollector(new AtomicReference<>(doc), null)));
//
// problemCollectors.values().forEach(c -> c.beginCollecting());
//
// int totalWork = 0;
// for (JavaReconciler jr : javaReconcilers) {
// totalWork += jr.getTotalWorkUnits(docs);
// }
//
// PercentageProgressTask progressTask = progressService.createPercentageProgressTask(
// "reconcile-java-" + project.getElementName(),
// totalWork,
// "Spring Tools: Reconciling Java Sources for '" + project.getElementName() + "'"
// );
//
// for (JavaReconciler jr : javaReconcilers) {
// try {
// Map<IDocument, Collection<ReconcileProblem>> problems = jr.reconcile(project, docs, () -> progressTask.increment());
// problems.entrySet().forEach(e -> {
// IProblemCollector collector = problemCollectors.get(e.getKey());
// e.getValue().forEach(p -> collector.accept(p));
// });
// } catch (Exception e) {
// log.error("", e);
// }
// }
//
// progressTask.done();
// problemCollectors.values().forEach(c -> c.endCollecting());
}
@Override
public void clear(IJavaProject project) {
// IClasspathUtil.getProjectJavaSourceFolders(project.getClasspath()).flatMap(folder -> {
// try {
// return Files.walk(folder.toPath()).filter(Files::isRegularFile);
// } catch (IOException e) {
// return Stream.empty();
// }
// })
// .filter(f -> f.getFileName().toString().endsWith(".java"))
// .filter(f -> server.getTextDocumentService().getLatestSnapshot(UriUtil.toUri(f.toFile()).toASCIIString()) == null)
// .forEach(p -> server.getTextDocumentService().publishDiagnostics(new TextDocumentIdentifier(p.toUri().toASCIIString()), Collections.emptyList()));
}
}

View File

@@ -17,7 +17,6 @@ import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -36,27 +35,21 @@ import java.util.stream.Collectors;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Parser;
import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.Result;
import org.openrewrite.SourceFile;
import org.openrewrite.TreeVisitor;
import org.openrewrite.Validated;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.config.RecipeDescriptor;
import org.openrewrite.config.YamlResourceLoader;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.tree.J.CompilationUnit;
import org.openrewrite.maven.MavenParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.IndefiniteProgressTask;
@@ -67,8 +60,6 @@ import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild;
import org.springframework.ide.vscode.commons.rewrite.LoadUtils;
import org.springframework.ide.vscode.commons.rewrite.LoadUtils.DurationTypeConverter;
import org.springframework.ide.vscode.commons.rewrite.ORDocUtils;
import org.springframework.ide.vscode.commons.rewrite.config.DefaultMarkerVisitorContext;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor;
import org.springframework.ide.vscode.commons.rewrite.config.StsEnvironment;
import org.springframework.ide.vscode.commons.rewrite.gradle.GradleIJavaProjectParser;
import org.springframework.ide.vscode.commons.rewrite.java.ProjectParser;
@@ -79,7 +70,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
public class RewriteRecipeRepository implements ApplicationContextAware {
public class RewriteRecipeRepository {
enum RecipeFilter {
ALL,
@@ -111,17 +102,11 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
final private SimpleLanguageServer server;
final private Map<String, Recipe> recipes;
final private JavaProjectFinder projectFinder;
final private List<RecipeCodeActionDescriptor> codeActionDescriptors;
final private ListenerList<Void> loadListeners;
private ApplicationContext applicationContext;
CompletableFuture<Void> loaded;
private CompletableFuture<Map<String, Recipe>> recipesFuture = null;
private Set<String> scanFiles;
private Set<String> scanDirs;
@@ -134,17 +119,13 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
public RewriteRecipeRepository(SimpleLanguageServer server, JavaProjectFinder projectFinder, BootJavaConfig config) {
this.server = server;
this.projectFinder = projectFinder;
this.recipes = new HashMap<>();
this.codeActionDescriptors = new ArrayList<>();
this.loadListeners = new ListenerList<>();
this.scanDirs = UNINITIALIZED_SET;
this.scanFiles = UNINITIALIZED_SET;
this.recipeFilters = UNINITIALIZED_SET;
CompletableFuture<Void> firstConfigLoaded = new CompletableFuture<>();
config.addListener(l -> {
Set<String> recipeFilterFromConfig = config.getRecipesFilters();
boolean firstTimeConfig = recipeFilters == UNINITIALIZED_SET && scanDirs == UNINITIALIZED_SET && scanFiles == UNINITIALIZED_SET;
if (recipeFilters == UNINITIALIZED_SET || !recipeFilters.equals(recipeFilterFromConfig)) {
recipeFilters = recipeFilterFromConfig;
}
@@ -154,38 +135,20 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
// Therefore it is best to store the scanDirs here right after it is received, not during scan process or anything else done async
scanDirs = config.getRecipeDirectories();
scanFiles = config.getRecipeFiles();
if (!firstTimeConfig) {
load();
}
}
// First time config loaded. Received when server fully initialized. Start load after the firstConfigLoaded then and assign it to loaded field such that it is never null
if (firstTimeConfig) {
firstConfigLoaded.complete(null);
clearRecipes();
}
});
// Initial configuration is followed by load() as a special case to have 'loaded' future value to never be null
this.loaded = firstConfigLoaded.thenCompose(v -> load());
registerCommands();
}
public CompletableFuture<Void> load() {
return this.loaded = CompletableFuture.runAsync(() -> {
clearRecipes();
loadRecipes();
loadListeners.fire(null);
});
}
private synchronized void clearRecipes() {
recipes.clear();
codeActionDescriptors.clear();
recipesFuture = null;
}
private synchronized void loadRecipes() {
private synchronized Map<String, Recipe> loadRecipes() {
IndefiniteProgressTask progressTask = server.getProgressService().createIndefiniteProgressTask(UUID.randomUUID().toString(), "Loading Rewrite Recipes", null);
Map<String, Recipe> recipes = new HashMap<>();
try {
log.info("Loading Rewrite Recipes...");
Recipe xmlbindRecipe = null;
@@ -211,13 +174,14 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
}
}
}
codeActionDescriptors.addAll(env.listCodeActionDescriptors());
// codeActionDescriptors.addAll(env.listCodeActionDescriptors());
log.info("Done loading Rewrite Recipes");
} catch (Throwable t) {
log.error("", t);
} finally {
progressTask.done();
}
return recipes;
}
private boolean isAcceptableGlobalCommandRecipe(Recipe r) {
@@ -285,52 +249,15 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
return builder.build();
}
public Optional<Recipe> getRecipe(String name) {
return Optional.ofNullable(recipes.get(name));
public CompletableFuture<Map<String, Recipe>> recipes() {
if (recipesFuture == null) {
recipesFuture = CompletableFuture.supplyAsync(this::loadRecipes);
}
return recipesFuture;
}
public RecipeCodeActionDescriptor getCodeActionRecipeDescriptor(String id) {
for (RecipeCodeActionDescriptor d : codeActionDescriptors) {
if (id.equals(d.getId())) {
return d;
}
}
return null;
}
public List<RecipeCodeActionDescriptor> getProblemRecipeDescriptors() {
List<RecipeCodeActionDescriptor> l = new ArrayList<>(codeActionDescriptors.size());
for (RecipeCodeActionDescriptor d : codeActionDescriptors) {
if (d.getProblemType() != null && server.getDiagnosticSeverityProvider().getDiagnosticSeverity(d.getProblemType()) != null) {
l.add(d);
}
}
return l;
}
public List<RecipeCodeActionDescriptor> getCodeActionRecipeDescriptors() {
List<RecipeCodeActionDescriptor> l = new ArrayList<>(codeActionDescriptors.size());
for (RecipeCodeActionDescriptor d : codeActionDescriptors) {
if (d.getProblemType() == null || server.getDiagnosticSeverityProvider().getDiagnosticSeverity(d.getProblemType()) == null) {
l.add(d);
}
}
return l;
}
public CompilationUnit mark(IJavaProject project, List<? extends RecipeCodeActionDescriptor> descriptors, CompilationUnit compilationUnit) {
CompilationUnit cu = compilationUnit;
for (RecipeCodeActionDescriptor d : descriptors) {
TreeVisitor<?, ExecutionContext> markVisitor = d.getMarkerVisitor(new DefaultMarkerVisitorContext(applicationContext, project));
if (markVisitor != null) {
try {
cu = (CompilationUnit) markVisitor.visit(cu, new InMemoryExecutionContext(e -> log.error("Marker visitor failed!", e)));
} catch (Exception e) {
// ignore - would happen in sources with compiler errors
}
}
}
return cu;
public CompletableFuture<Optional<Recipe>> getRecipe(String name) {
return recipes().thenApply(recipes -> Optional.ofNullable(recipes.get(name)));
}
private static JsonElement recipeToJson(Recipe r) {
@@ -342,26 +269,20 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
server.onCommand(CMD_REWRITE_LIST, params -> {
JsonElement uri = (JsonElement) params.getArguments().get(0);
RecipeFilter f = params.getArguments().size() > 1 ? RecipeFilter.valueOf(((JsonElement) params.getArguments().get(1)).getAsString()) : RecipeFilter.ALL;
return loaded.thenApply(v -> {
if (uri == null) {
return Collections.emptyList();
} else {
return listProjectRefactoringRecipes(uri.getAsString()).stream()
return listProjectRefactoringRecipes(uri.getAsString()).thenApply(recipes -> recipes.stream()
.filter(RECIPE_LIST_FILTERS.get(f))
.map(RewriteRecipeRepository::recipeToJson)
.collect(Collectors.toList());
}
});
.collect(Collectors.toList()));
});
server.onCommand(CMD_REWRITE_EXECUTE, params -> {
return loaded.thenCompose(v -> {
return recipes().thenCompose(recipes -> {
String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
JsonElement recipesJson = ((JsonElement) params.getArguments().get(1));
RecipeDescriptor d = serializationGson.fromJson(recipesJson, RecipeDescriptor.class);
Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> getRecipe(id).map(r -> r.getClass()).orElse(null));
Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> Optional.ofNullable(recipes.get(id)).map(r -> r.getClass()).orElse(null));
if (aggregateRecipe instanceof DeclarativeRecipe && aggregateRecipe.getRecipeList().isEmpty()) {
throw new RuntimeException("No recipes found to perform!");
@@ -383,14 +304,19 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
});
});
server.onCommand(CMD_REWRITE_RELOAD, params -> load().thenApply((v) -> "executed"));
server.onCommand(CMD_REWRITE_RELOAD, params -> {
clearRecipes();
return CompletableFuture.completedFuture("executed");
});
server.onCommand(CMD_REWRITE_RECIPE_EXECUTE, params -> {
String recipeId = ((JsonElement) params.getArguments().get(0)).getAsString();
Recipe r = getRecipe(recipeId).orElseThrow(() -> new IllegalArgumentException("No such recipe exists with name " + recipeId));
final String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft();
String uri = ((JsonElement) params.getArguments().get(1)).getAsString();
return apply(r, uri, progressToken);
return getRecipe(recipeId).thenCompose(optRecipe -> {
Recipe r = optRecipe.orElseThrow(() -> new IllegalArgumentException("No such recipe exists with name " + recipeId));
final String progressToken = params.getWorkDoneToken() == null || params.getWorkDoneToken().getLeft() == null ? r.getName() : params.getWorkDoneToken().getLeft();
String uri = ((JsonElement) params.getArguments().get(1)).getAsString();
return apply(r, uri, progressToken);
});
});
}
@@ -447,7 +373,7 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
return ORDocUtils.createWorkspaceEdit(absoluteProjectDir, server.getTextDocumentService(), results);
}
private List<Recipe> listProjectRefactoringRecipes(String uri) {
private CompletableFuture<List<Recipe>> listProjectRefactoringRecipes(String uri) {
if (uri != null) {
/*
* When LS started on listing rewrite recipes project lookup may not find any projects as classpath might still be resolving.
@@ -456,10 +382,10 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
*/
// Optional<IJavaProject> projectOpt = projectFinder.find(new TextDocumentIdentifier(uri));
// if (projectOpt.isPresent()) {
return recipes.values().stream().filter(this::isAcceptableGlobalCommandRecipe).collect(Collectors.toList());
return recipes().thenApply(recipes -> recipes.values().stream().filter(this::isAcceptableGlobalCommandRecipe).collect(Collectors.toList()));
// }
}
return Collections.emptyList();
return CompletableFuture.completedFuture(Collections.emptyList());
}
private static ProjectParser createRewriteProjectParser(IJavaProject jp, Function<Path, Parser.Input> inputProvider) {
@@ -480,11 +406,6 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
loadListeners.add(l);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private static String lastTokenAfterDot(String s) {
int idx = s.lastIndexOf('.');
if (idx >= 0 && idx < s.length() - 1) {

View File

@@ -105,75 +105,77 @@ public class RewriteReconciler implements JavaReconciler {
// }
}
private List<ReconcileProblem> createProblems(IDocument doc, FixAssistMarker m, J astNode) {
if (astNode != null) {
Range range = astNode.getMarkers().findFirst(Range.class).orElse(null);
if (range != null) {
RecipeCodeActionDescriptor recipeFixDescriptor = recipeRepo.getCodeActionRecipeDescriptor(m.getDescriptorId());
if (recipeFixDescriptor != null) {
return List.of(createProblem(doc, recipeFixDescriptor, m, range));
}
}
}
return Collections.emptyList();
}
private ReconcileProblemImpl createProblem(IDocument doc, RecipeCodeActionDescriptor recipeFixDescriptor,
FixAssistMarker m, Range range) {
ProblemType problemType = recipeFixDescriptor.getProblemType();
ReconcileProblemImpl problem = new ReconcileProblemImpl(problemType, m.getLabel() == null ? problemType.getLabel() : m.getLabel(), range.getStart().getOffset(), range.getEnd().getOffset() - range.getStart().getOffset());
QuickfixType quickfixType = quickfixRegistry.getQuickfixType(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX);
if (quickfixType != null) {
for (FixDescriptor f : m.getFixes()) {
if (recipeRepo.getRecipe(f.getRecipeId()).isPresent()) {
problem.addQuickfix(new QuickfixData<>(
quickfixType,
f,
f.getLabel()
));
}
}
}
return problem;
}
// private List<ReconcileProblem> createProblems(IDocument doc, FixAssistMarker m, J astNode) {
// if (astNode != null) {
// Range range = astNode.getMarkers().findFirst(Range.class).orElse(null);
// if (range != null) {
// RecipeCodeActionDescriptor recipeFixDescriptor = recipeRepo.getCodeActionRecipeDescriptor(m.getDescriptorId());
// if (recipeFixDescriptor != null) {
// return List.of(createProblem(doc, recipeFixDescriptor, m, range));
// }
// }
// }
// return Collections.emptyList();
// }
//
// private ReconcileProblemImpl createProblem(IDocument doc, RecipeCodeActionDescriptor recipeFixDescriptor,
// FixAssistMarker m, Range range) {
// ProblemType problemType = recipeFixDescriptor.getProblemType();
// ReconcileProblemImpl problem = new ReconcileProblemImpl(problemType, m.getLabel() == null ? problemType.getLabel() : m.getLabel(), range.getStart().getOffset(), range.getEnd().getOffset() - range.getStart().getOffset());
// QuickfixType quickfixType = quickfixRegistry.getQuickfixType(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX);
// if (quickfixType != null) {
// for (FixDescriptor f : m.getFixes()) {
// if (recipeRepo.getRecipe(f.getRecipeId()).isPresent()) {
// problem.addQuickfix(new QuickfixData<>(
// quickfixType,
// f,
// f.getLabel()
// ));
// }
// }
// }
// return problem;
// }
@Override
public Map<IDocument, Collection<ReconcileProblem>> reconcile(IJavaProject project, List<TextDocument> docs, Runnable incrementProgress) {
if (!config.isJavaSourceReconcileEnabled()) {
return Collections.emptyMap();
}
// if (!config.isJavaSourceReconcileEnabled()) {
// return Collections.emptyMap();
// }
//
// long start = System.currentTimeMillis();
//
// Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>();
// List<Path> testSourceFolders = IClasspathUtil.getProjectTestJavaSources(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toList());
// List<TextDocument> testSources = new ArrayList<>(docs.size());
// List<TextDocument> mainSources = new ArrayList<>(docs.size());
// for (TextDocument d : docs) {
// Path p = Paths.get(URI.create(d.getUri()));
// if (testSourceFolders.stream().anyMatch(t -> p.startsWith(t))) {
// testSources.add(d);
// } else {
// mainSources.add(d);
// }
// }
//
// List<Path> classpath = IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath()).collect(Collectors.toList());
// JavaParser javaParser = ORAstUtils.createJavaParser(() -> JavaParser.fromJavaVersion().classpath(classpath));
//
// // Pass in source sets created from classpath. (Perhaps it is a good idea to have separate classpath and parsers for test and main, TBD)
// // Perhaps it is even better to create empty classpath java source sets as reconcile step seem to only need name of the java source set
// // Usually java source set classpath is required to figure out how to organize imports for sources
// JavaSourceSet mainJavaSourceSet = JavaSourceSet.build(ProjectParser.MAIN, classpath, null, false);
// JavaSourceSet testJavaSourceSet = new JavaSourceSet(Tree.randomId(), ProjectParser.TEST, mainJavaSourceSet.getClasspath());
// allProblems.putAll(doReconcile(project, mainSources, javaParser, mainJavaSourceSet, incrementProgress));
// allProblems.putAll(doReconcile(project, testSources, javaParser, testJavaSourceSet, incrementProgress));
//
// long end = System.currentTimeMillis();
// log.info("reconciling project (OpenRewrite): " + project.getElementName() + " - " + docs.size() + " done in " + (end - start) + "ms");
//
// return allProblems;
long start = System.currentTimeMillis();
Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>();
List<Path> testSourceFolders = IClasspathUtil.getProjectTestJavaSources(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toList());
List<TextDocument> testSources = new ArrayList<>(docs.size());
List<TextDocument> mainSources = new ArrayList<>(docs.size());
for (TextDocument d : docs) {
Path p = Paths.get(URI.create(d.getUri()));
if (testSourceFolders.stream().anyMatch(t -> p.startsWith(t))) {
testSources.add(d);
} else {
mainSources.add(d);
}
}
List<Path> classpath = IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath()).collect(Collectors.toList());
JavaParser javaParser = ORAstUtils.createJavaParser(() -> JavaParser.fromJavaVersion().classpath(classpath));
// Pass in source sets created from classpath. (Perhaps it is a good idea to have separate classpath and parsers for test and main, TBD)
// Perhaps it is even better to create empty classpath java source sets as reconcile step seem to only need name of the java source set
// Usually java source set classpath is required to figure out how to organize imports for sources
JavaSourceSet mainJavaSourceSet = JavaSourceSet.build(ProjectParser.MAIN, classpath, null, false);
JavaSourceSet testJavaSourceSet = new JavaSourceSet(Tree.randomId(), ProjectParser.TEST, mainJavaSourceSet.getClasspath());
allProblems.putAll(doReconcile(project, mainSources, javaParser, mainJavaSourceSet, incrementProgress));
allProblems.putAll(doReconcile(project, testSources, javaParser, testJavaSourceSet, incrementProgress));
long end = System.currentTimeMillis();
log.info("reconciling project (OpenRewrite): " + project.getElementName() + " - " + docs.size() + " done in " + (end - start) + "ms");
return allProblems;
return Collections.emptyMap();
}
@@ -249,99 +251,99 @@ public class RewriteReconciler implements JavaReconciler {
private static final int BATCH = 50;
// Parse in batches and share the parser
private Map<IDocument, Collection<ReconcileProblem>> doReconcile(IJavaProject project, List<TextDocument> docs, JavaParser javaParser, JavaSourceSet javaSourceSet, Runnable incrementProgress) {
Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>();
if (javaParser != null && config.isJavaSourceReconcileEnabled()) {
try {
List<RecipeCodeActionDescriptor> descriptors = getProblemRecipeDescriptors(project);
if (!descriptors.isEmpty()) {
for (int i = 0; i < docs.size(); i += BATCH) {
List<TextDocument> batchList = docs.subList(i, Math.min(i + BATCH, docs.size()));
List<CompilationUnit> cus = ORAstUtils.parseInputs(javaParser,
batchList.stream().map(d -> new Parser.Input(Paths.get(URI.create(d.getUri())), () -> {
return new ByteArrayInputStream(d.get().getBytes());
})).collect(Collectors.toList()), source -> incrementProgress.run());
cus = ListUtils.map(cus, cu -> cu.withMarkers(cu.getMarkers().computeByType(javaSourceSet, (original, updated) -> updated)));
/*
* If exception occurs during parsing inputs the list of inputs would become shorter than the list of corresponding documents
*/
for (int j = 0, k = 0; j < batchList.size() && k < cus.size(); j++) {
final IDocument doc = batchList.get(j);
List<ReconcileProblem> problems = new ArrayList<>();
CompilationUnit cu = cus.get(k);
Path sourcePath = Paths.get(URI.create(doc.getUri()));
if (cu.getSourcePath().equals(sourcePath)) {
k++;
collectProblems(project, descriptors, doc, cu, problems::add);
if (!problems.isEmpty()) {
allProblems.put(doc, problems);
}
} else {
log.warn("(OpenRewrite) Failed to parse source for " + sourcePath);
}
incrementProgress.run();
}
}
}
} catch (Exception e) {
if (ORAstUtils.isExceptionFromInterrupedThread(e)) {
log.debug("", e);
} else {
log.error("", e);
}
}
}
return allProblems;
}
private List<RecipeCodeActionDescriptor> getProblemRecipeDescriptors(IJavaProject project)
throws InterruptedException, ExecutionException {
return recipeRepo.getProblemRecipeDescriptors().stream().filter(d -> d.getProblemType() != null).filter(d -> {
switch (config.getProblemApplicability(d.getProblemType())) {
case ON:
return SpringProjectUtil.isBootProject(project);
case OFF:
return false;
default: // AUTO
return d.isApplicable(project);
}
}).collect(Collectors.toList());
}
private void collectProblems(IJavaProject project, List<RecipeCodeActionDescriptor> descriptors, IDocument doc, CompilationUnit compilationUnit, Consumer<ReconcileProblem> problemHandler) {
CompilationUnit cu = recipeRepo.mark(project, descriptors, compilationUnit);
if (compilationUnit != cu) {
new JavaMarkerVisitor<ExecutionContext>() {
@Override
public J visit(Tree tree, ExecutionContext context) {
J t = super.visit(tree, context);
if (t instanceof J) {
for (Marker m : t.getMarkers().entries()) {
if (m instanceof FixAssistMarker) {
for (ReconcileProblem problem : createProblems(doc, (FixAssistMarker) m, t)) {
problemHandler.accept(problem);
}
}
}
}
return t;
}
}.visit(cu, new InMemoryExecutionContext(e -> log.error("", e)));
}
}
public int getTotalWorkUnits(List<TextDocument> docs) {
return docs.size() * 2;
}
// private Map<IDocument, Collection<ReconcileProblem>> doReconcile(IJavaProject project, List<TextDocument> docs, JavaParser javaParser, JavaSourceSet javaSourceSet, Runnable incrementProgress) {
// Map<IDocument, Collection<ReconcileProblem>> allProblems = new HashMap<>();
// if (javaParser != null && config.isJavaSourceReconcileEnabled()) {
// try {
// List<RecipeCodeActionDescriptor> descriptors = getProblemRecipeDescriptors(project);
//
//
// if (!descriptors.isEmpty()) {
//
// for (int i = 0; i < docs.size(); i += BATCH) {
// List<TextDocument> batchList = docs.subList(i, Math.min(i + BATCH, docs.size()));
//
// List<CompilationUnit> cus = ORAstUtils.parseInputs(javaParser,
// batchList.stream().map(d -> new Parser.Input(Paths.get(URI.create(d.getUri())), () -> {
// return new ByteArrayInputStream(d.get().getBytes());
// })).collect(Collectors.toList()), source -> incrementProgress.run());
//
// cus = ListUtils.map(cus, cu -> cu.withMarkers(cu.getMarkers().computeByType(javaSourceSet, (original, updated) -> updated)));
//
// /*
// * If exception occurs during parsing inputs the list of inputs would become shorter than the list of corresponding documents
// */
//
// for (int j = 0, k = 0; j < batchList.size() && k < cus.size(); j++) {
// final IDocument doc = batchList.get(j);
// List<ReconcileProblem> problems = new ArrayList<>();
// CompilationUnit cu = cus.get(k);
// Path sourcePath = Paths.get(URI.create(doc.getUri()));
// if (cu.getSourcePath().equals(sourcePath)) {
// k++;
// collectProblems(project, descriptors, doc, cu, problems::add);
// if (!problems.isEmpty()) {
// allProblems.put(doc, problems);
// }
// } else {
// log.warn("(OpenRewrite) Failed to parse source for " + sourcePath);
// }
// incrementProgress.run();
// }
//
// }
// }
// } catch (Exception e) {
// if (ORAstUtils.isExceptionFromInterrupedThread(e)) {
// log.debug("", e);
// } else {
// log.error("", e);
// }
// }
// }
// return allProblems;
// }
//
// private List<RecipeCodeActionDescriptor> getProblemRecipeDescriptors(IJavaProject project)
// throws InterruptedException, ExecutionException {
// return recipeRepo.getProblemRecipeDescriptors().stream().filter(d -> d.getProblemType() != null).filter(d -> {
// switch (config.getProblemApplicability(d.getProblemType())) {
// case ON:
// return SpringProjectUtil.isBootProject(project);
// case OFF:
// return false;
// default: // AUTO
// return d.isApplicable(project);
// }
// }).collect(Collectors.toList());
// }
//
// private void collectProblems(IJavaProject project, List<RecipeCodeActionDescriptor> descriptors, IDocument doc, CompilationUnit compilationUnit, Consumer<ReconcileProblem> problemHandler) {
// CompilationUnit cu = recipeRepo.mark(project, descriptors, compilationUnit);
// if (compilationUnit != cu) {
// new JavaMarkerVisitor<ExecutionContext>() {
//
// @Override
// public J visit(Tree tree, ExecutionContext context) {
// J t = super.visit(tree, context);
// if (t instanceof J) {
// for (Marker m : t.getMarkers().entries()) {
// if (m instanceof FixAssistMarker) {
// for (ReconcileProblem problem : createProblems(doc, (FixAssistMarker) m, t)) {
// problemHandler.accept(problem);
// }
// }
// }
// }
// return t;
// }
//
// }.visit(cu, new InMemoryExecutionContext(e -> log.error("", e)));
// }
// }
//
// public int getTotalWorkUnits(List<TextDocument> docs) {
// return docs.size() * 2;
// }
}

View File

@@ -18,6 +18,7 @@ import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.CodeAction;
@@ -32,7 +33,6 @@ import org.openrewrite.Recipe;
import org.openrewrite.RecipeRun;
import org.openrewrite.Result;
import org.openrewrite.SourceFile;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.openrewrite.java.JavaParser;
@@ -59,6 +59,8 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import reactor.core.publisher.Mono;
public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler {
public static final String REWRITE_RECIPE_QUICKFIX = "org.openrewrite.rewrite";
@@ -89,29 +91,27 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
}
@Override
public QuickfixEdit createEdits(Object p) {
public Mono<QuickfixEdit> createEdits(Object p) {
if (p instanceof JsonElement) {
return new QuickfixEdit(createEdit((JsonElement) p), null);
return Mono.fromFuture(createEdit((JsonElement) p).thenApply(we -> new QuickfixEdit(we, null)));
}
return null;
}
@Override
public void resolve(CodeAction codeAction) {
public CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction) {
if (codeAction.getData() instanceof JsonElement) {
try {
WorkspaceEdit edit = createEdit((JsonElement) codeAction.getData());
if (edit != null) {
codeAction.setEdit(edit);
}
return createEdit((JsonElement) codeAction.getData());
} catch (Exception e) {
log.error("", e);
}
}
return CompletableFuture.completedFuture(null);
}
public WorkspaceEdit createEdit(JsonElement o) {
public CompletableFuture<WorkspaceEdit> createEdit(JsonElement o) {
FixDescriptor data = gson.fromJson(o, FixDescriptor.class);
if (data != null && data.getRecipeId() != null) {
return perform(data);
@@ -139,69 +139,84 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
return workspaceEdit;
}
private WorkspaceEdit perform(FixDescriptor data) {
private CompletableFuture<WorkspaceEdit> perform(FixDescriptor data) {
Optional<IJavaProject> project = projectFinder.find(new TextDocumentIdentifier(data.getDocUris().get(0)));
if (project.isPresent()) {
boolean projectWide = data.getRecipeScope() == RecipeScope.PROJECT;
Recipe r = createRecipe(data);
if (r == null) {
log.warn("Code Action failed to resolve. Could not create recipe created with id '" + data.getRecipeId() + "'.");
}
List<CompilationUnit> cus = Collections.emptyList();
if (projectWide) {
JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build();
List<Input> inputs = ORAstUtils.getParserInputs(server.getTextDocumentService(), project.get());
PercentageProgressTask progress = server.getProgressService().createPercentageProgressTask(UUID.randomUUID().toString(), inputs.size() + 1, data.getLabel());
try {
cus = ORAstUtils.parseInputs(jp, inputs, s -> progress.increment());
return applyRecipe(r, project.get(), cus);
} finally {
progress.setCurrent(progress.getTotal());
progress.done();
return createRecipe(data).thenApply(r -> {
if (r == null) {
log.warn("Code Action failed to resolve. Could not create recipe created with id '" + data.getRecipeId() + "'.");
}
} else {
JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build();
List<Input> inputs = data.getDocUris().stream().map(URI::create).map(Paths::get).map(p -> ORAstUtils.getParserInput(server.getTextDocumentService(), p)).collect(Collectors.toList());
cus = ORAstUtils.parseInputs(jp, inputs, null);
return applyRecipe(r, project.get(), cus);
}
List<CompilationUnit> cus = Collections.emptyList();
if (projectWide) {
JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build();
List<Input> inputs = ORAstUtils.getParserInputs(server.getTextDocumentService(), project.get());
PercentageProgressTask progress = server.getProgressService().createPercentageProgressTask(UUID.randomUUID().toString(), inputs.size() + 1, data.getLabel());
try {
cus = ORAstUtils.parseInputs(jp, inputs, s -> progress.increment());
return applyRecipe(r, project.get(), cus);
} finally {
progress.setCurrent(progress.getTotal());
progress.done();
}
} else {
JavaParser jp = ORAstUtils.createJavaParserBuilder(project.get()).dependsOn(data.getTypeStubs()).build();
List<Input> inputs = data.getDocUris().stream().map(URI::create).map(Paths::get).map(p -> ORAstUtils.getParserInput(server.getTextDocumentService(), p)).collect(Collectors.toList());
cus = ORAstUtils.parseInputs(jp, inputs, null);
return applyRecipe(r, project.get(), cus);
}
});
}
return null;
return CompletableFuture.completedFuture(null);
}
private CompletableFuture<Optional<Class<?>>> findRecipeClass(String className) {
return CompletableFuture.supplyAsync(() -> {
try {
Optional<Class<?>> opt = Optional.of(getClass().getClassLoader().loadClass(className));
return opt;
} catch (Exception e) {
// ignore
log.info("Didn't find the recipe class '%s' trying recipe repository".formatted(className));
return Optional.empty();
}
});
}
private Recipe createRecipe(FixDescriptor d) {
Recipe r = recipeRepo.getRecipe(d.getRecipeId()).orElse(null);
if (!(r instanceof DeclarativeRecipe)) {
r = RecipeIntrospectionUtils.constructRecipe(r.getClass());
}
if (d.getParameters() != null) {
for (Entry<String, Object> entry : d.getParameters().entrySet()) {
try {
Field f = r.getClass().getDeclaredField(entry.getKey());
f.setAccessible(true);
f.set(r, entry.getValue());
} catch (Exception e) {
log.error("", e);;
private CompletableFuture<Recipe> createRecipe(FixDescriptor d) {
return findRecipeClass(d.getRecipeId())
.thenCompose(optRecipeClass -> optRecipeClass
.map(recipeClass -> CompletableFuture.completedFuture(RecipeIntrospectionUtils.constructRecipe(recipeClass)))
.orElseGet(() -> recipeRepo.getRecipe(d.getRecipeId()).thenApply(opt -> opt.orElseThrow())))
.thenApply(r -> {
if (d.getParameters() != null) {
for (Entry<String, Object> entry : d.getParameters().entrySet()) {
try {
Field f = r.getClass().getDeclaredField(entry.getKey());
f.setAccessible(true);
f.set(r, entry.getValue());
} catch (Exception e) {
log.error("", e);;
}
}
}
}
if (d.getRecipeScope() == RecipeScope.NODE) {
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
} else {
r = ORAstUtils.nodeRecipe(r, j -> {
if (j != null) {
Range range = j.getMarkers().findFirst(Range.class).orElse(null);
if (range != null) {
// Rewrite range end offset is up to not including hence -1
return d.getRangeScope().getStart().getOffset() <= range.getStart().getOffset() && range.getEnd().getOffset() - 1 <= d.getRangeScope().getEnd().getOffset();
}
}
return false;
});
if (d.getRecipeScope() == RecipeScope.NODE) {
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
} else {
r = ORAstUtils.nodeRecipe(r, j -> {
if (j != null) {
Range range = j.getMarkers().findFirst(Range.class).orElse(null);
if (range != null) {
// Rewrite range end offset is up to not including hence -1
return d.getRangeScope().getStart().getOffset() <= range.getStart().getOffset() && range.getEnd().getOffset() - 1 <= d.getRangeScope().getEnd().getOffset();
}
}
return false;
});
}
}
}
return r;
return r;
});
}
}

View File

@@ -49,11 +49,7 @@ public class SpringBootUpgrade {
"3.1", "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1"
);
private RewriteRecipeRepository recipeRepo;
public SpringBootUpgrade(SimpleLanguageServer server, RewriteRecipeRepository recipeRepo, JavaProjectFinder projectFinder) {
this.recipeRepo = recipeRepo;
server.onCommand(CMD_UPGRADE_SPRING_BOOT, params -> {
String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
Assert.isLegal(uri != null, "Project URI parameter must not be 'null'");
@@ -73,8 +69,8 @@ public class SpringBootUpgrade {
+ version.toMajorMinorVersionStr() + "' is newer or same as the target version '"
+ targetVersion.toMajorMinorVersionStr() + "'");
return recipeRepo.loaded.thenComposeAsync(load -> recipeRepo.apply(
createUpgradeRecipe(version, targetVersion),
return recipeRepo.recipes().thenComposeAsync(recipes -> recipeRepo.apply(
createUpgradeRecipe(recipes, version, targetVersion),
uri,
UUID.randomUUID().toString()
));
@@ -96,7 +92,7 @@ public class SpringBootUpgrade {
return ids;
}
private Recipe createUpgradeRecipe(Version version, Version targetVersion) {
private Recipe createUpgradeRecipe(Map<String, Recipe> recipes, Version version, Version targetVersion) {
Recipe recipe = new DeclarativeRecipe("upgrade-spring-boot", "Upgrade Spring Boot from " + version + " to " + targetVersion,
"", Collections.emptySet(), null, null, false, Collections.emptyList());
@@ -108,7 +104,7 @@ public class SpringBootUpgrade {
List<String> recipedIds = createRecipeIdsChain(version.getMajor(), version.getMinor() + 1, targetVersion.getMajor(), targetVersion.getMinor(), versionsToRecipeId);
if (!recipedIds.isEmpty()) {
String recipeId = recipedIds.get(recipedIds.size() - 1);
getRecipeFromId(recipeId).ifPresent(r -> recipe.getRecipeList().add(r));
Optional.ofNullable(recipes.get(recipeId)).ifPresent(r -> recipe.getRecipeList().add(r));
}
}
@@ -121,10 +117,6 @@ public class SpringBootUpgrade {
}
}
private Optional<Recipe> getRecipeFromId(String recipeId) {
return recipeRepo.getRecipe(recipeId);
}
private static String createVersionString(int major, int minor) {
StringBuilder sb = new StringBuilder();
sb.append(major);

View File

@@ -254,7 +254,8 @@ public class SpringIndexerJava implements SpringIndexer {
}
private void scanFile(IJavaProject project, DocumentDescriptor updatedDoc, String content) throws Exception {
ASTParser parser = createParser(project, false);
final boolean ignoreMethodBodies = false;
ASTParser parser = createParser(project, ignoreMethodBodies);
String docURI = updatedDoc.getDocURI();
long lastModified = updatedDoc.getLastModified();
@@ -289,7 +290,7 @@ public class SpringIndexerJava implements SpringIndexer {
IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator);
SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file,
lastModified, docRef, content, generatedSymbols, generatedBeans, problemCollector, SCAN_PASS.ONE, new ArrayList<>());
lastModified, docRef, content, generatedSymbols, generatedBeans, problemCollector, SCAN_PASS.ONE, new ArrayList<>(), !ignoreMethodBodies);
scanAST(context);
@@ -316,7 +317,8 @@ public class SpringIndexerJava implements SpringIndexer {
}
public List<EnhancedSymbolInformation> computeSymbols(IJavaProject project, String docURI, String content) throws Exception {
ASTParser parser = createParser(project, false);
final boolean ignoreMethodBodies = false;
ASTParser parser = createParser(project, ignoreMethodBodies);
if (content != null) {
String unitName = docURI.substring(docURI.lastIndexOf("/"));
@@ -347,7 +349,7 @@ public class SpringIndexerJava implements SpringIndexer {
AtomicReference<TextDocument> docRef = new AtomicReference<>();
String file = UriUtil.toFileString(docURI);
SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file,
0, docRef, content, generatedSymbols, generatedBeans, voidProblemCollector, SCAN_PASS.ONE, new ArrayList<>());
0, docRef, content, generatedSymbols, generatedBeans, voidProblemCollector, SCAN_PASS.ONE, new ArrayList<>(), !ignoreMethodBodies);
scanAST(context);
@@ -359,7 +361,8 @@ public class SpringIndexerJava implements SpringIndexer {
}
private Set<String> scanFilesInternally(IJavaProject project, DocumentDescriptor[] docs) throws Exception {
ASTParser parser = createParser(project, false);
final boolean ignoreMethodBodies = false;
ASTParser parser = createParser(project, ignoreMethodBodies);
// this is to keep track of already scanned files to avoid endless loops due to circular dependencies
Set<String> scannedTypes = new HashSet<>();
@@ -404,7 +407,7 @@ public class SpringIndexerJava implements SpringIndexer {
IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator);
SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath,
lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, SCAN_PASS.ONE, new ArrayList<>());
lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, SCAN_PASS.ONE, new ArrayList<>(), !ignoreMethodBodies);
scanAST(context);
@@ -537,7 +540,8 @@ public class SpringIndexerJava implements SpringIndexer {
javaFiles.length, "Spring Tools: Indexing Java Sources for '" + project.getElementName() + "'");
try {
ASTParser parser = createParser(project, SCAN_PASS.ONE.equals(pass));
final boolean ignoreMethodBodies = SCAN_PASS.ONE.equals(pass);
ASTParser parser = createParser(project, ignoreMethodBodies);
List<String> nextPassFiles = new ArrayList<>();
FileASTRequestor requestor = new FileASTRequestor() {
@@ -552,7 +556,7 @@ public class SpringIndexerJava implements SpringIndexer {
IProblemCollector problemCollector = problemCollectorCreator.apply(docRef, diagnosticsAggregator);
SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath,
lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, pass, nextPassFiles);
lastModified, docRef, null, generatedSymbols, generatedBeans, problemCollector, pass, nextPassFiles, !ignoreMethodBodies);
scanAST(context);
progressTask.increment();
@@ -674,7 +678,7 @@ public class SpringIndexerJava implements SpringIndexer {
try {
problemCollector.beginCollecting();
reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.getPass() == SCAN_PASS.TWO);
reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.isFullAst());
problemCollector.endCollecting();
} catch (RequiredCompleteAstException e) {
if (context.getPass() == SCAN_PASS.TWO) {

View File

@@ -40,6 +40,7 @@ public class SpringIndexerJavaContext {
private final IProblemCollector getProblemCollector;
private final SCAN_PASS pass;
private final List<String> nextPassFiles;
private final boolean fullAst;
private final Set<String> dependencies = new HashSet<>();
private final Set<String> scannedTypes = new HashSet<>();
@@ -57,7 +58,8 @@ public class SpringIndexerJavaContext {
List<CachedBean> beans,
IProblemCollector problemCollector,
SCAN_PASS pass,
List<String> nextPassFiles
List<String> nextPassFiles,
boolean fullAst
) {
super();
this.project = project;
@@ -72,6 +74,7 @@ public class SpringIndexerJavaContext {
this.beans = beans;
this.pass = pass;
this.nextPassFiles = nextPassFiles;
this.fullAst = fullAst;
}
public IJavaProject getProject() {
@@ -148,4 +151,7 @@ public class SpringIndexerJavaContext {
return this.getProblemCollector;
}
public boolean isFullAst() {
return fullAst;
}
}

View File

@@ -25,6 +25,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import reactor.core.publisher.Mono;
/**
* Boot app Properties file quick fix handlers
*
@@ -47,7 +49,7 @@ public class AppPropertiesQuickFixes {
public AppPropertiesQuickFixes(QuickfixRegistry r, CommonQuickfixes commonFixes) {
MISSING_PROPERTY = commonFixes.MISSING_PROPERTY;
DEPRECATED_PROPERTY = r.register("DEPRECATED_PROPERTY", (Object _params) -> {
DEPRECATED_PROPERTY = r.register("DEPRECATED_PROPERTY", (Object _params) -> Mono.fromSupplier(() -> {
DeprecatedPropertyData params = gson.fromJson((JsonElement)_params, DeprecatedPropertyData.class);
try {
if (params.getRange() != null && params.getReplacement() != null) {
@@ -64,7 +66,7 @@ public class AppPropertiesQuickFixes {
log.error("", e);
}
return NULL_FIX;
});
}));
}
}

View File

@@ -47,6 +47,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import reactor.core.publisher.Mono;
/**
* Common quick fixes for YAML and properties
*
@@ -96,7 +98,7 @@ public class CommonQuickfixes {
}
if (clientCapabilities.getWorkspace().getWorkspaceEdit() != null && clientCapabilities.getWorkspace().getWorkspaceEdit().getResourceOperations() != null && clientCapabilities.getWorkspace().getWorkspaceEdit().getResourceOperations().contains(ResourceOperationKind.Create)
&& Boolean.TRUE.equals(clientCapabilities.getWorkspace().getWorkspaceEdit().getDocumentChanges())) {
MISSING_PROPERTY = r.register(MISSING_PROPERTY_APP_QF_ID, (Object _params) -> {
MISSING_PROPERTY = r.register(MISSING_PROPERTY_APP_QF_ID, (Object _params) -> Mono.fromSupplier(() -> {
MissingPropertyData params = gson.fromJson((JsonElement)_params, MissingPropertyData.class);
try {
Optional<IJavaProject> p = projectFinder.find(params.getDoc());
@@ -138,7 +140,7 @@ public class CommonQuickfixes {
log.error("", e);
}
return NULL_FIX;
});
}));
} else {
MISSING_PROPERTY = null;
}

View File

@@ -38,6 +38,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import reactor.core.publisher.Mono;
/**
* Boot YAML file properties quick fix code action handlers
*
@@ -60,7 +62,7 @@ public class AppYamlQuickfixes {
public AppYamlQuickfixes(QuickfixRegistry r, SimpleTextDocumentService textDocumentService, YamlStructureProvider structureProvider, CommonQuickfixes commonQuickfixes) {
MISSING_PROPERTY = commonQuickfixes.MISSING_PROPERTY;
DEPRECATED_PROPERTY = r.register("DEPRECATED_YAML_PROPERTY", (Object _params) -> {
DEPRECATED_PROPERTY = r.register("DEPRECATED_YAML_PROPERTY", (Object _params) -> Mono.fromSupplier(() -> {
DeprecatedPropertyData params = gson.fromJson((JsonElement)_params, DeprecatedPropertyData.class);
try {
TextDocument _doc = textDocumentService.getLatestSnapshot(params.getUri());
@@ -112,7 +114,7 @@ public class AppYamlQuickfixes {
log.error("", e);
}
return NULL_FIX;
});
}));
}
}

View File

@@ -161,7 +161,7 @@ public class ValueSpelExpressionValidationTest {
new JdtReconciler(compilationUnitCache, config, new JdtAstReconciler[] {
new AnnotationNodeReconciler(config)
})
}, server);
});
}
@AfterEach