org.openrewrite.dotnet.UpgradeAssistantRecipe Maven / Gradle / Ivy
Show all versions of rewrite-dotnet Show documentation
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.dotnet;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.quark.Quark;
import org.openrewrite.scheduling.WorkingDirectoryExecutionContextView;
import org.openrewrite.text.PlainText;
import org.openrewrite.tree.ParseError;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.TimeUnit;
abstract class UpgradeAssistantRecipe extends ScanningRecipe {
private static final String FIRST_RECIPE = UpgradeAssistantRecipe.class.getName() + ".FIRST_RECIPE";
private static final String PREVIOUS_RECIPE = UpgradeAssistantRecipe.class.getName() + ".PREVIOUS_RECIPE";
private static final String INIT_REPO_DIR = UpgradeAssistantRecipe.class.getName() + ".INIT_REPO_DIR";
private static final String UPGRADE_ASSISTANT = "upgrade-assistant";
protected static final String DOTNET_HOME = System.getProperty("user.home") + File.separator + ".dotnet";
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
Path directory = createDirectory(ctx);
if (ctx.getMessage(INIT_REPO_DIR) == null) {
ctx.putMessage(INIT_REPO_DIR, directory);
ctx.putMessage(FIRST_RECIPE, ctx.getCycleDetails().getRecipePosition());
}
return new Accumulator(directory);
}
@Override
public TreeVisitor, ExecutionContext> getScanner(Accumulator acc) {
return new TreeVisitor() {
@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof SourceFile && !(tree instanceof Quark) && !(tree instanceof ParseError)) {
SourceFile sourceFile = (SourceFile) tree;
// Only extract initial source files for first upgrade-assistant recipe
if (Objects.equals(ctx.getMessage(FIRST_RECIPE), ctx.getCycleDetails().getRecipePosition())) {
acc.writeSource(sourceFile);
}
}
return tree;
}
};
}
@Override
public Collection extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
Path previous = ctx.getMessage(PREVIOUS_RECIPE);
if (previous != null &&
!Objects.equals(ctx.getMessage(FIRST_RECIPE), ctx.getCycleDetails().getRecipePosition())) {
acc.copyFromPrevious(previous);
}
if (ctx.getCycle() == 1) {
// upgrade-assistant run more than once on a project will log an "Unknown target framework" message
runUpgradeAssistant(acc, ctx);
}
ctx.putMessage(PREVIOUS_RECIPE, acc.getDirectory());
return Collections.emptyList();
}
abstract public void runUpgradeAssistant(Accumulator acc, ExecutionContext ctx);
protected void execUpgradeAssistant(Path inputFile, Accumulator acc, ExecutionContext ctx) {
List command = buildUpgradeAssistantCommand(acc, ctx, inputFile);
Path out = null;
Path err = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(command);
builder.directory(acc.getDirectory().toFile());
Map env = buildUpgradeAssistantEnv();
env.forEach(builder.environment()::put);
out = Files.createTempFile(
WorkingDirectoryExecutionContextView.view(ctx).getWorkingDirectory(),
UPGRADE_ASSISTANT,
null);
err = Files.createTempFile(
WorkingDirectoryExecutionContextView.view(ctx).getWorkingDirectory(),
UPGRADE_ASSISTANT,
null);
builder.redirectOutput(ProcessBuilder.Redirect.to(out.toFile()));
builder.redirectError(ProcessBuilder.Redirect.to(err.toFile()));
Process process = builder.start();
process.waitFor(20, TimeUnit.MINUTES);
if (process.exitValue() != 0) {
String error = "Command failed: " + String.join(" ", command);
if (Files.exists(err)) {
error += "\n" + new String(Files.readAllBytes(err));
}
throw new RuntimeException(error);
} else {
for (Map.Entry entry : acc.beforeModificationTimestamps.entrySet()) {
Path path = entry.getKey();
if (!Files.exists(path) || Files.getLastModifiedTime(path).toMillis() > entry.getValue()) {
acc.addModifiedFile(path);
}
}
processOutput(inputFile, out, acc);
}
} catch (
IOException e) {
throw new UncheckedIOException(e);
} catch (
InterruptedException e) {
throw new RuntimeException(e);
} finally {
deleteFile(out);
deleteFile(err);
}
}
private Map buildUpgradeAssistantEnv() {
Map env = new HashMap<>();
env.put("TERM", "dumb");
String path = System.getenv("PATH");
// This is required to find .NET SDKs
env.put("PATH", path + File.pathSeparator + DOTNET_HOME);
return env;
}
protected void deleteFile(@Nullable Path path) {
if (path != null) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
// FIXME recipe logger?
}
}
}
abstract public List buildUpgradeAssistantCommand(Accumulator acc, ExecutionContext ctx, Path projectFile);
protected Path getUpgradeAssistantPath() {
String cmdName = UPGRADE_ASSISTANT;
if (System.getProperty("os.name").contains("Windows")) {
cmdName += ".exe";
}
// Look for upgrade-assistant in conventional installation locations
Path cmdPath = Paths.get(DOTNET_HOME).resolve("tools").resolve(cmdName);
if (Files.exists(cmdPath)) {
return cmdPath;
}
for (String path : System.getenv("PATH").split(File.pathSeparator)) {
cmdPath = Paths.get(path).resolve(cmdName);
if (Files.exists(cmdPath)) {
return cmdPath;
}
}
throw new IllegalStateException("Unable to find " + cmdName + " on PATH");
}
abstract void processOutput(Path inputFile, Path output, Accumulator acc);
@Override
public TreeVisitor, ExecutionContext> getVisitor(Accumulator acc) {
return new TreeVisitor() {
@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof SourceFile) {
SourceFile sourceFile = (SourceFile) tree;
return createAfter(sourceFile, acc, ctx);
}
return tree;
}
};
}
protected SourceFile createAfter(SourceFile before, Accumulator acc, ExecutionContext ctx) {
String error = acc.getFileError(acc.resolvedPath(before));
if (error != null) {
throw new RecipeException(error);
}
if (!acc.wasModified(before)) {
return before;
}
return new PlainText(
before.getId(),
before.getSourcePath(),
before.getMarkers(),
Optional.ofNullable(before.getCharset()).map(Charset::name).orElse(null),
before.isCharsetBomMarked(),
before.getFileAttributes(),
null,
acc.content(before),
Collections.emptyList());
}
private static Path createDirectory(ExecutionContext ctx) {
WorkingDirectoryExecutionContextView view = WorkingDirectoryExecutionContextView.view(ctx);
return Optional.of(view.getWorkingDirectory()).map(d -> d.resolve("repo")).map(d -> {
try {
return Files.createDirectory(d).toRealPath();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}).orElseThrow(() -> new IllegalStateException("Failed to create working directory for repo"));
}
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public static class Accumulator {
@Getter
private final Path directory;
private final Map> fileResults = new HashMap<>();
private final Map fileErrors = new HashMap<>();
@Getter
private final Map beforeModificationTimestamps = new HashMap<>();
private final Set modified = new LinkedHashSet<>();
@Getter
private final List projectFiles = new ArrayList<>();
@Getter
private final List solutionFiles = new ArrayList<>();
private final Map rules = new HashMap<>();
private void copyFromPrevious(Path previous) {
try {
Files.walkFileTree(previous, new SimpleFileVisitor() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path target = directory.resolve(previous.relativize(dir));
if (!target.equals(directory)) {
Files.createDirectory(target);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
try {
Path target = directory.resolve(previous.relativize(file));
Files.copy(file, target);
beforeModificationTimestamps.put(target, Files.getLastModifiedTime(target).toMillis());
} catch (NoSuchFileException ignore) {
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void writeSource(SourceFile sourceFile) {
try {
Path path = resolvedPath(sourceFile);
Files.createDirectories(path.getParent());
PrintOutputCapture.MarkerPrinter markerPrinter = new PrintOutputCapture.MarkerPrinter() {
};
Path written = Files.write(
path,
sourceFile.printAll(new PrintOutputCapture<>(0, markerPrinter))
.getBytes(Optional.ofNullable(sourceFile.getCharset()).orElse(StandardCharsets.UTF_8)));
beforeModificationTimestamps.put(written, Files.getLastModifiedTime(written).toMillis());
String pathString = written.toString();
if (isProjectFile(pathString)) {
projectFiles.add(written);
} else if (isSolutionFile(pathString)) {
solutionFiles.add(written);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private boolean isProjectFile(String pathString) {
return pathString.endsWith(".csproj") || pathString.endsWith(".vbproj") || pathString.endsWith(".fsproj");
}
private boolean isSolutionFile(String pathString) {
return pathString.endsWith(".sln");
}
private void addModifiedFile(Path path) {
modified.add(path);
}
private boolean wasModified(SourceFile tree) {
return modified.contains(resolvedPath(tree));
}
public String content(SourceFile tree) {
try {
Path path = resolvedPath(tree);
return tree.getCharset() != null ?
new String(Files.readAllBytes(path), tree.getCharset()) :
new String(Files.readAllBytes(path));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public Path resolvedPath(SourceFile tree) {
return directory.resolve(tree.getSourcePath());
}
public void addFileResult(Path path, JsonNode jsonNode) {
fileResults.computeIfAbsent(path, key -> new ArrayList<>()).add(jsonNode);
}
public @Nullable List getFileResults(Path path) {
return fileResults.get(path);
}
public void addFileError(Path path, String error) {
fileErrors.put(path, error);
}
public @Nullable String getFileError(Path path) {
return fileErrors.get(path);
}
public void addRule(String ruleId, JsonNode jsonNode) {
rules.put(ruleId, jsonNode);
}
public String getRuleLabel(String ruleId) {
return rules.get(ruleId).get("label").asText();
}
}
}