Skip to content
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

Fix fatal error on constant('') #3013

Open
wants to merge 10 commits into
base: 1.11.x
Choose a base branch
from
1 change: 1 addition & 0 deletions conf/config.neon
Expand Up @@ -175,6 +175,7 @@ parameters:
- ../stubs/ext-ds.stub
- ../stubs/ImagickPixel.stub
- ../stubs/PDOStatement.stub
- ../stubs/PhpParserName.stub
Copy link
Member

Choose a reason for hiding this comment

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

Please register this stub only in build/phpstan.neon. It's only for internal PHPStan purposes.

Copy link
Member

Choose a reason for hiding this comment

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

And move the stub elsewhere, probably to build/stubs.

- ../stubs/date.stub
- ../stubs/ibm_db2.stub
- ../stubs/mysqli.stub
Expand Down
4 changes: 4 additions & 0 deletions src/Analyser/MutatingScope.php
Expand Up @@ -171,6 +171,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 @@ -5172,6 +5173,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
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
Expand Up @@ -1764,6 +1764,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
Expand Up @@ -2009,6 +2009,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 @@ -2030,6 +2031,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
17 changes: 16 additions & 1 deletion src/Reflection/InitializerExprContext.php
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 Expand Up @@ -127,6 +138,10 @@ public static function fromStubParameter(

public static function fromGlobalConstant(ReflectionConstant $constant): self
{
if ($constant->getNamespaceName() === '') {
throw new ShouldNotHappenException('Namespace cannot be empty.');
Copy link
Member

Choose a reason for hiding this comment

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

This could be tackled by a better typehint in BetterReflection.

}

return new self(
$constant->getFileName(),
$constant->getNamespaceName(),
Expand Down
3 changes: 3 additions & 0 deletions src/Reflection/NamespaceAnswerer.php
Expand Up @@ -6,6 +6,9 @@
interface NamespaceAnswerer
{

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

}
5 changes: 5 additions & 0 deletions src/Rules/Generics/TemplateTypeCheck.php
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
Expand Down Expand Up @@ -66,6 +67,10 @@ public function check(
{
$messages = [];
foreach ($templateTags as $templateTag) {
if ($templateTag->getName() === '') {
Copy link
Member

Choose a reason for hiding this comment

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

This should be tackled in TemplateTag by typehinting non-empty-string.

throw new ShouldNotHappenException('Template tag name cannot be empty.');
}

$templateTagName = $scope->resolveName(new Node\Name($templateTag->getName()));
if ($this->reflectionProvider->hasClass($templateTagName)) {
$messages[] = RuleErrorBuilder::message(sprintf(
Expand Down
3 changes: 3 additions & 0 deletions src/Type/BitwiseFlagHelper.php
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
Expand Up @@ -238,6 +238,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
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
Expand Up @@ -259,6 +259,9 @@ public function getReferencedClasses(): array

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

Expand Down
13 changes: 11 additions & 2 deletions src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php
Expand Up @@ -33,8 +33,8 @@
use function array_map;
use function count;
use function is_string;
use function ltrim;
use function strtolower;
use function substr;

class ArrayFilterFunctionReturnTypeReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
Expand Down Expand Up @@ -245,7 +245,16 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type
private static function createFunctionName(string $funcName): Name
{
if ($funcName[0] === '\\') {
return new Name\FullyQualified(substr($funcName, 1));
$fqcn = ltrim($funcName, '\\');
if ($fqcn === '') {
Copy link
Member

Choose a reason for hiding this comment

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

You can definitely write some userland code that will execute this condition

throw new ShouldNotHappenException();
}

return new Name\FullyQualified($fqcn);
}

if ($funcName === '') {
throw new ShouldNotHappenException();
Copy link
Member

Choose a reason for hiding this comment

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

You can definitely write some userland code that will execute this condition

}

return new Name($funcName);
Expand Down
3 changes: 3 additions & 0 deletions src/Type/Php/ConstantFunctionReturnTypeExtension.php
Expand Up @@ -36,6 +36,9 @@ public function getTypeFromFunctionCall(

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

Expand Down
11 changes: 10 additions & 1 deletion src/Type/Php/ConstantHelper.php
Expand Up @@ -8,18 +8,27 @@
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\ShouldNotHappenException;
use function count;
use function explode;
use function ltrim;

class ConstantHelper
{

/**
* @param non-empty-string $constantName
*/
public function createExprFromConstantName(string $constantName): Expr
{
$classConstParts = explode('::', $constantName);
if (count($classConstParts) >= 2) {
$classConstName = new FullyQualified(ltrim($classConstParts[0], '\\'));
$fqcn = ltrim($classConstParts[0], '\\');
if ($fqcn === '') {
throw new ShouldNotHappenException();
Copy link
Member

Choose a reason for hiding this comment

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

What if you can constant('::aa')? That will execute this condition

}

$classConstName = new FullyQualified($fqcn);
if ($classConstName->isSpecialClassName()) {
$classConstName = new Name($classConstName->toString());
}
Expand Down
3 changes: 3 additions & 0 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
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
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
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
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
25 changes: 25 additions & 0 deletions stubs/PhpParserName.stub
@@ -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 {
}
}
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -1330,6 +1330,12 @@ public function testBug10772(): 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
Expand Up @@ -1324,11 +1324,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
10 changes: 10 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10867.php
@@ -0,0 +1,10 @@
<?php
namespace Bug10867;

function doFoo():void {
$sysERR = '';
?>

<p><?php echo constant($sysERR); ?></p>
<?php
}
3 changes: 3 additions & 0 deletions tests/PHPStan/Reflection/FunctionReflectionTest.php
Expand Up @@ -37,6 +37,8 @@ public function dataPhpdocFunctions(): iterable

/**
* @dataProvider dataPhpdocFunctions
*
* @param non-empty-string $functionName
*/
public function testFunctionHasPhpdoc(string $functionName, ?string $expectedDoc): void
{
Expand Down Expand Up @@ -139,6 +141,7 @@ public function dataFunctionReturnsByReference(): iterable

/**
* @dataProvider dataFunctionReturnsByReference
* @param non-empty-string $functionName
*/
public function testFunctionReturnsByReference(string $functionName, TrinaryLogic $expectedReturnsByRef): void
{
Expand Down
7 changes: 7 additions & 0 deletions tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php
Expand Up @@ -73,6 +73,10 @@ private static function generateSymbolDescription(string $symbol): string
{
[$type, $name] = explode(' ', $symbol);

if ($name === '') {
throw new ShouldNotHappenException();
}

try {
switch ($type) {
case 'FUNCTION':
Expand Down Expand Up @@ -152,6 +156,9 @@ private static function getPhpSymbolsFile(): string
return __DIR__ . '/data/golden/phpSymbols.txt';
}

/**
* @param non-empty-string $functionName
*/
private static function generateFunctionDescription(string $functionName): string
{
$nameNode = new Name($functionName);
Expand Down