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
11 changed files
with
18,825 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 OversizedArrayTypeException 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,174 @@ | ||
<?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 OversizedConstantArrayVisitor extends NodeVisitorAbstract | ||
{ | ||
|
||
public const IS_OVERSIZED = 'oversized'; | ||
public const 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 (OversizedArrayTypeException) { | ||
// 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::IS_OVERSIZED, true); | ||
} elseif (!$node instanceof Expr) { | ||
// we reached the end of the expression | ||
$this->leavingOversized = false; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @throws OversizedArrayTypeException | ||
*/ | ||
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::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::ARRAY_TYPES, [$keyType, TypeCombinator::union(...$valueType)]); | ||
} | ||
|
||
/** | ||
* @throws OversizedArrayTypeException | ||
*/ | ||
private function getValueFromExpr(Expr $expr): mixed | ||
{ | ||
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 OversizedArrayTypeException(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
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.