diff --git a/build.gradle b/build.gradle index e60a320..9da478c 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ wrapper { intellij { version.set('2022.2') pluginName.set('Validation-File Comparison') + plugins = ['com.intellij.java'] } patchPluginXml { @@ -46,6 +47,8 @@ publishPlugin { } dependencies { + implementation 'de.cronn:validation-file-assertions:0.8.0' + testImplementation(platform('org.junit:junit-bom:5.10.2')) testRuntimeOnly("org.junit.platform:junit-platform-launcher") { because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions") diff --git a/src/main/java/de/cronn/validation_files_diff/ValidationDiffApplicationOptionsProvider.java b/src/main/java/de/cronn/validation_files_diff/ValidationDiffApplicationOptionsProvider.java index 109b792..de3dd1f 100644 --- a/src/main/java/de/cronn/validation_files_diff/ValidationDiffApplicationOptionsProvider.java +++ b/src/main/java/de/cronn/validation_files_diff/ValidationDiffApplicationOptionsProvider.java @@ -9,6 +9,7 @@ public interface ValidationDiffApplicationOptionsProvider { boolean DEFAULT_SHOW_NEW_ON_TARGET = false; boolean DEFAULT_SHOW_EQUAL = false; boolean DEFAULT_SHOW_DIFFERENT = true; + boolean DEFAULT_RENAME_VALIDATION_FILE_ENABLED = true; static ValidationDiffApplicationOptionsProvider getInstance() { return ApplicationManager.getApplication().getService(ValidationDiffApplicationOptionsProvider.class); @@ -33,4 +34,8 @@ static ValidationDiffApplicationOptionsProvider getInstance() { boolean getShowDifferent(); void setShowDifferent(boolean showDifferent); + + void setRenameValidationFilesEnabled(boolean enabled); + + boolean isRenamingValidationFilesEnabled(); } diff --git a/src/main/java/de/cronn/validation_files_diff/ValidationDiffProjectOptionsProvider.java b/src/main/java/de/cronn/validation_files_diff/ValidationDiffProjectOptionsProvider.java index 3424043..8cf0aca 100644 --- a/src/main/java/de/cronn/validation_files_diff/ValidationDiffProjectOptionsProvider.java +++ b/src/main/java/de/cronn/validation_files_diff/ValidationDiffProjectOptionsProvider.java @@ -1,14 +1,12 @@ package de.cronn.validation_files_diff; -import com.intellij.openapi.application.ApplicationManager; -import org.jetbrains.annotations.NotNull; - -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; +import de.cronn.assertions.validationfile.TestData; +import org.jetbrains.annotations.NotNull; public interface ValidationDiffProjectOptionsProvider { - String DEFAULT_OUTPUT_DIRECTORY = "data/test/output"; - String DEFAULT_VALIDATION_DIRECTORY = "data/test/validation"; + String DEFAULT_OUTPUT_DIRECTORY = TestData.TEST_OUTPUT_DATA_DIR.toString(); + String DEFAULT_VALIDATION_DIRECTORY = TestData.TEST_VALIDATION_DATA_DIR.toString(); static ValidationDiffProjectOptionsProvider getInstance(@NotNull Project project) { return project.getService(ValidationDiffProjectOptionsProvider.class); @@ -21,5 +19,4 @@ static ValidationDiffProjectOptionsProvider getInstance(@NotNull Project project String getRelativeOutputDirPath(); void setRelativeOutputDirPath(String relativeOutputDirPath); - } diff --git a/src/main/java/de/cronn/validation_files_diff/helper/PsiElementValidationFileFinder.java b/src/main/java/de/cronn/validation_files_diff/helper/PsiElementValidationFileFinder.java new file mode 100644 index 0000000..834ba86 --- /dev/null +++ b/src/main/java/de/cronn/validation_files_diff/helper/PsiElementValidationFileFinder.java @@ -0,0 +1,97 @@ +package de.cronn.validation_files_diff.helper; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import de.cronn.validation_files_diff.ValidationDiffProjectOptionsProvider; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class PsiElementValidationFileFinder { + + private final Project project; + private final Module module; + private final PsiElement element; + + private PsiElementValidationFileFinder(PsiElement element, Project project, Module module) { + this.project = project; + this.module = module; + this.element = element; + } + + public static PsiElementValidationFileFinder of(PsiElement element) { + Project project = element.getProject(); + Module currentModule = ModuleUtilCore.findModuleForFile(element.getContainingFile()); + return new PsiElementValidationFileFinder(element, project, currentModule); + } + + public boolean hasCorrespondingValidationFiles() { + return !findCorrespondingValidationFiles().isEmpty(); + } + + public List findCorrespondingValidationFiles() { + VirtualFile moduleRoot = getModuleRoot(); + if (moduleRoot == null) { + return Collections.emptyList(); + } + + String validationFilePrefix = parseValidationFilePrefix(); + + return getValidationFileRelatedDirectories() + .map(moduleRoot::findFileByRelativePath) + .filter(Objects::nonNull) + .map(VirtualFile::getChildren) + .flatMap(Arrays::stream) + .filter(validationFile -> fileNameStartsWith(validationFile, validationFilePrefix)) + .toList(); + } + + private Stream getValidationFileRelatedDirectories() { + ValidationDiffProjectOptionsProvider options = ValidationDiffProjectOptionsProvider.getInstance(project); + return Stream.of(options.getRelativeOutputDirPath(), options.getRelativeValidationDirPath()); + } + + private boolean fileNameStartsWith(VirtualFile file, String prefix) { + return file.getName().startsWith(prefix); + } + + private String parseValidationFilePrefix() { + if (element instanceof PsiMethod psiMethod) { + PsiClass psiClass = psiMethod.getContainingClass(); + return PsiTestNameUtils.getTestName(psiClass, psiMethod); + } + + if (element instanceof PsiClass psiClass) { + return PsiTestNameUtils.getTestClassName(psiClass); + } + return null; + } + + private LocalFileSystem getLocalFileSystem() { + return (LocalFileSystem) VirtualFileManager + .getInstance() + .getFileSystem(LocalFileSystem.PROTOCOL); + } + + private VirtualFile getModuleRoot() { + Module[] modules = ModuleManager.getInstance(project).getModules(); + Path moduleRootPath = new ModuleAnalyser(module, modules).getMatchingContentRootForNextNonLeafModule(); + if (moduleRootPath == null) { + return null; + } + + return getLocalFileSystem().findFileByNioFile(moduleRootPath); + } +} diff --git a/src/main/java/de/cronn/validation_files_diff/helper/PsiTestNameUtils.java b/src/main/java/de/cronn/validation_files_diff/helper/PsiTestNameUtils.java new file mode 100644 index 0000000..3bf2da0 --- /dev/null +++ b/src/main/java/de/cronn/validation_files_diff/helper/PsiTestNameUtils.java @@ -0,0 +1,28 @@ +package de.cronn.validation_files_diff.helper; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; + +public class PsiTestNameUtils { + + private PsiTestNameUtils() { + } + + public static String getTestName(PsiClass psiClass, PsiMethod psiMethod) { + return join(getTestClassName(psiClass), psiMethod.getName()); + } + + public static String getTestClassName(PsiClass psiClass) { + String nestingHierarchy = psiClass.getName(); + PsiClass enclosingClass = psiClass.getContainingClass(); + while (enclosingClass != null) { + nestingHierarchy = join(enclosingClass.getName(), nestingHierarchy); + enclosingClass = enclosingClass.getContainingClass(); + } + return nestingHierarchy; + } + + private static String join(String element, String other) { + return other.startsWith("_") ? (element + other) : (element + "_" + other); + } +} diff --git a/src/main/java/de/cronn/validation_files_diff/impl/ValidationDiffApplicationOptionsProviderImpl.java b/src/main/java/de/cronn/validation_files_diff/impl/ValidationDiffApplicationOptionsProviderImpl.java index 599ac25..55308d7 100644 --- a/src/main/java/de/cronn/validation_files_diff/impl/ValidationDiffApplicationOptionsProviderImpl.java +++ b/src/main/java/de/cronn/validation_files_diff/impl/ValidationDiffApplicationOptionsProviderImpl.java @@ -87,6 +87,16 @@ public void setShowDifferent(boolean showDifferent) { state.showDifferent = showDifferent; } + @Override + public void setRenameValidationFilesEnabled(boolean enabled) { + state.validationFileRenameEnabled = enabled; + } + + @Override + public boolean isRenamingValidationFilesEnabled() { + return state.validationFileRenameEnabled; + } + public static class State { public State() { outputSide = DEFAULT_OUTPUT_SIDE; @@ -94,6 +104,7 @@ public State() { showNewOnSource = DEFAULT_SHOW_NEW_ON_SOURCE; showEqual = DEFAULT_SHOW_EQUAL; showDifferent = DEFAULT_SHOW_DIFFERENT; + validationFileRenameEnabled = DEFAULT_RENAME_VALIDATION_FILE_ENABLED; } public DiffSide outputSide; @@ -101,6 +112,7 @@ public State() { public boolean showNewOnTarget; public boolean showEqual; public boolean showDifferent; + public boolean validationFileRenameEnabled; } } diff --git a/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenameFactory.java b/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenameFactory.java new file mode 100644 index 0000000..be5b38a --- /dev/null +++ b/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenameFactory.java @@ -0,0 +1,48 @@ +package de.cronn.validation_files_diff.impl; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.refactoring.rename.naming.AutomaticRenamer; +import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory; +import com.intellij.usageView.UsageInfo; +import de.cronn.validation_files_diff.ValidationDiffApplicationOptionsProvider; +import de.cronn.validation_files_diff.helper.PsiElementValidationFileFinder; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public class ValidationFileAutomaticRenameFactory implements AutomaticRenamerFactory { + + private static final String OPTION_NAME = "Rename Validation-Files"; + + @Override + public String getOptionName() { + return OPTION_NAME; + } + + @Override + public boolean isEnabled() { + return ValidationDiffApplicationOptionsProvider.getInstance().isRenamingValidationFilesEnabled(); + } + + @Override + public void setEnabled(boolean enabled) { + ValidationDiffApplicationOptionsProvider.getInstance().setRenameValidationFilesEnabled(enabled); + } + + @Override + public @NotNull AutomaticRenamer createRenamer(PsiElement element, String newName, Collection usages) { + return new ValidationFileAutomaticRenamer(element, newName); + } + + @Override + public boolean isApplicable(@NotNull PsiElement element) { + if (!(element instanceof PsiMethod || element instanceof PsiClass)) { + return false; + } + + return PsiElementValidationFileFinder.of(element).hasCorrespondingValidationFiles(); + } + +} diff --git a/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenamer.java b/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenamer.java new file mode 100644 index 0000000..8e2ba94 --- /dev/null +++ b/src/main/java/de/cronn/validation_files_diff/impl/ValidationFileAutomaticRenamer.java @@ -0,0 +1,65 @@ +package de.cronn.validation_files_diff.impl; + +import com.intellij.openapi.util.NlsContexts; +import com.intellij.psi.*; +import com.intellij.refactoring.rename.naming.AutomaticRenamer; +import de.cronn.validation_files_diff.helper.PsiElementValidationFileFinder; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class ValidationFileAutomaticRenamer extends AutomaticRenamer { + + private static final boolean ARE_SELECTED_BY_DEFAULT = true; + private static final String TITLE = "Rename Validation-/Temp-/Output-File"; + private static final String DESCRIPTION = "These Files Are Corresponding to the Test"; + private static final String ENTITY_NAME = "Validation/Tmp/Output File"; + + protected ValidationFileAutomaticRenamer(PsiElement element, String newName) { + if (!(element instanceof PsiMethod || element instanceof PsiClass)) { + return; + } + + PsiNamedElement psiNamedElement = (PsiNamedElement) element; + + PsiManager psiManager = PsiManager.getInstance(element.getProject()); + + String oldName = psiNamedElement.getName(); + PsiElementValidationFileFinder.of(element) + .findCorrespondingValidationFiles() + .stream() + .map(psiManager::findFile) + .filter(Objects::nonNull) + .forEach(psiFile -> suggestToRenameValidationFile(psiFile, getNewName(psiFile.getName(), oldName, newName))); + } + + @NotNull + private static String getNewName(String currentName, String oldName, String newName) { + return currentName.replaceFirst(oldName, newName); + } + + private void suggestToRenameValidationFile(PsiFile psiFile, String newFileName) { + myElements.add(psiFile); + suggestAllNames(psiFile.getName(), newFileName); + } + + @Override + public @NlsContexts.DialogTitle String getDialogTitle() { + return TITLE; + } + + @Override + public @NlsContexts.Button String getDialogDescription() { + return DESCRIPTION; + } + + @Override + public @NlsContexts.ColumnName String entityName() { + return ENTITY_NAME; + } + + @Override + public boolean isSelectedByDefault() { + return ARE_SELECTED_BY_DEFAULT; + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e3be1ae..6ed9ccb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,6 +4,7 @@ Cronn GmbH com.intellij.modules.platform + com.intellij.java Plugin on GitHub | @@ -55,6 +56,8 @@ serviceImplementation="de.cronn.validation_files_diff.impl.ValidationDiffProjectOptionsProviderImpl"/> + diff --git a/src/test/java/de/cronn/validation_files_diff/action/ValidationFileAutomaticRenamerTest.java b/src/test/java/de/cronn/validation_files_diff/action/ValidationFileAutomaticRenamerTest.java new file mode 100644 index 0000000..8147c98 --- /dev/null +++ b/src/test/java/de/cronn/validation_files_diff/action/ValidationFileAutomaticRenamerTest.java @@ -0,0 +1,158 @@ +package de.cronn.validation_files_diff.action; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.impl.JavaPsiFacadeEx; +import com.intellij.refactoring.rename.RenameProcessor; +import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory; +import com.intellij.testFramework.HeavyPlatformTestCase; +import com.intellij.testFramework.PsiTestUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static de.cronn.assertions.validationfile.TestData.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class ValidationFileAutomaticRenamerTest extends HeavyPlatformTestCase { + + public void testRenameClass_renamesAllValidationFiles() throws IOException { + Path projectDir = createDefaultJavaModuleStructure(); + + Path openedTestFile = projectDir.resolve("src/test/Test.java"); + Files.createFile(openedTestFile); + Files.writeString(openedTestFile, "public class Test {}"); + + Path validationFileDirectory = getValidationFileDirectory(projectDir); + Path outputFileDirectory = getOutputFileDirectory(projectDir); + Path tempFileDirectory = getTempFileDirectory(projectDir); + + Files.createDirectories(validationFileDirectory); + Files.createDirectories(outputFileDirectory); + Files.createDirectories(tempFileDirectory); + + createValidationFileInDirectories(projectDir, "Test_findStuff.json"); + + refreshFileSystem(projectDir); + + PsiClass testClass = JavaPsiFacadeEx.getInstanceEx(getProject()).findClass("Test"); + + executeRename(testClass, "Test2"); + + assertValidationFileExistsInDirectories(projectDir, "Test2_findStuff.json"); + assertValidationFileNotExistsDirectories(projectDir, "Test_findStuff.json"); + } + + public void testRenameMethod_renamesAllValidationFiles() throws IOException { + Path projectDir = createDefaultJavaModuleStructure(); + + Path openedTestFile = projectDir.resolve("src/test/Test.java"); + Files.createFile(openedTestFile); + Files.writeString(openedTestFile, "public class Test {\n" + + "\tpublic void testSomething() {" + + "}\n" + + "}"); + + Path validationFileDirectory = getValidationFileDirectory(projectDir); + Path outputFileDirectory = getOutputFileDirectory(projectDir); + Path tempFileDirectory = getTempFileDirectory(projectDir); + + Files.createDirectories(validationFileDirectory); + Files.createDirectories(outputFileDirectory); + Files.createDirectories(tempFileDirectory); + + String validationFileName = "Test_testSomething.json"; + String newValidationFileName = "Test_testSomething2.json"; + + createValidationFileInDirectories(projectDir, validationFileName); + + refreshFileSystem(projectDir); + + PsiMethod testMethod = JavaPsiFacadeEx.getInstanceEx(getProject()).findClass("Test").findMethodsByName("testSomething", false)[0]; + + executeRename(testMethod, "testSomething2"); + + assertValidationFileExistsInDirectories(projectDir, newValidationFileName); + assertValidationFileNotExistsDirectories(projectDir, validationFileName); + } + + @NotNull + private static Path getTempFileDirectory(Path projectDir) { + return projectDir.resolve(TEST_TEMPORARY_DATA_DIR); + } + + @NotNull + private static Path getValidationFileDirectory(Path projectDir) { + return projectDir.resolve(TEST_VALIDATION_DATA_DIR); + } + + @NotNull + private static Path getOutputFileDirectory(Path projectDir) { + return projectDir.resolve(TEST_OUTPUT_DATA_DIR); + } + + private static void assertValidationFileNotExistsDirectories(Path projectDir, String filename) { + assertThat(getValidationFileDirectory(projectDir).resolve(filename)).doesNotExist(); + assertThat(getOutputFileDirectory(projectDir).resolve(filename)).doesNotExist(); + assertThat(getTempFileDirectory(projectDir).resolve(filename)).doesNotExist(); + } + + private static void assertValidationFileExistsInDirectories(Path projectDir, String filename) { + assertThat(getValidationFileDirectory(projectDir).resolve(filename)).exists(); + assertThat(getOutputFileDirectory(projectDir).resolve(filename)).exists(); + assertThat(getTempFileDirectory(projectDir).resolve(filename)).exists(); + } + + private static void createValidationFileInDirectories(Path projectDir, String validationFileName) throws IOException { + Files.createFile(getValidationFileDirectory(projectDir).resolve(validationFileName)); + Files.createFile(getOutputFileDirectory(projectDir).resolve(validationFileName)); + Files.createFile(getTempFileDirectory(projectDir).resolve(validationFileName)); + } + + private void refreshFileSystem(Path projectDir) { + LocalFileSystem.getInstance().findFileByNioFile(projectDir).refresh(false, true); + PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); + } + + private void executeRename(PsiElement element, String newName) { + RenameProcessor renameProcessor = new RenameProcessor(getProject(), element, newName, false, false); + for (AutomaticRenamerFactory factory : AutomaticRenamerFactory.EP_NAME.getExtensionList()) { + renameProcessor.addRenamerFactory(factory); + } + renameProcessor.run(); + } + + private Path createDefaultJavaModuleStructure() throws IOException { + VirtualFile testProjectStructure = createTestProjectStructure(); + Path projectDir = testProjectStructure.toNioPath(); + + Module parent = getModule(); + + createMainModule(projectDir, parent); + createTestModule(projectDir, parent); + + return projectDir; + } + + private void createTestModule(Path parentDirectory, Module parent) throws IOException { + Module testModule = createModule(parent.getName() + ".test"); + Path testModuleRootPath = parentDirectory.resolve("src/test"); + Files.createDirectories(testModuleRootPath); + PsiTestUtil.addSourceRoot(testModule, getVirtualFile(testModuleRootPath.toFile())); + } + + private void createMainModule(Path parentDirectory, Module parent) throws IOException { + Module mainModule = createModule(parent.getName() + ".main"); + Path mainModuleRootPath = parentDirectory.resolve("src/main"); + Files.createDirectories(mainModuleRootPath); + PsiTestUtil.addSourceRoot(mainModule, getVirtualFile(mainModuleRootPath.toFile())); + } + +}