Skip to content

Commit 32d0ac3

Browse files
committed
improved return type generation
1 parent dffeddb commit 32d0ac3

12 files changed

+396
-159
lines changed

generator/src/GenerateCommand.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,22 @@ protected function execute(InputInterface $input, OutputInterface $output)
2828

2929
$paths = $scanner->getFunctionsPaths();
3030

31-
[
32-
'functions' => $functions,
33-
'overloadedFunctions' => $overloadedFunctions
34-
] = $scanner->getMethods($paths);
31+
/** @var Method[] $functions */
32+
/** @var string[] $overloadedFunctions */
33+
['functions' => $functions,'overloadedFunctions' => $overloadedFunctions] = $scanner->getMethods($paths);
3534

3635
$output->writeln('These functions have been ignored and must be dealt with manually: '.\implode(', ', $overloadedFunctions));
3736

3837
$fileCreator = new FileCreator();
39-
//$fileCreator->generateXlsFile($protoFunctions, __DIR__ . '/../generated/lib.xls');
4038
$fileCreator->generatePhpFile($functions, __DIR__ . '/../../generated/');
4139
$fileCreator->generateFunctionsList($functions, __DIR__ . '/../../generated/functionsList.php');
4240
$fileCreator->generateRectorFile($functions, __DIR__ . '/../../rector-migrate.yml');
4341

4442

4543
$modules = [];
4644
foreach ($functions as $function) {
47-
$modules[$function->getModuleName()] = $function->getModuleName();
45+
$moduleName = $function->getModuleName();
46+
$modules[$moduleName] = $moduleName;
4847
}
4948

5049
foreach ($modules as $moduleName => $foo) {

generator/src/Method.php

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Safe\PhpStanFunctions\PhpStanFunction;
66
use Safe\PhpStanFunctions\PhpStanFunctionMapReader;
7+
use Safe\PhpStanFunctions\PhpStanType;
78

89
class Method
910
{
@@ -25,22 +26,29 @@ class Method
2526
* @var Parameter[]|null
2627
*/
2728
private $params = null;
28-
/**
29-
* @var PhpStanFunctionMapReader
30-
*/
31-
private $phpStanFunctionMapReader;
3229
/**
3330
* @var int
3431
*/
3532
private $errorType;
33+
/**
34+
* The function prototype from the phpstan internal documentation (functionMap.php)
35+
* @var PhpStanFunction|null
36+
*/
37+
private $phpstanSignarure;
38+
/**
39+
* @var PhpStanType
40+
*/
41+
private $returnType;
3642

3743
public function __construct(\SimpleXMLElement $_functionObject, \SimpleXMLElement $rootEntity, string $moduleName, PhpStanFunctionMapReader $phpStanFunctionMapReader, int $errorType)
3844
{
3945
$this->functionObject = $_functionObject;
4046
$this->rootEntity = $rootEntity;
4147
$this->moduleName = $moduleName;
42-
$this->phpStanFunctionMapReader = $phpStanFunctionMapReader;
4348
$this->errorType = $errorType;
49+
$functionName = $this->getFunctionName();
50+
$this->phpstanSignarure = $phpStanFunctionMapReader->hasFunction($functionName) ? $phpStanFunctionMapReader->getFunction($functionName) : null;
51+
$this->returnType = $this->phpstanSignarure ? $this->phpstanSignarure->getReturnType() : new PhpStanType($this->functionObject->type->__toString());
4452
}
4553

4654
public function getFunctionName(): string
@@ -53,20 +61,9 @@ public function getErrorType(): int
5361
return $this->errorType;
5462
}
5563

56-
public function getReturnType(): string
64+
public function getSignatureReturnType(): string
5765
{
58-
// If the function returns a boolean, since false is for error, true is for success.
59-
// Let's replace this with a "void".
60-
$type = $this->functionObject->type->__toString();
61-
if ($type === 'bool') {
62-
return 'void';
63-
}
64-
// Some types are completely weird. For instance, oci_new_collection returns a "OCI-Collection" (with a dash, yup)
65-
if (\strpos($type, '-') !== false) {
66-
return 'mixed';
67-
}
68-
69-
return Type::toRootNamespace($type);
66+
return $this->returnType->getSignatureType($this->errorType);
7067
}
7168

7269
/**
@@ -78,7 +75,7 @@ public function getParams(): array
7875
if (!isset($this->functionObject->methodparam)) {
7976
return [];
8077
}
81-
$phpStanFunction = $this->getPhpStanData();
78+
$phpStanFunction = $this->phpstanSignarure;
8279
$params = [];
8380
$i=1;
8481
foreach ($this->functionObject->methodparam as $param) {
@@ -124,43 +121,57 @@ private function getDocBlock(): string
124121
$i++;
125122
}
126123

127-
$bestReturnType = $this->getBestReturnType();
128-
if ($bestReturnType !== 'void') {
129-
$str .= '@return '.$bestReturnType. ' ' .$this->getReturnDoc()."\n";
130-
}
124+
$str .= $this->getReturnDocBlock();
131125

132126
$str .= '@throws '.FileCreator::toExceptionName($this->getModuleName()). "\n";
133127

134128
return $str;
135129
}
136130

137-
private function getReturnDoc(): string
131+
public function getReturnDocBlock(): string
138132
{
139133
$returnDoc = $this->getStringForXPath("//docbook:refsect1[@role='returnvalues']/docbook:para");
140-
return $this->stripReturnFalseText($returnDoc);
134+
$returnDoc = $this->stripReturnFalseText($returnDoc);
135+
136+
$bestReturnType = $this->getDocBlockReturnType();
137+
if ($bestReturnType !== 'void') {
138+
return '@return '.$bestReturnType. ' ' .$returnDoc."\n";
139+
}
140+
return '';
141141
}
142142

143143
private function stripReturnFalseText(string $string): string
144144
{
145145
$string = \strip_tags($string);
146-
$string = $this->removeString($string, 'or FALSE on failure');
147-
$string = $this->removeString($string, 'may return FALSE');
148-
$string = $this->removeString($string, 'and FALSE on failure');
149-
$string = $this->removeString($string, 'on success, or FALSE otherwise');
150-
$string = $this->removeString($string, 'or FALSE on error');
151-
$string = $this->removeString($string, 'or FALSE if an error occurred');
152-
$string = $this->removeString($string, ' Returns FALSE otherwise.');
153-
$string = $this->removeString($string, ' and FALSE if an error occurred');
154-
$string = $this->removeString($string, ', NULL if the field does not exist');
155-
$string = $this->removeString($string, 'the function will return TRUE, or FALSE otherwise');
146+
switch ($this->errorType) {
147+
case self::NULLSY_TYPE:
148+
$string = $this->removeString($string, ', or NULL if an error occurs');
149+
$string = $this->removeString($string, ' and NULL on failure');
150+
$string = $this->removeString($string, ' or NULL on failure');
151+
break;
152+
153+
case self::FALSY_TYPE:
154+
$string = $this->removeString($string, 'or FALSE on failure');
155+
$string = $this->removeString($string, '. Returns FALSE on error');
156+
$string = $this->removeString($string, 'may return FALSE');
157+
$string = $this->removeString($string, 'and FALSE on failure');
158+
$string = $this->removeString($string, 'on success, or FALSE otherwise');
159+
$string = $this->removeString($string, 'or FALSE on error');
160+
$string = $this->removeString($string, 'or FALSE if an error occurred');
161+
$string = $this->removeString($string, ' Returns FALSE otherwise.');
162+
$string = $this->removeString($string, ' and FALSE if an error occurred');
163+
$string = $this->removeString($string, 'the function will return TRUE, or FALSE otherwise');
164+
break;
165+
166+
default:
167+
throw new \RuntimeException('Incorrect error type.');
168+
}
169+
156170
return $string;
157171
}
158172

159173
/**
160174
* Removes a string, even if the string is split on multiple lines.
161-
* @param string $string
162-
* @param string $search
163-
* @return string
164175
*/
165176
private function removeString(string $string, string $search): string
166177
{
@@ -185,24 +196,9 @@ private function getStringForXPath(string $xpath): string
185196
return trim($str);
186197
}
187198

188-
private function getBestReturnType(): ?string
199+
private function getDocBlockReturnType(): ?string
189200
{
190-
$phpStanFunction = $this->getPhpStanData();
191-
// Get the type from PhpStan database first, then from the php doc.
192-
if ($phpStanFunction !== null) {
193-
return Type::toRootNamespace($phpStanFunction->getReturnType());
194-
} else {
195-
return Type::toRootNamespace($this->getReturnType());
196-
}
197-
}
198-
199-
private function getPhpStanData(): ?PhpStanFunction
200-
{
201-
$functionName = $this->getFunctionName();
202-
if (!$this->phpStanFunctionMapReader->hasFunction($functionName)) {
203-
return null;
204-
}
205-
return $this->phpStanFunctionMapReader->getFunction($functionName);
201+
return $this->returnType->getDocBlockType($this->errorType);
206202
}
207203

208204
private function getInnerXml(\SimpleXMLElement $SimpleXMLElement): string

generator/src/Parameter.php

Lines changed: 14 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
namespace Safe;
33

44
use Safe\PhpStanFunctions\PhpStanFunction;
5+
use Safe\PhpStanFunctions\PhpStanParameter;
6+
use Safe\PhpStanFunctions\PhpStanType;
57

68
class Parameter
79
{
@@ -10,75 +12,36 @@ class Parameter
1012
*/
1113
private $parameter;
1214
/**
13-
* @var PhpStanFunction|null
15+
* @var PhpStanType
1416
*/
15-
private $phpStanFunction;
17+
private $type;
18+
/**
19+
* @var PhpStanParameter|null
20+
*/
21+
private $phpStanParams;
1622

1723
public function __construct(\SimpleXMLElement $parameter, ?PhpStanFunction $phpStanFunction)
1824
{
1925
$this->parameter = $parameter;
20-
$this->phpStanFunction = $phpStanFunction;
26+
$this->phpStanParams = $phpStanFunction ? $phpStanFunction->getParameter($this->getParameter()) : null;
27+
28+
$this->type = $this->phpStanParams ? $this->phpStanParams->getType() : new PhpStanType($this->parameter->type->__toString());
2129
}
2230

2331
/**
2432
* Tries to identify the typehint from the doc-block parameter
2533
*/
2634
public function getSignatureType(): string
2735
{
28-
$coreType = $this->getDocBlockType();
29-
//list all types in the doc-block
30-
$types = explode('|', $coreType);
31-
//no typehint exists for thoses cases
32-
if (in_array('resource', $types) || in_array('mixed', $types)) {
33-
return '';
34-
}
35-
//remove 'null' from the list to identify if the signature type should be nullable
36-
$nullablePosition = array_search('null', $types);
37-
if ($nullablePosition !== false) {
38-
array_splice($types, $nullablePosition, 1);
39-
}
40-
if (count($types) === 0) {
41-
throw new \RuntimeException('Error when trying to extract parameter type');
42-
}
43-
//if there is still several types, no typehint
44-
if (count($types) > 1) {
45-
return '';
46-
}
47-
48-
$finalType = $types[0];
49-
//strip callable type of its possible parenthesis and return (ex: callable(): void)
50-
if (\strpos($finalType, 'callable(') > -1) {
51-
$finalType = 'callable';
52-
} elseif (strpos($finalType, '[]') !== false) {
53-
$finalType = 'iterable'; //generics cannot be typehinted and have to be turned into iterable
54-
}
55-
return ($nullablePosition !== false ? '?' : '').$finalType;
36+
return $this->type->getSignatureType();
5637
}
5738

5839
/**
5940
* Try to fetch the complete type used in the doc_block, first from phpstan, then from the regular documentation
6041
*/
6142
public function getDocBlockType(): string
6243
{
63-
if ($this->phpStanFunction !== null) {
64-
$phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter());
65-
if ($phpStanParameter) {
66-
try {
67-
$type = $phpStanParameter->getType();
68-
// Let's make the parameter nullable if it is by reference and is used only for writing.
69-
if ($phpStanParameter->isWriteOnly() && $type !== 'resource' && $type !== 'mixed') {
70-
$type = $type.'|null';
71-
}
72-
return $type;
73-
} catch (EmptyTypeException $e) {
74-
// If the type is empty in PHPStan, we fallback to documentation.
75-
// @ignoreException
76-
}
77-
}
78-
}
79-
80-
$type = $this->parameter->type->__toString();
81-
return Type::toRootNamespace($type);
44+
return $this->type->getDocBlockType();
8245
}
8346

8447
public function getParameter(): string
@@ -125,13 +88,7 @@ public function isVariadic(): bool
12588

12689
public function isNullable(): bool
12790
{
128-
if ($this->phpStanFunction !== null) {
129-
$phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter());
130-
if ($phpStanParameter) {
131-
return $phpStanParameter->isNullable();
132-
}
133-
}
134-
return $this->hasDefaultValue() && $this->getDefaultValue() === 'null';
91+
return $this->type->isNullable();
13592
}
13693

13794
/*

generator/src/PhpStanFunctions/CustomPhpStanFunctionMap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
return [
99
'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'],
1010
'swoole_async_writefile' => ['bool', 'filename'=>'string', 'content'=>'string', 'callback='=>'callable', 'flags='=>'int'],
11+
'libxml_get_last_error' => ['LibXMLError|false'], //LibXMLError need to be uppercase
1112
];

generator/src/PhpStanFunctions/PhpStanFunction.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,16 @@ class PhpStanFunction
2020
*/
2121
public function __construct(array $signature)
2222
{
23-
$this->returnType = \array_shift($signature);
23+
$this->returnType = new PhpStanType(\array_shift($signature));
2424
foreach ($signature as $name => $type) {
2525
$param = new PhpStanParameter($name, $type);
2626
$this->parameters[$param->getName()] = $param;
2727
}
2828
}
29-
30-
/**
31-
* @return string
32-
*/
33-
public function getReturnType(): string
29+
30+
public function getReturnType(): PhpStanType
3431
{
32+
return $this->returnType;
3533
if ($this->returnType === 'bool') {
3634
$this->returnType = 'void';
3735
}

0 commit comments

Comments
 (0)