├── .gitattributes ├── .gitignore ├── bootstrap.php ├── composer.json ├── phpstan.neon └── src ├── ClassHelper.php ├── ConfigHelper.php ├── Reflection ├── CachedMethod.php ├── ComponentDBFieldProperty.php ├── ComponentHasManyMethod.php ├── ComponentHasOneMethod.php ├── ComponentHasOneProperty.php ├── ComponentManyManyMethod.php ├── MethodClassReflectionExtension.php ├── PropertyClassReflectionExtension.php ├── ViewableDataGetNullProperty.php └── ViewableDataGetProperty.php ├── Rule └── RequestFilterPreRequestRule.php ├── Type ├── DBFieldStaticReturnTypeExtension.php ├── DataListReturnTypeExtension.php ├── DataListType.php ├── DataObjectGetStaticReturnTypeExtension.php ├── DataObjectReturnTypeExtension.php ├── ExtensionReturnTypeExtension.php ├── FieldListType.php ├── FormFieldReturnTypeExtension.php ├── HasMethodTypeSpecifyingExtension.php ├── InjectorReturnTypeExtension.php └── SingletonReturnTypeExtension.php └── Utility.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /docs export-ignore 3 | /.travis.yml export-ignore 4 | /phpcs.xml.dist export-ignore 5 | /.editorconfig export-ignore 6 | /LICENSE.md export-ignore 7 | /README.md export-ignore 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /resources/ 3 | /assets/ 4 | /app/ 5 | .DS_Store 6 | .phpunit.result.cache 7 | composer.lock 8 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 'HTTP/1.1', 29 | 'HTTP_ACCEPT' => 'text/plain;q=0.5', 30 | 'HTTP_ACCEPT_LANGUAGE' => '*;q=0.5', 31 | 'HTTP_ACCEPT_ENCODING' => '', 32 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1;q=0.5', 33 | 'SERVER_SIGNATURE' => 'Command-line PHP/' . phpversion(), 34 | 'SERVER_SOFTWARE' => 'PHP/' . phpversion(), 35 | 'SERVER_NAME' => 'localhost', 36 | 'SERVER_ADDR' => '127.0.0.1', 37 | 'REMOTE_ADDR' => '127.0.0.1', 38 | 'REQUEST_METHOD' => 'GET', 39 | 'HTTP_USER_AGENT' => 'CLI', 40 | ), $_SERVER); 41 | 42 | // Default application 43 | try { 44 | $kernel = new CoreKernel(BASE_PATH); 45 | $kernel->boot(); 46 | } catch (HTTPResponse_Exception $e) { 47 | // ignore unconfigured DB error from CoreKernel::redirectToInstaller() 48 | } 49 | //$app = new HTTPApplication($kernel); 50 | //$app->addMiddleware(new ErrorControlChainMiddleware($app)); 51 | //$request = HTTPRequestBuilder::createFromEnvironment(); // Build request and detect flush 52 | //$response = $app->handle($request); 53 | //$response->output(); 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symbiote/silverstripe-phpstan", 3 | "description": "PHPStan for Silverstripe", 4 | "type": "library", 5 | "keywords": [ 6 | "php", 7 | "silverstripe", 8 | "static", 9 | "analysis", 10 | "phpstan", 11 | "scrutinizer" 12 | ], 13 | "license": "BSD-3-Clause", 14 | "authors": [ 15 | { 16 | "name": "Jake Bentvelzen", 17 | "email": "jake@symbiote.com.au" 18 | } 19 | ], 20 | "require": { 21 | "php": "~7.1", 22 | "silverstripe/framework": "~4.3", 23 | "silverstripe/vendor-plugin": "^1.0" 24 | }, 25 | "require-dev": { 26 | "squizlabs/php_codesniffer": "^3.0", 27 | "phpstan/phpstan": "~0.11.0", 28 | "phpstan/phpstan-phpunit": "~0.11.0", 29 | "phpunit/phpunit": "^7.5.14 || ^8.0" 30 | }, 31 | "scripts": { 32 | "phpcs": "phpcs -n -l src/ src/Reflection/ src/Rule/ src/Type tests/ tests/Reflection/ tests/Rule/ tests/Type/", 33 | "phpcbf": "phpcbf -n src/ src/Reflection/ src/Rule/ src/Type tests/ tests/Reflection/ tests/Rule/ tests/Type/", 34 | "phpunit": "bash ../../../vendor/bin/phpunit -c \"tests/phpunit.xml\" tests/", 35 | "phpstan": "bash ../../../vendor/bin/phpstan analyse src/ tests/ -c \"tests/phpstan.neon\" -a \"tests/bootstrap-phpstan.php\" --level 4" 36 | }, 37 | "suggest": { 38 | "phpstan/phpstan-shim": "~0.11.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Symbiote\\SilverstripePHPStan\\": "src/", 43 | "Symbiote\\SilverstripePHPStan\\Tests\\": "tests/" 44 | } 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "4.0.x-dev" 49 | } 50 | }, 51 | "replace": { 52 | "silbinarywolf/silverstripe-phpstan": "self.version" 53 | }, 54 | "prefer-stable": true, 55 | "minimum-stability": "dev" 56 | } 57 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | earlyTerminatingMethodCalls: 3 | SilverStripe\Control\Controller: 4 | - redirect 5 | universalObjectCratesClasses: 6 | - SilverStripe\View\ArrayData 7 | - SilverStripe\Core\Config\Config_ForClass 8 | - SilverStripe\Forms\GridField\GridState_Data 9 | - SilverStripe\ORM\DataObject 10 | - SilverStripe\ORM\DataObjectInterface 11 | - Symbiote\QueuedJobs\Services\AbstractQueuedJob # symbiote/silverstripe-queuedjobs module support 12 | excludes_analyse: 13 | - silverstripe-cache 14 | services: 15 | # This rule will throw an error if you `return false` from a RequestFilter::preRequest() method 16 | # as in SilverStripe 3.X, this throws an uncaught exception in the site. 17 | - 18 | class: Symbiote\SilverstripePHPStan\Rule\RequestFilterPreRequestRule 19 | tags: 20 | - phpstan.rules.rule 21 | # This adds additional methods from SilverStripe extensions, as well as determining proper 22 | # types when has_one() magic methods. 23 | - 24 | class: Symbiote\SilverstripePHPStan\Reflection\MethodClassReflectionExtension 25 | tags: 26 | - phpstan.broker.methodsClassReflectionExtension 27 | # This adds additional properties from 'db', 'has_one' and other config fields to 28 | # determine what types are returned for magic __get's 29 | - 30 | class: Symbiote\SilverstripePHPStan\Reflection\PropertyClassReflectionExtension 31 | tags: 32 | - phpstan.broker.propertiesClassReflectionExtension 33 | # This adds additional type info to `DataObject::get()` so that it knows what class 34 | # while be returned when iterating. 35 | - 36 | class: Symbiote\SilverstripePHPStan\Type\DataObjectGetStaticReturnTypeExtension 37 | tags: 38 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 39 | # This allows `singleton("File")` calls to understand the exact classes being returned 40 | # by using your configuration. (ie. uses Injector information if it's set) 41 | - 42 | class: Symbiote\SilverstripePHPStan\Type\SingletonReturnTypeExtension 43 | tags: 44 | - phpstan.broker.dynamicFunctionReturnTypeExtension 45 | # This allows `Injector::inst()->get("File")` calls to understand the exact classes being returned 46 | # by using your configuration. (ie. uses Injector information if it's set) 47 | - 48 | class: Symbiote\SilverstripePHPStan\Type\InjectorReturnTypeExtension 49 | tags: 50 | - phpstan.broker.dynamicMethodReturnTypeExtension 51 | # This allows `$this->getOwner()` to understand the possible types that it could 52 | # return by seeing what classes are using that extension currently. 53 | - 54 | class: Symbiote\SilverstripePHPStan\Type\ExtensionReturnTypeExtension 55 | tags: 56 | - phpstan.broker.dynamicMethodReturnTypeExtension 57 | # This makes a DataList keep it's current type information as-is when calling 58 | # certain functions. (So it still knows its a DataList of SiteTree objects for example) 59 | # 60 | # This will also allow "first"/"last" calls and similar to return the correct type. 61 | # 62 | - 63 | class: Symbiote\SilverstripePHPStan\Type\DataListReturnTypeExtension 64 | tags: 65 | - phpstan.broker.dynamicMethodReturnTypeExtension 66 | # This makes calls to `dbObject` and `newClassInstance` return the correct type info 67 | - 68 | class: Symbiote\SilverstripePHPStan\Type\DataObjectReturnTypeExtension 69 | tags: 70 | - phpstan.broker.dynamicMethodReturnTypeExtension 71 | 72 | - 73 | class: Symbiote\SilverstripePHPStan\Type\FormFieldReturnTypeExtension 74 | tags: 75 | - phpstan.broker.dynamicMethodReturnTypeExtension 76 | 77 | # This makes calls to `DBField::create_field('HTMLText', $value)` return the correct type info 78 | # ie. The injectored type of the first parameter 79 | - 80 | class: Symbiote\SilverstripePHPStan\Type\DBFieldStaticReturnTypeExtension 81 | tags: 82 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 83 | 84 | # Special handling for ->hasMethod() checks 85 | - 86 | class: Symbiote\SilverstripePHPStan\Type\HasMethodTypeSpecifyingExtension 87 | tags: 88 | - phpstan.typeSpecifier.methodTypeSpecifyingExtension 89 | -------------------------------------------------------------------------------- /src/ClassHelper.php: -------------------------------------------------------------------------------- 1 | get($className, $configKey); 20 | } 21 | 22 | /** 23 | * @param string $className 24 | * @param string $configKey 25 | * @param string $configValue 26 | * @return \SilverStripe\Config\Collections\MutableConfigCollectionInterface 27 | */ 28 | public static function update($className, $configKey, $configValue) 29 | { 30 | return Config::modify()->set($className, $configKey, $configValue); 31 | } 32 | 33 | /** 34 | * @param string $className 35 | * @return ObjectType[] 36 | */ 37 | public static function get_db($className) 38 | { 39 | $dbFields = array(); 40 | // NOTE(Jake): 2018-04-25 41 | // 42 | // Support the default DataObject DB fields 43 | // 44 | $dbFields = array( 45 | 'ID' => 'Int', // NOTE: DBInt in SS 3.6+ and 4.0 46 | 'ClassName' => 'Enum', 47 | 'Created' => 'SS_Datetime', 48 | 'LastEdited' => 'SS_Datetime', 49 | ); 50 | // Support Versioned fields for when grabbing records out of *_versions tables. 51 | $extensions = self::get_extensions($className); 52 | if ($extensions && isset($extensions[ClassHelper::Versioned])) { 53 | $dbFields['RecordID'] = 'Int'; 54 | } 55 | 56 | $db = self::get($className, 'db'); 57 | if ($db) { 58 | foreach ($db as $propertyName => $type) { 59 | // Ignore parameters 60 | $type = explode('(', $type, 2); 61 | $type = $type[0]; 62 | if (isset($dbFields[$propertyName]) 63 | || is_numeric($propertyName) 64 | ) { 65 | // Skip erroneous double-ups and skip numeric names 66 | continue; 67 | } 68 | $dbFields[$propertyName] = $type; 69 | } 70 | } 71 | foreach ($dbFields as $propertyName => $type) { 72 | $dbFields[$propertyName] = Utility::getClassFromInjectorString($type); 73 | } 74 | return $dbFields; 75 | } 76 | 77 | /** 78 | * @return bool[] 79 | */ 80 | public static function get_has_one($className) 81 | { 82 | $hasOne = self::get($className, 'has_one'); 83 | $properties = array(); 84 | if ($hasOne) { 85 | foreach ($hasOne as $propertyName => $type) { 86 | // Ignore parameters 87 | $type = explode('(', $type, 2); 88 | $type = $type[0]; 89 | 90 | $propertyName = $propertyName.'ID'; 91 | if (isset($properties[$propertyName])) { 92 | // Skip erroneous duplicates 93 | continue; 94 | } 95 | $properties[$propertyName] = true; 96 | } 97 | } 98 | return $properties; 99 | } 100 | 101 | /** 102 | * @return string[] 103 | */ 104 | public static function get_extensions($className) 105 | { 106 | $extensionClasses = array(); 107 | $extensions = self::get($className, 'extensions'); 108 | if ($extensions) { 109 | foreach ($extensions as $extensionClass) { 110 | // Ignore parameters (ie. "Versioned('Stage', 'Live')") 111 | $extensionClass = explode('(', $extensionClass, 2); 112 | $extensionClass = $extensionClass[0]; 113 | 114 | $extensionClasses[$extensionClass] = $extensionClass; 115 | } 116 | } 117 | return $extensionClasses; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Reflection/CachedMethod.php: -------------------------------------------------------------------------------- 1 | name = substr($methodReflection->getName(), 1); 39 | $this->methodReflection = $methodReflection; 40 | } 41 | 42 | public function getDeclaringClass(): ClassReflection 43 | { 44 | return $this->methodReflection->getDeclaringClass(); 45 | } 46 | 47 | public function getPrototype(): ClassMemberReflection 48 | { 49 | return $this->methodReflection->getPrototype(); 50 | } 51 | 52 | public function isStatic(): bool 53 | { 54 | return $this->methodReflection->isStatic(); 55 | } 56 | 57 | // public function getParameters(): array 58 | // { 59 | // return $this->methodReflection->getParameters(); 60 | // } 61 | 62 | // public function isVariadic(): bool 63 | // { 64 | // return $this->methodReflection->isVariadic(); 65 | // } 66 | 67 | public function isPrivate(): bool 68 | { 69 | return $this->methodReflection->isPrivate(); 70 | } 71 | 72 | public function isPublic(): bool 73 | { 74 | return $this->methodReflection->isPublic(); 75 | } 76 | 77 | public function getName(): string 78 | { 79 | return $this->name; 80 | } 81 | 82 | // public function getReturnType(): Type 83 | // { 84 | // return $this->methodReflection->getReturnType(); 85 | // } 86 | 87 | /** 88 | * @return \PHPStan\Reflection\ParametersAcceptor[] 89 | */ 90 | public function getVariants(): array 91 | { 92 | return $this->methodReflection->getVariants(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Reflection/ComponentDBFieldProperty.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | $this->declaringClass = $declaringClass; 39 | 40 | // Transform ObjectType 'DBInt' to 'IntegerType' for property access 41 | $className = $type->getClassName(); 42 | $this->returnType = Utility::get_primitive_from_dbfield($className); 43 | } 44 | 45 | public function getType(): Type 46 | { 47 | return $this->returnType; 48 | } 49 | 50 | public function getDeclaringClass(): ClassReflection 51 | { 52 | return $this->declaringClass; 53 | } 54 | 55 | public function isStatic(): bool 56 | { 57 | return false; 58 | } 59 | 60 | public function isPrivate(): bool 61 | { 62 | return false; 63 | } 64 | 65 | public function isPublic(): bool 66 | { 67 | return true; 68 | } 69 | 70 | public function isReadable(): bool 71 | { 72 | return true; 73 | } 74 | 75 | public function isWritable(): bool 76 | { 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Reflection/ComponentHasManyMethod.php: -------------------------------------------------------------------------------- 1 | name = $name; 40 | $this->declaringClass = $declaringClass; 41 | $this->returnType = new DataListType(ClassHelper::HasManyList, $type); 42 | } 43 | 44 | public function getDeclaringClass(): ClassReflection 45 | { 46 | return $this->declaringClass; 47 | } 48 | 49 | public function getPrototype(): ClassMemberReflection 50 | { 51 | return $this; 52 | } 53 | 54 | public function isStatic(): bool 55 | { 56 | return false; 57 | } 58 | 59 | public function getParameters(): array 60 | { 61 | return []; 62 | } 63 | 64 | public function isVariadic(): bool 65 | { 66 | return false; 67 | } 68 | 69 | public function isPrivate(): bool 70 | { 71 | return false; 72 | } 73 | 74 | public function isPublic(): bool 75 | { 76 | return true; 77 | } 78 | 79 | public function getName(): string 80 | { 81 | return $this->name; 82 | } 83 | 84 | public function getReturnType(): Type 85 | { 86 | return $this->returnType; 87 | } 88 | 89 | public function getVariants(): array 90 | { 91 | if ($this->variants === null) { 92 | $this->variants = [ 93 | new FunctionVariant( 94 | $this->getParameters(), 95 | $this->isVariadic(), 96 | $this->getReturnType() 97 | ), 98 | ]; 99 | } 100 | return $this->variants; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Reflection/ComponentHasOneMethod.php: -------------------------------------------------------------------------------- 1 | name = $name; 38 | $this->declaringClass = $declaringClass; 39 | $this->returnType = $type; 40 | } 41 | 42 | public function getDeclaringClass(): ClassReflection 43 | { 44 | return $this->declaringClass; 45 | } 46 | 47 | public function getPrototype(): ClassMemberReflection 48 | { 49 | return $this; 50 | } 51 | 52 | public function isStatic(): bool 53 | { 54 | return false; 55 | } 56 | 57 | public function getParameters(): array 58 | { 59 | return []; 60 | } 61 | 62 | public function isVariadic(): bool 63 | { 64 | return false; 65 | } 66 | 67 | public function isPrivate(): bool 68 | { 69 | return false; 70 | } 71 | 72 | public function isPublic(): bool 73 | { 74 | return true; 75 | } 76 | 77 | public function getName(): string 78 | { 79 | return $this->name; 80 | } 81 | 82 | public function getReturnType(): Type 83 | { 84 | return $this->returnType; 85 | } 86 | 87 | public function getVariants(): array 88 | { 89 | if ($this->variants === null) { 90 | $this->variants = [ 91 | new FunctionVariant( 92 | $this->getParameters(), 93 | $this->isVariadic(), 94 | $this->getReturnType() 95 | ), 96 | ]; 97 | } 98 | return $this->variants; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Reflection/ComponentHasOneProperty.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->declaringClass = $declaringClass; 38 | $this->returnType = new IntegerType; 39 | } 40 | 41 | public function getType(): Type 42 | { 43 | return $this->returnType; 44 | } 45 | 46 | public function getDeclaringClass(): ClassReflection 47 | { 48 | return $this->declaringClass; 49 | } 50 | 51 | public function isStatic(): bool 52 | { 53 | return false; 54 | } 55 | 56 | public function isPrivate(): bool 57 | { 58 | return false; 59 | } 60 | 61 | public function isPublic(): bool 62 | { 63 | return true; 64 | } 65 | 66 | public function isReadable(): bool 67 | { 68 | return true; 69 | } 70 | 71 | public function isWritable(): bool 72 | { 73 | return true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Reflection/ComponentManyManyMethod.php: -------------------------------------------------------------------------------- 1 | name = $name; 46 | $this->declaringClass = $declaringClass; 47 | $this->returnType = new DataListType(ClassHelper::ManyManyList, $type); 48 | } 49 | 50 | public function getDeclaringClass(): ClassReflection 51 | { 52 | return $this->declaringClass; 53 | } 54 | 55 | public function getPrototype(): ClassMemberReflection 56 | { 57 | return $this; 58 | } 59 | 60 | public function isStatic(): bool 61 | { 62 | return false; 63 | } 64 | 65 | public function getParameters(): array 66 | { 67 | return []; 68 | } 69 | 70 | public function isVariadic(): bool 71 | { 72 | return false; 73 | } 74 | 75 | public function isPrivate(): bool 76 | { 77 | return false; 78 | } 79 | 80 | public function isPublic(): bool 81 | { 82 | return true; 83 | } 84 | 85 | public function getName(): string 86 | { 87 | return $this->name; 88 | } 89 | 90 | public function getReturnType(): Type 91 | { 92 | return $this->returnType; 93 | } 94 | 95 | public function getVariants(): array 96 | { 97 | if ($this->variants === null) { 98 | $this->variants = [ 99 | new FunctionVariant( 100 | $this->getParameters(), 101 | $this->isVariadic(), 102 | $this->getReturnType() 103 | ), 104 | ]; 105 | } 106 | return $this->variants; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Reflection/MethodClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | methods[$classReflection->getName()])) { 39 | $this->methods[$classReflection->getName()] = $this->createMethods($classReflection); 40 | } 41 | return isset($this->methods[$classReflection->getName()][strtolower($methodName)]); 42 | } 43 | 44 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection 45 | { 46 | if (!isset($this->methods[$classReflection->getName()])) { 47 | $this->methods[$classReflection->getName()] = $this->createMethods($classReflection); 48 | } 49 | // Fallback to has_one/has_many/many_many 50 | return $this->methods[$classReflection->getName()][strtolower($methodName)]; 51 | } 52 | 53 | public function setBroker(Broker $broker): void 54 | { 55 | $this->broker = $broker; 56 | } 57 | 58 | /** 59 | * @param ClassReflection $classReflection 60 | * @return MethodReflection[] 61 | */ 62 | private function createMethods(ClassReflection $classReflection): array 63 | { 64 | if (!$classReflection->isSubclassOf(ClassHelper::ViewableData)) { 65 | return []; 66 | } 67 | 68 | $methods = []; 69 | 70 | $class = $classReflection->getName(); 71 | $isDataObjectOrContentController = $classReflection->getName() === ClassHelper::DataObject || 72 | $classReflection->isSubclassOf(ClassHelper::DataObject); 73 | 74 | // Add methods from extensions 75 | $extensionInstances = ConfigHelper::get_extensions($class); 76 | if ($extensionInstances) { 77 | foreach ($extensionInstances as $extensionClass) { 78 | $extensionClassReflection = $this->broker->getClass($extensionClass); 79 | foreach (get_class_methods($extensionClass) as $methodName) { 80 | $methodReflection = $extensionClassReflection->getNativeMethod($methodName); 81 | $methods[strtolower($methodName)] = $methodReflection; 82 | } 83 | } 84 | } 85 | 86 | // Detect little-known Silverstripe '_' cache function 87 | // ie. Define: function _MyFunction() 88 | // Call: $this->MyFunction() will be cached. 89 | // 90 | foreach (get_class_methods($class) as $methodName) { 91 | if ($methodName && $methodName[0] === '_' && isset($methodName[1]) && $methodName[1] !== '_') { 92 | $uncachedMethodName = substr($methodName, 1); 93 | $methods[strtolower($uncachedMethodName)] = new CachedMethod($classReflection->getNativeMethod($methodName)); 94 | } 95 | } 96 | 97 | // Handle Page_Controller where it has $failover 98 | // NOTE(Jake): This is not foolproof, but if people follow the general SS convention 99 | // it'll work. 100 | if (strpos($class, '_Controller') !== false 101 | && $classReflection->isSubclassOf(ClassHelper::ContentController) 102 | ) { 103 | $class = str_replace('_Controller', '', $class); 104 | $isDataObjectOrContentController = true; 105 | 106 | $failoverClassReflection = $this->broker->getClass($class); 107 | foreach (get_class_methods($class) as $methodName) { 108 | $methodReflection = $failoverClassReflection->getNativeMethod($methodName); 109 | $methods[strtolower($methodName)] = $methodReflection; 110 | } 111 | } 112 | 113 | // todo(Jake): Figure out if an extension magic __call() has precedence over a has_one magic call 114 | if ($isDataObjectOrContentController) { 115 | $components = array( 116 | 'has_one' => ComponentHasOneMethod::class, 117 | 'belongs_to' => ComponentHasOneMethod::class, 118 | 'has_many' => ComponentHasManyMethod::class, 119 | 'many_many' => ComponentManyManyMethod::class, 120 | 'belongs_many_many' => ComponentManyManyMethod::class, 121 | ); 122 | foreach ($components as $componentType => $componentClass) { 123 | $componentNameValueMap = ConfigHelper::get($class, $componentType); 124 | if (!$componentNameValueMap) { 125 | continue; 126 | } 127 | 128 | foreach ($componentNameValueMap as $methodName => $type) { 129 | if (is_array($type)) { 130 | if ($componentType !== 'many_many') { 131 | throw new Exception('Cannot use array format for "'.$componentType.'" component type.'); 132 | } 133 | if (!isset($type['through']) || 134 | !isset($type['from']) || 135 | !isset($type['to'])) { 136 | throw new Exception('Unknown array format. Expected string or array with "through", "from" and "to".'); 137 | } 138 | // Example data: 139 | // array(3) {["through" => "SilverStripe\Assets\Shortcodes\FileLink"] 140 | // ["from" => "Parent"] 141 | // ["to" => "Linked"] 142 | $toClass = $type['to']; 143 | $throughClass = $type['through']; 144 | $throughClassHasOne = ConfigHelper::get($throughClass, 'has_one'); 145 | if ($throughClassHasOne && isset($throughClassHasOne[$toClass])) { 146 | $type = $throughClassHasOne[$toClass]; 147 | } 148 | } 149 | // Ignore parameters 150 | $type = explode('(', $type, 2); 151 | $type = $type[0]; 152 | $componentMethodClass = new $componentClass($methodName, $classReflection, new ObjectType($type)); 153 | $methods[strtolower($methodName)] = $componentMethodClass; 154 | } 155 | } 156 | } 157 | return $methods; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Reflection/PropertyClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 27 | if (!isset($this->properties[$class])) { 28 | $this->properties[$class] = $this->createProperties($classReflection); 29 | } 30 | $result = isset($this->properties[$class][$propertyName]); 31 | return $result; 32 | } 33 | 34 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection 35 | { 36 | $class = $classReflection->getName(); 37 | if (!isset($this->properties[$class])) { 38 | $this->properties[$class] = $this->createProperties($classReflection); 39 | } 40 | if (isset($this->properties[$class][$propertyName])) { 41 | return $this->properties[$class][$propertyName]; 42 | } 43 | if ($classReflection->isSubclassOf(ClassHelper::ViewableData)) { 44 | // ViewableData has a magic __get() method that always at least 45 | // returns 'null'. 46 | // 47 | // If we couldn't determine a property from `get` methods or extensions. 48 | // 49 | $this->properties[$class][$propertyName] = new ViewableDataGetNullProperty($propertyName, $classReflection); 50 | return $this->properties[$class][$propertyName]; 51 | } 52 | throw new Exception('This should not happen.'); 53 | } 54 | 55 | /** 56 | * @param \PHPStan\Reflection\ClassReflection $classReflection 57 | * @return \PHPStan\Reflection\PropertyReflection[] 58 | */ 59 | private function createProperties(ClassReflection $classReflection): array 60 | { 61 | if (!$classReflection->isSubclassOf(ClassHelper::ViewableData)) { 62 | return []; 63 | } 64 | 65 | $properties = []; 66 | 67 | $class = $classReflection->getName(); 68 | $isDataObjectOrContentController = $classReflection->getName() === ClassHelper::DataObject || 69 | $classReflection->isSubclassOf(ClassHelper::DataObject); 70 | 71 | // Get extension classes 72 | $extensionClasses = array(); 73 | $extensions = ConfigHelper::get($class, 'extensions'); 74 | if ($extensions) { 75 | foreach ($extensions as $extensionClass) { 76 | // Ignore parameters (ie. "Versioned('Stage', 'Live')") 77 | $extensionClass = explode('(', $extensionClass, 2); 78 | $extensionClass = $extensionClass[0]; 79 | 80 | $extensionClasses[$extensionClass] = $extensionClass; 81 | } 82 | } 83 | unset($extensions); 84 | 85 | // Handle magic properties that use 'get$Method' on main class 86 | if ($classReflection->isSubclassOf(ClassHelper::ViewableData)) { 87 | $classesToGetFrom = [$class]; 88 | if ($extensionClasses) { 89 | $classesToGetFrom = array_merge($classesToGetFrom, $extensionClasses); 90 | } 91 | foreach ($classesToGetFrom as $getMethodPropClass) { 92 | // Ignore parameters (ie. "Versioned('Stage', 'Live')") 93 | $getMethodPropClass = explode('(', $getMethodPropClass, 2); 94 | $getMethodPropClass = $getMethodPropClass[0]; 95 | 96 | foreach (get_class_methods($getMethodPropClass) as $method) { 97 | if (substr($method, 0, 3) !== 'get') { 98 | continue; 99 | } 100 | $property = substr($method, 3); 101 | // todo(Jake): Better way to handle properties, if someone does '$this->myPrOp' 102 | // it should work with 'getMyProp' since PHP method aren't case sensitive. 103 | $propInstance = new ViewableDataGetProperty($property, $classReflection); 104 | if (!isset($properties[$property])) { 105 | $properties[$property] = $propInstance; 106 | } 107 | // ie. getOwner() -> owner 108 | $propertyToLower = strtolower($property); 109 | if (!isset($properties[$propertyToLower])) { 110 | $properties[$propertyToLower] = $propInstance; 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Handle Page_Controller where it has $failover 117 | // NOTE(Jake): This is not foolproof, but if people follow the general SS convention 118 | // it'll work. 119 | if (strpos($class, '_Controller') !== false 120 | && $classReflection->isSubclassOf(ClassHelper::ContentController) 121 | ) { 122 | $class = str_replace('_Controller', '', $class); 123 | $isDataObjectOrContentController = true; 124 | } 125 | 126 | if ($isDataObjectOrContentController) { 127 | $dbFields = ConfigHelper::get_db($class); 128 | if ($dbFields) { 129 | foreach ($dbFields as $propertyName => $type) { 130 | $properties[$propertyName] = new ComponentDBFieldProperty($propertyName, $classReflection, $type); 131 | } 132 | } 133 | 134 | $hasOne = ConfigHelper::get_has_one($class); 135 | if ($hasOne) { 136 | foreach ($hasOne as $propertyName => $_) { 137 | $properties[$propertyName] = new ComponentHasOneProperty($propertyName, $classReflection); 138 | } 139 | } 140 | } 141 | 142 | return $properties; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Reflection/ViewableDataGetNullProperty.php: -------------------------------------------------------------------------------- 1 | name = $name; 39 | $this->declaringClass = $declaringClass; 40 | $this->returnType = new NullType; 41 | } 42 | 43 | public function getType(): Type 44 | { 45 | return $this->returnType; 46 | } 47 | 48 | public function getDeclaringClass(): ClassReflection 49 | { 50 | return $this->declaringClass; 51 | } 52 | 53 | public function isStatic(): bool 54 | { 55 | return false; 56 | } 57 | 58 | public function isPrivate(): bool 59 | { 60 | return false; 61 | } 62 | 63 | public function isPublic(): bool 64 | { 65 | return true; 66 | } 67 | 68 | public function isReadable(): bool 69 | { 70 | return true; 71 | } 72 | 73 | public function isWritable(): bool 74 | { 75 | return true; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Reflection/ViewableDataGetProperty.php: -------------------------------------------------------------------------------- 1 | name = $name; 39 | $this->declaringClass = $declaringClass; 40 | $this->returnType = new MixedType; 41 | } 42 | 43 | public function getType(): Type 44 | { 45 | return $this->returnType; 46 | } 47 | 48 | public function getDeclaringClass(): ClassReflection 49 | { 50 | return $this->declaringClass; 51 | } 52 | 53 | public function isStatic(): bool 54 | { 55 | return false; 56 | } 57 | 58 | public function isPrivate(): bool 59 | { 60 | return false; 61 | } 62 | 63 | public function isPublic(): bool 64 | { 65 | return true; 66 | } 67 | 68 | public function isReadable(): bool 69 | { 70 | return true; 71 | } 72 | 73 | public function isWritable(): bool 74 | { 75 | return true; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Rule/RequestFilterPreRequestRule.php: -------------------------------------------------------------------------------- 1 | getClassReflection(); 25 | if (!$classRefl) { 26 | return []; 27 | } 28 | 29 | $className = $classRefl->getName(); 30 | if (!is_a($className, ClassHelper::RequestFilter, true)) { 31 | return []; 32 | } 33 | $functionName = $scope->getFunctionName(); 34 | if ($functionName !== 'preRequest') { 35 | return []; 36 | } 37 | if ($node->expr === null) { 38 | return []; 39 | } 40 | $returnType = $scope->filterByFalseyValue($node->expr)->getType($node->expr); 41 | if ($returnType instanceof ConstantBooleanType) { 42 | // NOTE(Jake): 2018-04-25 43 | // 44 | // Added for SS 3.X. This might not be true in SS 4.0 45 | // 46 | return [ 47 | sprintf( 48 | '%s::preRequest() should not return false as this will cause an uncaught "Invalid Request" exception to be thrown by the SilverStripe framework. (returning "null" will not cause this problem)', 49 | ClassHelper::RequestFilter 50 | ), 51 | ]; 52 | } 53 | return []; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Type/DBFieldStaticReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 24 | return $name === 'create_field'; 25 | } 26 | 27 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type 28 | { 29 | $name = $methodReflection->getName(); 30 | switch ($name) { 31 | case 'create_field': 32 | if (count($methodCall->args) === 0) { 33 | return ParametersAcceptorSelector::selectFromArgs( 34 | $scope, 35 | $methodCall->args, 36 | $methodReflection->getVariants() 37 | )->getReturnType(); 38 | } 39 | // Handle DBField::create_field('HTMLText', '
Value
') 40 | $arg = $methodCall->args[0]->value; 41 | $type = Utility::getTypeFromVariable($arg, $methodReflection); 42 | return $type; 43 | break; 44 | } 45 | $arg = $methodCall->args[0]->value; 46 | 47 | return $scope->getType($arg); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Type/DataListReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 27 | switch ($name) { 28 | // DataList 29 | case 'filter': 30 | case 'filterAny': 31 | case 'reverse': 32 | case 'where': 33 | case 'whereAny': 34 | case 'innerJoin': 35 | case 'sort': 36 | case 'limit': 37 | case 'exclude': 38 | case 'setDataQueryParam': 39 | case 'alterDataQuery': 40 | case 'setQueriedColumns': 41 | case 'byIDs': 42 | case 'addMany': 43 | case 'removeMany': 44 | case 'removeByFilter': 45 | case 'removeAll': 46 | // int[] 47 | case 'getIDList': 48 | // DataObject[] 49 | case 'toArray': 50 | // DataObject 51 | case 'find': 52 | case 'byID': 53 | case 'first': 54 | case 'last': 55 | return true; 56 | break; 57 | 58 | /*case 'min': 59 | case 'max': 60 | case 'avg': 61 | case 'dataClass': 62 | case 'column': 63 | case 'map': 64 | case 'count': 65 | // no-op 66 | break; 67 | 68 | default: 69 | // Debug: Find unused method names 70 | //var_dump($name); exit; 71 | break;*/ 72 | } 73 | return false; 74 | } 75 | 76 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 77 | { 78 | $name = $methodReflection->getName(); 79 | 80 | // NOTE(Jake): 2018-04-21 81 | // Said it could be simplified to this: 82 | // https://github.com/phpstan/phpstan/issues/350#issuecomment-339159006 83 | // 84 | $type = $scope->getType($methodCall->var); 85 | 86 | switch ($name) { 87 | // DataList 88 | case 'filter': 89 | case 'filterAny': 90 | case 'reverse': 91 | case 'where': 92 | case 'whereAny': 93 | case 'innerJoin': 94 | case 'sort': 95 | case 'limit': 96 | case 'exclude': 97 | case 'setDataQueryParam': 98 | case 'alterDataQuery': 99 | case 'setQueriedColumns': 100 | case 'byIDs': 101 | case 'addMany': 102 | case 'removeMany': 103 | case 'removeByFilter': 104 | case 'removeAll': 105 | return $type; 106 | break; 107 | 108 | case 'getIDList': 109 | return new ArrayType(new IntegerType, new IntegerType); 110 | break; 111 | 112 | // DataObject[] 113 | case 'toArray': 114 | // NOTE(Jake): 2018-04-29 115 | // 116 | // Since `instanceof` doesn't work with traits, I'm using this. 117 | // 118 | if (method_exists($type, 'getItemType')) { 119 | return new ArrayType(new IntegerType, $type->getItemType()); 120 | } 121 | return Utility::getMethodReturnType($methodReflection); 122 | break; 123 | 124 | // DataObject 125 | case 'find': 126 | case 'byID': 127 | case 'first': 128 | case 'last': 129 | return $type->getIterableValueType(); 130 | break; 131 | 132 | default: 133 | throw new Exception('Unhandled method call: '.$name); 134 | break; 135 | } 136 | return Utility::getMethodReturnType($methodReflection); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Type/DataListType.php: -------------------------------------------------------------------------------- 1 | itemType = $itemType; 24 | } 25 | 26 | public function describe(VerbosityLevel $level): string 27 | { 28 | $dataListTypeClass = count($this->getReferencedClasses()) === 1 ? $this->getReferencedClasses()[0] : ''; 29 | $itemTypeClass = count($this->itemType->getReferencedClasses()) === 1 ? $this->itemType->getReferencedClasses()[0] : ''; 30 | return sprintf('%s<%s>', $dataListTypeClass, $itemTypeClass); 31 | } 32 | 33 | public function getItemType(): Type 34 | { 35 | return $this->itemType; 36 | } 37 | 38 | public function getIterableValueType(): Type 39 | { 40 | return $this->itemType; 41 | } 42 | 43 | public function resolveStatic(string $className): Type 44 | { 45 | return $this; 46 | } 47 | 48 | public function changeBaseClass(string $className): StaticResolvableType 49 | { 50 | return $this; 51 | } 52 | 53 | public function isDocumentableNatively(): bool 54 | { 55 | return true; 56 | } 57 | 58 | // IterableTrait 59 | 60 | public function canCallMethods(): TrinaryLogic 61 | { 62 | return TrinaryLogic::createYes(); 63 | } 64 | 65 | public function isClonable(): TrinaryLogic 66 | { 67 | return TrinaryLogic::createYes(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Type/DataObjectGetStaticReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 28 | return $name === 'get' || 29 | $name === 'get_one' || 30 | $name === 'get_by_id'; 31 | } 32 | 33 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type 34 | { 35 | $name = $methodReflection->getName(); 36 | switch ($name) { 37 | case 'get': 38 | if (count($methodCall->args) > 0) { 39 | // Handle DataObject::get('Page') 40 | $arg = $methodCall->args[0]; 41 | $type = Utility::getTypeFromInjectorVariable($arg, new ObjectType('SilverStripe\ORM\DataObject')); 42 | return new DataListType(ClassHelper::DataList, $type); 43 | } 44 | // Handle Page::get() / self::get() 45 | $callerClass = $methodCall->class->toString(); 46 | if ($callerClass === 'static') { 47 | return Utility::getMethodReturnType($methodReflection); 48 | } 49 | if ($callerClass === 'self') { 50 | $callerClass = $scope->getClassReflection()->getName(); 51 | } 52 | return new DataListType(ClassHelper::DataList, new ObjectType($callerClass)); 53 | break; 54 | 55 | case 'get_one': 56 | case 'get_by_id': 57 | if (count($methodCall->args) > 0) { 58 | // Handle DataObject::get_one('Page') 59 | $arg = $methodCall->args[0]; 60 | $type = Utility::getTypeFromVariable($arg, $methodReflection); 61 | return $type; 62 | } 63 | // Handle Page::get() / self::get() 64 | $callerClass = $methodCall->class->toString(); 65 | if ($callerClass === 'static') { 66 | return Utility::getMethodReturnType($methodReflection); 67 | } 68 | if ($callerClass === 'self') { 69 | $callerClass = $scope->getClassReflection()->getName(); 70 | } 71 | return new ObjectType($callerClass); 72 | break; 73 | } 74 | // NOTE(mleutenegger): 2019-11-10 75 | // taken from https://github.com/phpstan/phpstan#dynamic-return-type-extensions 76 | if (count($methodCall->args) === 0) { 77 | return ParametersAcceptorSelector::selectFromArgs( 78 | $scope, 79 | $methodCall->args, 80 | $methodReflection->getVariants() 81 | )->getReturnType(); 82 | } 83 | $arg = $methodCall->args[0]->value; 84 | 85 | return $scope->getType($arg); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Type/DataObjectReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 35 | switch ($name) { 36 | case 'getCMSFields': 37 | return true; 38 | break; 39 | 40 | case 'dbObject': 41 | return true; 42 | break; 43 | 44 | case 'newClassInstance': 45 | return true; 46 | break; 47 | } 48 | return false; 49 | } 50 | 51 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 52 | { 53 | $type = $scope->getType($methodCall->var); 54 | 55 | $name = $methodReflection->getName(); 56 | switch ($name) { 57 | case 'getCMSFields': 58 | // todo(Jake): 2018-04-29 59 | // 60 | // This is very incomplete. 61 | // 62 | // Look into determining the values of a FieldList based on scaffolding 63 | // and SiteTree defaults. This is so `GridField` and other FormField 64 | // subclasses can be reasoned about. 65 | // 66 | // A current blocker in PHPStan 0.9.2 is that `parent::getCMSFields()` 67 | // won't work with `DataObjectReturnTypeExtension` but if I call it 68 | // directly from another function like `$this->getCMSFields()`, this will 69 | // execute. 70 | // 71 | $objectType = Utility::getMethodReturnType($methodReflection); 72 | if (!($objectType instanceof ObjectType)) { 73 | throw new Exception('Unexpected type: '.get_class($objectType).', expected ObjectType'); 74 | } 75 | $className = $objectType->getClassName(); 76 | return new FieldListType($className); 77 | break; 78 | 79 | case 'dbObject': 80 | $className = ''; 81 | if ($type instanceof StaticType) { 82 | if (count($type->getReferencedClasses()) === 1) { 83 | $className = $type->getReferencedClasses()[0]; 84 | } 85 | } else if ($type instanceof ObjectType) { 86 | $className = $type->getClassName(); 87 | } 88 | if (!$className) { 89 | throw new Exception('Unhandled type: '.get_class($type)); 90 | //return Utility::getMethodReturnType($methodReflection); 91 | } 92 | if (count($methodCall->args) === 0) { 93 | return Utility::getMethodReturnType($methodReflection); 94 | } 95 | // Handle $this->dbObject('Field') 96 | $arg = $methodCall->args[0]->value; 97 | $fieldName = ''; 98 | if ($arg instanceof Variable) { 99 | // Unhandled, cannot retrieve variable value even if set in this scope. 100 | return Utility::getMethodReturnType($methodReflection); 101 | } else if ($arg instanceof ClassConstFetch) { 102 | // Handle "SiteTree::class" constant 103 | $fieldName = (string)$arg->class; 104 | } else if ($arg instanceof String_) { 105 | $fieldName = $arg->value; 106 | } 107 | if (!$fieldName) { 108 | throw new Exception('Mishandled "newClassInstance" call.'); 109 | //return Utility::getMethodReturnType($methodReflection); 110 | } 111 | $dbFields = ConfigHelper::get_db($className); 112 | if (!isset($dbFields[$fieldName])) { 113 | return Utility::getMethodReturnType($methodReflection); 114 | } 115 | $dbFieldType = $dbFields[$fieldName]; 116 | // NOTE(mleutenegger): 2019-11-10 117 | // $dbFieldType is always truthy 118 | // 119 | // if (!$dbFieldType) { 120 | // return Utility::getMethodReturnType($methodReflection); 121 | // } 122 | return $dbFieldType; 123 | break; 124 | 125 | case 'newClassInstance': 126 | if (count($methodCall->args) === 0) { 127 | return Utility::getMethodReturnType($methodReflection); 128 | } 129 | $arg = $methodCall->args[0]->value; 130 | $type = Utility::getTypeFromVariable($arg, $methodReflection); 131 | return $type; 132 | break; 133 | 134 | default: 135 | throw new Exception('Unhandled method call: '.$name); 136 | break; 137 | } 138 | return Utility::getMethodReturnType($methodReflection); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Type/ExtensionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 32 | switch ($name) { 33 | case 'getOwner': 34 | return true; 35 | break; 36 | } 37 | return false; 38 | } 39 | 40 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 41 | { 42 | $name = $methodReflection->getName(); 43 | 44 | switch ($name) { 45 | // NOTE(Jake): 2018-04-25 46 | // 47 | // Ideally this would work with both '$this->owner' and '$this->getOwner()' 48 | // 49 | // However there doesn't seem to be a `DynamicPropertyReturnTypeExtension` hook and I'm 50 | // not sure on how I can apply that type info. 51 | // 52 | case 'getOwner': 53 | // Get the type of the `Extension` subclass 54 | $type = $scope->getType($methodCall->var); 55 | $objectType = null; 56 | if ($type instanceof ThisType) { 57 | $objectType = new ObjectType($type->getClassName()); 58 | } else { 59 | $objectType = Utility::getTypeFromVariable($methodCall->var, $methodReflection); 60 | } 61 | // NOTE(mleutenegger): 2019-11-10 62 | // $objectType is always truthy 63 | // 64 | // if (!$objectType) { 65 | // return $methodReflection->getReturnType(); 66 | // } 67 | if (!($objectType instanceof ObjectType)) { 68 | throw new Exception('Unexpected type: '.get_class($objectType).', expected ObjectType'); 69 | } 70 | 71 | // Lookup if this extension is configured by any class to be used in their 'extensions' 72 | $extensionClassName = $objectType->getClassName(); 73 | $ownerClassNamesByExtensionClassName = $this->getOwnerClassNamesByExtensionClassName(); 74 | if (!isset($ownerClassNamesByExtensionClassName[$extensionClassName])) { 75 | return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); 76 | } 77 | $classesUsingExtension = $ownerClassNamesByExtensionClassName[$extensionClassName]; 78 | 79 | // 80 | $types = []; 81 | if ($classesUsingExtension) { 82 | foreach ($classesUsingExtension as $class) { 83 | // Ignore classes that don't exist. 84 | if (!class_exists($class)) { 85 | continue; 86 | } 87 | $types[] = new ObjectType($class); 88 | } 89 | } 90 | if (!$types) { 91 | return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); 92 | } 93 | if (count($types) === 1) { 94 | // NOTE(Jake): 2018-04-25 95 | // 96 | // UnionType does not allow multiple types to be passed in 97 | // 98 | return $types[0]; 99 | } 100 | return new UnionType($types); 101 | break; 102 | 103 | default: 104 | throw new Exception('Unhandled method call: '.$name); 105 | break; 106 | } 107 | return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); 108 | } 109 | 110 | private function getOwnerClassNamesByExtensionClassName() 111 | { 112 | if ($this->ownerClassNamesByExtensionClassName !== null) { 113 | return $this->ownerClassNamesByExtensionClassName; 114 | } 115 | $extensionToClassName = array(); 116 | $classes = $this->getClassesUsingExtensibleTrait(); 117 | foreach ($classes as $class) { 118 | $extensions = ConfigHelper::get($class, 'extensions'); 119 | if (!$extensions) { 120 | continue; 121 | } 122 | foreach ($extensions as $extension) { 123 | if (!$extension) { 124 | continue; 125 | } 126 | $extensionToClassName[$extension][$class] = $class; 127 | } 128 | } 129 | return $this->ownerClassNamesByExtensionClassName = $extensionToClassName; 130 | } 131 | 132 | private function getClassesUsingExtensibleTrait() 133 | { 134 | $classes = get_declared_classes(); 135 | $result = array(); 136 | foreach ($classes as $class) { 137 | $hasTrait = false; 138 | foreach ($classes as $subclass) { 139 | if ($subclass === $class || is_subclass_of($subclass, $class)) { 140 | $hasTrait = false; 141 | foreach (class_uses($class) as $trait) { 142 | $hasTrait = $hasTrait || $trait == ClassHelper::Extensible; 143 | } 144 | if ($hasTrait) { 145 | $result[$subclass] = $subclass; 146 | } 147 | } 148 | } 149 | } 150 | $result = array_values($result); 151 | return $result; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Type/FieldListType.php: -------------------------------------------------------------------------------- 1 | itemType = new ObjectType(ClassHelper::FormField); 28 | } 29 | 30 | public function describe(VerbosityLevel $level): string 31 | { 32 | $fieldListClassName = count($this->getReferencedClasses()) === 1 ? $this->getReferencedClasses()[0] : ''; 33 | $itemTypeClass = count($this->itemType->getReferencedClasses()) === 1 ? $this->itemType->getReferencedClasses()[0] : ''; 34 | return sprintf('%s<%s>', $fieldListClassName, $itemTypeClass); 35 | } 36 | 37 | public function getItemType(): Type 38 | { 39 | return $this->itemType; 40 | } 41 | 42 | public function getIterableValueType(): Type 43 | { 44 | return $this->itemType; 45 | } 46 | 47 | public function resolveStatic(string $className): Type 48 | { 49 | return $this; 50 | } 51 | 52 | public function changeBaseClass(string $className): StaticResolvableType 53 | { 54 | return $this; 55 | } 56 | 57 | public function isDocumentableNatively(): bool 58 | { 59 | return true; 60 | } 61 | 62 | // IterableTrait 63 | 64 | public function canCallMethods(): TrinaryLogic 65 | { 66 | return TrinaryLogic::createYes(); 67 | } 68 | 69 | public function isClonable(): TrinaryLogic 70 | { 71 | return TrinaryLogic::createYes(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Type/FormFieldReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(); 28 | switch ($name) { 29 | case 'castedCopy': 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 36 | { 37 | $name = $methodReflection->getName(); 38 | switch ($name) { 39 | case 'castedCopy': 40 | if (sizeof($methodCall->args) > 0) { 41 | return Utility::getTypeFromInjectorVariable($methodCall->args[0], Utility::getMethodReturnType($methodReflection)); 42 | } 43 | break; 44 | } 45 | return Utility::getMethodReturnType($methodReflection); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Type/HasMethodTypeSpecifyingExtension.php: -------------------------------------------------------------------------------- 1 | typeSpecifier = $typeSpecifier; 30 | } 31 | 32 | public function isMethodSupported( 33 | MethodReflection $methodReflection, 34 | MethodCall $node, 35 | TypeSpecifierContext $context 36 | ): bool { 37 | return $methodReflection->getName() === 'hasMethod' 38 | && $context->truthy() 39 | && count($node->args) >= 1; 40 | } 41 | 42 | public function specifyTypes( 43 | MethodReflection $methodReflection, 44 | MethodCall $node, 45 | Scope $scope, 46 | TypeSpecifierContext $context 47 | ): SpecifiedTypes { 48 | $methodNameType = $scope->getType($node->args[0]->value); 49 | if (!$methodNameType instanceof ConstantStringType) { 50 | return new SpecifiedTypes([], []); 51 | } 52 | return $this->typeSpecifier->create( 53 | $node->var, 54 | new HasMethodType($methodNameType->getValue()), 55 | $context 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Type/InjectorReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | '', 19 | ]; 20 | 21 | public function getClass(): string 22 | { 23 | return ClassHelper::Injector; 24 | } 25 | 26 | public function isMethodSupported(MethodReflection $methodReflection): bool 27 | { 28 | return isset($this->methodNames[$methodReflection->getName()]); 29 | } 30 | 31 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 32 | { 33 | $name = $methodReflection->getName(); 34 | switch ($name) { 35 | case 'get': 36 | if (count($methodCall->args) === 0) { 37 | return Utility::getMethodReturnType($methodReflection); 38 | } 39 | $arg = $methodCall->args[0]->value; 40 | $type = Utility::getTypeFromInjectorVariable( 41 | $arg, 42 | Utility::getMethodReturnType($methodReflection) 43 | ); 44 | return $type; 45 | break; 46 | 47 | default: 48 | throw new LogicException('Unhandled method call: '.$name); 49 | break; 50 | } 51 | 52 | return Utility::getMethodReturnType($methodReflection); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Type/SingletonReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | '', 18 | ]; 19 | 20 | public function isFunctionSupported(FunctionReflection $functionReflection): bool 21 | { 22 | return isset($this->functionNames[strtolower($functionReflection->getName())]); 23 | } 24 | 25 | public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type 26 | { 27 | $name = $functionReflection->getName(); 28 | switch ($name) { 29 | case 'singleton': 30 | if (count($functionCall->args) === 0) { 31 | return Utility::getMethodReturnType($functionReflection); 32 | } 33 | // Handle singleton('HTMLText') 34 | $arg = $functionCall->args[0]->value; 35 | $type = Utility::getTypeFromInjectorVariable($arg, Utility::getMethodReturnType($functionReflection)); 36 | return $type; 37 | break; 38 | } 39 | return Utility::getMethodReturnType($functionReflection); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Utility.php: -------------------------------------------------------------------------------- 1 | value; 52 | } 53 | 54 | // Handle a case such as Injector::inst()->get(CacheInterface::class . '.VersionProvider_composerlock'); 55 | if ($node instanceof Concat && $node->right instanceof String_ && $node->right->value[0] === '.') { 56 | $node = $node->left; 57 | } 58 | 59 | if ($node instanceof String_) { 60 | // Handle string: 'HomePage' or '%$HomePage' 61 | $label = $node->value; 62 | } else if ($node instanceof ClassConstFetch) { 63 | // Handle type: 'HomePage::class' 64 | $label = (string)$node->class; 65 | } else if ($node instanceof PropertyFetch || $node instanceof ArrayDimFetch || $node instanceof MethodCall || $node instanceof Concat) { 66 | // Handle passing of: '$this->modelClass' in ModelAdmin to 'singleton' 67 | return $defaultType; 68 | } else if ($node instanceof Variable) { 69 | // NOTE(Jake): 2018-04-21 70 | // 71 | // If we pass in scope, we can get the variable type: 72 | // - $scope->getVariableType($node->name) 73 | // 74 | // However, it seems that PHPStan does not retain constant 75 | // strings / values in scope, so we just need to rely on 76 | // what the method returns in its type hinting. 77 | // 78 | return $defaultType; 79 | } else if ($node instanceof Class_) { 80 | // @todo __CLASS__ constant not currently supported, but could be. Note that self::class isn't yet supported by the ClassConstFetch check either 81 | return $defaultType; 82 | } 83 | 84 | if (!$label) { 85 | var_dump($node); 86 | throw new Exception(__FUNCTION__.': Unhandled or invalid "class" data. Type passed:'.get_class($node)); 87 | } 88 | return self::getClassFromInjectorString($label); 89 | } 90 | 91 | public static function getClassFromInjectorString($classNameOrLabel): ObjectType 92 | { 93 | if (preg_match('/^%\$/', $classNameOrLabel)) { 94 | $classNameOrLabel = substr($classNameOrLabel, 2); 95 | } 96 | 97 | $injectorInfo = ConfigHelper::get(ClassHelper::Injector, $classNameOrLabel); 98 | if (!$injectorInfo) { 99 | return new ObjectType($classNameOrLabel); 100 | } 101 | if (is_string($injectorInfo)) { 102 | // Recursive service lookup 103 | if (preg_match('/^%\$/', $injectorInfo)) { 104 | return self::getClassFromInjectorString($injectorInfo); 105 | } 106 | 107 | return new ObjectType($injectorInfo); 108 | } 109 | if (is_array($injectorInfo) && 110 | isset($injectorInfo['class'])) { 111 | return new ObjectType($injectorInfo['class']); 112 | } 113 | // NOTE(Jake): 2018-05-05 114 | // 115 | // If only "properties" is set on a class/label, like the `RequestProcessor` class. 116 | // Then we simply use the original class name passed in. (No override is configured) 117 | // 118 | return new ObjectType($classNameOrLabel); 119 | } 120 | 121 | public static function getTypeFromVariable(NodeAbstract $node, MethodReflection $methodOrFunctionReflection): Type 122 | { 123 | $class = ''; 124 | if ($node instanceof Arg) { 125 | $node = $node->value; 126 | } 127 | 128 | if ($node instanceof String_) { 129 | // Handle string: 'HomePage' 130 | $class = $node->value; 131 | } else if ($node instanceof ClassConstFetch) { 132 | // Handle type: 'HomePage::class' 133 | $class = (string)$node->class; 134 | } else if ($node instanceof Variable) { 135 | if ($node->name === 'this') { 136 | // NOTE(Jake): 2018-04-25 137 | // 138 | // We might want to handle $this better. 139 | // This would require having `Scope` 140 | // 141 | } 142 | // NOTE(Jake): 2018-04-21 143 | // 144 | // If we pass in scope, we can get the variable type: 145 | // - $scope->getVariableType($node->name) 146 | // 147 | // However, it seems that PHPStan does not retain constant 148 | // strings / values in scope, so we just need to rely on 149 | // what the method returns in its type hinting. 150 | // 151 | return self::getMethodReturnType($methodOrFunctionReflection); 152 | } else if ($node instanceof ArrayDimFetch || $node instanceof MethodCall || $node instanceof PropertyFetch) { 153 | // NOTE(Jake): 2018-05-19 154 | // 155 | // If we pass in scope, we can get the variable type: 156 | // - $scope->getVariableType($node->name) 157 | // 158 | // However, it seems that PHPStan does not retain constant 159 | // strings / values in scope, so we just need to rely on 160 | // what the method returns in its type hinting. 161 | // 162 | return self::getMethodReturnType($methodOrFunctionReflection); 163 | } 164 | if (!$class) { 165 | var_dump($node); 166 | throw new Exception(__FUNCTION__.':Unhandled or invalid "class" data. Type passed:'.get_class($node)); 167 | } 168 | 169 | // Most of these lookups are now injector-based 170 | return self::getClassFromInjectorString($class); 171 | } 172 | 173 | /** 174 | * Get a return type from a MethodReflection or FunctionReflection. 175 | * Calls its variants, and if necessary, creates a union type 176 | * 177 | * @param MethodReflection|FunctionReflection $methodReflection 178 | */ 179 | public static function getMethodReturnType($methodReflection): Type 180 | { 181 | $variants = $methodReflection->getVariants(); 182 | switch (sizeof($variants)) { 183 | case 0: 184 | throw new \LogicException('No method variants for method: ' . $methodReflection->getName()); 185 | 186 | case 1: 187 | return $variants[0]->getReturnType(); 188 | 189 | default: 190 | return new UnionType(array_map( 191 | $variants, 192 | function ($variant) { 193 | return $variant->getReturnType(); 194 | } 195 | )); 196 | } 197 | } 198 | } 199 | --------------------------------------------------------------------------------