Skip to content

Commit c9faa60

Browse files
authored
Fix fatal error on constant('')
1 parent 29ce55c commit c9faa60

29 files changed

+176
-11
lines changed

build/phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ parameters:
9595
- stubs/ReactChildProcess.stub
9696
- stubs/ReactStreams.stub
9797
- stubs/NetteDIContainer.stub
98+
- stubs/PhpParserName.stub
99+
98100
services:
99101
-
100102
class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension

build/stubs/PhpParserName.stub

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Node;
4+
5+
use PhpParser\NodeAbstract;
6+
7+
class Name extends NodeAbstract
8+
{
9+
/**
10+
* Constructs a name node.
11+
*
12+
* @param non-empty-string|non-empty-array<string>|self $name Name as string, part array or Name instance (copy ctor)
13+
* @param array<mixed> $attributes Additional attributes
14+
*/
15+
public function __construct($name, array $attributes = []) {
16+
}
17+
18+
/** @return non-empty-string */
19+
public function toString() : string {
20+
}
21+
22+
/** @return non-empty-string */
23+
public function toCodeString() : string {
24+
}
25+
}

src/Analyser/MutatingScope.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class MutatingScope implements Scope
173173
/** @var array<string, self> */
174174
private array $falseyScopes = [];
175175

176+
/** @var non-empty-string|null */
176177
private ?string $namespace;
177178

178179
private ?self $scopeOutOfFirstLevelStatement = null;
@@ -5230,6 +5231,9 @@ public function debug(): array
52305231
return $descriptions;
52315232
}
52325233

5234+
/**
5235+
* @param non-empty-string $className
5236+
*/
52335237
private function exactInstantiation(New_ $node, string $className): ?Type
52345238
{
52355239
$resolvedClassName = $this->resolveExactName(new Name($className));

src/Analyser/NameScope.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class NameScope
2424

2525
/**
2626
* @api
27+
* @param non-empty-string|null $namespace
2728
* @param array<string, string> $uses alias(string) => fullName(string)
2829
* @param array<string, string> $constUses alias(string) => fullName(string)
2930
* @param array<string, true> $typeAliasesMap

src/Analyser/NodeScopeResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,9 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
17861786
if ($const->namespacedName !== null) {
17871787
$constantName = new Name\FullyQualified($const->namespacedName->toString());
17881788
} else {
1789+
if ($const->name->toString() === '') {
1790+
throw new ShouldNotHappenException('Constant cannot have a empty name');
1791+
}
17891792
$constantName = new Name\FullyQualified($const->name->toString());
17901793
}
17911794
$scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));

src/Analyser/TypeSpecifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
20082008
$unwrappedLeftExpr->name instanceof Node\Identifier &&
20092009
$unwrappedRightExpr instanceof ClassConstFetch &&
20102010
$rightType instanceof ConstantStringType &&
2011+
$rightType->getValue() !== '' &&
20112012
strtolower($unwrappedLeftExpr->name->toString()) === 'class'
20122013
) {
20132014
return $this->specifyTypesInCondition(
@@ -2029,6 +2030,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
20292030
$unwrappedRightExpr->name instanceof Node\Identifier &&
20302031
$unwrappedLeftExpr instanceof ClassConstFetch &&
20312032
$leftType instanceof ConstantStringType &&
2033+
$leftType->getValue() !== '' &&
20322034
strtolower($unwrappedRightExpr->name->toString()) === 'class'
20332035
) {
20342036
return $this->specifyTypesInCondition(

src/Reflection/InitializerExprContext.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction;
99
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter;
1010
use PHPStan\BetterReflection\Reflection\ReflectionConstant;
11+
use PHPStan\ShouldNotHappenException;
1112
use function array_slice;
1213
use function count;
1314
use function explode;
@@ -18,6 +19,9 @@
1819
class InitializerExprContext implements NamespaceAnswerer
1920
{
2021

22+
/**
23+
* @param non-empty-string|null $namespace
24+
*/
2125
private function __construct(
2226
private ?string $file,
2327
private ?string $namespace,
@@ -43,11 +47,18 @@ public static function fromScope(Scope $scope): self
4347
);
4448
}
4549

50+
/**
51+
* @return non-empty-string|null
52+
*/
4653
private static function parseNamespace(string $name): ?string
4754
{
4855
$parts = explode('\\', $name);
4956
if (count($parts) > 1) {
50-
return implode('\\', array_slice($parts, 0, -1));
57+
$ns = implode('\\', array_slice($parts, 0, -1));
58+
if ($ns === '') {
59+
throw new ShouldNotHappenException('Namespace cannot be empty.');
60+
}
61+
return $ns;
5162
}
5263

5364
return null;

src/Reflection/NamespaceAnswerer.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
interface NamespaceAnswerer
77
{
88

9+
/**
10+
* @return non-empty-string|null
11+
*/
912
public function getNamespace(): ?string;
1013

1114
}

src/Type/BitwiseFlagHelper.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public function __construct(private ReflectionProvider $reflectionProvider)
1818
{
1919
}
2020

21+
/**
22+
* @param non-empty-string $constName
23+
*/
2124
public function bitwiseOrContainsConstant(Expr $expr, Scope $scope, string $constName): TrinaryLogic
2225
{
2326
if ($expr instanceof ConstFetch) {

src/Type/Constant/ConstantStringType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ public function isCallable(): TrinaryLogic
239239

240240
public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
241241
{
242+
if ($this->value === '') {
243+
return [];
244+
}
245+
242246
$reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
243247

244248
// 'my_function'

src/Type/FileTypeMapper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,10 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
482482
$functionName = $functionStack[count($functionStack) - 1] ?? null;
483483
$nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
484484

485+
if ($namespace === '') {
486+
throw new ShouldNotHappenException('Namespace cannot be empty.');
487+
}
488+
485489
if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
486490
if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
487491
$phpDocNode = $phpDocNodeMap[$nameScopeKey];

src/Type/ObjectType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ public function getReferencedClasses(): array
263263

264264
public function getObjectClassNames(): array
265265
{
266+
if ($this->className === '') {
267+
return [];
268+
}
266269
return [$this->className];
267270
}
268271

src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
2424
use PHPStan\Type\Constant\ConstantBooleanType;
2525
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
26+
use PHPStan\Type\ErrorType;
2627
use PHPStan\Type\MixedType;
2728
use PHPStan\Type\NeverType;
2829
use PHPStan\Type\NullType;
@@ -85,8 +86,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
8586
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
8687
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr);
8788
} elseif ($callbackArg instanceof String_) {
89+
$funcName = self::createFunctionName($callbackArg->value);
90+
if ($funcName === null) {
91+
return new ErrorType();
92+
}
93+
8894
$itemVar = new Variable('item');
89-
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar)]);
95+
$expr = new FuncCall($funcName, [new Arg($itemVar)]);
9096
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr);
9197
}
9298
}
@@ -100,8 +106,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
100106
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
101107
return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr);
102108
} elseif ($callbackArg instanceof String_) {
109+
$funcName = self::createFunctionName($callbackArg->value);
110+
if ($funcName === null) {
111+
return new ErrorType();
112+
}
113+
103114
$keyVar = new Variable('key');
104-
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($keyVar)]);
115+
$expr = new FuncCall($funcName, [new Arg($keyVar)]);
105116
return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr);
106117
}
107118
}
@@ -115,9 +126,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
115126
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
116127
return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr);
117128
} elseif ($callbackArg instanceof String_) {
129+
$funcName = self::createFunctionName($callbackArg->value);
130+
if ($funcName === null) {
131+
return new ErrorType();
132+
}
133+
118134
$itemVar = new Variable('item');
119135
$keyVar = new Variable('key');
120-
$expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]);
136+
$expr = new FuncCall($funcName, [new Arg($itemVar), new Arg($keyVar)]);
121137
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr);
122138
}
123139
}
@@ -242,10 +258,20 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type
242258
];
243259
}
244260

245-
private static function createFunctionName(string $funcName): Name
261+
private static function createFunctionName(string $funcName): ?Name
246262
{
263+
if ($funcName === '') {
264+
return null;
265+
}
266+
247267
if ($funcName[0] === '\\') {
248-
return new Name\FullyQualified(substr($funcName, 1));
268+
$funcName = substr($funcName, 1);
269+
270+
if ($funcName === '') {
271+
return null;
272+
}
273+
274+
return new Name\FullyQualified($funcName);
249275
}
250276

251277
return new Name($funcName);

src/Type/Php/ConstantFunctionReturnTypeExtension.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
88
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
9+
use PHPStan\Type\ErrorType;
910
use PHPStan\Type\Type;
1011
use PHPStan\Type\TypeCombinator;
1112
use function count;
@@ -36,7 +37,12 @@ public function getTypeFromFunctionCall(
3637

3738
$results = [];
3839
foreach ($nameType->getConstantStrings() as $constantName) {
39-
$results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue()));
40+
$expr = $this->constantHelper->createExprFromConstantName($constantName->getValue());
41+
if ($expr === null) {
42+
return new ErrorType();
43+
}
44+
45+
$results[] = $scope->getType($expr);
4046
}
4147

4248
if (count($results) > 0) {

src/Type/Php/ConstantHelper.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@
1515
class ConstantHelper
1616
{
1717

18-
public function createExprFromConstantName(string $constantName): Expr
18+
public function createExprFromConstantName(string $constantName): ?Expr
1919
{
20+
if ($constantName === '') {
21+
return null;
22+
}
23+
2024
$classConstParts = explode('::', $constantName);
2125
if (count($classConstParts) >= 2) {
22-
$classConstName = new FullyQualified(ltrim($classConstParts[0], '\\'));
26+
$fqcn = ltrim($classConstParts[0], '\\');
27+
if ($fqcn === '') {
28+
return null;
29+
}
30+
31+
$classConstName = new FullyQualified($fqcn);
2332
if ($classConstName->isSpecialClassName()) {
2433
$classConstName = new Name($classConstName->toString());
2534
}

src/Type/Php/DefinedConstantTypeSpecifyingExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,13 @@ public function specifyTypes(
5454
return new SpecifiedTypes([], []);
5555
}
5656

57+
$expr = $this->constantHelper->createExprFromConstantName($constantName->getValue());
58+
if ($expr === null) {
59+
return new SpecifiedTypes([], []);
60+
}
61+
5762
return $this->typeSpecifier->create(
58-
$this->constantHelper->createExprFromConstantName($constantName->getValue()),
63+
$expr,
5964
new MixedType(),
6065
$context,
6166
false,

src/Type/Php/FilterFunctionReturnTypeHelper.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ private function getFilterTypeOptions(): array
235235
return $this->filterTypeOptions;
236236
}
237237

238+
/**
239+
* @param non-empty-string $constantName
240+
*/
238241
private function getConstant(string $constantName): int
239242
{
240243
$constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null);

src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public function getTypeFromFunctionCall(
6565
return TypeCombinator::union($arrayType, new StringType());
6666
}
6767

68+
/**
69+
* @param non-empty-string $constantName
70+
*/
6871
private function getConstant(string $constantName): ?int
6972
{
7073
if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) {

src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect
3434

3535
$valueType = $scope->getType($methodCall->getArgs()[0]->value);
3636
foreach ($valueType->getConstantStrings() as $constantString) {
37+
if ($constantString->getValue() === '') {
38+
return null;
39+
}
40+
3741
if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) {
3842
return $methodReflection->getThrowType();
3943
}

src/Type/Type.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface Type
3232
*/
3333
public function getReferencedClasses(): array;
3434

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

3838
/**

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,12 @@ public function testBug11026(): void
13671367
$this->assertNoErrors($errors);
13681368
}
13691369

1370+
public function testBug10867(): void
1371+
{
1372+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-10867.php');
1373+
$this->assertNoErrors($errors);
1374+
}
1375+
13701376
/**
13711377
* @param string[]|null $allAnalysedFiles
13721378
* @return Error[]

tests/PHPStan/Analyser/TypeSpecifierTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,11 +1344,17 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array
13441344
return $descriptions;
13451345
}
13461346

1347+
/**
1348+
* @param non-empty-string $className
1349+
*/
13471350
private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_
13481351
{
13491352
return new Expr\Instanceof_(new Variable($variableName), new Name($className));
13501353
}
13511354

1355+
/**
1356+
* @param non-empty-string $functionName
1357+
*/
13521358
private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall
13531359
{
13541360
return new FuncCall(new Name($functionName), [new Arg(new Variable($variableName))]);

tests/PHPStan/Analyser/data/array-filter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ function withoutCallback(array $map1, array $map2, array $map3): void
3535
$filtered3 = array_filter($map3, null, ARRAY_FILTER_USE_BOTH);
3636
assertType('array<string, float|int<min, -1>|int<1, max>|non-falsy-string|true>', $filtered3);
3737
}
38+
39+
function invalidCallableName(array $arr) {
40+
assertType('*ERROR*', array_filter($arr, ''));
41+
assertType('*ERROR*', array_filter($arr, '\\'));
42+
}

0 commit comments

Comments
 (0)