From 68a57401714a1748d7671405a730db84da670de1 Mon Sep 17 00:00:00 2001 From: Alexey Kryuchkov Date: Mon, 4 Apr 2022 09:53:31 +0300 Subject: [PATCH] Generators for files and methods GitOrigin-RevId: 93b45a72817ce4571d2302b4cfd0726730644d9e --- java/java-impl/src/META-INF/JavaPlugin.xml | 1 + .../GenerateTestDataPathCommon.java | 109 ++++++++++++++++ .../GenerateTestDataPathFileAction.java | 119 ++++++++++++++++++ .../GenerateTestDataPathMethodAction.java | 106 ++++++++++++++++ .../resources/messages/JavaBundle.properties | 9 ++ .../src/messages/ActionsBundle.properties | 2 + resources/src/idea/JavaActions.xml | 2 + 7 files changed, 348 insertions(+) create mode 100644 java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java create mode 100644 java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java create mode 100644 java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index f107afb124df..dfc59b7fa211 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -2270,6 +2270,7 @@ + diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java new file mode 100644 index 000000000000..3fa324da0db3 --- /dev/null +++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathCommon.java @@ -0,0 +1,109 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.testIntegration; + +import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.codeInsight.CodeInsightActionHandler; +import com.intellij.codeInsight.generation.actions.BaseGenerateAction; +import com.intellij.find.FindBundle; +import com.intellij.java.JavaBundle; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.util.NlsSafe; +import com.intellij.psi.*; +import com.intellij.ui.components.JBPanel; +import com.intellij.ui.components.JBTextField; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.uast.UAnnotation; +import org.jetbrains.uast.UastContextKt; + +import javax.swing.*; +import java.awt.*; +import java.util.Collections; +import java.util.List; + +class GenerateTestDataPathCommon extends BaseGenerateAction { + + @NonNls + private static final String CONTENT_ROOT_VARIABLE = "$CONTENT_ROOT"; + @NonNls + private static final String PROJECT_ROOT_VARIABLE = "$PROJECT_ROOT"; + @NonNls + protected static final String ANNOTATION_FQN = "com.intellij.testFramework.TestDataPath"; + protected static final Logger LOG = Logger.getInstance(GenerateTestDataPathCommon.class); + + GenerateTestDataPathCommon(CodeInsightActionHandler handler) { + super(handler); + } + + @Nullable + protected static String annotationValue(@NotNull PsiModifierListOwner owner, String annotationFqName) { + var annotationNames = Collections.singleton(annotationFqName); + var nestedClass = owner instanceof PsiClass && ((PsiClass)owner).getContainingClass() != null; + var element = nestedClass + ? AnnotationUtil.findAnnotation(owner, annotationNames) + : AnnotationUtil.findAnnotationInHierarchy(owner, annotationNames); + final var annotation = UastContextKt.toUElement(element, UAnnotation.class); + if (annotation != null) { + var value = annotation.findAttributeValue(PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME); + if (value != null) { + final var project = owner.getProject(); + final var constantValue = value.evaluate(); + if (constantValue instanceof String) { + var path = (String)constantValue; + if (path.contains(CONTENT_ROOT_VARIABLE)) { + final var fileIndex = ProjectRootManager.getInstance(project).getFileIndex(); + final var file = owner.getContainingFile().getVirtualFile(); + if (file == null) return null; + final var contentRoot = fileIndex.getContentRootForFile(file); + if (contentRoot == null) return null; + path = path.replace(CONTENT_ROOT_VARIABLE, contentRoot.getPath()); + } + if (path.contains(PROJECT_ROOT_VARIABLE)) { + var baseDir = project.getBasePath(); + if (baseDir == null) { + return null; + } + path = path.replace(PROJECT_ROOT_VARIABLE, baseDir); + } + return path; + } + } + } + return null; + } + + @Override + protected boolean isValidForClass(PsiClass targetClass) { + return AnnotationUtil.findAnnotation(targetClass, "com.intellij.testFramework.TestDataPath") != null; + } + + protected static abstract class TestDataPathDialog extends DialogWrapper { + protected TestDataPathDialog(@Nullable Project project, @NlsSafe @NotNull String titleText, boolean canBeParent) { + super(project, canBeParent); + setTitle(titleText); + setOKButtonText(JavaBundle.message("generate.button.title")); + init(); + } + + @Override + protected @Nullable JComponent createCenterPanel() { + var mainPanel = createMainPanel(); + getPanelContent().forEach(mainPanel::add); + return mainPanel; + } + + abstract protected List getPanelContent(); + + protected JBPanel createMainPanel() { + var mainPanel = new JBPanel<>().withPreferredWidth(250); + var layout = new BoxLayout(mainPanel, BoxLayout.Y_AXIS); + mainPanel.setLayout(layout); + return mainPanel; + } + } +} diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java new file mode 100644 index 000000000000..38a4dddfb92e --- /dev/null +++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathFileAction.java @@ -0,0 +1,119 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.testIntegration; + +import com.intellij.java.JavaBundle; +import com.intellij.notification.NotificationGroupManager; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.psi.PsiClass; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.ui.components.*; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class GenerateTestDataPathFileAction extends GenerateTestDataPathCommon { + + public GenerateTestDataPathFileAction() { + super(((project, editor, file) -> { + var currentClass = PsiTreeUtil.findChildOfType(file, PsiClass.class); + if (currentClass == null) return; + + var annotationValue = annotationValue(currentClass, ANNOTATION_FQN); + if (annotationValue == null) return; + + var directoryPath = LocalFileSystem.getInstance().findFileByPath(annotationValue); + if (directoryPath == null || !directoryPath.isDirectory() || directoryPath.getChildren() == null) { + NotificationGroupManager.getInstance().getNotificationGroup("Test integration") + .createNotification(JavaBundle.message("generate.method.nosuites.warn", annotationValue), NotificationType.WARNING) + .notify(project); + return; + } + + var suggestedFileNames = Arrays + .stream(currentClass.getAllMethods()) + .filter(method -> method.getName().startsWith("test")) + .map(method -> method.getName().substring(4)) + .collect(Collectors.toSet()); + + if (suggestedFileNames.isEmpty()) { + NotificationGroupManager.getInstance().getNotificationGroup("Test integration") + .createNotification(JavaBundle.message("generate.method.nofiles.warn", currentClass.getName(), annotationValue), + NotificationType.WARNING) + .notify(project); + return; + } + + var dialog = new TestDataPathDialog(project, JavaBundle.message("dialog.title.testdatapath.file.generate"), false) { + + private JBList suggestedFileList; + private JCheckBox qfCheckbox; + private JBTextField extensionField; + + @Override + protected List getPanelContent() { + suggestedFileList = new JBList(suggestedFileNames); + suggestedFileList.setSelectionInterval(0, suggestedFileList.getItemsCount()); + var scrollPane = new JBScrollPane(suggestedFileList); + scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT); + + qfCheckbox = new JCheckBox(); + qfCheckbox.setText(JavaBundle.message("generate.quickfix.files")); + qfCheckbox.setAlignmentX(Component.LEFT_ALIGNMENT); + qfCheckbox.setSelected(false); + + extensionField = new JBTextField(); + extensionField.getEmptyText().setText(JavaBundle.message("generate.file.extension.text")); + extensionField.setAlignmentX(Component.LEFT_ALIGNMENT); + + return List.of(scrollPane, qfCheckbox, extensionField); + } + + @Override + protected void doOKAction() { + getSelectedMethod().forEach(fileName -> { + var requester = new Object(); + var folder = VirtualFileManager.getInstance().findFileByNioPath(Path.of(annotationValue)); + WriteCommandAction.runWriteCommandAction(project, (Runnable)() -> { + try { + folder.createChildData(requester, fileName + extensionField.getText()); + if (isQFFileNeeded()) folder.createChildData(requester, fileName + ".after" + extensionField.getText()); + } + catch (IOException | NullPointerException e) { + LOG.warn(e); + } + }); + }); + super.doOKAction(); + } + + @Override + protected @Nullable ValidationInfo doValidate() { + var in = extensionField.getText().trim(); + if (in.isBlank() || !in.startsWith(".") || in.contains(" ")) { + return new ValidationInfo(JavaBundle.message("generate.file.extension.validation.error", in)); + } + return null; + } + + List getSelectedMethod() { return suggestedFileList.getSelectedValuesList(); } + + boolean isQFFileNeeded() { return qfCheckbox.isSelected(); } + }; + + ApplicationManager.getApplication().invokeLater(() -> dialog.show()); + }) + ); + } +} + diff --git a/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java new file mode 100644 index 000000000000..ab80da41c708 --- /dev/null +++ b/java/java-impl/src/com/intellij/testIntegration/GenerateTestDataPathMethodAction.java @@ -0,0 +1,106 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.testIntegration; + +import com.intellij.java.JavaBundle; +import com.intellij.notification.NotificationGroupManager; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElementFactory; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.ui.components.JBList; +import com.intellij.ui.components.JBScrollPane; +import org.jetbrains.annotations.NotNull; + +import javax.lang.model.element.Modifier; +import java.awt.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class GenerateTestDataPathMethodAction extends GenerateTestDataPathCommon { + + public GenerateTestDataPathMethodAction() { + super(((project, editor, file) -> { + var currentClass = PsiTreeUtil.findChildOfType(file, PsiClass.class); + if (currentClass == null) return; + + var existingMethodNames = Arrays + .stream(currentClass.getAllMethods()) + .map(PsiMethod::getName) + .filter(name -> name.startsWith("test")) + .collect(Collectors.toCollection(HashSet::new)); + + var annotationValue = annotationValue(currentClass, ANNOTATION_FQN); + if (annotationValue == null) { + return; + } + + var directoryPath = LocalFileSystem.getInstance().findFileByPath(annotationValue); + if (directoryPath == null || !directoryPath.isDirectory() || directoryPath.getChildren() == null) { + NotificationGroupManager.getInstance().getNotificationGroup("Test integration") + .createNotification(JavaBundle.message("generate.method.nosuites.warn", annotationValue), NotificationType.WARNING) + .notify(project); + return; + } + + var preparedMethodNames = Arrays.stream(directoryPath.getChildren()) + .filter(vFile -> vFile.getExtension() != null && (!vFile.getExtension().equals("java") || !vFile.getExtension().equals("kt"))) + .map(vFile -> "test" + capitalizeFirstChar(normalizeMethodName(vFile.getName()))) + .filter(methodName -> !existingMethodNames.contains(methodName)) + .collect(Collectors.toSet()); + + var dialog = new TestDataPathDialog(project, JavaBundle.message("dialog.title.testdatapath.method.generate"), false) { + + private JBList suggestedFileList; + private ComboBox selectModifierBox; + + @Override + protected List getPanelContent() { + suggestedFileList = new JBList<>(preparedMethodNames); + suggestedFileList.setSelectionInterval(0, suggestedFileList.getItemsCount()); + var scrollPane = new JBScrollPane(suggestedFileList); + scrollPane.setAlignmentX(Component.LEFT_ALIGNMENT); + + final var choices = new String[]{JavaBundle.message("generate.select.default.modifier.text"), Modifier.PUBLIC.toString(), Modifier.PROTECTED.toString(), Modifier.PRIVATE.toString()}; + selectModifierBox = new ComboBox(choices); + selectModifierBox.setAlignmentX(Component.LEFT_ALIGNMENT); + + return List.of(scrollPane, selectModifierBox); + } + + @Override + protected void doOKAction() { + suggestedFileList.getSelectedValuesList().forEach(methodName -> { + WriteCommandAction.runWriteCommandAction(project, (Runnable)() -> currentClass.add( + PsiElementFactory.getInstance(project).createMethodFromText(getSelectedModifier() + "void " + methodName + "(){ }", currentClass)) + ); + }); + super.doOKAction(); + } + + String getSelectedModifier() { + return selectModifierBox.getSelectedIndex() == 0 ? "" : selectModifierBox.getSelectedItem().toString() + " "; + } + }; + + ApplicationManager.getApplication().invokeLater(() -> dialog.show()); + }) + ); + } + + private static String capitalizeFirstChar(@NotNull String inString) { + if (Character.isUpperCase(inString.charAt(0))) return inString; + if (inString.length() == 1) return inString.toUpperCase(); + return inString.charAt(0) + inString.substring(1); + } + + private static String normalizeMethodName(@NotNull String inString) { + return inString.substring(0, inString.indexOf(".")); + } +} diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index adb91d1f3ac5..71de41c953c1 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -223,6 +223,8 @@ dialog.title.infer.nullity.results=Infer Nullity Results dialog.title.no.test.roots.found=No Test Roots Found dialog.title.process.duplicates=Process Duplicates dialog.title.super.method.found=Super Method Found +dialog.title.testdatapath.method.generate=Generate methods from @TestDataPath +dialog.title.testdatapath.file.generate=Generate files from class to @TestDataPath do.not.import.inner.classes.for=Exclude inner classes by short name: do.not.import.inner.classes.no.classes=No inner classes defined editbox.blanklines.around.initializer=Around initializer: @@ -325,6 +327,12 @@ generate.tostring.sort.super=Super class members first generate.tostring.tab.title=toString() Generation Settings generate.tostring.template.label=&Template: generate.tostring.title=Generate toString() +generate.quickfix.files=Create QF scenarios(with '.after' suffix) +generate.select.default.modifier.text=Select modifier +generate.file.extension.text=Enter file extension: .php, .java, .kt, .cpp .... +generate.file.extension.validation.error=File extension ''{0}'' isn't valid. It must start from ''.'', can't contain space symbols or be blank +generate.method.nosuites.warn=No test suites were found in @TestDataPath("{0}") +generate.method.nofiles.warn=No test methods were found in class {0} goto.super.class.chooser.title=Choose super class or interface group.javadoc.alignment=Alignment group.javadoc.blank.lines=Blank lines @@ -1738,6 +1746,7 @@ notification.group.preview.features=Preview Java language level may be discontin notification.group.redundant.exports=Redundant exports/opens can be removed notification.group.setup.sdk=JDK configured notification.group.setup.external.annotations=Failed to load external annotations +notification.group.testintegration=Failed to generate tests for @TestDataPath popup.title.effective.visibility=Effective Visibility inspection.io.stream.constructor.description='InputStream' and 'OutputStream' can be constructed using 'Files' methods inspection.input.stream.constructor.message='InputStream' can be constructed using 'Files.newInputStream' diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index 8a27ff070b11..b755c9dce1c1 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -227,6 +227,8 @@ action.GenerateBeforeClassMethod.text=BeforeClass Method action.GenerateTearDownMethod.text=TearDown Method action.GenerateAfterClassMethod.text=AfterClass Method action.GenerateDataMethod.text=Parameters Method +action.GenerateTestDataPathMethods.text=@TestDataPath Methods +action.GenerateTestDataPathFiles.text=@TestDataPath Files action.GenerateConstructor.text=Constructor action.GenerateConstructor.description=Generate constructor action.GenerateCreateUI.text=createUI(...) diff --git a/resources/src/idea/JavaActions.xml b/resources/src/idea/JavaActions.xml index 7325804962c6..49b06ec88b27 100644 --- a/resources/src/idea/JavaActions.xml +++ b/resources/src/idea/JavaActions.xml @@ -280,6 +280,8 @@ + +