diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..504e67d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/vendor/ +/resources/ +/assets/ +/app/ +.DS_Store +.phpunit.result.cache +composer.lock diff --git a/.travis.yml b/.travis.yml index 7bbd9ee..821d3fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,19 @@ language: php +php: + - 7.1 + - 7.2 + - 7.3 env: global: - COMPOSER_ROOT_VERSION=5.0.x-dev - SS_BASE_URL="https://localhost/" + - DB=MYSQL + matrix: + - RECIPE_VERSION=4.3.x-dev + - RECIPE_VERSION=4.4.x-dev + -matrix: - include: - # NOTE(Jake): 2018-05-16 - # - # Running in PHP 7.0 doesn't seem to work. - # - # - php: 7.0 - # env: - # - DB=MYSQL - # - RECIPE_VERSION=1.0.x-dev - - php: 7.1 - env: - - DB=MYSQL - - RECIPE_VERSION=1.1.x-dev - - php: 7.2 - env: - - DB=MYSQL - - RECIPE_VERSION=1.2.x-dev before_script: - phpenv rehash diff --git a/README.md b/README.md index f8181b2..b946bd8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ composer require --dev symbiote/silverstripe-phpstan:1.0.0 phpstan/phpstan-shim: SilverStripe 4.X ``` -composer require --dev symbiote/silverstripe-phpstan:2.0.0 phpstan/phpstan-shim:0.9.2 +composer require --dev symbiote/silverstripe-phpstan:2.0.0 phpstan/phpstan-shim:~0.11.0 ``` NOTE: We recommend installing the phpstan-shim as currently in SilverStripe 3.X, the QueuedJobs module's dependence on superclosure forces the PHP-Parser dependency of PHPStan to be at a very outdated version. diff --git a/composer.json b/composer.json index 786183e..f30c896 100644 --- a/composer.json +++ b/composer.json @@ -12,19 +12,21 @@ ], "license": "BSD-3-Clause", "authors": [ - { - "name": "Jake Bentvelzen", - "email": "jake@symbiote.com.au" - }], + { + "name": "Jake Bentvelzen", + "email": "jake@symbiote.com.au" + } + ], "require": { - "php": "~7.0", - "silverstripe/framework": "~4.0", + "php": "~7.1", + "silverstripe/framework": "~4.3", "silverstripe/vendor-plugin": "^1.0" }, "require-dev": { "squizlabs/php_codesniffer": "^3.0", - "phpstan/phpstan": "~0.9.0", - "phpstan/phpstan-phpunit": "~0.9.0" + "phpstan/phpstan": "~0.11.0", + "phpstan/phpstan-phpunit": "~0.11.0", + "phpunit/phpunit": "^7.5.14 || ^8.0" }, "scripts": { "phpcs": "phpcs -n -l src/ src/Reflection/ src/Rule/ src/Type tests/ tests/Reflection/ tests/Rule/ tests/Type/", @@ -33,7 +35,7 @@ "phpstan": "bash ../../../vendor/bin/phpstan analyse src/ tests/ -c \"tests/phpstan.neon\" -a \"tests/bootstrap-phpstan.php\" --level 4" }, "suggest": { - "phpstan/phpstan-shim": "~0.9.0" + "phpstan/phpstan-shim": "~0.11.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index 0eb893d..3251222 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,9 @@ parameters: universalObjectCratesClasses: - SilverStripe\View\ArrayData - SilverStripe\Core\Config\Config_ForClass + - SilverStripe\Forms\GridField\GridState_Data + - SilverStripe\ORM\DataObject + - SilverStripe\ORM\DataObjectInterface - Symbiote\QueuedJobs\Services\AbstractQueuedJob # symbiote/silverstripe-queuedjobs module support excludes_analyse: - silverstripe-cache @@ -65,9 +68,21 @@ services: class: Symbiote\SilverstripePHPStan\Type\DataObjectReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: Symbiote\SilverstripePHPStan\Type\FormFieldReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + # This makes calls to `DBField::create_field('HTMLText', $value)` return the correct type info # ie. The injectored type of the first parameter - class: Symbiote\SilverstripePHPStan\Type\DBFieldStaticReturnTypeExtension tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension + + # Special handling for ->hasMethod() checks + - + class: Symbiote\SilverstripePHPStan\Type\HasMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/src/ConfigHelper.php b/src/ConfigHelper.php index 998e552..26bf160 100644 --- a/src/ConfigHelper.php +++ b/src/ConfigHelper.php @@ -23,7 +23,7 @@ public static function get($className, $configKey) * @param string $className * @param string $configKey * @param string $configValue - * @return array|scalar + * @return \SilverStripe\Config\Collections\MutableConfigCollectionInterface */ public static function update($className, $configKey, $configValue) { diff --git a/src/Reflection/CachedMethod.php b/src/Reflection/CachedMethod.php index 844d5dd..8f2b358 100644 --- a/src/Reflection/CachedMethod.php +++ b/src/Reflection/CachedMethod.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Type\Type; use PHPStan\Type\MixedType; use PHPStan\Reflection\Php\PhpMethodReflection; @@ -43,7 +44,7 @@ public function getDeclaringClass(): ClassReflection return $this->methodReflection->getDeclaringClass(); } - public function getPrototype(): MethodReflection + public function getPrototype(): ClassMemberReflection { return $this->methodReflection->getPrototype(); } @@ -53,15 +54,15 @@ public function isStatic(): bool return $this->methodReflection->isStatic(); } - public function getParameters(): array - { - return $this->methodReflection->getParameters(); - } + // public function getParameters(): array + // { + // return $this->methodReflection->getParameters(); + // } - public function isVariadic(): bool - { - return $this->methodReflection->isVariadic(); - } + // public function isVariadic(): bool + // { + // return $this->methodReflection->isVariadic(); + // } public function isPrivate(): bool { @@ -78,8 +79,16 @@ public function getName(): string return $this->name; } - public function getReturnType(): Type + // public function getReturnType(): Type + // { + // return $this->methodReflection->getReturnType(); + // } + + /** + * @return \PHPStan\Reflection\ParametersAcceptor[] + */ + public function getVariants(): array { - return $this->methodReflection->getReturnType(); + return $this->methodReflection->getVariants(); } } diff --git a/src/Reflection/ComponentHasManyMethod.php b/src/Reflection/ComponentHasManyMethod.php index 4e89999..6e06299 100644 --- a/src/Reflection/ComponentHasManyMethod.php +++ b/src/Reflection/ComponentHasManyMethod.php @@ -5,7 +5,9 @@ use Symbiote\SilverstripePHPStan\ClassHelper; use Symbiote\SilverstripePHPStan\Type\DataListType; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\FunctionVariant; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; @@ -13,26 +15,25 @@ class ComponentHasManyMethod implements MethodReflection { /** - * - * * @var string */ private $name; /** - * - * * @var \PHPStan\Reflection\ClassReflection */ private $declaringClass; /** - * - * * @var DataListType */ private $returnType; + /** + * @var FunctionVariant[]|null + */ + private $variants; + public function __construct(string $name, ClassReflection $declaringClass, ObjectType $type) { $this->name = $name; @@ -45,7 +46,7 @@ public function getDeclaringClass(): ClassReflection return $this->declaringClass; } - public function getPrototype(): MethodReflection + public function getPrototype(): ClassMemberReflection { return $this; } @@ -84,4 +85,18 @@ public function getReturnType(): Type { return $this->returnType; } + + public function getVariants(): array + { + if ($this->variants === null) { + $this->variants = [ + new FunctionVariant( + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType() + ), + ]; + } + return $this->variants; + } } diff --git a/src/Reflection/ComponentHasOneMethod.php b/src/Reflection/ComponentHasOneMethod.php index 4763df5..a84bbd2 100644 --- a/src/Reflection/ComponentHasOneMethod.php +++ b/src/Reflection/ComponentHasOneMethod.php @@ -4,6 +4,8 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ClassMemberReflection; +use PHPStan\Reflection\FunctionVariant; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; @@ -11,26 +13,25 @@ class ComponentHasOneMethod implements MethodReflection { /** - * - * * @var string */ private $name; /** - * - * * @var \PHPStan\Reflection\ClassReflection */ private $declaringClass; /** - * - * * @var ObjectType */ private $returnType; + /** + * @var FunctionVariant[]|null + */ + private $variants; + public function __construct(string $name, ClassReflection $declaringClass, ObjectType $type) { $this->name = $name; @@ -43,7 +44,7 @@ public function getDeclaringClass(): ClassReflection return $this->declaringClass; } - public function getPrototype(): MethodReflection + public function getPrototype(): ClassMemberReflection { return $this; } @@ -82,4 +83,18 @@ public function getReturnType(): Type { return $this->returnType; } + + public function getVariants(): array + { + if ($this->variants === null) { + $this->variants = [ + new FunctionVariant( + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType() + ), + ]; + } + return $this->variants; + } } diff --git a/src/Reflection/ComponentManyManyMethod.php b/src/Reflection/ComponentManyManyMethod.php index 8478c94..02f0cff 100644 --- a/src/Reflection/ComponentManyManyMethod.php +++ b/src/Reflection/ComponentManyManyMethod.php @@ -6,6 +6,8 @@ use Symbiote\SilverstripePHPStan\Type\DataListType; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; @@ -33,6 +35,11 @@ class ComponentManyManyMethod implements MethodReflection */ private $returnType; + /** + * @var FunctionVariant[]|null + */ + private $variants; + public function __construct(string $name, ClassReflection $declaringClass, ObjectType $type) { $this->name = $name; @@ -45,7 +52,7 @@ public function getDeclaringClass(): ClassReflection return $this->declaringClass; } - public function getPrototype(): MethodReflection + public function getPrototype(): ClassMemberReflection { return $this; } @@ -84,4 +91,18 @@ public function getReturnType(): Type { return $this->returnType; } + + public function getVariants(): array + { + if ($this->variants === null) { + $this->variants = [ + new FunctionVariant( + $this->getParameters(), + $this->isVariadic(), + $this->getReturnType() + ), + ]; + } + return $this->variants; + } } diff --git a/src/Reflection/MethodClassReflectionExtension.php b/src/Reflection/MethodClassReflectionExtension.php index a2cda95..259a845 100644 --- a/src/Reflection/MethodClassReflectionExtension.php +++ b/src/Reflection/MethodClassReflectionExtension.php @@ -50,7 +50,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): return $this->methods[$classReflection->getName()][strtolower($methodName)]; } - public function setBroker(Broker $broker) + public function setBroker(Broker $broker): void { $this->broker = $broker; } diff --git a/src/Rule/RequestFilterPreRequestRule.php b/src/Rule/RequestFilterPreRequestRule.php index 5b665b2..6d0afde 100644 --- a/src/Rule/RequestFilterPreRequestRule.php +++ b/src/Rule/RequestFilterPreRequestRule.php @@ -5,7 +5,7 @@ use Symbiote\SilverstripePHPStan\ClassHelper; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Type\FalseBooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; class RequestFilterPreRequestRule implements \PHPStan\Rules\Rule { @@ -21,7 +21,12 @@ public function getNodeType(): string */ public function processNode(\PhpParser\Node $node, Scope $scope): array { - $className = $scope->getClassReflection()->getName(); + $classRefl = $scope->getClassReflection(); + if (!$classRefl) { + return []; + } + + $className = $classRefl->getName(); if (!is_a($className, ClassHelper::RequestFilter, true)) { return []; } @@ -32,8 +37,8 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array if ($node->expr === null) { return []; } - $returnType = $scope->getType($node->expr); - if ($returnType instanceof FalseBooleanType) { + $returnType = $scope->filterByFalseyValue($node->expr)->getType($node->expr); + if ($returnType instanceof ConstantBooleanType) { // NOTE(Jake): 2018-04-25 // // Added for SS 3.X. This might not be true in SS 4.0 diff --git a/src/Type/DBFieldStaticReturnTypeExtension.php b/src/Type/DBFieldStaticReturnTypeExtension.php index 7bd16aa..faaafb9 100644 --- a/src/Type/DBFieldStaticReturnTypeExtension.php +++ b/src/Type/DBFieldStaticReturnTypeExtension.php @@ -6,6 +6,7 @@ use Symbiote\SilverstripePHPStan\Utility; use PhpParser\Node\Expr\StaticCall; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Analyser\Scope; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; @@ -29,7 +30,11 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, switch ($name) { case 'create_field': if (count($methodCall->args) === 0) { - return $methodReflection->getReturnType(); + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->args, + $methodReflection->getVariants() + )->getReturnType(); } // Handle DBField::create_field('HTMLText', '

Value

') $arg = $methodCall->args[0]->value; @@ -37,6 +42,8 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return $type; break; } - return $methodReflection->getReturnType(); + $arg = $methodCall->args[0]->value; + + return $scope->getType($arg); } } diff --git a/src/Type/DataListReturnTypeExtension.php b/src/Type/DataListReturnTypeExtension.php index d12966d..fc8eaa1 100644 --- a/src/Type/DataListReturnTypeExtension.php +++ b/src/Type/DataListReturnTypeExtension.php @@ -12,6 +12,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\IterableTypeTrait; +use Symbiote\SilverstripePHPStan\Utility; class DataListReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension { @@ -117,7 +118,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if (method_exists($type, 'getItemType')) { return new ArrayType(new IntegerType, $type->getItemType()); } - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); break; // DataObject @@ -132,6 +133,6 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method throw new Exception('Unhandled method call: '.$name); break; } - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } } diff --git a/src/Type/DataListType.php b/src/Type/DataListType.php index c884d81..82c2c13 100644 --- a/src/Type/DataListType.php +++ b/src/Type/DataListType.php @@ -9,10 +9,13 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\IterableTypeTrait; use PHPStan\Type\StaticResolvableType; +use PHPStan\TrinaryLogic; +use PHPStan\Type\VerbosityLevel; class DataListType extends ObjectType implements StaticResolvableType { - use IterableTypeTrait; + /** @var Type */ + private $itemType; public function __construct(string $dataListClassName, Type $itemType) { @@ -20,7 +23,7 @@ public function __construct(string $dataListClassName, Type $itemType) $this->itemType = $itemType; } - public function describe(): string + public function describe(VerbosityLevel $level): string { $dataListTypeClass = count($this->getReferencedClasses()) === 1 ? $this->getReferencedClasses()[0] : ''; $itemTypeClass = count($this->itemType->getReferencedClasses()) === 1 ? $this->itemType->getReferencedClasses()[0] : ''; @@ -54,38 +57,13 @@ public function isDocumentableNatively(): bool // IterableTrait - public function canCallMethods(): bool + public function canCallMethods(): TrinaryLogic { - return true; - } - - public function hasMethod(string $methodName): bool - { - return parent::hasMethod($methodName); - } - - public function getMethod(string $methodName, Scope $scope): MethodReflection - { - return parent::getMethod($methodName, $scope); - } - - public function isClonable(): bool - { - return true; - } - - public function canAccessProperties(): bool - { - return parent::canAccessProperties(); - } - - public function hasProperty(string $propertyName): bool - { - return parent::hasProperty($propertyName); + return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, Scope $scope): PropertyReflection + public function isClonable(): TrinaryLogic { - return parent::getProperty($propertyName, $scope); + return TrinaryLogic::createYes(); } } diff --git a/src/Type/DataObjectGetStaticReturnTypeExtension.php b/src/Type/DataObjectGetStaticReturnTypeExtension.php index 2c2af49..edb9fea 100644 --- a/src/Type/DataObjectGetStaticReturnTypeExtension.php +++ b/src/Type/DataObjectGetStaticReturnTypeExtension.php @@ -10,6 +10,7 @@ use PhpParser\Node\Scalar\String_; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Analyser\Scope; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; @@ -37,13 +38,13 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if (count($methodCall->args) > 0) { // Handle DataObject::get('Page') $arg = $methodCall->args[0]; - $type = Utility::getTypeFromVariable($arg, $methodReflection); + $type = Utility::getTypeFromInjectorVariable($arg, new ObjectType('SilverStripe\ORM\DataObject')); return new DataListType(ClassHelper::DataList, $type); } // Handle Page::get() / self::get() $callerClass = $methodCall->class->toString(); if ($callerClass === 'static') { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } if ($callerClass === 'self') { $callerClass = $scope->getClassReflection()->getName(); @@ -62,7 +63,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, // Handle Page::get() / self::get() $callerClass = $methodCall->class->toString(); if ($callerClass === 'static') { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } if ($callerClass === 'self') { $callerClass = $scope->getClassReflection()->getName(); @@ -70,6 +71,17 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return new ObjectType($callerClass); break; } - return $methodReflection->getReturnType(); + // NOTE(mleutenegger): 2019-11-10 + // taken from https://github.com/phpstan/phpstan#dynamic-return-type-extensions + if (count($methodCall->args) === 0) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->args, + $methodReflection->getVariants() + )->getReturnType(); + } + $arg = $methodCall->args[0]->value; + + return $scope->getType($arg); } } diff --git a/src/Type/DataObjectReturnTypeExtension.php b/src/Type/DataObjectReturnTypeExtension.php index 0e92ee8..cf38a26 100644 --- a/src/Type/DataObjectReturnTypeExtension.php +++ b/src/Type/DataObjectReturnTypeExtension.php @@ -68,7 +68,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method // directly from another function like `$this->getCMSFields()`, this will // execute. // - $objectType = $methodReflection->getReturnType(); + $objectType = Utility::getMethodReturnType($methodReflection); if (!($objectType instanceof ObjectType)) { throw new Exception('Unexpected type: '.get_class($objectType).', expected ObjectType'); } @@ -87,17 +87,17 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (!$className) { throw new Exception('Unhandled type: '.get_class($type)); - //return $methodReflection->getReturnType(); + //return Utility::getMethodReturnType($methodReflection); } if (count($methodCall->args) === 0) { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } // Handle $this->dbObject('Field') $arg = $methodCall->args[0]->value; $fieldName = ''; if ($arg instanceof Variable) { // Unhandled, cannot retrieve variable value even if set in this scope. - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } else if ($arg instanceof ClassConstFetch) { // Handle "SiteTree::class" constant $fieldName = (string)$arg->class; @@ -106,22 +106,25 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } if (!$fieldName) { throw new Exception('Mishandled "newClassInstance" call.'); - //return $methodReflection->getReturnType(); + //return Utility::getMethodReturnType($methodReflection); } $dbFields = ConfigHelper::get_db($className); if (!isset($dbFields[$fieldName])) { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } $dbFieldType = $dbFields[$fieldName]; - if (!$dbFieldType) { - return $methodReflection->getReturnType(); - } + // NOTE(mleutenegger): 2019-11-10 + // $dbFieldType is always truthy + // + // if (!$dbFieldType) { + // return Utility::getMethodReturnType($methodReflection); + // } return $dbFieldType; break; case 'newClassInstance': if (count($methodCall->args) === 0) { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } $arg = $methodCall->args[0]->value; $type = Utility::getTypeFromVariable($arg, $methodReflection); @@ -132,6 +135,6 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method throw new Exception('Unhandled method call: '.$name); break; } - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } } diff --git a/src/Type/ExtensionReturnTypeExtension.php b/src/Type/ExtensionReturnTypeExtension.php index 917e35f..db6f6a4 100644 --- a/src/Type/ExtensionReturnTypeExtension.php +++ b/src/Type/ExtensionReturnTypeExtension.php @@ -8,6 +8,7 @@ use Symbiote\SilverstripePHPStan\Utility; use PhpParser\Node\Expr\MethodCall; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Analyser\Scope; use PHPStan\Type\Type; use PHPStan\Type\ArrayType; @@ -57,9 +58,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } else { $objectType = Utility::getTypeFromVariable($methodCall->var, $methodReflection); } - if (!$objectType) { - return $methodReflection->getReturnType(); - } + // NOTE(mleutenegger): 2019-11-10 + // $objectType is always truthy + // + // if (!$objectType) { + // return $methodReflection->getReturnType(); + // } if (!($objectType instanceof ObjectType)) { throw new Exception('Unexpected type: '.get_class($objectType).', expected ObjectType'); } @@ -68,7 +72,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $extensionClassName = $objectType->getClassName(); $ownerClassNamesByExtensionClassName = $this->getOwnerClassNamesByExtensionClassName(); if (!isset($ownerClassNamesByExtensionClassName[$extensionClassName])) { - return $methodReflection->getReturnType(); + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } $classesUsingExtension = $ownerClassNamesByExtensionClassName[$extensionClassName]; @@ -84,7 +88,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } } if (!$types) { - return $methodReflection->getReturnType(); + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } if (count($types) === 1) { // NOTE(Jake): 2018-04-25 @@ -100,7 +104,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method throw new Exception('Unhandled method call: '.$name); break; } - return $methodReflection->getReturnType(); + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } private function getOwnerClassNamesByExtensionClassName() diff --git a/src/Type/FieldListType.php b/src/Type/FieldListType.php index 0ce10c6..2c1ed50 100644 --- a/src/Type/FieldListType.php +++ b/src/Type/FieldListType.php @@ -10,10 +10,16 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\IterableTypeTrait; use PHPStan\Type\StaticResolvableType; +use PHPStan\TrinaryLogic; +use PHPStan\Type\VerbosityLevel; class FieldListType extends ObjectType implements StaticResolvableType { - use IterableTypeTrait; + + /** + * @var ObjectType + */ + private $itemType; public function __construct(string $fieldListClassName) { @@ -21,7 +27,7 @@ public function __construct(string $fieldListClassName) $this->itemType = new ObjectType(ClassHelper::FormField); } - public function describe(): string + public function describe(VerbosityLevel $level): string { $fieldListClassName = count($this->getReferencedClasses()) === 1 ? $this->getReferencedClasses()[0] : ''; $itemTypeClass = count($this->itemType->getReferencedClasses()) === 1 ? $this->itemType->getReferencedClasses()[0] : ''; @@ -55,38 +61,13 @@ public function isDocumentableNatively(): bool // IterableTrait - public function canCallMethods(): bool - { - return true; - } - - public function hasMethod(string $methodName): bool - { - return parent::hasMethod($methodName); - } - - public function getMethod(string $methodName, Scope $scope): MethodReflection - { - return parent::getMethod($methodName, $scope); - } - - public function isClonable(): bool - { - return true; - } - - public function canAccessProperties(): bool - { - return parent::canAccessProperties(); - } - - public function hasProperty(string $propertyName): bool + public function canCallMethods(): TrinaryLogic { - return parent::hasProperty($propertyName); + return TrinaryLogic::createYes(); } - public function getProperty(string $propertyName, Scope $scope): PropertyReflection + public function isClonable(): TrinaryLogic { - return parent::getProperty($propertyName, $scope); + return TrinaryLogic::createYes(); } } diff --git a/src/Type/FormFieldReturnTypeExtension.php b/src/Type/FormFieldReturnTypeExtension.php new file mode 100644 index 0000000..7f222fc --- /dev/null +++ b/src/Type/FormFieldReturnTypeExtension.php @@ -0,0 +1,47 @@ +getName(); + switch ($name) { + case 'castedCopy': + return true; + } + return false; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $name = $methodReflection->getName(); + switch ($name) { + case 'castedCopy': + if (sizeof($methodCall->args) > 0) { + return Utility::getTypeFromInjectorVariable($methodCall->args[0], Utility::getMethodReturnType($methodReflection)); + } + break; + } + return Utility::getMethodReturnType($methodReflection); + } +} diff --git a/src/Type/HasMethodTypeSpecifyingExtension.php b/src/Type/HasMethodTypeSpecifyingExtension.php new file mode 100644 index 0000000..74a179d --- /dev/null +++ b/src/Type/HasMethodTypeSpecifyingExtension.php @@ -0,0 +1,58 @@ +typeSpecifier = $typeSpecifier; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool { + return $methodReflection->getName() === 'hasMethod' + && $context->truthy() + && count($node->args) >= 1; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes { + $methodNameType = $scope->getType($node->args[0]->value); + if (!$methodNameType instanceof ConstantStringType) { + return new SpecifiedTypes([], []); + } + return $this->typeSpecifier->create( + $node->var, + new HasMethodType($methodNameType->getValue()), + $context + ); + } +} diff --git a/src/Type/InjectorReturnTypeExtension.php b/src/Type/InjectorReturnTypeExtension.php index be3166a..1a1a8a3 100644 --- a/src/Type/InjectorReturnTypeExtension.php +++ b/src/Type/InjectorReturnTypeExtension.php @@ -2,7 +2,7 @@ namespace Symbiote\SilverstripePHPStan\Type; -use Exception; +use LogicException; use Symbiote\SilverstripePHPStan\ClassHelper; use Symbiote\SilverstripePHPStan\Utility; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -34,17 +34,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method switch ($name) { case 'get': if (count($methodCall->args) === 0) { - return $methodReflection->getReturnType(); + return Utility::getMethodReturnType($methodReflection); } $arg = $methodCall->args[0]->value; - $type = Utility::getTypeFromInjectorVariable($arg, $methodReflection->getReturnType()); + $type = Utility::getTypeFromInjectorVariable( + $arg, + Utility::getMethodReturnType($methodReflection) + ); return $type; break; default: - throw new Exception('Unhandled method call: '.$name); + throw new LogicException('Unhandled method call: '.$name); break; } - return $methodReflection->getReturnType(); + + return Utility::getMethodReturnType($methodReflection); } } diff --git a/src/Type/SingletonReturnTypeExtension.php b/src/Type/SingletonReturnTypeExtension.php index d4e2075..915fd4e 100644 --- a/src/Type/SingletonReturnTypeExtension.php +++ b/src/Type/SingletonReturnTypeExtension.php @@ -28,14 +28,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, switch ($name) { case 'singleton': if (count($functionCall->args) === 0) { - return $functionReflection->getReturnType(); + return Utility::getMethodReturnType($functionReflection); } // Handle singleton('HTMLText') $arg = $functionCall->args[0]->value; - $type = Utility::getTypeFromInjectorVariable($arg, $functionReflection->getReturnType()); + $type = Utility::getTypeFromInjectorVariable($arg, Utility::getMethodReturnType($functionReflection)); return $type; break; } - return $functionReflection->getReturnType(); + return Utility::getMethodReturnType($functionReflection); } } diff --git a/src/Utility.php b/src/Utility.php index 83f8566..b70b2b0 100644 --- a/src/Utility.php +++ b/src/Utility.php @@ -9,16 +9,21 @@ use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Scalar\MagicConst\Class_; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Expr\MethodCall; use PHPStan\Type\Type; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\IntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\MixedType; +use PHPStan\Type\UnionType; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParametersAcceptor; class Utility { @@ -45,13 +50,19 @@ public static function getTypeFromInjectorVariable(NodeAbstract $node, $defaultT if ($node instanceof Arg) { $node = $node->value; } + + // Handle a case such as Injector::inst()->get(CacheInterface::class . '.VersionProvider_composerlock'); + if ($node instanceof Concat && $node->right instanceof String_ && $node->right->value[0] === '.') { + $node = $node->left; + } + if ($node instanceof String_) { - // Handle string: 'HomePage' + // Handle string: 'HomePage' or '%$HomePage' $label = $node->value; } else if ($node instanceof ClassConstFetch) { // Handle type: 'HomePage::class' $label = (string)$node->class; - } else if ($node instanceof PropertyFetch) { + } else if ($node instanceof PropertyFetch || $node instanceof ArrayDimFetch || $node instanceof MethodCall || $node instanceof Concat) { // Handle passing of: '$this->modelClass' in ModelAdmin to 'singleton' return $defaultType; } else if ($node instanceof Variable) { @@ -65,8 +76,13 @@ public static function getTypeFromInjectorVariable(NodeAbstract $node, $defaultT // what the method returns in its type hinting. // return $defaultType; + } else if ($node instanceof Class_) { + // @todo __CLASS__ constant not currently supported, but could be. Note that self::class isn't yet supported by the ClassConstFetch check either + return $defaultType; } + if (!$label) { + var_dump($node); throw new Exception(__FUNCTION__.': Unhandled or invalid "class" data. Type passed:'.get_class($node)); } return self::getClassFromInjectorString($label); @@ -74,11 +90,20 @@ public static function getTypeFromInjectorVariable(NodeAbstract $node, $defaultT public static function getClassFromInjectorString($classNameOrLabel): ObjectType { + if (preg_match('/^%\$/', $classNameOrLabel)) { + $classNameOrLabel = substr($classNameOrLabel, 2); + } + $injectorInfo = ConfigHelper::get(ClassHelper::Injector, $classNameOrLabel); if (!$injectorInfo) { return new ObjectType($classNameOrLabel); } if (is_string($injectorInfo)) { + // Recursive service lookup + if (preg_match('/^%\$/', $injectorInfo)) { + return self::getClassFromInjectorString($injectorInfo); + } + return new ObjectType($injectorInfo); } if (is_array($injectorInfo) && @@ -93,7 +118,7 @@ public static function getClassFromInjectorString($classNameOrLabel): ObjectType return new ObjectType($classNameOrLabel); } - public static function getTypeFromVariable(NodeAbstract $node, ParametersAcceptorWithPhpDocs $methodOrFunctionReflection): Type + public static function getTypeFromVariable(NodeAbstract $node, MethodReflection $methodOrFunctionReflection): Type { $class = ''; if ($node instanceof Arg) { @@ -123,8 +148,8 @@ public static function getTypeFromVariable(NodeAbstract $node, ParametersAccepto // strings / values in scope, so we just need to rely on // what the method returns in its type hinting. // - return $methodOrFunctionReflection->getReturnType(); - } else if ($node instanceof ArrayDimFetch) { + return self::getMethodReturnType($methodOrFunctionReflection); + } else if ($node instanceof ArrayDimFetch || $node instanceof MethodCall || $node instanceof PropertyFetch) { // NOTE(Jake): 2018-05-19 // // If we pass in scope, we can get the variable type: @@ -134,11 +159,40 @@ public static function getTypeFromVariable(NodeAbstract $node, ParametersAccepto // strings / values in scope, so we just need to rely on // what the method returns in its type hinting. // - return $methodOrFunctionReflection->getReturnType(); + return self::getMethodReturnType($methodOrFunctionReflection); } if (!$class) { + var_dump($node); throw new Exception(__FUNCTION__.':Unhandled or invalid "class" data. Type passed:'.get_class($node)); } - return new ObjectType($class); + + // Most of these lookups are now injector-based + return self::getClassFromInjectorString($class); + } + + /** + * Get a return type from a MethodReflection or FunctionReflection. + * Calls its variants, and if necessary, creates a union type + * + * @param MethodReflection|FunctionReflection $methodReflection + */ + public static function getMethodReturnType($methodReflection): Type + { + $variants = $methodReflection->getVariants(); + switch (sizeof($variants)) { + case 0: + throw new \LogicException('No method variants for method: ' . $methodReflection->getName()); + + case 1: + return $variants[0]->getReturnType(); + + default: + return new UnionType(array_map( + $variants, + function ($variant) { + return $variant->getReturnType(); + } + )); + } } } diff --git a/tests/Reflection/SiteTreeMethodClassReflectionExtensionTest.php b/tests/Reflection/SiteTreeMethodClassReflectionExtensionTest.php index 4b4d77c..09b25fe 100644 --- a/tests/Reflection/SiteTreeMethodClassReflectionExtensionTest.php +++ b/tests/Reflection/SiteTreeMethodClassReflectionExtensionTest.php @@ -5,6 +5,7 @@ use Symbiote\SilverstripePHPStan\ClassHelper; use Symbiote\SilverstripePHPStan\Reflection\MethodClassReflectionExtension; use Symbiote\SilverstripePHPStan\Type\DataListType; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; @@ -64,7 +65,7 @@ public function testParentMethod(): void $classReflection = $this->broker->getClass(ClassHelper::SiteTree); $methodReflection = $this->method->getMethod($classReflection, 'Parent'); self::assertSame('Parent', $methodReflection->getName()); - $resultType = $methodReflection->getReturnType(); + $resultType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); self::assertSame(ObjectType::class, get_class($resultType)); if (!($resultType instanceof ObjectType)) { // This statement is needed so PHPStan knows $resultType is ObjectType. @@ -78,7 +79,7 @@ public function testLinkTrackingMethod(): void $classReflection = $this->broker->getClass(ClassHelper::SiteTree); $methodReflection = $this->method->getMethod($classReflection, 'LinkTracking'); self::assertSame('LinkTracking', $methodReflection->getName()); - $dataListType = $methodReflection->getReturnType(); + $dataListType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); self::assertSame(DataListType::class, get_class($dataListType)); if (!($dataListType instanceof DataListType)) { // This statement is needed so PHPStan knows $dataListType is DataListType. diff --git a/tests/ResolverTest.php b/tests/ResolverTest.php index cb90f9d..70901d7 100644 --- a/tests/ResolverTest.php +++ b/tests/ResolverTest.php @@ -5,13 +5,20 @@ use ReflectionProperty; use PHPStan\PhpDoc; use PHPStan\Analyser\Scope; +use PHPStan\Analyser\ScopeFactory; +use PHPStan\Analyser\ScopeContext; use PHPStan\Cache\Cache; use PHPStan\File\FileHelper; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\VerbosityLevel; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Reflection\BrokerAwareExtension; +use PHPStan\File\FuzzyRelativePathHelper; +use PHPStan\Broker\AnonymousClassNameHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Node\VirtualNode; abstract class ResolverTest extends \PHPStan\Testing\TestCase { @@ -43,15 +50,18 @@ protected function assertTypes( // - phpstan\tests\PHPStan\Analyser\NodeScopeResolverTest.php // $this->processFile($file, function (\PhpParser\Node $node, Scope $scope) use ($description, $expression, $evaluatedPointExpression) { + if ($node instanceof VirtualNode) { + return; + } $printer = new \PhpParser\PrettyPrinter\Standard(); $printedNode = $printer->prettyPrint([$node]); if ($printedNode === $evaluatedPointExpression) { - /** @var \PhpParser\Node\Expr $expressionNode */ + /** @var \PhpParser\Node\Stmt\Expression $expressionNode */ $expressionNode = $this->getParser()->parseString(sprintf('getType($expressionNode); + $type = $scope->getType($expressionNode->expr); $this->assertTypeDescribe( $description, - $type->describe(), + $type->describe(VerbosityLevel::precise()), sprintf('%s at %s', $expression, $evaluatedPointExpression) ); } @@ -91,20 +101,38 @@ private function processFile( $refProperty->setAccessible(true); $refProperty->setValue($broker, $hack); } + $workingDirectory = __DIR__; + $relativePathHelper = new FuzzyRelativePathHelper($workingDirectory, DIRECTORY_SEPARATOR, []); + $anonymousClassNameHelper = new AnonymousClassNameHelper(new FileHelper($workingDirectory), $relativePathHelper); + + $typeSpecifier = $this->createTypeSpecifier( + $printer, + $broker, + [], + [] + ); $resolver = new NodeScopeResolver( $broker, $this->getParser(), - $printer, - new FileTypeMapper($this->getParser(), $phpDocStringResolver, $this->createMock(Cache::class)), - new FileHelper('/'), + new FileTypeMapper( + $this->getParser(), + $phpDocStringResolver, + $this->createMock(Cache::class), + $anonymousClassNameHelper, + new \PHPStan\PhpDoc\TypeNodeResolver([]) + ), + new FileHelper($workingDirectory), + $typeSpecifier, + true, true, true, [ //\EarlyTermination\Foo::class => [ // 'doFoo', //], - ] + ], + true ); $broker = $this->createBroker( $dynamicMethodReturnTypeExtensions, @@ -129,14 +157,11 @@ private function processFile( $refProperty->setValue($broker, $hack); } + $scopeFactory = $this->createScopeFactory($broker, $typeSpecifier); + $scope = $scopeFactory->create(ScopeContext::create($file)); $resolver->processNodes( $this->getParser()->parseFile($file), - new Scope( - $broker, - $printer, - new TypeSpecifier($printer), - $file - ), + $scope, $callback ); } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 3146123..7f0f407 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -4,10 +4,13 @@ parameters: ignoreErrors: # Ignore as PHPStan has no support for method_exists() - - '%Call to an undefined method PHPStan\\Type\\Type::getItemType()%' + # - '%Call to an undefined method PHPStan\\Type\\Type::getItemType()%' exclude_files: # Exclude bootstrap file - tests/bootstrap-phpstan.php + excludes_analyse: + # Exclude test data files + - %currentWorkingDirectory%/tests/*/Data/* autoload_directories: # NOTE(Jake): 2018-05-20 #