diff --git a/build/phpstan.neon b/build/phpstan.neon index 511d5735c7..6358eeae9b 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -95,6 +95,8 @@ parameters: - stubs/ReactChildProcess.stub - stubs/ReactStreams.stub - stubs/NetteDIContainer.stub + - stubs/PhpParserName.stub + services: - class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension diff --git a/build/stubs/PhpParserName.stub b/build/stubs/PhpParserName.stub new file mode 100644 index 0000000000..a044fbf684 --- /dev/null +++ b/build/stubs/PhpParserName.stub @@ -0,0 +1,25 @@ +|self $name Name as string, part array or Name instance (copy ctor) + * @param array $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 { + } +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bd88539070..361ad5299b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -173,6 +173,7 @@ class MutatingScope implements Scope /** @var array */ private array $falseyScopes = []; + /** @var non-empty-string|null */ private ?string $namespace; private ?self $scopeOutOfFirstLevelStatement = null; @@ -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)); diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index 24370dfe0b..e083daf91b 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -24,6 +24,7 @@ class NameScope /** * @api + * @param non-empty-string|null $namespace * @param array $uses alias(string) => fullName(string) * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index bd3608aecd..f396c55263 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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)); diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index e65c907163..f466effb3d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -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( @@ -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( diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index c71a75c7d0..c6e2f890a8 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -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; @@ -18,6 +19,9 @@ class InitializerExprContext implements NamespaceAnswerer { + /** + * @param non-empty-string|null $namespace + */ private function __construct( private ?string $file, private ?string $namespace, @@ -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; diff --git a/src/Reflection/NamespaceAnswerer.php b/src/Reflection/NamespaceAnswerer.php index a0c4e74d5d..4e908a6d8e 100644 --- a/src/Reflection/NamespaceAnswerer.php +++ b/src/Reflection/NamespaceAnswerer.php @@ -6,6 +6,9 @@ interface NamespaceAnswerer { + /** + * @return non-empty-string|null + */ public function getNamespace(): ?string; } diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php index 9b7175cd3f..7d5e4c3db9 100644 --- a/src/Type/BitwiseFlagHelper.php +++ b/src/Type/BitwiseFlagHelper.php @@ -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) { diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 78e474380d..95ea40c601 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -239,6 +239,10 @@ public function isCallable(): TrinaryLogic public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + if ($this->value === '') { + return []; + } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 7886d40e7e..307be3446f 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -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]; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index cc2030bb4e..cba8401bff 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -263,6 +263,9 @@ public function getReferencedClasses(): array public function getObjectClassNames(): array { + if ($this->className === '') { + return []; + } return [$this->className]; } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 73d0f31e36..d697380439 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -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; @@ -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); } } @@ -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); } } @@ -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); } } @@ -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); diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php index 3c32b6c360..1e1075c812 100644 --- a/src/Type/Php/ConstantFunctionReturnTypeExtension.php +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -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; @@ -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) { diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php index 790f169ed9..7f056f90ec 100644 --- a/src/Type/Php/ConstantHelper.php +++ b/src/Type/Php/ConstantHelper.php @@ -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()); } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 43ab4c48db..5f37995240 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -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, diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index a2ed6c792b..7ae801bfd9 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -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); diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index 2139200e0b..bc41f983e0 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -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)) { diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index 078301bb12..56478c55c9 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -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(); } diff --git a/src/Type/Type.php b/src/Type/Type.php index 9c1f14fe5a..8b56ea16af 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -32,7 +32,7 @@ interface Type */ public function getReferencedClasses(): array; - /** @return list */ + /** @return list */ public function getObjectClassNames(): array; /** diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e7d04c0b4f..c81de40b38 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -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[] diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index be93b16fdb..c2b21e92c4 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -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))]); diff --git a/tests/PHPStan/Analyser/data/array-filter.php b/tests/PHPStan/Analyser/data/array-filter.php index ad12ea6db6..682e117812 100644 --- a/tests/PHPStan/Analyser/data/array-filter.php +++ b/tests/PHPStan/Analyser/data/array-filter.php @@ -35,3 +35,8 @@ function withoutCallback(array $map1, array $map2, array $map3): void $filtered3 = array_filter($map3, null, ARRAY_FILTER_USE_BOTH); assertType('array|int<1, max>|non-falsy-string|true>', $filtered3); } + +function invalidCallableName(array $arr) { + assertType('*ERROR*', array_filter($arr, '')); + assertType('*ERROR*', array_filter($arr, '\\')); +} diff --git a/tests/PHPStan/Analyser/data/bug-10867.php b/tests/PHPStan/Analyser/data/bug-10867.php new file mode 100644 index 0000000000..82620c277c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10867.php @@ -0,0 +1,10 @@ + + +

+