Skip to content

Commit

Permalink
Merge pull request #122 from boesing/bugfix/template-stored-in-constants
Browse files Browse the repository at this point in the history
Argument count detection bugfixes
  • Loading branch information
boesing authored Sep 7, 2022
2 parents d2aed00 + eb346b2 commit f1f7e67
Show file tree
Hide file tree
Showing 8 changed files with 382 additions and 31 deletions.
19 changes: 11 additions & 8 deletions src/Parser/PhpParser/ArgumentValueParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use Psalm\Context;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Union;

Expand All @@ -24,16 +25,18 @@ final class ArgumentValueParser

private Expr $expr;
private Context $context;
private StatementsSource $statementsSource;

private function __construct(Expr $expr, Context $context)
private function __construct(Expr $expr, Context $context, StatementsSource $statementsSource)
{
$this->expr = $expr;
$this->context = $context;
$this->expr = $expr;
$this->context = $context;
$this->statementsSource = $statementsSource;
}

public static function create(Expr $expr, Context $context): self
public static function create(Expr $expr, Context $context, StatementsSource $statementsSource): self
{
return new self($expr, $context);
return new self($expr, $context, $statementsSource);
}

public function toString(): string
Expand Down Expand Up @@ -85,12 +88,12 @@ private function parse(Expr $expr, Context $context, bool $cast): string

if ($expr instanceof Expr\Variable) {
return $cast
? StringableVariableInContextParser::parse($expr, $context)
: LiteralStringVariableInContextParser::parse($expr, $context);
? StringableVariableInContextParser::parse($expr, $context, $this->statementsSource)
: LiteralStringVariableInContextParser::parse($expr, $context, $this->statementsSource);
}

if ($expr instanceof Expr\ClassConstFetch || $expr instanceof Expr\ConstFetch) {
return VariableFromConstInContextParser::parse($expr, $context);
return VariableFromConstInContextParser::parse($expr, $context, $this->statementsSource);
}

throw new InvalidArgumentException(sprintf(self::UNPARSABLE_ARGUMENT_VALUE, $expr->getType()));
Expand Down
9 changes: 5 additions & 4 deletions src/Parser/PhpParser/LiteralStringVariableInContextParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use InvalidArgumentException;
use PhpParser\Node\Expr;
use Psalm\Context;
use Psalm\StatementsSource;

use function sprintf;

Expand All @@ -20,16 +21,16 @@ private function __construct(Expr\Variable $variable)
$this->variable = $variable;
}

public static function parse(Expr\Variable $variable, Context $context): string
public static function parse(Expr\Variable $variable, Context $context, StatementsSource $statementsSource): string
{
return (new self($variable))->toString($context);
return (new self($variable))->toString($context, $statementsSource);
}

private function toString(Context $context): string
private function toString(Context $context, StatementsSource $statementsSource): string
{
$name = $this->variable->name;
if ($name instanceof Expr) {
return ArgumentValueParser::create($name, $context)->toString();
return ArgumentValueParser::create($name, $context, $statementsSource)->toString();
}

$variableName = sprintf('$%s', $name);
Expand Down
9 changes: 5 additions & 4 deletions src/Parser/PhpParser/StringableVariableInContextParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use Psalm\Context;
use Psalm\StatementsSource;

use function sprintf;

Expand All @@ -23,16 +24,16 @@ private function __construct(Variable $variable)
$this->variable = $variable;
}

public static function parse(Variable $variable, Context $context): string
public static function parse(Variable $variable, Context $context, StatementsSource $statementsSource): string
{
return (new self($variable))->toString($context);
return (new self($variable))->toString($context, $statementsSource);
}

private function toString(Context $context): string
private function toString(Context $context, StatementsSource $statementsSource): string
{
$name = $this->variable->name;
if ($name instanceof Expr) {
return ArgumentValueParser::create($name, $context)->stringify();
return ArgumentValueParser::create($name, $context, $statementsSource)->stringify();
}

$variableName = sprintf('$%s', $name);
Expand Down
110 changes: 101 additions & 9 deletions src/Parser/PhpParser/VariableFromConstInContextParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use Psalm\Context;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\StatementsSource;
use Psalm\Type\TypeNode;
use Psalm\Type\Union;
use ReflectionClass;
use SplObjectStorage;

use function assert;
use function get_class;
Expand All @@ -23,23 +30,27 @@ final class VariableFromConstInContextParser

private Context $context;

private StatementsSource $statementsSource;

/**
* @param ClassConstFetch|ConstFetch $expr
*/
private function __construct(Expr $expr, Context $context)
private function __construct(Expr $expr, Context $context, StatementsSource $statementsSource)
{
$this->expr = $expr;
$this->context = $context;
$this->expr = $expr;
$this->context = $context;
$this->statementsSource = $statementsSource;
}

/**
* @param ClassConstFetch|ConstFetch $expr
*/
public static function parse(
Expr $expr,
Context $context
Context $context,
StatementsSource $statementsSource
): string {
return (new self($expr, $context))->toString();
return (new self($expr, $context, $statementsSource))->toString();
}

private function toString(): string
Expand Down Expand Up @@ -78,8 +89,10 @@ private function parseClassConstant(ClassConstFetch $expr, Context $context): st

$constant = sprintf('%s::%s', $className, $expr->name->toString());

if (isset($context->vars_in_scope[$constant])) {
return $context->vars_in_scope[$constant]->getId();
if ($context->hasVariable($constant)) {
return $this->extractMostAccurateStringRepresentationOfType(
$context->vars_in_scope[$constant],
);
}

throw new InvalidArgumentException(sprintf('Could not find class constant "%s" in scope.', $constant));
Expand All @@ -89,10 +102,89 @@ private function parseConstant(ConstFetch $expr, Context $context): string
{
$constant = (string) $expr->name;

if (isset($context->vars_in_scope[$constant])) {
return $context->vars_in_scope[$constant]->getId();
if ($context->hasVariable($constant)) {
return $this->extractMostAccurateStringRepresentationOfType(
$context->vars_in_scope[$constant],
);
}

throw new InvalidArgumentException(sprintf('Could not find constant "%s" in scope.', $constant));
}

/**
* @param ClassConstFetch|ConstFetch $expr
*/
private function extractMostAccurateStringRepresentationOfType(
Union $type
): string {
if ($type->isSingleStringLiteral()) {
return $type->getSingleStringLiteral()->value;
}

if ($type->isSingleFloatLiteral()) {
return (string) $type->getSingleFloatLiteral()->value;
}

if ($type->isSingleIntLiteral()) {
return (string) $type->getSingleIntLiteral()->value;
}

$nodeTypeProvider = $this->statementsSource->getNodeTypeProvider();
if ($nodeTypeProvider instanceof NodeDataProvider) {
return $this->extractMostAccurateStringRepresentationOfTypeFromNodeDataProvider(
$nodeTypeProvider,
$type,
);
}

throw $this->createInvalidArgumentException($type);
}

/**
* Method uses reflection to hijack the native string which was inferred by php-parser. By doing this, we can
* bypass the `maxStringLength` psalm setting.
*/
private function extractMostAccurateStringRepresentationOfTypeFromNodeDataProvider(
NodeDataProvider $nodeDataProvider,
Union $type
): string {
$reflectionClass = new ReflectionClass($nodeDataProvider);
if (! $reflectionClass->hasProperty('node_types')) {
throw $this->createInvalidArgumentException($type);
}

$nodeTypesProperty = $reflectionClass->getProperty('node_types');
$nodeTypesProperty->setAccessible(true);
$nodeTypes = $nodeTypesProperty->getValue($nodeDataProvider);
if (! $nodeTypes instanceof SplObjectStorage) {
throw $this->createInvalidArgumentException($type);
}

foreach ($nodeTypes as $phpParserType) {
if (! $phpParserType instanceof String_) {
continue;
}

$psalmType = $nodeTypes->offsetGet($phpParserType);
if (! $psalmType instanceof TypeNode) {
continue;
}

if ($psalmType !== $type) {
continue;
}

return $phpParserType->value;
}

throw $this->createInvalidArgumentException($type);
}

private function createInvalidArgumentException(Union $type): InvalidArgumentException
{
return new InvalidArgumentException(sprintf(
'Unable to parse a string representation of the provided type: %s',
$type->getId(),
));
}
}
2 changes: 1 addition & 1 deletion src/Parser/TemplatedStringParser/Placeholder.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,6 @@ private function getArgumentValueType(Expr $value, Context $context): Union
return ReturnTypeParser::create($this->statementsSource, $context, $value)->toType();
}

return ArgumentValueParser::create($value, $context)->toType();
return ArgumentValueParser::create($value, $context, $this->statementsSource)->toType();
}
}
2 changes: 1 addition & 1 deletion src/Parser/TemplatedStringParser/TemplatedStringParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static function fromArgument(
): self {
return new self(
$functionName,
ArgumentValueParser::create($templateArgument->value, $context)->toString(),
ArgumentValueParser::create($templateArgument->value, $context, $statementsSource)->toString(),
$phpVersion,
$allowIntegerForStringPlaceholder,
$statementsSource,
Expand Down
Loading

0 comments on commit f1f7e67

Please sign in to comment.