From 9791a8b90f5d76abc40f06fa0c21ca7ddbfb49a7 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Sun, 7 May 2017 12:24:38 +0200 Subject: [PATCH] refactoring of ObjectManager::findBy* and support more possible repository usages #925 #898 --- META-INF/plugin.xml | 1 + .../SymfonyPhpReferenceContributor.java | 36 ----- .../doctrine/ModelFieldReference.java | 60 --------- ...RepositoryFindGotoCompletionRegistrar.java | 124 ++++++++++++++++++ .../metadata/util/DoctrineMetadataUtil.java | 26 ++++ .../util/EventDispatcherTypeProvider.java | 3 +- .../symfony2plugin/util/PhpElementsUtil.java | 26 ++++ .../config/ServiceLineMarkerProviderTest.java | 2 +- .../SymfonyPhpReferenceContributorTest.java | 30 +---- .../fixtures/ServiceLineMarkerProvider.php | 9 ++ ...sitoryFindGotoCompletionRegistrarTest.java | 87 ++++++++++++ ...tRepositoryFindGotoCompletionRegistrar.php | 62 +++++++++ .../tests/util/PhpElementsUtilTest.java | 27 ++++ 13 files changed, 365 insertions(+), 128 deletions(-) delete mode 100644 src/fr/adrienbrault/idea/symfony2plugin/doctrine/ModelFieldReference.java create mode 100644 src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/ObjectRepositoryFindGotoCompletionRegistrar.java create mode 100644 tests/fr/adrienbrault/idea/symfony2plugin/tests/config/fixtures/ServiceLineMarkerProvider.php create mode 100644 tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/ObjectRepositoryFindGotoCompletionRegistrarTest.java create mode 100644 tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/fixtures/ObjectRepositoryFindGotoCompletionRegistrar.php diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml index c04180031..c37c9de34 100644 --- a/META-INF/plugin.xml +++ b/META-INF/plugin.xml @@ -662,6 +662,7 @@ + diff --git a/src/fr/adrienbrault/idea/symfony2plugin/config/SymfonyPhpReferenceContributor.java b/src/fr/adrienbrault/idea/symfony2plugin/config/SymfonyPhpReferenceContributor.java index 515acca00..41ae1efbe 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/config/SymfonyPhpReferenceContributor.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/config/SymfonyPhpReferenceContributor.java @@ -4,14 +4,12 @@ import com.intellij.psi.*; import com.intellij.util.ProcessingContext; import com.jetbrains.php.lang.PhpLanguage; -import com.jetbrains.php.lang.parser.PhpElementTypes; import com.jetbrains.php.lang.psi.elements.*; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.dic.ConstraintPropertyReference; import fr.adrienbrault.idea.symfony2plugin.dic.ServiceReference; import fr.adrienbrault.idea.symfony2plugin.doctrine.EntityHelper; import fr.adrienbrault.idea.symfony2plugin.doctrine.EntityReference; -import fr.adrienbrault.idea.symfony2plugin.doctrine.ModelFieldReference; import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineTypes; import fr.adrienbrault.idea.symfony2plugin.templating.TemplateReference; import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; @@ -20,8 +18,6 @@ import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import org.jetbrains.annotations.NotNull; -import java.util.Collection; - /** * @author Daniel Espendiller */ @@ -151,38 +147,6 @@ public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @No } ); - psiReferenceRegistrar.registerReferenceProvider( - // @TODO: implement global pattern for array parameters - PlatformPatterns.psiElement(StringLiteralExpression.class).withParent( - PlatformPatterns.or( - PlatformPatterns.psiElement(PhpElementTypes.ARRAY_VALUE), - PlatformPatterns.psiElement(PhpElementTypes.ARRAY_KEY) - ) - ).inside(PlatformPatterns.psiElement(ParameterList.class)), - new PsiReferenceProvider() { - @NotNull - @Override - public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { - - MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.ArrayParameterMatcher(psiElement, 0) - .withSignature("\\Doctrine\\Common\\Persistence\\ObjectRepository", "findOneBy") - .withSignature("\\Doctrine\\Common\\Persistence\\ObjectRepository", "findBy") - .match(); - - if(methodMatchParameter == null) { - return new PsiReference[0]; - } - - Collection phpClasses = PhpElementsUtil.getClassFromPhpTypeSetArrayClean(psiElement.getProject(), methodMatchParameter.getMethodReference().getType().getTypes()); - if(phpClasses.size() == 0) { - return new PsiReference[0]; - } - - return new PsiReference[]{ new ModelFieldReference((StringLiteralExpression) psiElement, phpClasses)}; - } - } - ); - psiReferenceRegistrar.registerReferenceProvider( PlatformPatterns.psiElement(StringLiteralExpression.class), new PsiReferenceProvider() { diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ModelFieldReference.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ModelFieldReference.java deleted file mode 100644 index 5da0c716c..000000000 --- a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/ModelFieldReference.java +++ /dev/null @@ -1,60 +0,0 @@ -package fr.adrienbrault.idea.symfony2plugin.doctrine; - -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiElementResolveResult; -import com.intellij.psi.PsiPolyVariantReferenceBase; -import com.intellij.psi.ResolveResult; -import com.jetbrains.php.lang.psi.elements.PhpClass; -import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; -import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelField; -import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelFieldLookupElement; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * @author Daniel Espendiller - */ -public class ModelFieldReference extends PsiPolyVariantReferenceBase { - - private Collection phpClasses; - private String fieldName; - - public ModelFieldReference(StringLiteralExpression psiElement, Collection phpClasses) { - super(psiElement); - this.phpClasses = phpClasses; - this.fieldName = psiElement.getContents(); - } - - @NotNull - @Override - public ResolveResult[] multiResolve(boolean incompleteCode) { - List results = new ArrayList<>(); - - for(PhpClass phpClass: phpClasses) { - for(PsiElement psiElement: EntityHelper.getModelFieldTargets(phpClass, fieldName)) { - results.add(new PsiElementResolveResult(psiElement)); - } - } - - return results.toArray(new ResolveResult[results.size()]); - } - - @NotNull - @Override - public Object[] getVariants() { - List results = new ArrayList<>(); - - for(PhpClass phpClass: phpClasses) { - for(DoctrineModelField field: EntityHelper.getModelFields(phpClass)) { - results.add(new DoctrineModelFieldLookupElement(field)); - } - } - - return results.toArray(); - } - -} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/ObjectRepositoryFindGotoCompletionRegistrar.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/ObjectRepositoryFindGotoCompletionRegistrar.java new file mode 100644 index 000000000..83b0c92ac --- /dev/null +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/ObjectRepositoryFindGotoCompletionRegistrar.java @@ -0,0 +1,124 @@ +package fr.adrienbrault.idea.symfony2plugin.doctrine.metadata; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.elements.PhpExpression; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.jetbrains.php.lang.psi.resolve.types.PhpType; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter; +import fr.adrienbrault.idea.symfony2plugin.codeInsight.utils.GotoCompletionUtil; +import fr.adrienbrault.idea.symfony2plugin.doctrine.EntityHelper; +import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelField; +import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelFieldLookupElement; +import fr.adrienbrault.idea.symfony2plugin.doctrine.dict.DoctrineModelInterface; +import fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.util.DoctrineMetadataUtil; +import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; +import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Daniel Espendiller + */ +public class ObjectRepositoryFindGotoCompletionRegistrar implements GotoCompletionRegistrar { + public void register(GotoCompletionRegistrarParameter registrar) { + + // "@var $om \Doctrine\Common\Persistence\ObjectManager" + // "$om->getRepository('Foo\Bar')->" + s + "(['foo' => 'foo', '' => 'foo'])" + registrar.register(PhpElementsUtil.getParameterListArrayValuePattern(), psiElement -> { + PsiElement context = psiElement.getContext(); + if (!(context instanceof StringLiteralExpression)) { + return null; + } + + MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.ArrayParameterMatcher(context, 0) + .withSignature("\\Doctrine\\Common\\Persistence\\ObjectRepository", "findOneBy") + .withSignature("\\Doctrine\\Common\\Persistence\\ObjectRepository", "findBy") + .match(); + + if(methodMatchParameter != null) { + MethodReference methodReference = methodMatchParameter.getMethodReference(); + + // extract from type provide on completion: + // $foo->getRepository('MODEL')->findBy() + Collection phpClasses = PhpElementsUtil.getClassFromPhpTypeSetArrayClean(psiElement.getProject(), methodReference.getType().getTypes()); + + // resolve every direct repository instance $this->findBy() + // or direct repository instance $repository->findBy() + if(phpClasses.size() == 0) { + PhpExpression classReference = methodReference.getClassReference(); + if(classReference != null) { + PhpType type = classReference.getType(); + for (String s : type.getTypes()) { + // dont visit type providers + if(PhpType.isUnresolved(s)) { + continue; + } + + for (DoctrineModelInterface doctrineModel : DoctrineMetadataUtil.findMetadataModelForRepositoryClass(psiElement.getProject(), s)) { + phpClasses.addAll(PhpElementsUtil.getClassesInterface(psiElement.getProject(), doctrineModel.getClassName())); + } + } + } + } + + if(phpClasses.size() == 0) { + return null; + } + + return new MyArrayFieldMetadataGotoCompletionRegistrar(psiElement, phpClasses); + } + + return null; + }); + } + + private static class MyArrayFieldMetadataGotoCompletionRegistrar extends GotoCompletionProvider { + @NotNull + private final Collection phpClasses; + + MyArrayFieldMetadataGotoCompletionRegistrar(@NotNull PsiElement element, @NotNull Collection phpClasses) { + super(element); + this.phpClasses = phpClasses; + } + + @NotNull + @Override + public Collection getLookupElements() { + List results = new ArrayList<>(); + + phpClasses.forEach(phpClass -> + results.addAll(EntityHelper.getModelFields(phpClass).stream() + .map((Function) DoctrineModelFieldLookupElement::new) + .collect(Collectors.toList()) + ) + ); + + return results; + } + + @NotNull + @Override + public Collection getPsiTargets(PsiElement element) { + String content = GotoCompletionUtil.getTextValueForElement(element); + if(content == null) { + return Collections.emptyList(); + } + + Collection results = new ArrayList<>(); + + phpClasses.forEach(phpClass -> + results.addAll(Arrays.asList(EntityHelper.getModelFieldTargets(phpClass, content))) + ); + + return results; + } + } +} diff --git a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/util/DoctrineMetadataUtil.java b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/util/DoctrineMetadataUtil.java index 5e4a2a7ca..70b09a68e 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/util/DoctrineMetadataUtil.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/doctrine/metadata/util/DoctrineMetadataUtil.java @@ -136,6 +136,7 @@ public static Collection findMetadataForRepositoryClass(final @NotN project.putUserData(DOCTRINE_REPOSITORY_CACHE, cache); } + repositoryClass = StringUtils.stripStart(repositoryClass,"\\"); if(!cache.getValue().containsKey(repositoryClass)) { return Collections.emptyList(); } @@ -151,6 +152,31 @@ public static Collection findMetadataForRepositoryClass(final @NotN return virtualFiles; } + /** + * Find metadata model in which the given repository class is used + * eg "@ORM\Entity(repositoryClass="FOOBAR")", xml or yaml + */ + @NotNull + public static Collection findMetadataModelForRepositoryClass(final @NotNull Project project, @NotNull String repositoryClass) { + repositoryClass = StringUtils.stripStart(repositoryClass,"\\"); + + Collection models = new ArrayList<>(); + + for (String key : FileIndexCaches.getIndexKeysCache(project, CLASS_KEYS, DoctrineMetadataFileStubIndex.KEY)) { + for (DoctrineModelInterface repositoryDefinition : FileBasedIndex.getInstance().getValues(DoctrineMetadataFileStubIndex.KEY, key, GlobalSearchScope.allScope(project))) { + String myRepositoryClass = repositoryDefinition.getRepositoryClass(); + if(StringUtils.isBlank(myRepositoryClass) || + !repositoryClass.equalsIgnoreCase(StringUtils.stripStart(myRepositoryClass, "\\"))) { + continue; + } + + models.add(repositoryDefinition); + } + } + + return models; + } + @NotNull public static Collection> getTables(@NotNull Project project) { diff --git a/src/fr/adrienbrault/idea/symfony2plugin/util/EventDispatcherTypeProvider.java b/src/fr/adrienbrault/idea/symfony2plugin/util/EventDispatcherTypeProvider.java index cdaed196b..6b47a815a 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/util/EventDispatcherTypeProvider.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/util/EventDispatcherTypeProvider.java @@ -30,8 +30,7 @@ public char getKey() { @Nullable @Override public PhpType getType(PsiElement e) { - - if (DumbService.getInstance(e.getProject()).isDumb() || !Settings.getInstance(e.getProject()).pluginEnabled || !Settings.getInstance(e.getProject()).symfonyContainerTypeProvider) { + if (!Settings.getInstance(e.getProject()).pluginEnabled || !Settings.getInstance(e.getProject()).symfonyContainerTypeProvider) { return null; } diff --git a/src/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java b/src/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java index 55514c614..60e78369f 100644 --- a/src/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java +++ b/src/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java @@ -1389,6 +1389,32 @@ public static Set getVariablesInScope(@NotNull PsiElement psiElement, return MyVariableRecursiveElementVisitor.visit(psiElement, name); } + /** + * Provide array key pattern. we need incomplete array key support, too. + * + * foo(['']) + * foo(['' => 'foobar']) + */ + @NotNull + public static PsiElementPattern.Capture getParameterListArrayValuePattern() { + return PlatformPatterns.psiElement() + .withParent(PlatformPatterns.psiElement(StringLiteralExpression.class).withParent( + PlatformPatterns.or( + PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_VALUE) + .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class) + .withParent(ParameterList.class) + ), + + PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_KEY) + .withParent(PlatformPatterns.psiElement(ArrayHashElement.class) + .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class) + .withParent(ParameterList.class) + ) + ) + )) + ); + } + /** * Visit and collect all variables in given scope */ diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/ServiceLineMarkerProviderTest.java b/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/ServiceLineMarkerProviderTest.java index 2adc286a5..e319df5e7 100644 --- a/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/ServiceLineMarkerProviderTest.java +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/ServiceLineMarkerProviderTest.java @@ -28,7 +28,7 @@ public class ServiceLineMarkerProviderTest extends SymfonyLightCodeInsightFixtur public void setUp() throws Exception { super.setUp(); - myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("SymfonyPhpReferenceContributor.php")); + myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("ServiceLineMarkerProvider.php")); } public String getTestDataPath() { diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/SymfonyPhpReferenceContributorTest.java b/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/SymfonyPhpReferenceContributorTest.java index dddcd339b..6236fdc64 100644 --- a/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/SymfonyPhpReferenceContributorTest.java +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/config/SymfonyPhpReferenceContributorTest.java @@ -14,42 +14,14 @@ public class SymfonyPhpReferenceContributorTest extends SymfonyLightCodeInsightF public void setUp() throws Exception { super.setUp(); - myFixture.copyFileToProject("SymfonyPhpReferenceContributor.php"); myFixture.copyFileToProject("services.xml"); + myFixture.copyFileToProject("ServiceLineMarkerProvider.php"); } public String getTestDataPath() { return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath(); } - /** - * @see fr.adrienbrault.idea.symfony2plugin.doctrine.ModelFieldReference - */ - public void testModelFieldReference() { - for (String s : new String[]{"findBy", "findOneBy"}) { - assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "([''])", - "phonenumbers", "email" - ); - - assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['foo', '' => 'foo'])", - "phonenumbers", "email" - ); - - assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['foo' => 'foo', '' => 'foo'])", - "phonenumbers", "email" - ); - - // migrate: @TODO: fr.adrienbrault.idea.symfony2plugin.doctrine.ModelFieldReference.multiResolve() - // add navigation testing - } - } - public void testThatPrivateServiceAreNotInCompletionListForContainerGet() { assertCompletionContains(PhpFileType.INSTANCE, " + * @see fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.ObjectRepositoryFindGotoCompletionRegistrar + */ +public class ObjectRepositoryFindGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase { + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("ObjectRepositoryFindGotoCompletionRegistrar.php"); + } + + public String getTestDataPath() { + return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath(); + } + + public void testThatCompletionForDoctrineMetadataInArrayIsProvided() { + for (String s : new String[]{"findBy", "findOneBy"}) { + assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "([''])", + "phonenumbers", "email" + ); + + assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['foo', '' => 'foo'])", + "phonenumbers", "email" + ); + + assertCompletionContains(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['foo' => 'foo', '' => 'foo'])", + "phonenumbers", "email" + ); + } + } + + public void testThatNavigationForDoctrineMetadataInArrayIsProvided() { + for (String s : new String[]{"findBy", "findOneBy"}) { + assertNavigationMatch(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['phonenumbers'])", + PlatformPatterns.psiElement() + ); + + assertNavigationMatch(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['foo', 'phonenumbers' => 'foo'])", + PlatformPatterns.psiElement() + ); + + assertNavigationMatch(PhpFileType.INSTANCE, "getRepository('Foo\\Bar')->" + s + "(['phonenumbers'])", + PlatformPatterns.psiElement() + ); + } + } + + public void testThatRepositoryIsResolved() { + assertCompletionContains(PhpFileType.INSTANCE, "findBy([''])", + "phonenumbers", "email" + ); + + assertCompletionContains(PhpFileType.INSTANCE, "findBy([''])"+ + " }\n" + + "}\n" + + "phonenumbers", "email" + ); + } +} diff --git a/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/fixtures/ObjectRepositoryFindGotoCompletionRegistrar.php b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/fixtures/ObjectRepositoryFindGotoCompletionRegistrar.php new file mode 100644 index 000000000..2d8c09b23 --- /dev/null +++ b/tests/fr/adrienbrault/idea/symfony2plugin/tests/doctrine/metadata/fixtures/ObjectRepositoryFindGotoCompletionRegistrar.php @@ -0,0 +1,62 @@ +modify()".equals(variable.getParent().getText())) ); } + + /** + * @see fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil#getParameterListArrayValuePattern + */ + public void testGetParameterListArrayValuePattern() { + String[] strings = { + "foo(['']", + "foo(['' => 'foo']", + "foo(['foo' => null, '' => null]" + }; + + for (String s : strings) { + myFixture.configureByText(PhpFileType.INSTANCE, " ''])" + ); + + assertFalse( + PhpElementsUtil.getParameterListArrayValuePattern().accepts(myFixture.getFile().findElementAt(myFixture.getCaretOffset())) + ); + } }