Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement OversizedArrayVisitor to improve huge constant array perfor…
…mance
- Loading branch information
Showing
9 changed files
with
8,424 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Analyser; | ||
|
||
use PHPStan\Type\ArrayType; | ||
use PHPStan\Type\BooleanType; | ||
use PHPStan\Type\FloatType; | ||
use PHPStan\Type\IntegerType; | ||
use PHPStan\Type\MixedType; | ||
use PHPStan\Type\NullType; | ||
use PHPStan\Type\ObjectWithoutClassType; | ||
use PHPStan\Type\ResourceType; | ||
use PHPStan\Type\StringType; | ||
use PHPStan\Type\Type; | ||
|
||
final class GetTypeHelper | ||
{ | ||
|
||
public static function typeFromString(string $typeName): ?Type | ||
{ | ||
$type = null; | ||
|
||
if ($typeName === 'string') { | ||
$type = new StringType(); | ||
} | ||
if ($typeName === 'array') { | ||
$type = new ArrayType(new MixedType(), new MixedType()); | ||
} | ||
if ($typeName === 'boolean') { | ||
$type = new BooleanType(); | ||
} | ||
if ($typeName === 'resource' || $typeName === 'resource (closed)') { | ||
$type = new ResourceType(); | ||
} | ||
if ($typeName === 'integer') { | ||
$type = new IntegerType(); | ||
} | ||
if ($typeName === 'double') { | ||
$type = new FloatType(); | ||
} | ||
if ($typeName === 'NULL') { | ||
$type = new NullType(); | ||
} | ||
if ($typeName === 'object') { | ||
$type = new ObjectWithoutClassType(); | ||
} | ||
|
||
return $type; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Parser; | ||
|
||
use Exception; | ||
|
||
class OversizedArrayException extends Exception | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Parser; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Expr; | ||
use PhpParser\Node\Expr\Array_; | ||
use PhpParser\Node\Expr\ConstFetch; | ||
use PhpParser\Node\Scalar\DNumber; | ||
use PhpParser\Node\Scalar\LNumber; | ||
use PhpParser\Node\Scalar\String_; | ||
use PhpParser\NodeVisitorAbstract; | ||
use PHPStan\Analyser\GetTypeHelper; | ||
use PHPStan\ShouldNotHappenException; | ||
use PHPStan\Type\Accessory\NonEmptyArrayType; | ||
use PHPStan\Type\ArrayType; | ||
use PHPStan\Type\BenevolentUnionType; | ||
use PHPStan\Type\Constant\ConstantArrayTypeBuilder; | ||
use PHPStan\Type\IntegerType; | ||
use PHPStan\Type\StringType; | ||
use PHPStan\Type\TypeCombinator; | ||
use function array_keys; | ||
use function array_shift; | ||
use function count; | ||
use function get_class; | ||
use function gettype; | ||
use function is_array; | ||
use function sprintf; | ||
use function strtolower; | ||
|
||
class OversizedArrayVisitor extends NodeVisitorAbstract | ||
{ | ||
|
||
public const OVERSIZED = 'oversized'; | ||
public const OVERSIZED_ARRAY_TYPES = 'oversizedArrayTypes'; | ||
|
||
private ?Node $inOversizedArray = null; | ||
|
||
private bool $leavingOversized = false; | ||
|
||
public function enterNode(Node $node): ?Node | ||
{ | ||
if ($node instanceof Array_ && $node->items !== null && count($node->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { | ||
$this->inOversizedArray = $node; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public function leaveNode(Node $node): ?Node | ||
{ | ||
if ($node instanceof Array_ && $this->inOversizedArray !== null) { | ||
try { | ||
$this->inspectArrayItems($node); | ||
} catch (OversizedArrayException) { | ||
// the array contains values we are not able to inspect just using AST. | ||
// -> skip the optimization | ||
$this->inOversizedArray = null; | ||
$this->leavingOversized = false; | ||
return null; | ||
} | ||
} | ||
|
||
if ($node === $this->inOversizedArray) { | ||
$this->inOversizedArray = null; | ||
$this->leavingOversized = true; | ||
} | ||
|
||
if ($this->leavingOversized) { | ||
if ($node instanceof Array_) { | ||
$node->setAttribute(self::OVERSIZED, true); | ||
} elseif (!$node instanceof Expr) { | ||
// we reached the end of the expression | ||
$this->leavingOversized = false; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @throws OversizedArrayException | ||
*/ | ||
private function inspectArrayItems(Array_ $node): void | ||
{ | ||
$valueType = []; | ||
|
||
$itemKeyTypes = []; | ||
$itemValueTypes = []; | ||
foreach ($node->items as $item) { | ||
if ($item === null) { | ||
continue; | ||
} | ||
|
||
$key = $item->key; | ||
if ($key === null) { | ||
continue; | ||
} | ||
$keyValue = $this->getValueFromExpr($key); | ||
|
||
$value = $item->value; | ||
if ($value instanceof Array_) { | ||
$arrayTypes = $value->getAttribute(self::OVERSIZED_ARRAY_TYPES); | ||
|
||
if (!is_array($arrayTypes)) { | ||
throw new ShouldNotHappenException(); | ||
} | ||
|
||
if (count($value->items) > 0) { | ||
$valueType[] = TypeCombinator::intersect( | ||
new ArrayType($arrayTypes[0], $arrayTypes[1]), | ||
new NonEmptyArrayType(), | ||
); | ||
|
||
continue; | ||
} | ||
|
||
$valueType[] = new ArrayType($arrayTypes[0], $arrayTypes[1]); | ||
continue; | ||
} | ||
$valueValue = $this->getValueFromExpr($value); | ||
|
||
// de-duplicate values | ||
$itemKeyTypes[gettype($keyValue)] = true; | ||
$itemValueTypes[gettype($valueValue)] = true; | ||
} | ||
|
||
$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]); | ||
if (count($itemKeyTypes) === 1) { | ||
$itemKeyTypes = array_keys($itemKeyTypes); | ||
$type = GetTypeHelper::typeFromString(array_shift($itemKeyTypes)); | ||
if ($type === null) { | ||
throw new ShouldNotHappenException(); | ||
} | ||
$keyType = $type; | ||
} | ||
|
||
foreach (array_keys($itemValueTypes) as $itemValueType) { | ||
$type = GetTypeHelper::typeFromString($itemValueType); | ||
if ($type === null) { | ||
throw new ShouldNotHappenException(); | ||
} | ||
$valueType[] = $type; | ||
} | ||
|
||
$node->setAttribute(self::OVERSIZED_ARRAY_TYPES, [$keyType, TypeCombinator::union(...$valueType)]); | ||
} | ||
|
||
private function getValueFromExpr(Expr $expr) | ||
{ | ||
if ($expr instanceof LNumber || $expr instanceof DNumber || $expr instanceof String_) { | ||
return $expr->value; | ||
} | ||
|
||
if ($expr instanceof ConstFetch) { | ||
$constName = (string) $expr->name; | ||
$loweredConstName = strtolower($constName); | ||
|
||
if ($loweredConstName === 'true') { | ||
return true; | ||
} elseif ($loweredConstName === 'false') { | ||
return false; | ||
} elseif ($loweredConstName === 'null') { | ||
return null; | ||
} | ||
} | ||
|
||
throw new OversizedArrayException(sprintf('Unexpected expression type %s', get_class($expr))); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.