Skip to content

Commit

Permalink
improved return type generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Kharhamel committed Oct 18, 2019
1 parent dffeddb commit 32d0ac3
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 159 deletions.
11 changes: 5 additions & 6 deletions generator/src/GenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,22 @@ 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');


$modules = [];
foreach ($functions as $function) {
$modules[$function->getModuleName()] = $function->getModuleName();
$moduleName = $function->getModuleName();
$modules[$moduleName] = $moduleName;
}

foreach ($modules as $moduleName => $foo) {
Expand Down
106 changes: 51 additions & 55 deletions generator/src/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Safe\PhpStanFunctions\PhpStanFunction;
use Safe\PhpStanFunctions\PhpStanFunctionMapReader;
use Safe\PhpStanFunctions\PhpStanType;

class Method
{
Expand All @@ -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
Expand All @@ -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);
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -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
{
Expand All @@ -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
Expand Down
71 changes: 14 additions & 57 deletions generator/src/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
namespace Safe;

use Safe\PhpStanFunctions\PhpStanFunction;
use Safe\PhpStanFunctions\PhpStanParameter;
use Safe\PhpStanFunctions\PhpStanType;

class Parameter
{
Expand All @@ -10,75 +12,36 @@ 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());
}

/**
* Tries to identify the typehint from the doc-block parameter
*/
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();
}

/**
* Try to fetch the complete type used in the doc_block, first from phpstan, then from the regular documentation
*/
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
Expand Down Expand Up @@ -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();
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
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
];
10 changes: 4 additions & 6 deletions generator/src/PhpStanFunctions/PhpStanFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +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
{
return $this->returnType;
if ($this->returnType === 'bool') {
$this->returnType = 'void';
}
Expand Down
Loading

0 comments on commit 32d0ac3

Please sign in to comment.