diff --git a/generator/src/GenerateCommand.php b/generator/src/GenerateCommand.php index 8d569a50..9d6ea35e 100644 --- a/generator/src/GenerateCommand.php +++ b/generator/src/GenerateCommand.php @@ -28,15 +28,13 @@ protected function execute(InputInterface $input, OutputInterface $output) $paths = $scanner->getFunctionsPaths(); - [ - 'functions' => $functions, - 'overloadedFunctions' => $overloadedFunctions - ] = $scanner->getMethods($paths); + /** @var Method[] $functions */ + /** @var string[] $overloadedFunctions */ + ['functions' => $functions,'overloadedFunctions' => $overloadedFunctions] = $scanner->getMethods($paths); $output->writeln('These functions have been ignored and must be dealt with manually: '.\implode(', ', $overloadedFunctions)); $fileCreator = new FileCreator(); - //$fileCreator->generateXlsFile($protoFunctions, __DIR__ . '/../generated/lib.xls'); $fileCreator->generatePhpFile($functions, __DIR__ . '/../../generated/'); $fileCreator->generateFunctionsList($functions, __DIR__ . '/../../generated/functionsList.php'); $fileCreator->generateRectorFile($functions, __DIR__ . '/../../rector-migrate.yml'); @@ -44,7 +42,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $modules = []; foreach ($functions as $function) { - $modules[$function->getModuleName()] = $function->getModuleName(); + $moduleName = $function->getModuleName(); + $modules[$moduleName] = $moduleName; } foreach ($modules as $moduleName => $foo) { diff --git a/generator/src/Method.php b/generator/src/Method.php index b2726bf5..be0a6386 100644 --- a/generator/src/Method.php +++ b/generator/src/Method.php @@ -4,6 +4,7 @@ use Safe\PhpStanFunctions\PhpStanFunction; use Safe\PhpStanFunctions\PhpStanFunctionMapReader; +use Safe\PhpStanFunctions\PhpStanType; class Method { @@ -25,22 +26,29 @@ class Method * @var Parameter[]|null */ private $params = null; - /** - * @var PhpStanFunctionMapReader - */ - private $phpStanFunctionMapReader; /** * @var int */ private $errorType; + /** + * The function prototype from the phpstan internal documentation (functionMap.php) + * @var PhpStanFunction|null + */ + private $phpstanSignarure; + /** + * @var PhpStanType + */ + private $returnType; public function __construct(\SimpleXMLElement $_functionObject, \SimpleXMLElement $rootEntity, string $moduleName, PhpStanFunctionMapReader $phpStanFunctionMapReader, int $errorType) { $this->functionObject = $_functionObject; $this->rootEntity = $rootEntity; $this->moduleName = $moduleName; - $this->phpStanFunctionMapReader = $phpStanFunctionMapReader; $this->errorType = $errorType; + $functionName = $this->getFunctionName(); + $this->phpstanSignarure = $phpStanFunctionMapReader->hasFunction($functionName) ? $phpStanFunctionMapReader->getFunction($functionName) : null; + $this->returnType = $this->phpstanSignarure ? $this->phpstanSignarure->getReturnType() : new PhpStanType($this->functionObject->type->__toString()); } public function getFunctionName(): string @@ -53,20 +61,9 @@ public function getErrorType(): int return $this->errorType; } - public function getReturnType(): string + public function getSignatureReturnType(): string { - // If the function returns a boolean, since false is for error, true is for success. - // Let's replace this with a "void". - $type = $this->functionObject->type->__toString(); - if ($type === 'bool') { - return 'void'; - } - // Some types are completely weird. For instance, oci_new_collection returns a "OCI-Collection" (with a dash, yup) - if (\strpos($type, '-') !== false) { - return 'mixed'; - } - - return Type::toRootNamespace($type); + return $this->returnType->getSignatureType($this->errorType); } /** @@ -78,7 +75,7 @@ public function getParams(): array if (!isset($this->functionObject->methodparam)) { return []; } - $phpStanFunction = $this->getPhpStanData(); + $phpStanFunction = $this->phpstanSignarure; $params = []; $i=1; foreach ($this->functionObject->methodparam as $param) { @@ -124,43 +121,57 @@ private function getDocBlock(): string $i++; } - $bestReturnType = $this->getBestReturnType(); - if ($bestReturnType !== 'void') { - $str .= '@return '.$bestReturnType. ' ' .$this->getReturnDoc()."\n"; - } + $str .= $this->getReturnDocBlock(); $str .= '@throws '.FileCreator::toExceptionName($this->getModuleName()). "\n"; return $str; } - private function getReturnDoc(): string + public function getReturnDocBlock(): string { $returnDoc = $this->getStringForXPath("//docbook:refsect1[@role='returnvalues']/docbook:para"); - return $this->stripReturnFalseText($returnDoc); + $returnDoc = $this->stripReturnFalseText($returnDoc); + + $bestReturnType = $this->getDocBlockReturnType(); + if ($bestReturnType !== 'void') { + return '@return '.$bestReturnType. ' ' .$returnDoc."\n"; + } + return ''; } private function stripReturnFalseText(string $string): string { $string = \strip_tags($string); - $string = $this->removeString($string, 'or FALSE on failure'); - $string = $this->removeString($string, 'may return FALSE'); - $string = $this->removeString($string, 'and FALSE on failure'); - $string = $this->removeString($string, 'on success, or FALSE otherwise'); - $string = $this->removeString($string, 'or FALSE on error'); - $string = $this->removeString($string, 'or FALSE if an error occurred'); - $string = $this->removeString($string, ' Returns FALSE otherwise.'); - $string = $this->removeString($string, ' and FALSE if an error occurred'); - $string = $this->removeString($string, ', NULL if the field does not exist'); - $string = $this->removeString($string, 'the function will return TRUE, or FALSE otherwise'); + switch ($this->errorType) { + case self::NULLSY_TYPE: + $string = $this->removeString($string, ', or NULL if an error occurs'); + $string = $this->removeString($string, ' and NULL on failure'); + $string = $this->removeString($string, ' or NULL on failure'); + break; + + case self::FALSY_TYPE: + $string = $this->removeString($string, 'or FALSE on failure'); + $string = $this->removeString($string, '. Returns FALSE on error'); + $string = $this->removeString($string, 'may return FALSE'); + $string = $this->removeString($string, 'and FALSE on failure'); + $string = $this->removeString($string, 'on success, or FALSE otherwise'); + $string = $this->removeString($string, 'or FALSE on error'); + $string = $this->removeString($string, 'or FALSE if an error occurred'); + $string = $this->removeString($string, ' Returns FALSE otherwise.'); + $string = $this->removeString($string, ' and FALSE if an error occurred'); + $string = $this->removeString($string, 'the function will return TRUE, or FALSE otherwise'); + break; + + default: + throw new \RuntimeException('Incorrect error type.'); + } + return $string; } /** * Removes a string, even if the string is split on multiple lines. - * @param string $string - * @param string $search - * @return string */ private function removeString(string $string, string $search): string { @@ -185,24 +196,9 @@ private function getStringForXPath(string $xpath): string return trim($str); } - private function getBestReturnType(): ?string + private function getDocBlockReturnType(): ?string { - $phpStanFunction = $this->getPhpStanData(); - // Get the type from PhpStan database first, then from the php doc. - if ($phpStanFunction !== null) { - return Type::toRootNamespace($phpStanFunction->getReturnType()); - } else { - return Type::toRootNamespace($this->getReturnType()); - } - } - - private function getPhpStanData(): ?PhpStanFunction - { - $functionName = $this->getFunctionName(); - if (!$this->phpStanFunctionMapReader->hasFunction($functionName)) { - return null; - } - return $this->phpStanFunctionMapReader->getFunction($functionName); + return $this->returnType->getDocBlockType($this->errorType); } private function getInnerXml(\SimpleXMLElement $SimpleXMLElement): string diff --git a/generator/src/Parameter.php b/generator/src/Parameter.php index 3a0a2b73..248b8653 100644 --- a/generator/src/Parameter.php +++ b/generator/src/Parameter.php @@ -2,6 +2,8 @@ namespace Safe; use Safe\PhpStanFunctions\PhpStanFunction; +use Safe\PhpStanFunctions\PhpStanParameter; +use Safe\PhpStanFunctions\PhpStanType; class Parameter { @@ -10,14 +12,20 @@ class Parameter */ private $parameter; /** - * @var PhpStanFunction|null + * @var PhpStanType */ - private $phpStanFunction; + private $type; + /** + * @var PhpStanParameter|null + */ + private $phpStanParams; public function __construct(\SimpleXMLElement $parameter, ?PhpStanFunction $phpStanFunction) { $this->parameter = $parameter; - $this->phpStanFunction = $phpStanFunction; + $this->phpStanParams = $phpStanFunction ? $phpStanFunction->getParameter($this->getParameter()) : null; + + $this->type = $this->phpStanParams ? $this->phpStanParams->getType() : new PhpStanType($this->parameter->type->__toString()); } /** @@ -25,34 +33,7 @@ public function __construct(\SimpleXMLElement $parameter, ?PhpStanFunction $phpS */ public function getSignatureType(): string { - $coreType = $this->getDocBlockType(); - //list all types in the doc-block - $types = explode('|', $coreType); - //no typehint exists for thoses cases - if (in_array('resource', $types) || in_array('mixed', $types)) { - return ''; - } - //remove 'null' from the list to identify if the signature type should be nullable - $nullablePosition = array_search('null', $types); - if ($nullablePosition !== false) { - array_splice($types, $nullablePosition, 1); - } - if (count($types) === 0) { - throw new \RuntimeException('Error when trying to extract parameter type'); - } - //if there is still several types, no typehint - if (count($types) > 1) { - return ''; - } - - $finalType = $types[0]; - //strip callable type of its possible parenthesis and return (ex: callable(): void) - if (\strpos($finalType, 'callable(') > -1) { - $finalType = 'callable'; - } elseif (strpos($finalType, '[]') !== false) { - $finalType = 'iterable'; //generics cannot be typehinted and have to be turned into iterable - } - return ($nullablePosition !== false ? '?' : '').$finalType; + return $this->type->getSignatureType(); } /** @@ -60,25 +41,7 @@ public function getSignatureType(): string */ public function getDocBlockType(): string { - if ($this->phpStanFunction !== null) { - $phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter()); - if ($phpStanParameter) { - try { - $type = $phpStanParameter->getType(); - // Let's make the parameter nullable if it is by reference and is used only for writing. - if ($phpStanParameter->isWriteOnly() && $type !== 'resource' && $type !== 'mixed') { - $type = $type.'|null'; - } - return $type; - } catch (EmptyTypeException $e) { - // If the type is empty in PHPStan, we fallback to documentation. - // @ignoreException - } - } - } - - $type = $this->parameter->type->__toString(); - return Type::toRootNamespace($type); + return $this->type->getDocBlockType(); } public function getParameter(): string @@ -125,13 +88,7 @@ public function isVariadic(): bool public function isNullable(): bool { - if ($this->phpStanFunction !== null) { - $phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter()); - if ($phpStanParameter) { - return $phpStanParameter->isNullable(); - } - } - return $this->hasDefaultValue() && $this->getDefaultValue() === 'null'; + return $this->type->isNullable(); } /* diff --git a/generator/src/PhpStanFunctions/CustomPhpStanFunctionMap.php b/generator/src/PhpStanFunctions/CustomPhpStanFunctionMap.php index 942f4edb..ca0f1019 100644 --- a/generator/src/PhpStanFunctions/CustomPhpStanFunctionMap.php +++ b/generator/src/PhpStanFunctions/CustomPhpStanFunctionMap.php @@ -8,4 +8,8 @@ return [ 'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], 'swoole_async_writefile' => ['bool', 'filename'=>'string', 'content'=>'string', 'callback='=>'callable', 'flags='=>'int'], + 'libxml_get_last_error' => ['LibXMLError|false'], //LibXMLError need to be uppercase + 'gmp_random_seed' => ['void', 'seed'=>'GMP|string|int'], //gmp_random_seed doesn't return + 'imageconvolution' => ['bool', 'src_im'=>'resource', 'matrix3x3'=>'array', 'div'=>'float', 'offset'=>'float'], //imageconvolution return a bool + 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], //iptcembed return either a string, true or false ]; diff --git a/generator/src/PhpStanFunctions/PhpStanFunction.php b/generator/src/PhpStanFunctions/PhpStanFunction.php index 7edd6ec6..c937d08e 100644 --- a/generator/src/PhpStanFunctions/PhpStanFunction.php +++ b/generator/src/PhpStanFunctions/PhpStanFunction.php @@ -6,7 +6,7 @@ class PhpStanFunction { /** - * @var string + * @var PhpStanType */ private $returnType; @@ -20,22 +20,16 @@ class PhpStanFunction */ public function __construct(array $signature) { - $this->returnType = \array_shift($signature); + $this->returnType = new PhpStanType(\array_shift($signature)); foreach ($signature as $name => $type) { $param = new PhpStanParameter($name, $type); $this->parameters[$param->getName()] = $param; } } - - /** - * @return string - */ - public function getReturnType(): string + + public function getReturnType(): PhpStanType { - if ($this->returnType === 'bool') { - $this->returnType = 'void'; - } - return \str_replace(['|bool', '|false'], '', $this->returnType); + return $this->returnType; } /** diff --git a/generator/src/PhpStanFunctions/PhpStanParameter.php b/generator/src/PhpStanFunctions/PhpStanParameter.php index 57cea136..d14fd02e 100644 --- a/generator/src/PhpStanFunctions/PhpStanParameter.php +++ b/generator/src/PhpStanFunctions/PhpStanParameter.php @@ -12,7 +12,7 @@ class PhpStanParameter */ private $name; /** - * @var string + * @var PhpStanType */ private $type; @@ -28,10 +28,6 @@ class PhpStanParameter * @var bool */ private $byReference = false; - /** - * @var bool - */ - private $nullable = false; /** * Whether the parameter is "write only" (applies only to "by reference" parameters) * @var bool @@ -40,6 +36,7 @@ class PhpStanParameter public function __construct(string $name, string $type) { + $writeOnly = false; if (\strpos($name, '=') !== false) { $this->optional = true; } @@ -50,19 +47,13 @@ public function __construct(string $name, string $type) $this->byReference = true; } if (\strpos($name, '&w_') !== false) { - $this->writeOnly = true; + $writeOnly = true; } $name = \str_replace(['&rw_', '&w_'], '', $name); $name = trim($name, '=.&'); $this->name = $name; - - if (\strpos($type, '?') !== false) { - $type = \str_replace('?', '', $type).'|null'; - $this->nullable = true; - } - - $this->type = $type; + $this->type = new PhpStanType($type, $writeOnly); } /** @@ -73,12 +64,9 @@ public function getName(): string return $this->name; } - /** - * @return string - */ - public function getType(): string + public function getType(): PhpStanType { - return Type::toRootNamespace($this->type); + return $this->type; } /** @@ -114,11 +102,8 @@ public function isWriteOnly(): bool return $this->writeOnly; } - /** - * @return bool - */ public function isNullable(): bool { - return $this->nullable; + return $this->type->isNullable(); } } diff --git a/generator/src/PhpStanFunctions/PhpStanType.php b/generator/src/PhpStanFunctions/PhpStanType.php new file mode 100644 index 00000000..634c5697 --- /dev/null +++ b/generator/src/PhpStanFunctions/PhpStanType.php @@ -0,0 +1,146 @@ +types = $returnTypes; + $this->nullable = $nullable; + $this->falsable = $falsable; + } + + public function getDocBlockType(?int $errorType = null): string + { + $returnTypes = $this->types; + //add back either null or false to the return types unless the target function return null or false on error (only relevant on return type) + if ($this->falsable && $errorType !== Method::FALSY_TYPE) { + $returnTypes[] = 'false'; + } elseif ($this->nullable && $errorType !== Method::NULLSY_TYPE) { + $returnTypes[] = 'null'; + } + $type = join('|', $returnTypes); + if ($type === 'bool' && !$this->nullable && $errorType === Method::FALSY_TYPE) { + // If the function only returns a boolean, since false is for error, true is for success. + // Let's replace this with a "void". + return 'void'; + } + return $type; + } + + public function getSignatureType(?int $errorType = null): string + { + $nullable = $errorType === Method::NULLSY_TYPE ? false : $this->nullable; + $falsable = $errorType === Method::FALSY_TYPE ? false : $this->falsable; + $types = $this->types; + //no typehint exists for thoses cases + if (\array_intersect(self::NO_SIGNATURE_TYPES, $types)) { + return ''; + } + + foreach ($types as &$type) { + if (\strpos($type, 'callable(') > -1) { + $type = 'callable'; //strip callable type of its possible parenthesis and return (ex: callable(): void) + } elseif (\strpos($type, 'array<') !== false || \strpos($type, 'array{') !== false) { + $type = 'array'; //typed array has to be untyped + } elseif (\strpos($type, '[]') !== false) { + $type = 'iterable'; //generics cannot be typehinted and have to be turned into iterable + } + } + + //if there are several types, no typehint + if (count(array_unique($types)) > 1) { + return ''; + } elseif (\in_array('void', $types) || (count($types) === 0 && !$nullable && !$falsable)) { + return 'void'; + } + + + $finalType = $types[0]; + if ($finalType === 'bool' && !$nullable && $errorType === Method::FALSY_TYPE) { + // If the function only returns a boolean, since false is for error, true is for success. + // Let's replace this with a "void". + return 'void'; + } + return ($nullable !== false ? '?' : '').$finalType; + } + + public function isNullable(): bool + { + return $this->nullable; + } + + public function isFalsable(): bool + { + return $this->falsable; + } + + public function removeFalsable(): void + { + $this->falsable = false; + } + + public function removeNullable(): void + { + $this->nullable = false; + } +} diff --git a/generator/src/Type.php b/generator/src/Type.php index 5073fc1e..b4569198 100644 --- a/generator/src/Type.php +++ b/generator/src/Type.php @@ -14,7 +14,7 @@ class Type private static function isClass(string $type): bool { if ($type === '') { - throw new EmptyTypeException('Empty type passed'); + return false; } if ($type === 'stdClass') { return true; @@ -28,9 +28,6 @@ private static function isClass(string $type): bool /** * Put classes in the root namespace - * - * @param string $type - * @return string */ public static function toRootNamespace(string $type): string { diff --git a/generator/src/WritePhpFunction.php b/generator/src/WritePhpFunction.php index 835bbee3..1a38f366 100644 --- a/generator/src/WritePhpFunction.php +++ b/generator/src/WritePhpFunction.php @@ -20,7 +20,9 @@ public function __construct(Method $method) public function getPhpPrototypeFunction(): string { if ($this->method->getFunctionName()) { - return 'function '.$this->method->getFunctionName().'('.$this->displayParamsWithType($this->method->getParams()).')'.': '.$this->method->getReturnType().'{}'; + $returnType = $this->method->getSignatureReturnType(); + $returnType = $returnType ? ': '.$returnType : ''; + return 'function '.$this->method->getFunctionName().'('.$this->displayParamsWithType($this->method->getParams()).')'.$returnType.'{}'; } return ''; } @@ -42,13 +44,10 @@ public function getPhpFunctionalFunction(): string private function writePhpFunction(): string { $phpFunction = $this->method->getPhpDoc(); - if ($this->method->getReturnType() !== 'mixed' && $this->method->getReturnType() !== 'resource') { - $returnType = ': ' . $this->method->getReturnType(); - } else { - $returnType = ''; - } + $returnType = $this->method->getSignatureReturnType(); + $returnType = $returnType ? ': '.$returnType : ''; $returnStatement = ''; - if ($this->method->getReturnType() !== 'void') { + if ($this->method->getSignatureReturnType() !== 'void') { $returnStatement = " return \$result;\n"; } $moduleName = $this->method->getModuleName(); diff --git a/generator/tests/MethodTest.php b/generator/tests/MethodTest.php index d61080e9..9ebc0da6 100644 --- a/generator/tests/MethodTest.php +++ b/generator/tests/MethodTest.php @@ -21,7 +21,7 @@ public function testGetFunctionType() $docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/pcre/functions/preg-match.xml'); $xmlObject = $docPage->getMethodSynopsis(); $method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE); - $type = $method->getReturnType(); + $type = $method->getSignatureReturnType(); $this->assertEquals('int', $type); } @@ -80,4 +80,24 @@ public function testGetInitializer() $params = $method->getParams(); $this->assertEquals('', $params[0]->getDefaultValue()); } + + public function testGetReturnDocBlock(): void + { + $docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/array/functions/array-replace.xml'); + $xmlObject = $docPage->getMethodSynopsis(); + $method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::NULLSY_TYPE); + $this->assertEquals("@return array Returns an array.\n", $method->getReturnDocBlock()); + + $docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/shmop/functions/shmop-delete.xml'); + $xmlObject = $docPage->getMethodSynopsis(); + $method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE); + $this->assertEquals('', $method->getReturnDocBlock()); + $this->assertEquals('void', $method->getSignatureReturnType()); + + $docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/sqlsrv/functions/sqlsrv-next-result.xml'); + $xmlObject = $docPage->getMethodSynopsis(); + $method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE); + $this->assertEquals("@return bool|null Returns TRUE if the next result was successfully retrieved, FALSE if an error \n occurred, and NULL if there are no more results to retrieve.\n", $method->getReturnDocBlock()); + $this->assertEquals('?bool', $method->getSignatureReturnType()); + } } diff --git a/generator/tests/PhpStanFunctions/PhpStanFunctionMapReaderTest.php b/generator/tests/PhpStanFunctions/PhpStanFunctionMapReaderTest.php index 9a923578..7507322a 100644 --- a/generator/tests/PhpStanFunctions/PhpStanFunctionMapReaderTest.php +++ b/generator/tests/PhpStanFunctions/PhpStanFunctionMapReaderTest.php @@ -20,11 +20,11 @@ public function testGet(): void // 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], - $this->assertSame('mixed', $function->getReturnType()); + $this->assertSame('mixed', $function->getReturnType()->getDocBlockType()); $parameters = $function->getParameters(); $this->assertCount(2, $parameters); $this->assertSame('success', $parameters['success']->getName()); - $this->assertSame('bool', $parameters['success']->getType()); + $this->assertSame('bool|null', $parameters['success']->getType()->getDocBlockType()); $this->assertFalse($parameters['success']->isVariadic()); $this->assertTrue($parameters['success']->isByReference()); $this->assertTrue($parameters['success']->isOptional()); diff --git a/generator/tests/PhpStanFunctions/PhpStanTypeTest.php b/generator/tests/PhpStanFunctions/PhpStanTypeTest.php new file mode 100644 index 00000000..cb2644f8 --- /dev/null +++ b/generator/tests/PhpStanFunctions/PhpStanTypeTest.php @@ -0,0 +1,145 @@ +assertEquals('array|string|int', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + } + + public function testCallable(): void + { + $param = new PhpStanType('callable(string): void'); + $this->assertEquals('callable(string): void', $param->getDocBlockType()); + $this->assertEquals('callable', $param->getSignatureType()); + } + + public function testGenerics(): void + { + $param = new PhpStanType('string[]'); + $this->assertEquals('string[]', $param->getDocBlockType()); + $this->assertEquals('iterable', $param->getSignatureType()); + + $param = new PhpStanType('int[]'); + $this->assertEquals('int[]', $param->getDocBlockType()); + $this->assertEquals('iterable', $param->getSignatureType()); + + $param = new PhpStanType('array'); + $this->assertEquals('array', $param->getDocBlockType()); + $this->assertEquals('array', $param->getSignatureType()); + + $param = new PhpStanType('array{0:float,1:float,2:float,3:float,4:float,5:float}'); + $this->assertEquals('array{0:float,1:float,2:float,3:float,4:float,5:float}', $param->getDocBlockType()); + $this->assertEquals('array', $param->getSignatureType()); + } + + public function testNullable(): void + { + $param = new PhpStanType('array|null'); + $this->assertEquals(true, $param->isNullable()); + $this->assertEquals('array|null', $param->getDocBlockType()); + $this->assertEquals('?array', $param->getSignatureType()); + + $param = new PhpStanType('?int|?string'); + $this->assertEquals(true, $param->isNullable()); + $this->assertEquals('int|string|null', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + + $param = new PhpStanType('?string'); + $this->assertEquals(true, $param->isNullable()); + $this->assertEquals('string|null', $param->getDocBlockType()); + $this->assertEquals('?string', $param->getSignatureType()); + + $param = new PhpStanType('?HashContext'); + $this->assertEquals(true, $param->isNullable()); + $this->assertEquals('\HashContext|null', $param->getDocBlockType()); + $this->assertEquals('?\HashContext', $param->getSignatureType()); + } + + public function testParenthesisOutsideOfCallable(): void + { + $param = new PhpStanType('(?int)|(?string)'); + $this->assertEquals(true, $param->isNullable()); + $this->assertEquals('int|string|null', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + } + + public function testFalsable(): void + { + $param = new PhpStanType('string|false'); + $this->assertEquals(true, $param->isFalsable()); + $this->assertEquals('string|false', $param->getDocBlockType()); + $this->assertEquals('string', $param->getSignatureType()); + } + + public function testResource(): void + { + $param = new PhpStanType('resource'); + $this->assertEquals('resource', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + } + + public function testNamespace(): void + { + $param = new PhpStanType('GMP'); + $this->assertEquals('\GMP', $param->getDocBlockType()); + $this->assertEquals('\GMP', $param->getSignatureType()); + } + + public function testVoid(): void + { + $param = new PhpStanType(''); + $this->assertEquals('', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + + $param = new PhpStanType('void'); + $this->assertEquals('void', $param->getDocBlockType()); + $this->assertEquals('void', $param->getSignatureType()); + } + + public function testOciSpecialCases(): void + { + $param = new PhpStanType('OCI-Collection'); + $this->assertEquals('\OCI-Collection', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + + $param = new PhpStanType('OCI-Lob'); + $this->assertEquals('\OCI-Lob', $param->getDocBlockType()); + $this->assertEquals('', $param->getSignatureType()); + } + + public function testErrorTypeInteraction(): void + { + //bool => void if the method is falsy + $param = new PhpStanType('bool'); + $this->assertEquals('void', $param->getDocBlockType(Method::FALSY_TYPE)); + $this->assertEquals('void', $param->getSignatureType(Method::FALSY_TYPE)); + + //int|false => int if the method is falsy + $param = new PhpStanType('int|false'); + $this->assertEquals('int', $param->getDocBlockType(Method::FALSY_TYPE)); + $this->assertEquals('int', $param->getSignatureType(Method::FALSY_TYPE)); + + //int|null => int if the method is nullsy + $param = new PhpStanType('int|null'); + $this->assertEquals('int', $param->getDocBlockType(Method::NULLSY_TYPE)); + $this->assertEquals('int', $param->getSignatureType(Method::NULLSY_TYPE)); + } + + public function testDuplicateType(): void + { + $param = new PhpStanType('array|array|array>'); + $this->assertEquals('array|array|array>', $param->getDocBlockType()); + $this->assertEquals('array', $param->getSignatureType()); + } + +} \ No newline at end of file