Skip to content

Fix fatal error on constant('') #3013

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 18 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to remove this stub after the next php-parser release because of nikic/PHP-Parser#993

{
/**
* 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 @@
);
}

/**
* @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 Expand Up @@ -113,7 +124,7 @@
$namespace = self::parseNamespace($function->namespacedName->toString());
}
}
return new self(

Check failure on line 127 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.2, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 127 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.2, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.
$stubFile,
$namespace,
$className,
Expand All @@ -129,13 +140,13 @@
{
return new self(
$constant->getFileName(),
$constant->getNamespaceName(),

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.1)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 143 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.
null,
null,
null,
null,
);
}

Check failure on line 149 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 149 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

public static function createEmpty(): self
{
Expand All @@ -150,7 +161,7 @@
public function getClassName(): ?string
{
return $this->className;
}

Check failure on line 164 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.3, ubuntu-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

Check failure on line 164 in src/Reflection/InitializerExprContext.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.3, windows-latest)

Parameter #2 $namespace of class PHPStan\Reflection\InitializerExprContext constructor expects non-empty-string|null, string|null given.

public function getNamespace(): ?string
{
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
Loading