Skip to content

Commit

Permalink
Merge pull request #118 from alfasoftware/chiknas
Browse files Browse the repository at this point in the history
UpdateTypeRefactor to move source code files
  • Loading branch information
RadikalJin authored Apr 15, 2024
2 parents 1853484 + c19aebc commit 9815d3a
Show file tree
Hide file tree
Showing 20 changed files with 243 additions and 127 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.alfasoftware.astra.core.refactoring.operations.javapattern;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import org.alfasoftware.astra.core.utils.ASTOperation;
Expand Down Expand Up @@ -33,7 +33,7 @@ public class JavaPatternASTOperation implements ASTOperation {
* @param refactorFile the reference to the Matcher file to use
* @throws IOException
*/
public JavaPatternASTOperation(File refactorFile) throws IOException {
public JavaPatternASTOperation(Path refactorFile) throws IOException {
javaPatternFileParser = new JavaPatternFileParser();
javaPatternFileParser.buildMatchers(refactorFile);
}
Expand All @@ -45,7 +45,7 @@ public JavaPatternASTOperation(File refactorFile) throws IOException {
* @param sources the sources to use to resolve bindings
* @throws IOException
*/
public JavaPatternASTOperation(File refactorFile, String[] sources) throws IOException {
public JavaPatternASTOperation(Path refactorFile, String[] sources) throws IOException {
javaPatternFileParser = new JavaPatternFileParser();
javaPatternFileParser.buildMatchersWithSources(refactorFile, sources);
}
Expand All @@ -59,7 +59,7 @@ public JavaPatternASTOperation(File refactorFile, String[] sources) throws IOExc
* @param classpath the classpath to use to resolve bindings
* @throws IOException
*/
public JavaPatternASTOperation(File refactorFile, String[] sources, String[] classpath) throws IOException {
public JavaPatternASTOperation(Path refactorFile, String[] sources, String[] classpath) throws IOException {
javaPatternFileParser = new JavaPatternFileParser();
javaPatternFileParser.buildMatchersWithSourcesAndClassPath(refactorFile, sources, classpath);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package org.alfasoftware.astra.core.refactoring.operations.javapattern;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -33,17 +32,17 @@ class JavaPatternFileParser {
private final Collection<MethodDeclaration> substituteMethods = new ArrayList<>();
private ASTNode patternToRefactorTo;

public void buildMatchers(File javaFile) throws IOException {
public void buildMatchers(Path javaFile) throws IOException {
buildMatchersWithSourcesAndClassPath(javaFile, new String[]{}, new String[]{});
}

public void buildMatchersWithSources(File javaFile, String[] sources) throws IOException {
public void buildMatchersWithSources(Path javaFile, String[] sources) throws IOException {
buildMatchersWithSourcesAndClassPath(javaFile, sources, new String[]{});
}

public void buildMatchersWithSourcesAndClassPath(File javaFile, String[] sources, String[] classpath) throws IOException {
String matcherFile = new String(Files.readAllBytes(Paths.get(javaFile.getAbsolutePath())));
CompilationUnit compilationUnit = AstraUtils.readAsCompilationUnit(matcherFile, sources, classpath);
public void buildMatchersWithSourcesAndClassPath(Path javaFile, String[] sources, String[] classpath) throws IOException {
String matcherFile = new String(Files.readAllBytes(javaFile.toAbsolutePath()));
CompilationUnit compilationUnit = AstraUtils.readAsCompilationUnit(javaFile, matcherFile, sources, classpath);

MethodDeclarationVisitor visitor = new MethodDeclarationVisitor();
compilationUnit.accept(visitor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.Collection;
Expand Down Expand Up @@ -52,8 +53,10 @@ public class InlineInterfaceRefactor implements ASTOperation {
public InlineInterfaceRefactor(String interfaceName, String interfacePath) {
this.interfaceName = interfaceName;
try {
Path filePath = Paths.get(interfacePath);
final CompilationUnit compilationUnit = AstraUtils.readAsCompilationUnit(
new String(Files.readAllBytes(Paths.get(interfacePath))),
filePath,
new String(Files.readAllBytes(filePath)),
new String[] {""}, new String[] {System.getProperty("java.home") + "/lib/rt.jar"});
interfaceVisitor = new ClassVisitor();
compilationUnit.accept(interfaceVisitor);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
package org.alfasoftware.astra.core.refactoring.operations.types;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.stream.Stream;

import org.alfasoftware.astra.core.utils.ASTOperation;
import org.alfasoftware.astra.core.utils.AstraUtils;
import org.alfasoftware.astra.core.utils.ClassVisitor;
import org.alfasoftware.astra.core.utils.CompilationUnitProperty;
import org.apache.log4j.Logger;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.MalformedTreeException;

/**
* While the {@link TypeReferenceRefactor} will update references of one type and change it to another,
* special handling is needed to update the type itself.
*
* For example, the package declaration at the top of the file will need to be updated,
* and imports might need to be added for types that may not previously have been imported due to being in the same package.
*
* Note that this doesn't currently move the file - the package declaration will be updated,
* but the file itself will still live in the folder matching the "fromType".
* This means that the version control move will need to be performed manually outside Astra.
* Moves a file to the specified 'toType' location. Makes all the necessary updates on referencing classes.
* <h3>Actions that will be performed on the 'fromType' class:</h3>
* <ul>
* <li>Update package declaration</li>
* <li>Rename type (if necessary)</li>
* <li>Update internal references to itself (if necessary)</li>
* <li>Update all types referencing refactored type</li>
* </ul>
*
* Limitation: this operation does <b>not</b> currently move the <b>nested types</b>. In scenarios where a nested class is specified
* to be moved, this operation will still update types referencing the nested class, but the actual movement has
* to be performed manually.
*/
public class UpdateTypeRefactor implements ASTOperation {

Expand Down Expand Up @@ -79,52 +90,118 @@ public UpdateTypeRefactor build() {
public String getFromType() {
return fromType;
}



@Override
public void run(CompilationUnit compilationUnit, ASTNode node, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException {

// If it is the type we're changing
if (node instanceof TypeDeclaration) {
TypeDeclaration typeDeclaration = (TypeDeclaration) node;
if (AstraUtils.getFullyQualifiedName(typeDeclaration).equals(fromType) &&
// and if we're updating the package name
! AstraUtils.getPackageName(fromType).equals(AstraUtils.getPackageName(toType))) {


if (fromType.equals(toType)) {
return;
}

// If it is the fromType we're changing
if (node instanceof TypeDeclaration && AstraUtils.getFullyQualifiedName((TypeDeclaration) node).equals(fromType)) {

boolean isNewPackage = !AstraUtils.getPackageName(fromType).equals(AstraUtils.getPackageName(toType));
if (isNewPackage) {
updatePackageDeclaration(compilationUnit, rewriter);
addImportsFromOldPackage(compilationUnit, rewriter);
}


moveType(compilationUnit, node, rewriter);

} else {
typeReferenceRefactor.run(compilationUnit, node, rewriter);
}

}

private static boolean isInnerType(ASTNode node) {
return node.getParent() instanceof TypeDeclaration;
}

/**
* Applies any necessary rename operations on the file content and moves the underline Java source file to its new location.
*/
private void moveType(CompilationUnit compilationUnit, ASTNode node, ASTRewrite rewriter) {
if (isInnerType(node)) {
log.warn(String.format("Moving inner types is currently not supported. Skipping moving [%s]", AstraUtils.getNameForCompilationUnit(compilationUnit)));
return;
}

try {
Path fileAbsolutePath = null;
Object property = compilationUnit.getProperty(CompilationUnitProperty.ABSOLUTE_PATH);
if (property instanceof String) {
fileAbsolutePath = Path.of((String) property);
} else if (property instanceof Path) {
fileAbsolutePath = (Path) property;
} else {
throw new RuntimeException("Unknown property type for [" + property + "]");
}

// Compute new file location
String fromTypePath = fromType.replace(".", File.separator);
String toTypePath = toType.replace(".", File.separator);
Path fileNewPath = Path.of(fileAbsolutePath.toString().replace(fromTypePath, toTypePath));
fileNewPath.getParent().toFile().mkdirs();

// Create the new Java file
updateInternalTypeReferences(compilationUnit, rewriter);
String content = AstraUtils.makeChangesFromAST(new String(Files.readAllBytes(fileAbsolutePath)), rewriter);
Files.write(fileNewPath, content.getBytes(), StandardOpenOption.CREATE);

// Delete old file
Files.delete(fileAbsolutePath);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* Updates references to the {@link UpdateTypeRefactor#fromType} type to itself.
* For example, nested classes can reference the parent class being refactored.
*/
private void updateInternalTypeReferences(CompilationUnit compilationUnit, ASTRewrite rewriter) {
final ClassVisitor visitor = new ClassVisitor();
compilationUnit.accept(visitor);
visitor.getVisitedNodes().forEach(visitedNode -> {
if (visitedNode instanceof SimpleName) {
IBinding binding = ((SimpleName) visitedNode).resolveBinding();
if (binding instanceof ITypeBinding && ((ITypeBinding) binding).getQualifiedName().equals(getFromType())) {
log.info("Refactoring simple type [" + visitedNode + "] to [" + AstraUtils.getSimpleName(toType) + "] in [" +
AstraUtils.getNameForCompilationUnit(compilationUnit) + "]");
rewriter.set(visitedNode, SimpleName.IDENTIFIER_PROPERTY, AstraUtils.getSimpleName(toType), null);
}
}
});
}


private void updatePackageDeclaration(CompilationUnit compilationUnit, ASTRewrite rewriter) {
if (! AstraUtils.getPackageName(fromType).equals(AstraUtils.getPackageName(toType))) {

log.info("Refactoring package declaration [" + AstraUtils.getPackageName(fromType) + "] "
+ "to [" + AstraUtils.getPackageName(toType) + "] "
+ "in [" + AstraUtils.getNameForCompilationUnit(compilationUnit) + "]");

rewriter.replace(
compilationUnit.getPackage().getName(),
rewriter.createStringPlaceholder(AstraUtils.getPackageName(toType), ASTNode.STRING_LITERAL),
compilationUnit.getPackage().getName(),
rewriter.createStringPlaceholder(AstraUtils.getPackageName(toType), ASTNode.STRING_LITERAL),
null);
}
}


/*
* Imports need to be added for types not previously imported due to being in the same package.
*/
private void addImportsFromOldPackage(CompilationUnit compilationUnit, ASTRewrite rewriter) {
ClassVisitor visitor = new ClassVisitor();
compilationUnit.accept(visitor);

Stream.of(
visitor.getSimpleTypes(),
visitor.getQualifiedTypes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ protected void runOperations(String directoryPath, UseCase useCase, String[] sou
AtomicLong currentPercentage = new AtomicLong();
log.info("Counting files (this may take a few seconds)");
Instant startTime = Instant.now();

List<Path> javaFilesInDirectory;
try (Stream<Path> walk = Files.walk(Paths.get(directoryPath))) {
javaFilesInDirectory = walk
Expand All @@ -99,14 +99,14 @@ protected void runOperations(String directoryPath, UseCase useCase, String[] sou

for (Path f : filteredJavaFiles) {
// TODO Naively we can multi-thread here (i.e. per file) but simple testing indicated that this slowed us down.
applyOperationsAndSave(new File(f.toString()), operations, sources, classPath);
applyOperationsAndSave(f, operations, sources, classPath);
long newPercentage = currentFileIndex.incrementAndGet() * 100 / filteredJavaFiles.size();
if (newPercentage != currentPercentage.get()) {
currentPercentage.set(newPercentage);
logProgress(currentFileIndex.get(), currentPercentage.get(), startTime, filteredJavaFiles.size());
}
}

log.info(getPrintableDuration(Duration.between(startTime, Instant.now())));
}

Expand All @@ -131,7 +131,7 @@ private void logProgress(long currentFileIndex, long currentPercentage, Instant
log.info(progressMessage);
}


private String getPrintableDuration(Duration duration) {
long minutes = duration.toMinutes();
long seconds = TimeUnit.MILLISECONDS.toSeconds(duration.minusMinutes(minutes).toMillis());
Expand All @@ -149,7 +149,7 @@ private String getPrintableDuration(Duration duration) {
}
return builder.append(".").toString();
}


/**
* Validates that the provided source and classpath entries exist
Expand All @@ -172,17 +172,18 @@ protected static void validateSourceAndClasspath(String[] sources, String[] clas

/**
* Applies the operations to a source file and then overwrites that file with the result.
* Operations that result in an empty file, will cause the file to be deleted.
*/
protected void applyOperationsAndSave(File javaFile, Set<? extends ASTOperation> operations, String[] sources, String[] classpath) {
protected void applyOperationsAndSave(Path javaFile, Set<? extends ASTOperation> operations, String[] sources, String[] classpath) {
try {
String fileContentBefore = new String(Files.readAllBytes(Paths.get(javaFile.getAbsolutePath())));
String fileContentBefore = new String(Files.readAllBytes(javaFile.toAbsolutePath()));
// apply the operations
final String fileContentAfter = applyOperationsToFile(fileContentBefore, operations, sources, classpath);
final String fileContentAfter = applyOperationsToFile(javaFile, fileContentBefore, operations, sources, classpath);

// If the file content has changed
if (! fileContentAfter.equals(fileContentBefore)) {
// save the file (over the original)
Files.write(Paths.get(javaFile.getAbsolutePath()), fileContentAfter.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
Files.write(javaFile.toAbsolutePath(), fileContentAfter.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
}
} catch (IOException e) {
log.error("ioE: " + e);
Expand All @@ -193,39 +194,39 @@ protected void applyOperationsAndSave(File javaFile, Set<? extends ASTOperation>
}
}


/**
* For a given Java source file, this models the effect of applying a set of operations to the source,
* and then removing any unused imports if a change was made.
*
* @param fileContentBefore Source file content before any operations are applied
* @param operations Operations to apply
*/
public String applyOperationsToFile(String fileContentBefore, Set<? extends ASTOperation> operations, String[] sources, String[] classpath) throws BadLocationException {
public String applyOperationsToFile(Path file, String fileContentBefore, Set<? extends ASTOperation> operations, String[] sources, String[] classpath) throws BadLocationException {

String fileContentAfter = applyOperationsToSource(operations, sources, classpath, fileContentBefore);
String fileContentAfter = applyOperationsToSource(operations, sources, classpath, file, fileContentBefore);

// If file hasn't changed, return as-is
if (fileContentAfter.equals(fileContentBefore)) {
return fileContentAfter;
} else {
// If we've modified the file, remove any unused imports
return applyOperationsToSource(new HashSet<>(Arrays.asList(new UnusedImportRefactor())), sources, classpath, fileContentAfter);
return applyOperationsToSource(new HashSet<>(Arrays.asList(new UnusedImportRefactor())), sources, classpath, file, fileContentAfter);
}
}


/**
* Runs operations over the source file, and returns the result of running those operations
*/
protected String applyOperationsToSource(Set<? extends ASTOperation> operations, String[] sources, String[] classpath, String source)
private String applyOperationsToSource(Set<? extends ASTOperation> operations, String[] sources, String[] classpath, Path file, String source)
throws BadLocationException {
CompilationUnit compilationUnit = AstraUtils.readAsCompilationUnit(source, sources, classpath);
CompilationUnit compilationUnit = AstraUtils.readAsCompilationUnit(file, source, sources, classpath);
ASTRewrite rewriter = runOperations(operations, compilationUnit);
return makeChangesFromAST(source, rewriter);
}


/**
* Run the provided operations over the ASTNodes in the compilation unit,
* recording any changes to be made to that compilation unit in the ASTRewrite.
Expand Down
Loading

0 comments on commit 9815d3a

Please sign in to comment.