Skip to content

Commit

Permalink
Fix fatal error on constant('')
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored May 31, 2024
1 parent 29ce55c commit c9faa60
Show file tree
Hide file tree
Showing 29 changed files with 176 additions and 11 deletions.
2 changes: 2 additions & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ parameters:
- stubs/ReactChildProcess.stub
- stubs/ReactStreams.stub
- stubs/NetteDIContainer.stub
- stubs/PhpParserName.stub

services:
-
class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension
Expand Down
25 changes: 25 additions & 0 deletions build/stubs/PhpParserName.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

namespace PhpParser\Node;

use PhpParser\NodeAbstract;

class Name extends NodeAbstract
{
/**
* Constructs a name node.
*
* @param non-empty-string|non-empty-array<string>|self $name Name as string, part array or Name instance (copy ctor)
* @param array<mixed> $attributes Additional attributes
*/
public function __construct($name, array $attributes = []) {
}

/** @return non-empty-string */
public function toString() : string {
}

/** @return non-empty-string */
public function toCodeString() : string {
}
}
4 changes: 4 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class MutatingScope implements Scope
/** @var array<string, self> */
private array $falseyScopes = [];

/** @var non-empty-string|null */
private ?string $namespace;

private ?self $scopeOutOfFirstLevelStatement = null;
Expand Down Expand Up @@ -5230,6 +5231,9 @@ public function debug(): array
return $descriptions;
}

/**
* @param non-empty-string $className
*/
private function exactInstantiation(New_ $node, string $className): ?Type
{
$resolvedClassName = $this->resolveExactName(new Name($className));
Expand Down
1 change: 1 addition & 0 deletions src/Analyser/NameScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class NameScope

/**
* @api
* @param non-empty-string|null $namespace
* @param array<string, string> $uses alias(string) => fullName(string)
* @param array<string, string> $constUses alias(string) => fullName(string)
* @param array<string, true> $typeAliasesMap
Expand Down
3 changes: 3 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,9 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
if ($const->namespacedName !== null) {
$constantName = new Name\FullyQualified($const->namespacedName->toString());
} else {
if ($const->name->toString() === '') {
throw new ShouldNotHappenException('Constant cannot have a empty name');
}
$constantName = new Name\FullyQualified($const->name->toString());
}
$scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
$unwrappedLeftExpr->name instanceof Node\Identifier &&
$unwrappedRightExpr instanceof ClassConstFetch &&
$rightType instanceof ConstantStringType &&
$rightType->getValue() !== '' &&
strtolower($unwrappedLeftExpr->name->toString()) === 'class'
) {
return $this->specifyTypesInCondition(
Expand All @@ -2029,6 +2030,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
$unwrappedRightExpr->name instanceof Node\Identifier &&
$unwrappedLeftExpr instanceof ClassConstFetch &&
$leftType instanceof ConstantStringType &&
$leftType->getValue() !== '' &&
strtolower($unwrappedRightExpr->name->toString()) === 'class'
) {
return $this->specifyTypesInCondition(
Expand Down
13 changes: 12 additions & 1 deletion src/Reflection/InitializerExprContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction;
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter;
use PHPStan\BetterReflection\Reflection\ReflectionConstant;
use PHPStan\ShouldNotHappenException;
use function array_slice;
use function count;
use function explode;
Expand All @@ -18,6 +19,9 @@
class InitializerExprContext implements NamespaceAnswerer
{

/**
* @param non-empty-string|null $namespace
*/
private function __construct(
private ?string $file,
private ?string $namespace,
Expand All @@ -43,11 +47,18 @@ public static function fromScope(Scope $scope): self
);
}

/**
* @return non-empty-string|null
*/
private static function parseNamespace(string $name): ?string
{
$parts = explode('\\', $name);
if (count($parts) > 1) {
return implode('\\', array_slice($parts, 0, -1));
$ns = implode('\\', array_slice($parts, 0, -1));
if ($ns === '') {
throw new ShouldNotHappenException('Namespace cannot be empty.');
}
return $ns;
}

return null;
Expand Down
3 changes: 3 additions & 0 deletions src/Reflection/NamespaceAnswerer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
interface NamespaceAnswerer
{

/**
* @return non-empty-string|null
*/
public function getNamespace(): ?string;

}
3 changes: 3 additions & 0 deletions src/Type/BitwiseFlagHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public function __construct(private ReflectionProvider $reflectionProvider)
{
}

/**
* @param non-empty-string $constName
*/
public function bitwiseOrContainsConstant(Expr $expr, Scope $scope, string $constName): TrinaryLogic
{
if ($expr instanceof ConstFetch) {
Expand Down
4 changes: 4 additions & 0 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ public function isCallable(): TrinaryLogic

public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
{
if ($this->value === '') {
return [];
}

$reflectionProvider = ReflectionProviderStaticAccessor::getInstance();

// 'my_function'
Expand Down
4 changes: 4 additions & 0 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,10 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
$functionName = $functionStack[count($functionStack) - 1] ?? null;
$nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);

if ($namespace === '') {
throw new ShouldNotHappenException('Namespace cannot be empty.');
}

if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
$phpDocNode = $phpDocNodeMap[$nameScopeKey];
Expand Down
3 changes: 3 additions & 0 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ public function getReferencedClasses(): array

public function getObjectClassNames(): array
{
if ($this->className === '') {
return [];
}
return [$this->className];
}

Expand Down
36 changes: 31 additions & 5 deletions src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
Expand Down Expand Up @@ -85,8 +86,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$funcName = self::createFunctionName($callbackArg->value);
if ($funcName === null) {
return new ErrorType();
}

$itemVar = new Variable('item');
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar)]);
$expr = new FuncCall($funcName, [new Arg($itemVar)]);
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr);
}
}
Expand All @@ -100,8 +106,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$funcName = self::createFunctionName($callbackArg->value);
if ($funcName === null) {
return new ErrorType();
}

$keyVar = new Variable('key');
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($keyVar)]);
$expr = new FuncCall($funcName, [new Arg($keyVar)]);
return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr);
}
}
Expand All @@ -115,9 +126,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr);
} elseif ($callbackArg instanceof String_) {
$funcName = self::createFunctionName($callbackArg->value);
if ($funcName === null) {
return new ErrorType();
}

$itemVar = new Variable('item');
$keyVar = new Variable('key');
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]);
$expr = new FuncCall($funcName, [new Arg($itemVar), new Arg($keyVar)]);
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr);
}
}
Expand Down Expand Up @@ -242,10 +258,20 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type
];
}

private static function createFunctionName(string $funcName): Name
private static function createFunctionName(string $funcName): ?Name
{
if ($funcName === '') {
return null;
}

if ($funcName[0] === '\\') {
return new Name\FullyQualified(substr($funcName, 1));
$funcName = substr($funcName, 1);

if ($funcName === '') {
return null;
}

return new Name\FullyQualified($funcName);
}

return new Name($funcName);
Expand Down
8 changes: 7 additions & 1 deletion src/Type/Php/ConstantFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
Expand Down Expand Up @@ -36,7 +37,12 @@ public function getTypeFromFunctionCall(

$results = [];
foreach ($nameType->getConstantStrings() as $constantName) {
$results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue()));
$expr = $this->constantHelper->createExprFromConstantName($constantName->getValue());
if ($expr === null) {
return new ErrorType();
}

$results[] = $scope->getType($expr);
}

if (count($results) > 0) {
Expand Down
13 changes: 11 additions & 2 deletions src/Type/Php/ConstantHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@
class ConstantHelper
{

public function createExprFromConstantName(string $constantName): Expr
public function createExprFromConstantName(string $constantName): ?Expr
{
if ($constantName === '') {
return null;
}

$classConstParts = explode('::', $constantName);
if (count($classConstParts) >= 2) {
$classConstName = new FullyQualified(ltrim($classConstParts[0], '\\'));
$fqcn = ltrim($classConstParts[0], '\\');
if ($fqcn === '') {
return null;
}

$classConstName = new FullyQualified($fqcn);
if ($classConstName->isSpecialClassName()) {
$classConstName = new Name($classConstName->toString());
}
Expand Down
7 changes: 6 additions & 1 deletion src/Type/Php/DefinedConstantTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ public function specifyTypes(
return new SpecifiedTypes([], []);
}

$expr = $this->constantHelper->createExprFromConstantName($constantName->getValue());
if ($expr === null) {
return new SpecifiedTypes([], []);
}

return $this->typeSpecifier->create(
$this->constantHelper->createExprFromConstantName($constantName->getValue()),
$expr,
new MixedType(),
$context,
false,
Expand Down
3 changes: 3 additions & 0 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ private function getFilterTypeOptions(): array
return $this->filterTypeOptions;
}

/**
* @param non-empty-string $constantName
*/
private function getConstant(string $constantName): int
{
$constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null);
Expand Down
3 changes: 3 additions & 0 deletions src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public function getTypeFromFunctionCall(
return TypeCombinator::union($arrayType, new StringType());
}

/**
* @param non-empty-string $constantName
*/
private function getConstant(string $constantName): ?int
{
if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect

$valueType = $scope->getType($methodCall->getArgs()[0]->value);
foreach ($valueType->getConstantStrings() as $constantString) {
if ($constantString->getValue() === '') {
return null;
}

if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) {
return $methodReflection->getThrowType();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface Type
*/
public function getReferencedClasses(): array;

/** @return list<string> */
/** @return list<non-empty-string> */
public function getObjectClassNames(): array;

/**
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,12 @@ public function testBug11026(): void
$this->assertNoErrors($errors);
}

public function testBug10867(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-10867.php');
$this->assertNoErrors($errors);
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1344,11 +1344,17 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array
return $descriptions;
}

/**
* @param non-empty-string $className
*/
private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_
{
return new Expr\Instanceof_(new Variable($variableName), new Name($className));
}

/**
* @param non-empty-string $functionName
*/
private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall
{
return new FuncCall(new Name($functionName), [new Arg(new Variable($variableName))]);
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/data/array-filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ function withoutCallback(array $map1, array $map2, array $map3): void
$filtered3 = array_filter($map3, null, ARRAY_FILTER_USE_BOTH);
assertType('array<string, float|int<min, -1>|int<1, max>|non-falsy-string|true>', $filtered3);
}

function invalidCallableName(array $arr) {
assertType('*ERROR*', array_filter($arr, ''));
assertType('*ERROR*', array_filter($arr, '\\'));
}
Loading

0 comments on commit c9faa60

Please sign in to comment.