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
11 changes: 3 additions & 8 deletions src/Rules/IssetCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,10 @@ static function (Type $type) use ($typeMessageCallback): ?string {
}

if (!$scope->hasExpressionType($expr)->yes()) {
if ($expr instanceof Node\Expr\PropertyFetch) {
Comment thread
VincentLanglet marked this conversation as resolved.
return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier);
}

if ($expr->class instanceof Expr) {
return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier);
$nativeReflection = $propertyReflection->getNativeReflection();
if ($nativeReflection !== null && !$nativeReflection->getNativeReflection()->hasDefaultValue()) {
return null;
}

return null;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Type/Php/ArrayFindFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return null;
}

$arrayArg = $args[0]->value ?? null;
$callbackArg = $args[1]->value ?? null;
$arrayArg = $args[0]->value;
$callbackArg = $args[1]->value;

$resultTypes = $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, null);
$resultType = TypeCombinator::union(...array_map(static fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays()));
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Variables/EmptyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,18 @@ public function testBug7806(): void
$this->analyse([__DIR__ . '/data/bug-7806.php'], []);
}

public function testBug14393(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/bug-14393.php'], [
[
'Variable $undefinedVar in empty() is never defined.',
166,
],
]);
}

#[RequiresPhp('>= 8.0')]
public function testIssetAfterRememberedConstructor(): void
{
Expand Down
43 changes: 38 additions & 5 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,10 @@ public function testNativePropertyTypes(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/isset-native-property-types.php'], [
/*[
// no way to achieve this with current PHP Reflection API
// There's ReflectionClass::getDefaultProperties()
// but it cannot differentiate between `public int $foo` and `public int $foo = null`;
[
'Property IssetNativePropertyTypes\Foo::$hasDefaultValue (int) in isset() is not nullable.',
17,
],*/
],
[
'Property IssetNativePropertyTypes\Foo::$isAssignedBefore (int) in isset() is not nullable.',
20,
Expand Down Expand Up @@ -529,4 +526,40 @@ public function testBug9503(): void
$this->analyse([__DIR__ . '/data/bug-9503.php'], []);
}

public function testBug14393(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/bug-14393.php'], [
[
'Property Bug14393\MyClass::$i (int) in isset() is not nullable.',
12,
],
[
'Property Bug14393\MyClassPhpDoc::$i (int) in isset() is not nullable.',
37,
],
[
'Property Bug14393\MyClass::$i (int) in isset() is not nullable.',
81,
],
[
'Property Bug14393\MyClassPhpDoc::$i (int) in isset() is not nullable.',
93,
],
[
'Static property Bug14393\MyClassStatic::$i (int) in isset() is not nullable.',
121,
],
[
'Static property Bug14393\MyClassStatic::$i (int) in isset() is not nullable.',
151,
],
[
'Variable $undefinedVar in isset() is never defined.',
165,
],
]);
}

}
44 changes: 44 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,48 @@ public function testBug13921(): void
]);
}

public function testBug4846(): void
{
$this->analyse([__DIR__ . '/data/bug-4846.php'], [
[
'Property Bug4846\Foo::$alwaysString (string) on left side of ?? is not nullable.',
13,
],
]);
}

public function testBug14393(): void
{
$this->analyse([__DIR__ . '/data/bug-14393.php'], [
[
'Property Bug14393\MyClass::$i (int) on left side of ?? is not nullable.',
11,
],
[
'Property Bug14393\MyClassPhpDoc::$i (int) on left side of ?? is not nullable.',
36,
],
[
'Property Bug14393\MyClass::$i (int) on left side of ?? is not nullable.',
80,
],
[
'Property Bug14393\MyClassPhpDoc::$i (int) on left side of ?? is not nullable.',
92,
],
[
'Static property Bug14393\MyClassStatic::$i (int) on left side of ?? is not nullable.',
120,
],
[
'Static property Bug14393\MyClassStatic::$i (int) on left side of ?? is not nullable.',
150,
],
[
'Variable $undefinedVar on left side of ?? is never defined.',
164,
],
]);
}

}
167 changes: 167 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-14393.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php declare(strict_types = 1);

namespace Bug14393;

class MyClass
{
public int $i = 10;

public function doFoo(): void
{
var_dump($this->i ?? -1);
Comment thread
staabm marked this conversation as resolved.
Comment thread
staabm marked this conversation as resolved.
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClassUninitialized
{
public int $i;
Comment thread
staabm marked this conversation as resolved.

public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClassPhpDoc
{
/** @var int */
public $i;

public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClassNullable
{
public ?int $i;

public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClassNullableWithDefault
{
public ?int $i = 10;

public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClassNullableWithNullDefault
{
public ?int $i = null;

public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

$o = new MyClass();

var_dump($o->i ?? -1);
var_dump(isset($o->i));
var_dump(empty($o->i));

$o2 = new MyClassUninitialized();

var_dump($o2->i ?? -1);
var_dump(isset($o2->i));
var_dump(empty($o2->i));

$o3 = new MyClassPhpDoc();

var_dump($o3->i ?? -1);
var_dump(isset($o3->i));
var_dump(empty($o3->i));

$o4 = new MyClassNullable();

var_dump($o4->i ?? -1);
var_dump(isset($o4->i));
var_dump(empty($o4->i));

$o5 = new MyClassNullableWithDefault();

var_dump($o5->i ?? -1);
var_dump(isset($o5->i));
var_dump(empty($o5->i));

$o6 = new MyClassNullableWithNullDefault();

var_dump($o6->i ?? -1);
var_dump(isset($o6->i));
var_dump(empty($o6->i));

class MyClassStatic
{
public static int $i = 10;

public function doFoo(): void
{
var_dump(self::$i ?? -1);
var_dump(isset(self::$i));
var_dump(empty(self::$i));
}
}

class MyClassStaticUninitialized
{
public static int $i;

public function doFoo(): void
{
var_dump(self::$i ?? -1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we are missing a error here, because the static property referenced by self::$i is not nullable

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The fix applies the same hasDefaultValue() check to static properties that was already in place for instance properties. Now self::$i ?? -1 and isset(self::$i) correctly report errors when the static property has a native type and a default value (like public static int $i = 10), while still allowing the bail-out for uninitialized static properties (no default value) or dynamic class references.

var_dump(isset(self::$i));
var_dump(empty(self::$i));
}
}

class MyClassStaticNullable
{
public static ?int $i = null;

public function doFoo(): void
{
var_dump(self::$i ?? -1);
var_dump(isset(self::$i));
var_dump(empty(self::$i));
}
}

var_dump(MyClassStatic::$i ?? -1);
var_dump(isset(MyClassStatic::$i));
var_dump(empty(MyClassStatic::$i));

var_dump(MyClassStaticUninitialized::$i ?? -1);
var_dump(isset(MyClassStaticUninitialized::$i));
var_dump(empty(MyClassStaticUninitialized::$i));

var_dump(MyClassStaticNullable::$i ?? -1);
var_dump(isset(MyClassStaticNullable::$i));
var_dump(empty(MyClassStaticNullable::$i));

// Test undefined variable with native-typed property access
function testUndefinedVar(): void {
var_dump($undefinedVar->i ?? -1);
var_dump(isset($undefinedVar->i));
var_dump(empty($undefinedVar->i));
}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-4846.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace Bug4846;

class Foo
{
public string $alwaysString = '';

public ?string $nullableString = null;
}

function (Foo $foo): void {
echo $foo->alwaysString ?? 'string';

echo $foo->nullableString ?? 'string';
};
Loading