diff --git a/README.md b/README.md index f95f93dc..02bfb1e2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This extension provides following features: * Provides correct return type for `Request::getContent()` method based on the `$asResource` parameter. * Provides correct return type for `HeaderBag::get()` method based on the `$first` parameter. * Provides correct return type for `Envelope::all()` method based on the `$stampFqcn` parameter. +* Provides correct return types for `TreeBuilder` and `NodeDefinition` objects. * Notifies you when you try to get an unregistered service from the container. * Notifies you when you try to get a private service from the container. * Optionally correct return types for `InputInterface::getArgument()`, `::getOption`, `::hasArgument`, and `::hasOption`. diff --git a/extension.neon b/extension.neon index 819442f3..5046baa2 100644 --- a/extension.neon +++ b/extension.neon @@ -44,7 +44,8 @@ services: # console resolver - factory: PHPStan\Symfony\ConsoleApplicationResolver - arguments: [%symfony.console_application_loader%] + arguments: + consoleApplicationLoader: %symfony.console_application_loader% # service map symfony.serviceMapFactory: @@ -146,12 +147,57 @@ services: factory: PHPStan\Type\Symfony\InputInterfaceHasOptionDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + # ArrayNodeDefinition::*prototype() return type + - + factory: PHPStan\Type\Symfony\Config\ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # ExprBuilder::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\ExprBuilder + methods: [end] + + # NodeBuilder::*node() return type + - + factory: PHPStan\Type\Symfony\Config\PassParentObjectDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeBuilder + methods: [arrayNode, scalarNode, booleanNode, integerNode, floatNode, enumNode, variableNode] + + # NodeBuilder::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeBuilder + methods: [end] + + # NodeDefinition::children() return type + - + factory: PHPStan\Type\Symfony\Config\PassParentObjectDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeDefinition + methods: [children, validate] + + # NodeDefinition::end() return type + - + factory: PHPStan\Type\Symfony\Config\ReturnParentDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + arguments: + className: Symfony\Component\Config\Definition\Builder\NodeDefinition + methods: [end] + # new TreeBuilder() return type - - factory: PHPStan\Type\Symfony\TreeBuilderDynamicReturnTypeExtension + factory: PHPStan\Type\Symfony\Config\TreeBuilderDynamicReturnTypeExtension tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] # TreeBuilder::getRootNode() return type - - factory: PHPStan\Type\Symfony\TreeBuilderGetRootNodeDynamicReturnTypeExtension + factory: PHPStan\Type\Symfony\Config\TreeBuilderGetRootNodeDynamicReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php new file mode 100644 index 00000000..71e4fb17 --- /dev/null +++ b/src/Type/Symfony/Config/ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension.php @@ -0,0 +1,77 @@ + 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition', + 'scalar' => 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition', + 'boolean' => 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition', + 'integer' => 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition', + 'float' => 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition', + 'array' => 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', + 'enum' => 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition', + ]; + + public function getClass(): string + { + return 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition'; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'prototype' || in_array($methodReflection->getName(), self::PROTOTYPE_METHODS, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $calledOnType = $scope->getType($methodCall->var); + + $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + if ($methodReflection->getName() === 'prototype') { + if (!isset($methodCall->args[0])) { + return $defaultType; + } + + $argStrings = TypeUtils::getConstantStrings($scope->getType($methodCall->args[0]->value)); + if (count($argStrings) === 1 && isset(self::MAPPING[$argStrings[0]->getValue()])) { + $type = $argStrings[0]->getValue(); + + return new ParentObjectType(self::MAPPING[$type], $calledOnType); + } + } + + return new ParentObjectType( + $defaultType->describe(VerbosityLevel::typeOnly()), + $calledOnType + ); + } + +} diff --git a/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php new file mode 100644 index 00000000..5d19b60f --- /dev/null +++ b/src/Type/Symfony/Config/PassParentObjectDynamicReturnTypeExtension.php @@ -0,0 +1,56 @@ +className = $className; + $this->methods = $methods; + } + + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), $this->methods, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $calledOnType = $scope->getType($methodCall->var); + + $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + return new ParentObjectType($defaultType->describe(VerbosityLevel::typeOnly()), $calledOnType); + } + +} diff --git a/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php new file mode 100644 index 00000000..8dd47a90 --- /dev/null +++ b/src/Type/Symfony/Config/ReturnParentDynamicReturnTypeExtension.php @@ -0,0 +1,56 @@ +className = $className; + $this->methods = $methods; + } + + public function getClass(): string + { + return $this->className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), $this->methods, true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $calledOnType = $scope->getType($methodCall->var); + if ($calledOnType instanceof ParentObjectType) { + return $calledOnType->getParent(); + } + + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + +} diff --git a/src/Type/Symfony/TreeBuilderDynamicReturnTypeExtension.php b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php similarity index 94% rename from src/Type/Symfony/TreeBuilderDynamicReturnTypeExtension.php rename to src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php index 56f29d35..25cd12dc 100644 --- a/src/Type/Symfony/TreeBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/Config/TreeBuilderDynamicReturnTypeExtension.php @@ -1,12 +1,13 @@ getType($methodCall->var); + + $defaultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if ($calledOnType instanceof TreeBuilderType) { - return new ObjectType($calledOnType->getRootNodeClassName()); + return new ParentObjectType( + $calledOnType->getRootNodeClassName(), + $calledOnType + ); } - return $methodReflection->getVariants()[0]->getReturnType(); + return $defaultType; } } diff --git a/src/Type/Symfony/Config/ValueObject/ParentObjectType.php b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php new file mode 100644 index 00000000..87435e56 --- /dev/null +++ b/src/Type/Symfony/Config/ValueObject/ParentObjectType.php @@ -0,0 +1,26 @@ +parent = $parent; + } + + public function getParent(): Type + { + return $this->parent; + } + +} diff --git a/src/Type/Symfony/TreeBuilderType.php b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php similarity index 89% rename from src/Type/Symfony/TreeBuilderType.php rename to src/Type/Symfony/Config/ValueObject/TreeBuilderType.php index faf26c6a..bacd0f0d 100644 --- a/src/Type/Symfony/TreeBuilderType.php +++ b/src/Type/Symfony/Config/ValueObject/TreeBuilderType.php @@ -1,6 +1,6 @@ getRootNode(); + $r = $arrayRootNode + ->children() + ->arrayNode('methods') + ->prototype('scalar') + ->validate() + ->ifNotInArray(['one', 'two']) + ->thenInvalid('%s is not a valid method.') + ->end() + ->end() + ->end() + ->end(); + + $this->processFile( + __DIR__ . '/tree_builder.php', + $expression, + $type, + [ + new ArrayNodeDefinitionPrototypeDynamicReturnTypeExtension(), + new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\ExprBuilder', ['end']), + new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeBuilder', ['end']), + new ReturnParentDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeDefinition', ['end']), + new PassParentObjectDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeBuilder', ['arrayNode', 'scalarNode', 'booleanNode', 'integerNode', 'floatNode', 'enumNode', 'variableNode']), + new PassParentObjectDynamicReturnTypeExtension('Symfony\Component\Config\Definition\Builder\NodeDefinition', ['children', 'validate']), + new TreeBuilderGetRootNodeDynamicReturnTypeExtension(), + ], + [new TreeBuilderDynamicReturnTypeExtension()] + ); + } + + /** + * @return \Iterator + */ + public function getProvider(): Iterator + { + yield ['$treeRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\NodeBuilder']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->scalarNode("protocol") + ->end() + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $treeRootNode + ->children() + ->arrayNode("protocols") + ->children() + ->booleanNode("auto_connect") + ->defaultTrue() + ->end() + ->scalarNode("default_connection") + ->defaultValue("default") + ->end() + ->integerNode("positive_value") + ->min(0) + ->end() + ->floatNode("big_value") + ->max(5E45) + ->end() + ->enumNode("delivery") + ->values(["standard", "expedited", "priority"]) + ->end() + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + + yield ['$arrayRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield ['$arrayRootNode->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + yield [' + $arrayRootNode + ->children() + ->arrayNode("methods") + ->prototype("scalar") + ->defaultNull() + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $arrayRootNode + ->children() + ->arrayNode("methods") + ->scalarPrototype() + ->defaultNull() + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + yield [' + $arrayRootNode + ->children() + ->arrayNode("methods") + ->prototype("scalar") + ->validate() + ->ifNotInArray(["one", "two"]) + ->thenInvalid("%s is not a valid method.") + ->end() + ->end() + ->end() + ->end() + ', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; + + yield ['$variableRootNode', 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition']; + yield ['$variableRootNode->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + + yield ['$scalarRootNode', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; + yield ['$scalarRootNode->defaultValue("default")', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; + yield ['$scalarRootNode->defaultValue("default")->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + + yield ['$booleanRootNode', 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition']; + yield ['$booleanRootNode->defaultTrue()', 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition']; + yield ['$booleanRootNode->defaultTrue()->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + + yield ['$integerRootNode', 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition']; + yield ['$integerRootNode->min(0)', 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition']; + yield ['$integerRootNode->min(0)->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + + yield ['$floatRootNode', 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition']; + yield ['$floatRootNode->max(5E45)', 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition']; + yield ['$floatRootNode->max(5E45)->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + + yield ['$enumRootNode', 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition']; + yield ['$enumRootNode->values(["standard", "expedited", "priority"])', 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition']; + yield ['$enumRootNode->values(["standard", "expedited", "priority"])->end()', 'Symfony\Component\Config\Definition\Builder\TreeBuilder']; + } + +} diff --git a/tests/Type/Symfony/tree_builder.php b/tests/Type/Symfony/Config/tree_builder.php similarity index 100% rename from tests/Type/Symfony/tree_builder.php rename to tests/Type/Symfony/Config/tree_builder.php index 24d60d71..f1126608 100644 --- a/tests/Type/Symfony/tree_builder.php +++ b/tests/Type/Symfony/Config/tree_builder.php @@ -5,6 +5,9 @@ $treeBuilder = new TreeBuilder('my_tree'); $treeRootNode = $treeBuilder->getRootNode(); +$arrayTreeBuilder = new TreeBuilder('my_tree', 'array'); +$arrayRootNode = $arrayTreeBuilder->getRootNode(); + $variableTreeBuilder = new TreeBuilder('my_tree', 'variable'); $variableRootNode = $variableTreeBuilder->getRootNode(); @@ -20,9 +23,6 @@ $floatTreeBuilder = new TreeBuilder('my_tree', 'float'); $floatRootNode = $floatTreeBuilder->getRootNode(); -$arrayTreeBuilder = new TreeBuilder('my_tree', 'array'); -$arrayRootNode = $arrayTreeBuilder->getRootNode(); - $enumTreeBuilder = new TreeBuilder('my_tree', 'enum'); $enumRootNode = $enumTreeBuilder->getRootNode(); diff --git a/tests/Type/Symfony/ExtensionTestCase.php b/tests/Type/Symfony/ExtensionTestCase.php index 26a805a6..21b95b69 100644 --- a/tests/Type/Symfony/ExtensionTestCase.php +++ b/tests/Type/Symfony/ExtensionTestCase.php @@ -84,7 +84,7 @@ function (Node $node, Scope $scope) use ($expression, $type, &$run): void { } /** @var \PhpParser\Node\Stmt\Expression $expNode */ $expNode = $this->getParser()->parseString(sprintf('getType($expNode->expr)->describe(VerbosityLevel::typeOnly())); + self::assertSame($type, $scope->getType($expNode->expr)->describe(VerbosityLevel::typeOnly()), sprintf('Expression "%s"', $expression)); $run = true; } ); diff --git a/tests/Type/Symfony/TreeBuilderDynamicReturnTypeExtensionTest.php b/tests/Type/Symfony/TreeBuilderDynamicReturnTypeExtensionTest.php deleted file mode 100644 index bbb90af6..00000000 --- a/tests/Type/Symfony/TreeBuilderDynamicReturnTypeExtensionTest.php +++ /dev/null @@ -1,39 +0,0 @@ -processFile( - __DIR__ . '/tree_builder.php', - $expression, - $type, - [new TreeBuilderGetRootNodeDynamicReturnTypeExtension()], - [new TreeBuilderDynamicReturnTypeExtension()] - ); - } - - /** - * @return \Iterator - */ - public function getProvider(): Iterator - { - yield ['$treeRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield ['$variableRootNode', 'Symfony\Component\Config\Definition\Builder\VariableNodeDefinition']; - yield ['$scalarRootNode', 'Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition']; - yield ['$booleanRootNode', 'Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition']; - yield ['$integerRootNode', 'Symfony\Component\Config\Definition\Builder\IntegerNodeDefinition']; - yield ['$floatRootNode', 'Symfony\Component\Config\Definition\Builder\FloatNodeDefinition']; - yield ['$arrayRootNode', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition']; - yield ['$enumRootNode', 'Symfony\Component\Config\Definition\Builder\EnumNodeDefinition']; - } - -}

AltStyle によって変換されたページ (->オリジナル) /