Skip to content

Generate array shapes for PHPStan #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
29e97f7
Add a custom node visitor which generates array shapes for parameters.
johnbillion Nov 29, 2021
30c59aa
Don't generate an array shape if any of its elements keys are unknown.
johnbillion Nov 29, 2021
dede957
Handle more wonky spacing.
johnbillion Nov 30, 2021
67bba4c
Make this easier to read.
johnbillion Nov 30, 2021
555e8f6
PHPStan doesn't allow typed shaped arrays.
johnbillion Nov 30, 2021
722310f
Don't use an array shape if it's not an array.
johnbillion Nov 30, 2021
fee75f8
Push the array type to the end of the list.
johnbillion Nov 30, 2021
fb0d9f1
Ignore broken parameter references.
johnbillion Nov 30, 2021
49b5e0e
Coding standards.
johnbillion Nov 30, 2021
bb213a1
Reuse the docblock factory.
johnbillion Nov 30, 2021
e557e53
PHPStan config.
johnbillion Nov 30, 2021
b598e90
Docs.
johnbillion Dec 1, 2021
14489d5
Fix these.
johnbillion Dec 1, 2021
04206ad
Split this up a bit.
johnbillion Dec 2, 2021
b78ffe3
Prepare this to be applicable to any tag type.
johnbillion Dec 2, 2021
29fc893
More splitting up.
johnbillion Dec 2, 2021
c7698d2
Generate `@phpstan-return` tags too.
johnbillion Dec 2, 2021
adfcbcb
Remove some duplication.
johnbillion Dec 2, 2021
1066744
This isn't needed.
johnbillion Dec 2, 2021
7dfacf5
Naming.
johnbillion Dec 2, 2021
8dff3b0
Add an explanation for this.
johnbillion Dec 2, 2021
c7f4c6e
Update stubs.
johnbillion Dec 2, 2021
6368f64
Merge branch 'master' into phpstan-param-arrays
johnbillion Dec 2, 2021
62a1d4b
Update the stubs.
johnbillion Dec 2, 2021
5f82c9e
Bump the stubs generator.
johnbillion Dec 2, 2021
88fa57e
Make this more accurate.
johnbillion Dec 2, 2021
b394261
Update the stubs again.
johnbillion Dec 2, 2021
d7780d1
Elements in return arrays generally aren't optional.
johnbillion Dec 2, 2021
4ddacfb
Updated stubs.
johnbillion Dec 2, 2021
57a18b1
PHP 7.3 syntax.
johnbillion Dec 2, 2021
99f196b
Remove temporary PHPStan for testing.
johnbillion Dec 2, 2021
294da8d
More guard conditions.
johnbillion Dec 2, 2021
f6c7d9d
Add PHPStan for our lovely new code.
johnbillion Dec 2, 2021
f92b002
Fix everything.
johnbillion Dec 2, 2021
3003673
Update visitor.php
johnbillion Dec 3, 2021
547148f
Update visitor.php
johnbillion Dec 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ script:
- "git diff --exit-code"
# Execute stubs
- "php -f wordpress-stubs.php"
# Analyse our code
- "vendor/bin/phpstan"
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"require-dev": {
"php": "~7.3 || ~8.0",
"php-stubs/generator": "^0.7.0",
"phpdocumentor/reflection-docblock": "^5.3",
"php-stubs/generator": "^0.8.0",
"phpstan/phpstan": "^1.2",
"nikic/php-parser": "< 4.12.0"
},
"suggest": {
Expand Down
1 change: 1 addition & 0 deletions generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fi
"$(dirname "$0")/vendor/bin/generate-stubs" \
--force \
--finder=finder.php \
--visitor=visitor.php \
--header="$HEADER" \
--functions \
--classes \
Expand Down
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: 8
paths:
- visitor.php
242 changes: 242 additions & 0 deletions visitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php

declare(strict_types = 1);

use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\Type;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use StubsGenerator\NodeVisitor;

return new class extends NodeVisitor {

/**
* @var \phpDocumentor\Reflection\DocBlockFactory
*/
private $docBlockFactory;

public function __construct()
{
$this->docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
}

public function enterNode(Node $node)
{
parent::enterNode($node);

if (!($node instanceof Function_) && !($node instanceof ClassMethod)) {
return null;
}

$docComment = $node->getDocComment();

if (!($docComment instanceof Doc)) {
return null;
}

$newDocComment = $this->addArrayHashNotation($docComment);

if ($newDocComment !== null) {
$node->setDocComment($newDocComment);
}

return null;
}

private function addArrayHashNotation(Doc $docComment): ?Doc
{
$docCommentText = $docComment->getText();

try {
$docblock = $this->docBlockFactory->create($docCommentText);
} catch ( \RuntimeException $e ) {
return null;
} catch ( \InvalidArgumentException $e ) {
return null;
}

/** @var \phpDocumentor\Reflection\DocBlock\Tags\Param[] */
$params = $docblock->getTagsByName('param');

/** @var \phpDocumentor\Reflection\DocBlock\Tags\Return_[] */
$returns = $docblock->getTagsByName('return');

if (!$params && !$returns) {
return null;
}

$additions = [];

foreach ($params as $param) {
$addition = $this->getAdditionFromParam($param);

if ($addition !== null) {
$additions[] = $addition;
}
}

if ($returns) {
$addition = $this->getAdditionFromReturn($returns[0]);

if ($addition !== null) {
$additions[] = $addition;
}
}

if (!$additions) {
return null;
}

$newDocComment = sprintf(
"%s\n%s\n */",
substr($docCommentText, 0, -4),
implode("\n", $additions)
);

return new Doc($newDocComment, $docComment->getLine(), $docComment->getFilePos());
}

private function getAdditionFromParam(Param $tag): ?string
{
$tagDescription = $tag->getDescription();
$tagVariableName = $tag->getVariableName();
$tagVariableType = $tag->getType();

// Skip if information we need is missing.
if (!$tagDescription || !$tagVariableName || !$tagVariableType) {
return null;
}

$elements = $this->getElementsFromDescription($tagDescription, true);

if ($elements === null) {
return null;
}

$tagVariableType = $this->getTypeNameFromType($tagVariableType);

if ($tagVariableType === null) {
return null;
}

// It's common for an args parameter to accept a query var string or array with `string|array`.
// Remove the accepted string type for these so we get the strongest typing we can manage.
$tagVariableType = str_replace(['|string', 'string|'], '', $tagVariableType);

return sprintf(
" * @phpstan-param %1\$s{\n * %2\$s,\n * } $%3\$s",
$tagVariableType,
implode(",\n * ", $elements),
$tagVariableName
);
}

private function getAdditionFromReturn(Return_ $tag): ?string
{
$tagDescription = $tag->getDescription();
$tagVariableType = $tag->getType();

// Skip if information we need is missing.
if (!$tagDescription || !$tagVariableType) {
return null;
}

$elements = $this->getElementsFromDescription($tagDescription, false);

if ($elements === null) {
return null;
}

$tagVariableType = $this->getTypeNameFromType($tagVariableType);

if ($tagVariableType === null) {
return null;
}

return sprintf(
" * @phpstan-return %1\$s{\n * %2\$s,\n * }",
$tagVariableType,
implode(",\n * ", $elements)
);
}

private function getTypeNameFromType(Type $tagVariableType): ?string
{
// PHPStan dosn't support typed array shapes (`int[]{...}`) so replace
// typed arrays such as `int[]` with `array`.
$tagVariableType = preg_replace('#[a-zA-Z0-9_]+\[\]#', 'array', $tagVariableType->__toString());

if ($tagVariableType === null) {
return null;
}

if (strpos($tagVariableType, 'array') === false) {
// Skip if we have hash notation that's not for an array (ie. for `object`).
return null;
}

if (strpos($tagVariableType, 'array|') !== false) {
// Move `array` to the end of union types so the appended array shape works.
$tagVariableType = str_replace('array|', '', $tagVariableType) . '|array';
}

return $tagVariableType;
}

/**
* @return ?string[]
*/
private function getElementsFromDescription(Description $tagDescription, bool $optional): ?array
{
$text = $tagDescription->__toString();

// Skip if the description doesn't contain at least one correctly
// formatted `@type`, which indicates an array hash.
if (strpos($text, ' @type ') === false) {
return null;
}

// Populate `$types` with the value of each top level `@type`.
$types = preg_split('/\R+ @type /', $text);

if ($types === false) {
return null;
}

unset($types[0]);
$elements = [];

foreach ($types as $typeTag) {
$parts = preg_split('#\s+#', trim($typeTag));

if ($parts === false || count($parts) < 2) {
return null;
}

list($type, $name) = $parts;

// Bail out completely if any element doesn't have a static key.
if (strpos($name, '...$') !== false) {
return null;
}

// Bail out completely if the name of any element is invalid.
if (strpos($name, '$') !== 0) {
return null;
}

$elements[] = sprintf(
'%1$s%2$s: %3$s',
substr($name, 1),
$optional ? '?' : '',
$type
);
}

return $elements;
}
};
Loading