diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlElementPatternHelper.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlElementPatternHelper.java index b6d792067..9bc4c91a1 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlElementPatternHelper.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlElementPatternHelper.java @@ -126,6 +126,23 @@ public static ElementPattern getSingleLineScalarKey(String... keyNam ); } + public static ElementPattern getNamedArgumentPattern() { + return PlatformPatterns + .psiElement(YAMLTokenTypes.SCALAR_KEY).withText(PlatformPatterns.string().startsWith("$")) + .withParent( + PlatformPatterns.psiElement(YAMLKeyValue.class) + .withParent(PlatformPatterns.psiElement(YAMLMapping.class) + .withParent(PlatformPatterns.psiElement(YAMLKeyValue.class).with(new PatternCondition<>("YAMLKeyValue: with key 'arguments'") { + @Override + public boolean accepts(@NotNull YAMLKeyValue yamlKeyValue, ProcessingContext context) { + return "arguments".equals(yamlKeyValue.getKeyText()); + } + })) + ) + ) + .withLanguage(YAMLLanguage.INSTANCE); + } + public static ElementPattern getSingleLineText() { return PlatformPatterns diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java index 4687d4028..e4810b128 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java @@ -545,6 +545,44 @@ public static Parameter getYamlNamedArgument(@NotNull PsiElement psiElement, @No return null; } + /** + * arguments: ['$foobar': '@foo'] + */ + public static boolean hasMissingYamlNamedArgumentForInspection(@NotNull PsiElement psiElement, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) { + PsiElement context = psiElement.getContext(); + if(context instanceof YAMLKeyValue) { + // arguments: ['$foobar': '@foo'] + + String parameterName = ((YAMLKeyValue) context).getKeyText(); + if(parameterName.startsWith("$") && parameterName.length() > 1) { + PsiElement yamlMapping = context.getParent(); + if(yamlMapping instanceof YAMLMapping) { + PsiElement yamlKeyValue = yamlMapping.getParent(); + if(yamlKeyValue instanceof YAMLKeyValue) { + String keyText = ((YAMLKeyValue) yamlKeyValue).getKeyText(); + if(keyText.equals("arguments")) { + YAMLMapping parentMapping = ((YAMLKeyValue) yamlKeyValue).getParentMapping(); + if(parentMapping != null) { + String serviceId = getServiceClassFromServiceMapping(parentMapping); + if(StringUtils.isNotBlank(serviceId)) { + PhpClass serviceClass = ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), serviceId, lazyServiceCollector); + // class not found don't need a hint + if (serviceClass == null) { + return false; + } + + return PhpElementsUtil.getConstructorParameterArgumentByName(serviceClass, StringUtils.stripStart(parameterName, "$")) == null; + } + } + } + } + } + } + } + + return false; + } + /** * services: * _defaults: diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/ServiceNamedArgumentExistsInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/ServiceNamedArgumentExistsInspection.java new file mode 100644 index 000000000..b0c731b1b --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/ServiceNamedArgumentExistsInspection.java @@ -0,0 +1,61 @@ +package fr.adrienbrault.idea.symfony2plugin.dic.inspection; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper; +import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil; +import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.psi.YAMLKeyValue; +import org.jetbrains.yaml.psi.YAMLMapping; + +/** + * @author Daniel Espendiller + */ +public class ServiceNamedArgumentExistsInspection extends LocalInspectionTool { + public static final String INSPECTION_MESSAGE = "Symfony: named argument does not exists"; + + @NotNull + public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (!Symfony2ProjectComponent.isEnabled(holder.getProject())) { + return super.buildVisitor(holder, isOnTheFly); + } + + return new PsiElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + if (YamlElementPatternHelper.getNamedArgumentPattern().accepts(element)) { + if (isSupportedDefinition(element) && ServiceContainerUtil.hasMissingYamlNamedArgumentForInspection(element, new ContainerCollectionResolver.LazyServiceCollector(element.getProject()))) { + holder.registerProblem(element, INSPECTION_MESSAGE, ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + } + } + + super.visitElement(element); + } + }; + } + + private boolean isSupportedDefinition(@NotNull PsiElement element) { + PsiElement context = element.getContext(); + + if (context instanceof YAMLKeyValue) { + // arguments: ['$foobar': '@foo'] + PsiElement yamlMapping = context.getParent(); + if (yamlMapping instanceof YAMLMapping) { + PsiElement yamlKeyValue = yamlMapping.getParent(); + if (yamlKeyValue instanceof YAMLKeyValue) { + YAMLMapping parentMapping = ((YAMLKeyValue) yamlKeyValue).getParentMapping(); + if (parentMapping != null) { + return parentMapping.getKeyValueByKey("factory") == null; + } + } + } + } + + return true; + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2a117ddb4..a6ae77e98 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -501,6 +501,12 @@ level="WARNING" implementationClass="fr.adrienbrault.idea.symfony2plugin.routing.RouteControllerDeprecatedInspection"/> + + fr.adrienbrault.idea.symfony2plugin.intentions.php.PhpServiceIntention PHP diff --git a/src/main/resources/inspectionDescriptions/ServiceNamedArgumentExistsInspection.html b/src/main/resources/inspectionDescriptions/ServiceNamedArgumentExistsInspection.html new file mode 100644 index 000000000..c09576e0f --- /dev/null +++ b/src/main/resources/inspectionDescriptions/ServiceNamedArgumentExistsInspection.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/ServiceNamedArgumentExistsInspectionTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/ServiceNamedArgumentExistsInspectionTest.java new file mode 100644 index 000000000..db999501a --- /dev/null +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/ServiceNamedArgumentExistsInspectionTest.java @@ -0,0 +1,45 @@ +package fr.adrienbrault.idea.symfony2plugin.tests.dic.inspection; + +import fr.adrienbrault.idea.symfony2plugin.dic.inspection.ServiceNamedArgumentExistsInspection; +import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; + +/** + * @author Daniel Espendiller + */ +public class ServiceNamedArgumentExistsInspectionTest extends SymfonyLightCodeInsightFixtureTestCase { + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + myFixture.copyFileToProject("services.xml"); + } + + public String getTestDataPath() { + return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/fixtures"; + } + + public void testMissingArgumentForYaml() { + assertLocalInspectionContains("foo.yml", + "Foobar\\NamedArgument:\n" + + " arguments:\n" + + " $foobar1: ~", + ServiceNamedArgumentExistsInspection.INSPECTION_MESSAGE + ); + + assertLocalInspectionNotContains("foo.yml", + "Foobar\\UnknownClassNamedArgument:\n" + + " arguments:\n" + + " $foobar: ~", + ServiceNamedArgumentExistsInspection.INSPECTION_MESSAGE + ); + } + + public void testMissingArgumentForFactoryServiceIsNotTriggeredYaml() { + assertLocalInspectionNotContains("foo.yml", + "Foobar\\NamedArgument:\n" + + " factory: ~\n" + + " arguments:\n" + + " $foobar1: ~", + ServiceNamedArgumentExistsInspection.INSPECTION_MESSAGE + ); + } +} diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/fixtures/classes.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/fixtures/classes.php index ef91cf9e8..8a7418fa9 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/fixtures/classes.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/inspection/fixtures/classes.php @@ -14,4 +14,11 @@ class Car { const FOOBAR = null; } + + class NamedArgument + { + public function __construct($foobar) + { + } + } } \ No newline at end of file