diff --git a/org.moreunit.plugin/src/org/moreunit/matching/CorrespondingTypeSearcher.java b/org.moreunit.plugin/src/org/moreunit/matching/CorrespondingTypeSearcher.java index f820d1cc..4b8ccc41 100644 --- a/org.moreunit.plugin/src/org/moreunit/matching/CorrespondingTypeSearcher.java +++ b/org.moreunit.plugin/src/org/moreunit/matching/CorrespondingTypeSearcher.java @@ -3,11 +3,19 @@ import static java.util.Collections.emptySet; import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.moreunit.log.LogHandler; import org.moreunit.preferences.Preferences; @@ -26,6 +34,7 @@ */ public class CorrespondingTypeSearcher { + private final IType type; private final ProjectPreferences preferences; private final ClassNameEvaluation nameEvaluation; private final IJavaSearchScope searchScope; @@ -34,11 +43,12 @@ public class CorrespondingTypeSearcher public CorrespondingTypeSearcher(ICompilationUnit compilationUnit, Preferences preferences) { + this.type = compilationUnit.findPrimaryType(); this.preferences = preferences.getProjectView(compilationUnit.getJavaProject()); - nameEvaluation = this.preferences.getTestClassNamePattern().evaluate(compilationUnit.findPrimaryType()); + nameEvaluation = this.preferences.getTestClassNamePattern().evaluate(this.type); IPackageFragmentRoot sourceFolder = nameEvaluation.isTestCase() ? preferences.getTestSourceFolder(compilationUnit.getJavaProject(), PluginTools.getSourceFolder(compilationUnit)) - : preferences.getMainSourceFolder(compilationUnit.getJavaProject(), PluginTools.getSourceFolder(compilationUnit)); + : this.preferences.getMainSourceFolder(PluginTools.getSourceFolder(compilationUnit)); searchScope = SearchScopeSingelton.getInstance().getSearchScope(sourceFolder); } @@ -74,6 +84,109 @@ public Collection getMatches(boolean alsoIncludeLikelyMatches) private Collection findPotentialTargets(boolean withLikelyMatches) throws CoreException { boolean qualifyWithPackage = ! withLikelyMatches; - return SearchTools.searchFor(nameEvaluation.getAllCorrespondingClassPatterns(qualifyWithPackage), searchScope); + Set patterns = new LinkedHashSet<>(nameEvaluation.getAllCorrespondingClassPatterns(qualifyWithPackage)); + + Set matches = SearchTools.searchFor(patterns, searchScope); + + if(matches.size() == 1 && ! matches.iterator().next().isInterface()) + { + return matches; + } + + if(type != null && ! nameEvaluation.isTestCase()) + { + try + { + ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor()); + for (IType superType : hierarchy.getAllSupertypes(type)) + { + if(! superType.getFullyQualifiedName().startsWith("java.lang.")) + { + ClassNameEvaluation superEval = preferences.getTestClassNamePattern().evaluate(superType); + patterns.addAll(superEval.getAllCorrespondingClassPatterns(qualifyWithPackage)); + } + } + + if(type.isInterface() || Flags.isAbstract(type.getFlags())) + { + IType[] subtypes = hierarchy.getAllSubtypes(type); + for (IType subType : subtypes) + { + if(! Flags.isAbstract(subType.getFlags()) && ! subType.isInterface()) + { + ClassNameEvaluation subEval = preferences.getTestClassNamePattern().evaluate(subType); + patterns.addAll(subEval.getAllCorrespondingClassPatterns(qualifyWithPackage)); + } + } + } + } + catch (JavaModelException e) + { + LogHandler.getInstance().handleExceptionLog(e); + } + } + + matches = SearchTools.searchFor(patterns, searchScope); + + if(nameEvaluation.isTestCase()) + { + Set allMatches = new LinkedHashSet<>(matches); + Set concreteImplementations = new LinkedHashSet<>(); + for (IType match : matches) + { + try + { + if(match.isInterface() || Flags.isAbstract(match.getFlags())) + { + concreteImplementations.addAll(SearchTools.findConcreteSubclasses(match)); + } + } + catch (JavaModelException e) + { + // ignore + } + } + + if(! concreteImplementations.isEmpty()) + { + allMatches.addAll(concreteImplementations); + + for (Iterator it = allMatches.iterator(); it.hasNext();) + { + IType match = it.next(); + try + { + if((match.isInterface() || Flags.isAbstract(match.getFlags())) && ! hasImplementation(match)) + { + it.remove(); + } + } + catch (JavaModelException e) + { + // ignore + } + } + } + + return allMatches; + } + + return matches; + } + + private boolean hasImplementation(IType type) throws JavaModelException + { + for (IMethod method : type.getMethods()) + { + if(Flags.isDefaultMethod(method.getFlags())) + { + return true; + } + if(! Flags.isAbstract(method.getFlags()) && ! type.isInterface()) + { + return true; + } + } + return false; } } diff --git a/org.moreunit.plugin/src/org/moreunit/util/SearchTools.java b/org.moreunit.plugin/src/org/moreunit/util/SearchTools.java index f74c0965..2dc3002b 100644 --- a/org.moreunit.plugin/src/org/moreunit/util/SearchTools.java +++ b/org.moreunit.plugin/src/org/moreunit/util/SearchTools.java @@ -50,7 +50,7 @@ public static Set findConcreteSubclasses(IType type) throws JavaModelExce return concreteSubclasses; } - private static Set search(SearchPattern pattern, IJavaSearchScope scope) throws CoreException + public static Set search(SearchPattern pattern, IJavaSearchScope scope) throws CoreException { SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }; MatchCollector collector = new MatchCollector(); diff --git a/org.moreunit.test/test/org/moreunit/matching/InterfaceCorrespondingTypeSearcherTest.java b/org.moreunit.test/test/org/moreunit/matching/InterfaceCorrespondingTypeSearcherTest.java new file mode 100644 index 00000000..9db951a7 --- /dev/null +++ b/org.moreunit.test/test/org/moreunit/matching/InterfaceCorrespondingTypeSearcherTest.java @@ -0,0 +1,91 @@ +package org.moreunit.matching; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; + +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; +import org.junit.Test; +import org.moreunit.test.context.ContextTestCase; +import org.moreunit.test.context.Preferences; +import org.moreunit.test.context.Project; + +@Preferences(testClassNameTemplate = "${srcFile}Test", testSrcFolder = "test") +public class InterfaceCorrespondingTypeSearcherTest extends ContextTestCase +{ + @Project(mainCls = "class Foo", testCls = "FooTest") + @Test + public void getMatches_should_return_test_for_class() + { + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("Foo"), getPreferences()); + Collection matches = searcher.getMatches(false); + + assertThat(matches).extracting("elementName").containsExactly("FooTest"); + } + + @Project(mainCls = "interface Foo", testCls = "FooTest") + @Test + public void getMatches_should_return_test_for_interface() + { + context.getPrimaryTypeHandler("Foo").createSubclass("FooImpl"); + + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("Foo"), getPreferences()); + Collection matches = searcher.getMatches(false); + + assertThat(matches).extracting("elementName").containsExactly("FooTest"); + } + + @Project(mainCls = "interface Foo", testCls = "FooTest") + @Test + public void getMatches_should_return_implementation_for_interface_test_and_exclude_interface() + { + context.getPrimaryTypeHandler("Foo").createSubclass("FooImpl"); + + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("FooTest"), getPreferences()); + Collection matches = searcher.getMatches(false); + + // Should return FooImpl and EXCLUDE Foo because Foo is a pure interface + assertThat(matches).extracting("elementName").containsExactly("FooImpl"); + } + + @Project(mainCls = "interface Foo", testCls = "FooTest") + @Test + public void getMatches_should_return_interface_test_for_implementation() + { + context.getPrimaryTypeHandler("Foo").createSubclass("FooImpl"); + + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("FooImpl"), getPreferences()); + Collection matches = searcher.getMatches(false); + + assertThat(matches).extracting("elementName").contains("FooTest"); + } + + @Project(mainCls = "interface Foo") + @Test + public void getMatches_should_return_interface_for_implementation_test() + { + context.getPrimaryTypeHandler("Foo").createSubclass("FooImpl"); + context.getProjectHandler().getTestSrcFolderHandler().createClass("FooImplTest"); + + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("FooImplTest"), getPreferences()); + Collection matches = searcher.getMatches(false); + + assertThat(matches).extracting("elementName").containsExactly("FooImpl"); + } + + @Project(mainCls = "interface Foo", testCls = "FooTest") + @Test + public void getMatches_should_include_interface_if_it_has_default_method() throws JavaModelException + { + IType foo = context.getPrimaryTypeHandler("Foo").get(); + foo.createMethod("default void bar() {}", null, true, null); + context.getPrimaryTypeHandler("Foo").createSubclass("FooImpl"); + + CorrespondingTypeSearcher searcher = new CorrespondingTypeSearcher(context.getCompilationUnit("FooTest"), getPreferences()); + Collection matches = searcher.getMatches(false); + + // Should return both Foo and FooImpl because Foo has a default method + assertThat(matches).extracting("elementName").containsExactlyInAnyOrder("Foo", "FooImpl"); + } +}