Skip to content

Commit 46bb15b

Browse files
committed
improved parameter generation (both signature and doc-block)
1 parent 753599a commit 46bb15b

File tree

9 files changed

+133
-40
lines changed

9 files changed

+133
-40
lines changed

generator/src/GenerateCommand.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ protected function execute(InputInterface $input, OutputInterface $output)
5555

5656
// Let's require the generated file to check there is no error.
5757
$files = \glob(__DIR__.'/../../generated/*.php');
58+
if ($files === false) {
59+
throw new \RuntimeException('Failed to require the generated file');
60+
}
5861

5962
foreach ($files as $file) {
6063
require($file);
6164
}
6265

6366
$files = \glob(__DIR__.'/../../generated/Exceptions/*.php');
67+
if ($files === false) {
68+
throw new \RuntimeException('Failed to require the generated exception file');
69+
}
6470

6571
require_once __DIR__.'/../../lib/Exceptions/SafeExceptionInterface.php';
6672
require_once __DIR__.'/../../lib/Exceptions/AbstractSafeException.php';
@@ -76,12 +82,18 @@ protected function execute(InputInterface $input, OutputInterface $output)
7682
private function rmGenerated(): void
7783
{
7884
$exceptions = \glob(__DIR__.'/../../generated/Exceptions/*.php');
85+
if ($exceptions === false) {
86+
throw new \RuntimeException('Failed to require the generated exception files');
87+
}
7988

8089
foreach ($exceptions as $exception) {
8190
\unlink($exception);
8291
}
8392

8493
$files = \glob(__DIR__.'/../../generated/*.php');
94+
if ($files === false) {
95+
throw new \RuntimeException('Failed to require the generated files');
96+
}
8597

8698
foreach ($files as $file) {
8799
\unlink($file);

generator/src/Method.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private function getDocBlock(): string
119119

120120
$i=1;
121121
foreach ($this->getParams() as $parameter) {
122-
$str .= '@param '.$parameter->getBestType().' $'.$parameter->getParameter().' ';
122+
$str .= '@param '.$parameter->getDocBlockType().' $'.$parameter->getParameter().' ';
123123
$str .= $this->getStringForXPath("(//docbook:refsect1[@role='parameters']//docbook:varlistentry)[$i]//docbook:para")."\n";
124124
$i++;
125125
}
@@ -149,6 +149,9 @@ private function stripReturnFalseText(string $string): string
149149
$string = $this->removeString($string, 'on success, or FALSE otherwise');
150150
$string = $this->removeString($string, 'or FALSE on error');
151151
$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');
152155
$string = $this->removeString($string, 'the function will return TRUE, or FALSE otherwise');
153156
return $string;
154157
}

generator/src/Parameter.php

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,66 @@ public function __construct(\SimpleXMLElement $parameter, ?PhpStanFunction $phpS
2121
}
2222

2323
/**
24-
* Returns the type as declared in the doc.
25-
* @return string
24+
* Tries to identify the typehint from the doc-block parameter
2625
*/
27-
public function getType(): string
26+
public function getSignatureType(): string
2827
{
29-
$type = $this->parameter->type->__toString();
30-
$strType = Type::toRootNamespace($type);
31-
if ($strType !== 'mixed' && $strType !== 'resource' && $this->phpStanFunction !== null) {
32-
$phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter());
33-
if ($phpStanParameter) {
34-
// Let's make the parameter nullable if it is by reference and is used only for writing.
35-
if ($phpStanParameter->isWriteOnly()) {
36-
$strType = '?'.$strType;
37-
}
38-
}
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
3954
}
40-
return $strType;
55+
return ($nullablePosition !== false ? '?' : '').$finalType;
4156
}
4257

4358
/**
44-
* Returns the type as declared in the doc.
45-
* @return string
59+
* Try to fetch the complete type used in the doc_block, first from phpstan, then from the regular documentation
4660
*/
47-
public function getBestType(): string
61+
public function getDocBlockType(): string
4862
{
49-
// Get the type from PhpStan database first, then from the php doc.
5063
if ($this->phpStanFunction !== null) {
5164
$phpStanParameter = $this->phpStanFunction->getParameter($this->getParameter());
5265
if ($phpStanParameter) {
5366
try {
54-
return $phpStanParameter->getType();
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;
5573
} catch (EmptyTypeException $e) {
56-
// If the type is empty in PHPStan, let's fallback to documentation.
57-
return $this->getType();
74+
// If the type is empty in PHPStan, we fallback to documentation.
75+
// @ignoreException
5876
}
5977
}
6078
}
61-
return $this->getType();
79+
80+
$type = $this->parameter->type->__toString();
81+
return Type::toRootNamespace($type);
6282
}
6383

64-
/*
65-
* @return string
66-
*/
6784
public function getParameter(): string
6885
{
6986
if ($this->isVariadic()) {
@@ -157,9 +174,4 @@ private function getInnerXml(\SimpleXMLElement $SimpleXMLElement): string
157174
$inner_xml = trim($inner_xml);
158175
return $inner_xml;
159176
}
160-
161-
public function isTypeable(): bool
162-
{
163-
return $this->getType() !== 'mixed' && $this->getType() !== 'resource' && \count(\explode("|", $this->getType())) < 2;
164-
}
165177
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
/**
4+
* Our own custom function map, used to quickly correct errors in phpstan's function map.
5+
* This file must always be check against phpstan file to remove duplicates as phpstan keep getting corrected.
6+
*/
7+
8+
return [
9+
'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'],
10+
'swoole_async_writefile' => ['bool', 'filename'=>'string', 'content'=>'string', 'callback='=>'callable', 'flags='=>'int'],
11+
];

generator/src/PhpStanFunctions/PhpStanFunctionMapReader.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ class PhpStanFunctionMapReader
99
* @var array<string, array>
1010
*/
1111
private $functionMap;
12+
/**
13+
* @var array<string, array>
14+
*/
15+
private $customFunctionMap;
1216

1317
public function __construct()
1418
{
1519
$this->functionMap = require __DIR__.'/../../vendor/phpstan/phpstan/src/Reflection/SignatureMap/functionMap.php';
20+
$this->customFunctionMap = require __DIR__.'/CustomPhpStanFunctionMap.php';
1621
}
1722

1823
public function hasFunction(string $functionName): bool
@@ -22,6 +27,14 @@ public function hasFunction(string $functionName): bool
2227

2328
public function getFunction(string $functionName): PhpStanFunction
2429
{
25-
return new PhpStanFunction($this->functionMap[$functionName]);
30+
$map = $this->functionMap[$functionName];
31+
$customMap = $this->customFunctionMap[$functionName] ?? null;
32+
if ($map && $customMap) {
33+
if ($customMap === $map) {
34+
throw new \RuntimeException('Useless custom function map: '.var_export($customMap, true)."\nPlease delete this line from the custom file");
35+
}
36+
$map = $customMap;
37+
}
38+
return new PhpStanFunction($map);
2639
}
2740
}

generator/src/Scanner.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ public function getMethods(array $paths): array
8585
$phpStanFunctionMapReader = new PhpStanFunctionMapReader();
8686
$ignoredFunctions = $this->getIgnoredFunctions();
8787
$ignoredFunctions = \array_combine($ignoredFunctions, $ignoredFunctions);
88+
if ($ignoredFunctions === false) {
89+
throw new \RuntimeException('Failed when combining arrays');
90+
}
8891
$ignoredModules = $this->getIgnoredModules();
8992
$ignoredModules = \array_combine($ignoredModules, $ignoredModules);
93+
if ($ignoredModules === false) {
94+
throw new \RuntimeException('Failed when combining arrays');
95+
}
9096
foreach ($paths as $path) {
9197
$module = \basename(\dirname($path, 2));
9298
if (isset($ignoredModules[$module])) {

generator/src/WritePhpFunction.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,7 @@ private function displayParamsWithType(array $params): string
141141
$optDetected = false;
142142

143143
foreach ($params as $param) {
144-
$paramAsString = '';
145-
if ($param->isTypeable()) {
146-
if ($param->isNullable()) {
147-
$paramAsString .= '?';
148-
}
149-
$paramAsString .= $param->getType().' ';
150-
}
144+
$paramAsString = $param->getSignatureType().' ';
151145

152146
$paramName = $param->getParameter();
153147
if ($param->isVariadic()) {

generator/tests/MethodTest.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,46 @@ public function testGetFunctionParam()
3131
$xmlObject = $docPage->getMethodSynopsis();
3232
$method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE);
3333
$params = $method->getParams();
34-
$this->assertEquals('string', $params[0]->getType());
34+
$this->assertEquals('string', $params[0]->getSignatureType());
3535
$this->assertEquals('pattern', $params[0]->getParameter());
3636
}
3737

38+
public function testGetTypeHintFromRessource()
39+
{
40+
$docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/mbstring/functions/mb-ereg-replace-callback.xml');
41+
$xmlObject = $docPage->getMethodSynopsis();
42+
$method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE);
43+
$params = $method->getParams();
44+
$this->assertEquals('string', $params[0]->getDocBlockType());
45+
$this->assertEquals('callable', $params[1]->getDocBlockType());
46+
$this->assertEquals('string', $params[0]->getSignatureType());
47+
$this->assertEquals('callable', $params[1]->getSignatureType());
48+
49+
50+
$docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/gmp/functions/gmp-export.xml');
51+
$xmlObject = $docPage->getMethodSynopsis();
52+
$method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE);
53+
$params = $method->getParams();
54+
$this->assertEquals('\GMP|string|int', $params[0]->getDocBlockType());
55+
$this->assertEquals('', $params[0]->getSignatureType());
56+
$this->assertEquals('int', $params[1]->getDocBlockType());
57+
$this->assertEquals('int', $params[1]->getSignatureType());
58+
59+
$docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/hash/functions/hash-update.xml');
60+
$xmlObject = $docPage->getMethodSynopsis();
61+
$method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE);
62+
$params = $method->getParams();
63+
$this->assertEquals('\HashContext', $params[0]->getDocBlockType());
64+
$this->assertEquals('\HashContext', $params[0]->getSignatureType());
65+
66+
$docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/imap/functions/imap-open.xml');
67+
$xmlObject = $docPage->getMethodSynopsis();
68+
$method = new Method($xmlObject[0], $docPage->loadAndResolveFile(), $docPage->getModule(), new PhpStanFunctionMapReader(), Method::FALSY_TYPE);
69+
$params = $method->getParams();
70+
$this->assertEquals('array|null', $params[5]->getDocBlockType());
71+
$this->assertEquals('?array', $params[5]->getSignatureType());
72+
}
73+
3874
public function testGetInitializer()
3975
{
4076
$docPage = new DocPage(__DIR__ . '/../doc/doc-en/en/reference/apc/functions/apc-cache-info.xml');

generator/tests/PhpStanFunctions/PhpStanFunctionMapReaderTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ public function testGet(): void
2929
$this->assertTrue($parameters['success']->isByReference());
3030
$this->assertTrue($parameters['success']->isOptional());
3131
}
32+
33+
//todo: find a way to test custom map
34+
/*public function testCustomMapThrowExceptionIfOutdated()
35+
{
36+
$mapReader = new PhpStanFunctionMapReader();
37+
}*/
3238
}

0 commit comments

Comments
 (0)