Skip to content
Closed
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
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,11 @@ services:
ignoreErrors: %ignoreErrors%
reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors%

-
class: PHPStan\Analyser\IssetHelper
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%

-
class: PHPStan\Analyser\LazyScopeFactory
arguments:
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/DirectScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function __construct(
private bool $explicitMixedInUnknownGenericNew,
private bool $explicitMixedForGlobalVariables,
private ConstantResolver $constantResolver,
private IssetHelper $issetHelper,
)
{
}
Expand Down Expand Up @@ -91,6 +92,7 @@ public function create(
$this->parser,
$this->nodeScopeResolver,
$this->constantResolver,
$this->issetHelper,
$context,
$this->phpVersion,
$declareStrictTypes,
Expand Down
192 changes: 192 additions & 0 deletions src/Analyser/IssetHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node\Expr;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use function is_string;

class IssetHelper
{

public function __construct(
private readonly PropertyReflectionFinder $propertyReflectionFinder,
private readonly bool $treatPhpDocTypesAsCertain,
)
{
}

/**
* @param callable(Type): ?bool $typeCallback
*/
public function isset(Expr $expr, Scope $scope, callable $typeCallback, ?bool $result = null): ?bool
{
// mirrored in PHPStan\Rules\IssetCheck
if ($expr instanceof Expr\Variable && is_string($expr->name)) {
$hasVariable = $scope->hasVariableType($expr->name);
if ($hasVariable->maybe()) {
return null;
}

if ($result === null) {
if ($hasVariable->yes()) {
if ($expr->name === '_SESSION') {
return null;
}

return $typeCallback($scope->getVariableType($expr->name));
}

return false;
}

return $result;
} elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
$type = $this->treatPhpDocTypesAsCertain
? $scope->getType($expr->var)
: $scope->getNativeType($expr->var);
$dimType = $this->treatPhpDocTypesAsCertain
? $scope->getType($expr->dim)
: $scope->getNativeType($expr->dim);
$hasOffsetValue = $type->hasOffsetValueType($dimType);
if (!$type->isOffsetAccessible()->yes()) {
return $result ?? $this->issetCheckUndefined($expr->var, $scope);
}

if ($hasOffsetValue->no()) {
if ($result !== null) {
return $result;
}

return false;
}

if ($hasOffsetValue->maybe()) {
return null;
}

// If offset is cannot be null, store this error message and see if one of the earlier offsets is.
// E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null.
if ($hasOffsetValue->yes()) {
if ($result !== null) {
return $result;
}

$result = $typeCallback($type->getOffsetValueType($dimType));

if ($result !== null) {
return $this->isset($expr->var, $scope, $typeCallback, $result);
}
}

// Has offset, it is nullable
return null;

} elseif ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\StaticPropertyFetch) {

$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope);

if ($propertyReflection === null) {
if ($expr instanceof Expr\PropertyFetch) {
return $this->issetCheckUndefined($expr->var, $scope);
}

if ($expr->class instanceof Expr) {
return $this->issetCheckUndefined($expr->class, $scope);
}

return null;
}

if (!$propertyReflection->isNative()) {
if ($expr instanceof Expr\PropertyFetch) {
return $this->issetCheckUndefined($expr->var, $scope);
}

if ($expr->class instanceof Expr) {
return $this->issetCheckUndefined($expr->class, $scope);
}

return null;
}

$nativeType = $propertyReflection->getNativeType();
if (!$nativeType instanceof MixedType) {
if (!$scope->isSpecified($expr)) {
if ($expr instanceof Expr\PropertyFetch) {
return $this->issetCheckUndefined($expr->var, $scope);
}

if ($expr->class instanceof Expr) {
return $this->issetCheckUndefined($expr->class, $scope);
}

return null;
}
}

if ($result !== null) {
return $result;
}

$result = $typeCallback($propertyReflection->getWritableType());
if ($result !== null) {
if ($expr instanceof Expr\PropertyFetch) {
return $this->isset($expr->var, $scope, $typeCallback, $result);
}

if ($expr->class instanceof Expr) {
return $this->isset($expr->class, $scope, $typeCallback, $result);
}
}

return $result;
}

if ($result !== null) {
return $result;
}

return $typeCallback($scope->getType($expr));
}

private function issetCheckUndefined(Expr $expr, Scope $scope): ?bool
{
if ($expr instanceof Expr\Variable && is_string($expr->name)) {
$hasVariable = $scope->hasVariableType($expr->name);
if (!$hasVariable->no()) {
return null;
}

return false;
}

if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
$type = $scope->getType($expr->var);
$dimType = $scope->getType($expr->dim);
$hasOffsetValue = $type->hasOffsetValueType($dimType);
if (!$type->isOffsetAccessible()->yes()) {
return $this->issetCheckUndefined($expr->var, $scope);
}

if (!$hasOffsetValue->no()) {
return $this->issetCheckUndefined($expr->var, $scope);
}

return false;
}

if ($expr instanceof Expr\PropertyFetch) {
return $this->issetCheckUndefined($expr->var, $scope);
}

if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
return $this->issetCheckUndefined($expr->class, $scope);
}

return null;
}

}
1 change: 1 addition & 0 deletions src/Analyser/LazyScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public function create(
$this->container->getService('currentPhpVersionSimpleParser'),
$this->container->getByType(NodeScopeResolver::class),
$this->container->getByType(ConstantResolver::class),
$this->container->getByType(IssetHelper::class),
$context,
$this->container->getByType(PhpVersion::class),
$declareStrictTypes,
Expand Down
Loading