Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\ExistingArrayDimFetch;
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
use PHPStan\Node\Expr\TypeExpr;
Expand Down Expand Up @@ -1327,7 +1328,10 @@ public function applyWrite(
} else {
$dimExpr = $arrayItem->key;
}
$getOffsetValueTypeExpr = new TypeExpr($scope->getType($assignedExpr)->getOffsetValueType($scope->getType($dimExpr)));
$getOffsetValueTypeExpr = new NativeTypeExpr(
$scope->getType($assignedExpr)->getOffsetValueType($scope->getType($dimExpr)),
$scope->getNativeType($assignedExpr)->getOffsetValueType($scope->getNativeType($dimExpr)),
);
$itemTarget = $this->prepareTarget(
$nodeScopeResolver,
$scope,
Expand Down
8 changes: 6 additions & 2 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3002,7 +3002,7 @@ private function resolveIntertwinedAssignedType(self $scope, Type $rootType, Exp

if ($assignedExpr instanceof Expr\ArrayDimFetch && $assignedExpr->dim !== null) {
return $this->resolveIntertwinedAssignedType($scope, $rootType, $assignedExpr->var, $rootVariableName, $native)
->getOffsetValueType($scope->getType($assignedExpr->dim));
->getOffsetValueType($native ? $scope->getNativeType($assignedExpr->dim) : $scope->getType($assignedExpr->dim));
}

if ($assignedExpr instanceof SetExistingOffsetValueTypeExpr) {
Expand All @@ -3013,7 +3013,11 @@ private function resolveIntertwinedAssignedType(self $scope, Type $rootType, Exp
? $scope->getNativeType($assignedExpr->getVar())
: $scope->getType($assignedExpr->getVar());

return $iterateeType->setExistingOffsetValueType($scope->getType($assignedExpr->getDim()), $rootType);
$dimType = $native
? $scope->getNativeType($assignedExpr->getDim())
: $scope->getType($assignedExpr->getDim());

return $iterateeType->setExistingOffsetValueType($dimType, $rootType);
}

throw new ShouldNotHappenException();
Expand Down
11 changes: 9 additions & 2 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
use PHPStan\DependencyInjection\ExtensionsCollection;
use PHPStan\Node\ClosureReturnStatementsNode;
use PHPStan\Node\ExecutionEndNode;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\FunctionCallableNode;
use PHPStan\Node\FunctionCallExpressionNode;
use PHPStan\Node\InArrowFunctionNode;
Expand Down Expand Up @@ -2303,12 +2303,19 @@ public function processArgs(
$byRefType = new MixedType();
}

// what the call writes back is described by PHPDoc (@param, @param-out,
// a parameter-out extension) - natively only the parameter's own
// type declaration is guaranteed
$byRefNativeType = $currentParameter instanceof ExtendedParameterReflection
? $currentParameter->getNativeType()
: $byRefType;

$scope = $this->processVirtualAssign(
$scope,
$storage,
$stmt,
$argValue,
new TypeExpr($byRefType),
new NativeTypeExpr($byRefType, $byRefNativeType),
$nodeCallback,
)->getScope();
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
Expand Down
9 changes: 7 additions & 2 deletions src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,11 @@ private function tryProcessUnrolledConstantArrayForeach(

$nativeConstantArrays = $nativeIterateeType->getConstantArrays();
$matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null;
// the native fallback must not be the PHPDoc key/value type - the shape the
// unrolling is driven by can be PHPDoc-only, in which case natively we only
// know what the native iteratee type says about its keys and values
$nativeIterateeKeyType = $originalScope->getIterableKeyType($nativeIterateeType);
$nativeIterateeValueType = $originalScope->getIterableValueType($nativeIterateeType);

$valueVarName = $stmt->valueVar->name;
$keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null;
Expand Down Expand Up @@ -617,10 +622,10 @@ private function tryProcessUnrolledConstantArrayForeach(

$nativeKeyType = $nativeConstantArray !== null && isset($nativeConstantArray->getKeyTypes()[$i])
? $nativeConstantArray->getKeyTypes()[$i]
: $keyType;
: $nativeIterateeKeyType;
$nativeValueType = $nativeConstantArray !== null && isset($nativeConstantArray->getValueTypes()[$i])
? $nativeConstantArray->getValueTypes()[$i]
: $valueType;
: $nativeIterateeValueType;

$iterScope = $chainScope->assignVariable(
$valueVarName,
Expand Down
117 changes: 117 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15250.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php declare(strict_types = 1);

namespace Bug15250;

use function PHPStan\Testing\assertNativeType;
use function PHPStan\Testing\assertType;

/**
* @param array<'a'|'b', int> $array
*/
function doFoo(array $array): void
{
assertNativeType('array', $array);
foreach ($array as $key => $value) {
if ($key === 'a') {
throw new \LogicException();
}
}
assertType("array<'b', int>", $array);
assertNativeType('array', $array);

foreach ($array as $key => $value) {
assertType("'b'", $key);
assertNativeType('(int|string)', $key);
}
}

/**
* @param array{a: int, b: string} $array
*/
function unrolledShape(array $array): void
{
$lastKey = null;
$lastValue = null;
foreach ($array as $key => $value) {
assertType("'a'|'b'", $key);
assertNativeType('(int|string)', $key);
assertType('int|string', $value);
assertNativeType('mixed', $value);
$lastKey = $key;
$lastValue = $value;
}

assertType("'b'", $lastKey);
assertNativeType('(int|string)', $lastKey);
assertType('string', $lastValue);
assertNativeType('mixed', $lastValue);
}

function unrolledNativeShape(): void
{
$array = ['a' => 1, 'b' => 'foo'];
$lastKey = null;
$lastValue = null;
foreach ($array as $key => $value) {
$lastKey = $key;
$lastValue = $value;
}

assertType("'b'", $lastKey);
assertNativeType("'b'", $lastKey);
assertType("'foo'", $lastValue);
assertNativeType("'foo'", $lastValue);
}

/**
* @param array{int, string} $x
* @param array<int, array{int, string}> $rows
*/
function destructuring(array $x, array $rows): void
{
[$a, $b] = $x;
assertType('int', $a);
assertNativeType('mixed', $a);
assertType('string', $b);
assertNativeType('mixed', $b);

foreach ($rows as [$c, $d]) {
assertType('int', $c);
assertNativeType('mixed', $c);
assertType('string', $d);
assertNativeType('mixed', $d);
}
}

function nativeDestructuring(): void
{
$x = [1, 'foo'];
[$a, $b] = $x;
assertType('1', $a);
assertNativeType('1', $a);
assertType("'foo'", $b);
assertNativeType("'foo'", $b);
}

/** @param positive-int $i */
function byRefParam(int &$i): void
{
$i = 1;
}

/** @param-out non-empty-string $s */
function byRefParamOut(string &$s): void
{
$s = 'foo';
}

function byRef(int $v, string $s): void
{
byRefParam($v);
assertType('int<1, max>', $v);
assertNativeType('int', $v);

byRefParamOut($s);
assertType('non-empty-string', $s);
assertNativeType('string', $s);
}
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,54 @@ public function testReportTypesFromPhpDocs(): void
]);
}

public function testBug15250(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-15250.php'], []);
}

public function testBug15250TreatPhpDocTypesAsCertain(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-15250.php'], [
[
'Call to function is_int() with \'b\' will always evaluate to false.',
17,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_int() with \'b\' will always evaluate to false.',
35,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_float() with string will always evaluate to false.',
38,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_float() with int will always evaluate to false.',
49,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_float() with string will always evaluate to false.',
52,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_int() with int will always evaluate to true.',
76,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_int() with int will always evaluate to true.',
81,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

public function testBug2550(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
84 changes: 84 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-15250.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Bug15250Comparison;

/**
* @param array<'a'|'b', int> $array
*/
function narrowedForeachKey(array $array): void
{
foreach ($array as $key => $value) {
if ($key === 'a') {
throw new \LogicException();
}
}

foreach ($array as $key => $value) {
if (is_int($key)) {
echo $key;
}
}
}

/**
* @param array{a: int, b: string} $array
*/
function unrolledShape(array $array): void
{
$lastKey = null;
$lastValue = null;
foreach ($array as $key => $value) {
$lastKey = $key;
$lastValue = $value;
}

if (is_int($lastKey)) {
echo $lastKey;
}
if (is_float($lastValue)) {
echo $lastValue;
}
}

/**
* @param array{int, string} $x
*/
function destructuring(array $x): void
{
[$a, $b] = $x;
if (is_float($a)) {
echo $a;
}
if (is_float($b)) {
echo $b;
}
}

/**
* @param int $x
*/
function byRefParam(&$x): void
{
$x = 1;
}

/**
* @param-out int $y
*/
function byRefParamOut(&$y): void
{
$y = 1;
}

function byRef($v, $w): void
{
byRefParam($v);
if (is_int($v)) {
echo $v;
}

byRefParamOut($w);
if (is_int($w)) {
echo $w;
}
}
Loading