Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/Analyser/ExprHandler/ArrayDimFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\IllegalOffsetTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\NoopNodeCallback;
Expand All @@ -23,9 +25,11 @@
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Php\PhpVersion;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use TypeError;
use function array_merge;

/**
Expand All @@ -35,6 +39,10 @@
final class ArrayDimFetchHandler implements ExprHandler
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function supports(Expr $expr): bool
{
return $expr instanceof ArrayDimFetch;
Expand Down Expand Up @@ -109,6 +117,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
)->getThrowPoints());
}

if (
$this->phpVersion->throwsTypeErrorForIllegalOffsets()
// only array and string offset reads throw TypeError for illegal offsets,
// reads on null and other scalars emit a warning, ArrayAccess objects go through offsetGet()
&& (!$varType->isArray()->no() || !$varType->isString()->no())
&& IllegalOffsetTypeHelper::mayOffsetThrowTypeError($scope->getType($expr->dim))
) {
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $expr, false);
}

return new ExpressionResult(
$scope,
hasYield: $dimResult->hasYield() || $varResult->hasYield(),
Expand Down
19 changes: 19 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExpressionTypeHolder;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\IllegalOffsetTypeHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
Expand Down Expand Up @@ -608,6 +609,24 @@ public function processAssignVar(
}
}

if ($this->phpVersion->throwsTypeErrorForIllegalOffsets()) {
foreach ($offsetTypes as [$offsetType, $offsetDimFetch]) {
// $arr[] = ... never throws for the offset
if ($offsetType === null) {
continue;
}
if (!IllegalOffsetTypeHelper::mayOffsetThrowTypeError($offsetType)) {
continue;
}
// writes to ArrayAccess objects go through offsetSet() whose throw points are modelled below
$containerType = $scope->getType($offsetDimFetch->var);
if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($containerType)->yes()) {
continue;
}
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $offsetDimFetch, false);
}
}

$valueToWrite = $scope->getType($assignedExpr);
$nativeValueToWrite = $scope->getNativeType($assignedExpr);
$scopeBeforeAssignEval = $scope;
Expand Down
20 changes: 20 additions & 0 deletions src/Analyser/ExprHandler/Helper/IllegalOffsetTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PHPStan\Type\Type;

final class IllegalOffsetTypeHelper
{

/**
* On PHP 8.0+ using an array or an object as an array/string offset throws TypeError
* (https://wiki.php.net/rfc/engine_warnings). Float, bool and null keys are coerced
* with at most a deprecation, resource keys with a warning - no TypeError.
*/
public static function mayOffsetThrowTypeError(Type $offsetType): bool
{
return !$offsetType->isArray()->no() || !$offsetType->isObject()->no();
}

}
6 changes: 6 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ public function throwsValueErrorForInternalFunctions(): bool
return $this->versionId >= 80000;
}

// see https://wiki.php.net/rfc/engine_warnings - "Illegal offset type" family
public function throwsTypeErrorForIllegalOffsets(): bool
{
return $this->versionId >= 80000;
}

public function supportsHhPrintfSpecifier(): bool
{
return $this->versionId >= 80000;
Expand Down
10 changes: 0 additions & 10 deletions tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,8 @@

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertVariableCertainty;
use function ThrowPoints\Helpers\doesntThrow;
use function ThrowPoints\Helpers\maybeThrows;

function () {
try {
[][doesntThrow()];
$foo = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
[][maybeThrows()];
Expand Down
8 changes: 0 additions & 8 deletions tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ function () {
}
};

function () {
try {
$foo[doesntThrow()] .= 0;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
$foo[maybeThrows()] .= 0;
Expand Down
16 changes: 0 additions & 16 deletions tests/PHPStan/Analyser/nsrt/throw-points/assign.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ function () {
}
};

function () {
try {
$foo[doesntThrow()] = 0;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
$foo[maybeThrows()] = 0;
Expand Down Expand Up @@ -153,14 +145,6 @@ function () {
}
};

function () {
try {
[$foo[doesntThrow()]] = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
[$foo[maybeThrows()]] = 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php // lint < 8.0

namespace ThrowPoints\IllegalArrayOffset;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertVariableCertainty;
use function ThrowPoints\Helpers\doesntThrow;

// Before PHP 8.0 an illegal (array/object) offset does not throw TypeError, so a
// non-throwing offset expression introduces no throw point and $foo is always assigned.

function () {
try {
$foo[doesntThrow()] = 0;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
[$foo[doesntThrow()]] = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
$foo[doesntThrow()] .= 0;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};

function () {
try {
[][doesntThrow()];
$foo = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php // lint >= 8.0

namespace ThrowPoints\IllegalArrayOffset;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertVariableCertainty;
use function ThrowPoints\Helpers\doesntThrow;

// doesntThrow() never throws by itself, but on PHP 8.0+ its mixed return value used
// as an array/string offset may be an array or an object, which throws TypeError.
// That TypeError is the only throw point here, so $foo may be left unassigned.

function () {
try {
$foo[doesntThrow()] = 0;
} finally {
assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);
}
};

function () {
try {
[$foo[doesntThrow()]] = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);
}
};

function () {
try {
$foo[doesntThrow()] .= 0;
} finally {
assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);
}
};

function () {
try {
[][doesntThrow()];
$foo = 1;
} finally {
assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);
}
};
82 changes: 1 addition & 81 deletions tests/PHPStan/Levels/data/arrayOffsetAccess-4.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,6 @@
"line": 18,
"ignorable": true
},
{
"message": "Expression \"$a[$objectOrInt]\" on a separate line does not do anything.",
"line": 19,
"ignorable": true
},
{
"message": "Expression \"$a[$objectOrNull]\" on a separate line does not do anything.",
"line": 20,
"ignorable": true
},
{
"message": "Expression \"$a[$explicitlyMixed]\" on a separate line does not do anything.",
"line": 21,
"ignorable": true
},
{
"message": "Expression \"$a[$implicitlyMixed]\" on a separate line does not do anything.",
"line": 22,
"ignorable": true
},
{
"message": "Expression \"$arrayOrObject[42]\" on a separate line does not do anything.",
"line": 24,
Expand All @@ -49,26 +29,6 @@
"line": 27,
"ignorable": true
},
{
"message": "Expression \"$arrayOrObject[$objectOrInt]\" on a separate line does not do anything.",
"line": 28,
"ignorable": true
},
{
"message": "Expression \"$arrayOrObject[$objectOrNull]\" on a separate line does not do anything.",
"line": 29,
"ignorable": true
},
{
"message": "Expression \"$arrayOrObject[$explicitlyMixed]\" on a separate line does not do anything.",
"line": 30,
"ignorable": true
},
{
"message": "Expression \"$arrayOrObject[$implicitlyMixed]\" on a separate line does not do anything.",
"line": 31,
"ignorable": true
},
{
"message": "Expression \"$explicitlyMixed[42]\" on a separate line does not do anything.",
"line": 33,
Expand All @@ -84,26 +44,6 @@
"line": 36,
"ignorable": true
},
{
"message": "Expression \"$explicitlyMixed[$objectOrInt]\" on a separate line does not do anything.",
"line": 37,
"ignorable": true
},
{
"message": "Expression \"$explicitlyMixed[$objectOrNull]\" on a separate line does not do anything.",
"line": 38,
"ignorable": true
},
{
"message": "Expression \"$explicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.",
"line": 39,
"ignorable": true
},
{
"message": "Expression \"$explicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.",
"line": 40,
"ignorable": true
},
{
"message": "Expression \"$implicitlyMixed[42]\" on a separate line does not do anything.",
"line": 42,
Expand All @@ -118,25 +58,5 @@
"message": "Expression \"$implicitlyMixed[$intOrNull]\" on a separate line does not do anything.",
"line": 45,
"ignorable": true
},
{
"message": "Expression \"$implicitlyMixed[$objectOrInt]\" on a separate line does not do anything.",
"line": 46,
"ignorable": true
},
{
"message": "Expression \"$implicitlyMixed[$objectOrNull]\" on a separate line does not do anything.",
"line": 47,
"ignorable": true
},
{
"message": "Expression \"$implicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.",
"line": 48,
"ignorable": true
},
{
"message": "Expression \"$implicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.",
"line": 49,
"ignorable": true
}
]
]
Loading
Loading