From 56ab51289baee44e884fce239697d61cb584745a Mon Sep 17 00:00:00 2001 From: Yaroslav Pankratyev Date: Tue, 3 Oct 2017 16:35:20 +0700 Subject: [PATCH] IDEA-178996 References for @TestDataFile-annotated method arguments for @TestDataPath-annotated testcases --- plugins/devkit/resources/META-INF/plugin.xml | 1 + .../TestDataFilesReferencesContributor.java | 145 ++++++++++++++++++ .../TestDataGuessByExistingFilesUtil.java | 6 +- .../TestDataNavigationHandler.java | 24 ++- .../TestDataReferenceCollector.java | 2 +- 5 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 plugins/devkit/src/references/TestDataFilesReferencesContributor.java diff --git a/plugins/devkit/resources/META-INF/plugin.xml b/plugins/devkit/resources/META-INF/plugin.xml index 1bca4d7de9a9..41c8c17c9254 100644 --- a/plugins/devkit/resources/META-INF/plugin.xml +++ b/plugins/devkit/resources/META-INF/plugin.xml @@ -174,6 +174,7 @@ + diff --git a/plugins/devkit/src/references/TestDataFilesReferencesContributor.java b/plugins/devkit/src/references/TestDataFilesReferencesContributor.java new file mode 100644 index 000000000000..55d9c7b8a608 --- /dev/null +++ b/plugins/devkit/src/references/TestDataFilesReferencesContributor.java @@ -0,0 +1,145 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.references; + +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.patterns.PsiJavaElementPattern; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.testFramework.TestDataFile; +import com.intellij.util.PathUtil; +import com.intellij.util.ProcessingContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.devkit.testAssistant.TestDataNavigationHandler; + +import java.util.Collections; +import java.util.List; + +import static com.intellij.patterns.PsiJavaPatterns.psiElement; +import static com.intellij.patterns.PsiJavaPatterns.psiMethod; + +public class TestDataFilesReferencesContributor extends PsiReferenceContributor { + private static final String TEST_DATA_FILE_ANNOTATION_QUALIFIED_NAME = TestDataFile.class.getCanonicalName(); + + @Override + public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { + PsiJavaElementPattern.Capture capture = psiElement().methodCallParameter(psiMethod()); + registrar.registerReferenceProvider(capture, new PsiReferenceProvider() { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + PsiLiteralExpression expression = (PsiLiteralExpression)element; + PsiElement expressionParent = expression.getParent(); + if (!(expressionParent instanceof PsiExpressionList)) { + return PsiReference.EMPTY_ARRAY; + } + PsiExpressionList expressionList = (PsiExpressionList)expressionParent; + PsiExpression[] expressions = expressionList.getExpressions(); + + int index = getExpressionIndex(expression, expressions); + if (index < 0) { + // shouldn't happen + return PsiReference.EMPTY_ARRAY; + } + + PsiMethodCallExpression methodCallExpression = getMethodCallExpression(expression); + PsiParameter targetParameter = getTargetParameter(index, methodCallExpression); + if (!checkTestDataFileAnnotationPresent(targetParameter)) { + return PsiReference.EMPTY_ARRAY; + } + + String testDataFilePath = getTestDataFilePath(expression, methodCallExpression); + if (testDataFilePath == null) { + return PsiReference.EMPTY_ARRAY; + } + + String directory = PathUtil.getParentPath(testDataFilePath); + FileReferenceSet fileReferenceSet = new FileReferenceSet(element); + fileReferenceSet.addCustomization( + FileReferenceSet.DEFAULT_PATH_EVALUATOR_OPTION, + ignore -> { + VirtualFile file = LocalFileSystem.getInstance().findFileByPath(directory); + return file == null ? null : Collections.singleton(element.getManager().findDirectory(file)); + }); + + return fileReferenceSet.getAllReferences(); + } + }); + } + + private static int getExpressionIndex(@NotNull PsiLiteralExpression expression, @NotNull PsiExpression[] expressions) { + int index = -1; + for (int i = 0; i < expressions.length; i++) { + PsiExpression e = expressions[i]; + if (expression.equals(e)) { + index = i; + break; + } + } + return index; + } + + @Nullable + private static PsiMethodCallExpression getMethodCallExpression(@NotNull PsiLiteralExpression expression) { + PsiElement expressionContext = expression.getContext(); + if (expressionContext == null) { + return null; + } + PsiElement methodCallExpression = expressionContext.getContext(); + if (!(methodCallExpression instanceof PsiMethodCallExpression)) { + return null; + } + return (PsiMethodCallExpression)methodCallExpression; + } + + @Nullable + private static PsiParameter getTargetParameter(int index, @Nullable PsiMethodCallExpression methodCallExpression) { + if (methodCallExpression == null) { + return null; + } + PsiMethod callMethod = methodCallExpression.resolveMethod(); + if (callMethod == null) { + return null; + } + + PsiParameter[] parameters = callMethod.getParameterList().getParameters(); + // index may be greater than parameters length in case of varargs + return index >= parameters.length ? parameters[parameters.length - 1] : parameters[index]; + } + + private static boolean checkTestDataFileAnnotationPresent(@Nullable PsiParameter targetParameter) { + if (targetParameter == null) { + return false; + } + PsiAnnotation[] annotations = targetParameter.getAnnotations(); + for (PsiAnnotation annotation : annotations) { + if (TEST_DATA_FILE_ANNOTATION_QUALIFIED_NAME.equals(annotation.getQualifiedName())) { + return true; + } + } + return false; + } + + @Nullable + private static String getTestDataFilePath(@NotNull PsiLiteralExpression expression, + @NotNull PsiMethodCallExpression methodCallExpression) { + Object value = expression.getValue(); + if (!(value instanceof String)) { + return null; + } + + String relativePath = (String)value; + PsiMethod testMethod = PsiTreeUtil.getParentOfType(methodCallExpression, PsiMethod.class); + if (testMethod == null) { + return null; + } + + List filePaths = TestDataNavigationHandler.fastGetTestDataPathsByRelativePath(relativePath, testMethod); + if (filePaths.size() != 1) { + return null; + } + return filePaths.get(0); + } +} diff --git a/plugins/devkit/src/testAssistant/TestDataGuessByExistingFilesUtil.java b/plugins/devkit/src/testAssistant/TestDataGuessByExistingFilesUtil.java index de171ed7d751..3c8ed104fda6 100644 --- a/plugins/devkit/src/testAssistant/TestDataGuessByExistingFilesUtil.java +++ b/plugins/devkit/src/testAssistant/TestDataGuessByExistingFilesUtil.java @@ -74,11 +74,11 @@ public class TestDataGuessByExistingFilesUtil { * @return collection of paths to the test data files for the given test if it's possible to guess them; * {@code null} otherwise */ - @Nullable + @NotNull static List collectTestDataByExistingFiles(@NotNull PsiMethod psiMethod) { TestDataDescriptor descriptor = buildDescriptorFromExistingTestData(psiMethod); if (descriptor == null || !descriptor.isComplete()) { - return null; + return Collections.emptyList(); } return descriptor.generate(); } @@ -108,7 +108,7 @@ public class TestDataGuessByExistingFilesUtil { @Nullable private static String getFilePath(PsiMethod psiMethod, String testName) { List strings = collectTestDataByExistingFiles(psiMethod); - if (strings != null && !strings.isEmpty()) { + if (!strings.isEmpty()) { String s = strings.get(0); return new File(new File(s).getParent(), testName + "." + FileUtilRt.getExtension(new File(s).getName())).getPath(); } diff --git a/plugins/devkit/src/testAssistant/TestDataNavigationHandler.java b/plugins/devkit/src/testAssistant/TestDataNavigationHandler.java index b0683a110f0d..50f68a99be93 100644 --- a/plugins/devkit/src/testAssistant/TestDataNavigationHandler.java +++ b/plugins/devkit/src/testAssistant/TestDataNavigationHandler.java @@ -26,35 +26,43 @@ import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.JBList; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.testAssistant.vfs.TestDataGroupVirtualFile; import javax.swing.*; import java.awt.event.MouseEvent; import java.util.*; +import java.util.stream.Collectors; public class TestDataNavigationHandler implements GutterIconNavigationHandler { @Override public void navigate(MouseEvent e, PsiMethod elt) { List fileNames = getFileNames(elt); - if (fileNames == null || fileNames.isEmpty()) { + if (fileNames.isEmpty()) { return; } navigate(new RelativePoint(e), fileNames, elt.getProject()); } - @Nullable + @NotNull static List getFileNames(PsiMethod method) { + return getFileNames(method, true); + } + + @NotNull + static List getFileNames(PsiMethod method, boolean collectByExistingFiles) { List fileNames = null; String testDataPath = TestDataLineMarkerProvider.getTestDataBasePath(method.getContainingClass()); if (testDataPath != null) { fileNames = new TestDataReferenceCollector(testDataPath, method.getName().substring(4)).collectTestDataReferences(method); } - if (fileNames == null || fileNames.isEmpty()) { + if (collectByExistingFiles && (fileNames == null || fileNames.isEmpty())) { fileNames = TestDataGuessByExistingFilesUtil.collectTestDataByExistingFiles(method); } + if (fileNames == null) { + fileNames = Collections.emptyList(); + } return fileNames; } @@ -75,6 +83,14 @@ public class TestDataNavigationHandler implements GutterIconNavigationHandler fastGetTestDataPathsByRelativePath(@NotNull String testDataFileRelativePath, PsiMethod method) { + return getFileNames(method, false).stream() + .filter(path -> path.endsWith(testDataFileRelativePath.startsWith("/") ? testDataFileRelativePath : "/" + testDataFileRelativePath)) + .distinct() + .collect(Collectors.toList()); + } + /** * Shows navigation popup with list of testdata files and (optionally) "Create missing files" option. * @param project project. diff --git a/plugins/devkit/src/testAssistant/TestDataReferenceCollector.java b/plugins/devkit/src/testAssistant/TestDataReferenceCollector.java index dfcfe9a2041f..41287b1c1dc4 100644 --- a/plugins/devkit/src/testAssistant/TestDataReferenceCollector.java +++ b/plugins/devkit/src/testAssistant/TestDataReferenceCollector.java @@ -54,7 +54,7 @@ public class TestDataReferenceCollector { myTestName = testName; } - @Nullable + @NotNull List collectTestDataReferences(@NotNull final PsiMethod method) { myContainingClass = method.getContainingClass(); List result = collectTestDataReferences(method, new HashMap<>(), new HashSet<>());