io.codemodder.DefaultCodemodExecutor Maven / Gradle / Ivy
package io.codemodder;
import static java.util.Collections.emptyMap;
import com.github.difflib.DiffUtils;
import com.github.difflib.UnifiedDiffUtils;
import io.codemodder.codetf.CodeTFChange;
import io.codemodder.codetf.CodeTFChangesetEntry;
import io.codemodder.codetf.CodeTFPackageAction;
import io.codemodder.codetf.CodeTFResult;
import io.codemodder.javaparser.CachingJavaParser;
import io.codemodder.javaparser.JavaParserChanger;
import io.codemodder.javaparser.JavaParserCodemodRunner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
final class DefaultCodemodExecutor implements CodemodExecutor {
private final CodemodIdPair codemod;
private final List projectProviders;
private final List codetfProviders;
private final CachingJavaParser cachingJavaParser;
private final Path projectDir;
private final IncludesExcludes includesExcludes;
private final EncodingDetector encodingDetector;
DefaultCodemodExecutor(
final Path projectDir,
final IncludesExcludes includesExcludes,
final CodemodIdPair codemod,
final List projectProviders,
final List codetfProviders,
final CachingJavaParser cachingJavaParser,
final EncodingDetector encodingDetector) {
this.projectDir = Objects.requireNonNull(projectDir);
this.includesExcludes = Objects.requireNonNull(includesExcludes);
this.codemod = Objects.requireNonNull(codemod);
this.codetfProviders = Objects.requireNonNull(codetfProviders);
this.projectProviders = Objects.requireNonNull(projectProviders);
this.cachingJavaParser = Objects.requireNonNull(cachingJavaParser);
this.encodingDetector = Objects.requireNonNull(encodingDetector);
}
@Override
public CodeTFResult execute(final List filePaths) {
List changeset = new ArrayList<>();
Set unscannableFiles = new HashSet<>();
DefaultCodeDirectory codeDirectory = new DefaultCodeDirectory(projectDir);
CodeChanger codeChanger = codemod.getChanger();
/*
* Create the right CodemodRunner based on the type of CodeChanger.
*/
CodemodRunner codemodRunner;
if (codeChanger instanceof JavaParserChanger) {
codemodRunner =
new JavaParserCodemodRunner(
cachingJavaParser, (JavaParserChanger) codeChanger, encodingDetector);
} else if (codeChanger instanceof RawFileChanger) {
codemodRunner = new RawFileCodemodRunner((RawFileChanger) codeChanger);
} else {
throw new UnsupportedOperationException(
"unsupported codeChanger type: " + codeChanger.getClass().getName());
}
/*
* Filter the files to those that the CodemodRunner supports.
*/
List codemodTargetFiles =
filePaths.stream().filter(codemodRunner::supports).sorted().toList();
for (Path filePath : codemodTargetFiles) {
// create the context necessary for the codemod to run
LineIncludesExcludes lineIncludesExcludes =
includesExcludes.getIncludesExcludesForFile(filePath.toFile());
CodemodInvocationContext context =
new DefaultCodemodInvocationContext(
codeDirectory, filePath, codemod.getId(), lineIncludesExcludes);
try {
// capture the "before" for the diff, if needed
List beforeFile = Files.readAllLines(filePath);
// run the codemod on the file
List codemodChanges = codemodRunner.run(context);
if (!codemodChanges.isEmpty()) {
// update the dependencies in the manifest file if needed
List dependencies =
codemodChanges.stream()
.map(CodemodChange::getDependenciesNeeded)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
List pkgActions;
List dependencyChangesetEntries = Collections.emptyList();
if (!dependencies.isEmpty()) {
CodemodPackageUpdateResult packageAddResult = addPackages(filePath, dependencies);
unscannableFiles.addAll(packageAddResult.filesFailedToChange());
pkgActions = packageAddResult.packageActions();
dependencyChangesetEntries = packageAddResult.manifestChanges();
} else {
pkgActions = Collections.emptyList();
}
// record the change for the file
List changes =
codemodChanges.stream()
.map(
change ->
translateCodemodChangetoCodeTFChange(
codeChanger, filePath, change, pkgActions))
.collect(Collectors.toList());
// make sure we add the file's entry first, then the dependency entries, so the causality
// is clear
List afterFile = Files.readAllLines(filePath);
List patchDiff =
UnifiedDiffUtils.generateUnifiedDiff(
filePath.getFileName().toString(),
filePath.getFileName().toString(),
beforeFile,
DiffUtils.diff(beforeFile, afterFile),
3);
String diff = String.join("\n", patchDiff);
changeset.add(
new CodeTFChangesetEntry(getRelativePath(projectDir, filePath), diff, changes));
changeset.addAll(dependencyChangesetEntries);
}
} catch (Exception e) {
unscannableFiles.add(filePath);
e.printStackTrace();
}
}
CodeTFResult result =
new CodeTFResult(
codemod.getId(),
codeChanger.getSummary(),
codeChanger.getDescription(),
unscannableFiles.stream()
.map(file -> getRelativePath(projectDir, file))
.collect(Collectors.toSet()),
codeChanger.getReferences(),
emptyMap(),
changeset);
for (CodeTFProvider provider : codetfProviders) {
result = provider.onResultCreated(result);
}
return result;
}
@NotNull
private CodeTFChange translateCodemodChangetoCodeTFChange(
final CodeChanger codeChanger,
final Path filePath,
final CodemodChange codemodChange,
final List pkgActions) {
Optional customizedChangeDescription = codemodChange.getDescription();
String changeDescription =
customizedChangeDescription.orElse(
codeChanger.getIndividualChangeDescription(filePath, codemodChange));
CodeTFChange change =
new CodeTFChange(
codemodChange.lineNumber(),
emptyMap(),
changeDescription,
pkgActions,
codeChanger.getSourceControlUrl().orElse(null),
codemodChange.getParameters());
for (CodeTFProvider provider : codetfProviders) {
change = provider.onChangeCreated(filePath, codemod.getId(), change);
}
return change;
}
/**
* After updating the files, this method asks the project providers to apply any corrective
* changers to the project as a whole regarding the dependencies. This is useful for things like
* adding a dependency to the project's pom.xml file. Eventually we want support other operations
* besides "add" (like, obviously, "remove")
*/
private CodemodPackageUpdateResult addPackages(
final Path file, final List dependencies) throws IOException {
List pkgActions = new ArrayList<>();
Set unscannableFiles = new HashSet<>();
List skippedDependencies = new ArrayList<>();
List pkgChanges = new ArrayList<>();
for (ProjectProvider projectProvider : projectProviders) {
DependencyUpdateResult result =
projectProvider.updateDependencies(projectDir, file, dependencies);
unscannableFiles.addAll(result.erroredFiles().stream().map(Path::toAbsolutePath).toList());
pkgChanges.addAll(result.packageChanges());
for (DependencyGAV dependency : result.injectedPackages()) {
String packageUrl = toPackageUrl(dependency);
pkgActions.add(
new CodeTFPackageAction(
CodeTFPackageAction.CodeTFPackageActionType.ADD,
CodeTFPackageAction.CodeTFPackageActionResult.COMPLETED,
packageUrl));
}
for (DependencyGAV dependency : result.skippedPackages()) {
String packageUrl = toPackageUrl(dependency);
skippedDependencies.add(dependency);
pkgActions.add(
new CodeTFPackageAction(
CodeTFPackageAction.CodeTFPackageActionType.ADD,
CodeTFPackageAction.CodeTFPackageActionResult.SKIPPED,
packageUrl));
}
dependencies.removeAll(new HashSet<>(result.injectedPackages()));
}
dependencies.stream()
.filter(d -> !skippedDependencies.contains(d))
.forEach(
dep -> {
pkgActions.add(
new CodeTFPackageAction(
CodeTFPackageAction.CodeTFPackageActionType.ADD,
CodeTFPackageAction.CodeTFPackageActionResult.FAILED,
toPackageUrl(dep)));
});
return CodemodPackageUpdateResult.from(pkgActions, pkgChanges, unscannableFiles);
}
@VisibleForTesting
static String toPackageUrl(DependencyGAV dependency) {
return "pkg:maven/"
+ dependency.group()
+ "/"
+ dependency.artifact()
+ "@"
+ dependency.version();
}
/** Return the relative path name (e.g., src/test/foo) of a file within the project dir. */
private String getRelativePath(final Path projectDir, final Path filePath) {
String path = projectDir.relativize(filePath).toString();
if (path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy