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

Use trait properties in uninitialized props checks #1340

Merged
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
2 changes: 1 addition & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ private function processStmtNode(

$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer);
$nodeCallback(new ClassPropertiesNode($stmt, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope);
$nodeCallback(new ClassPropertiesNode($stmt, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getTraitProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope);
$nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope);
$nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope);
$classReflection->evictPrivateSymbols();
Expand Down
14 changes: 12 additions & 2 deletions src/Node/ClassPropertiesNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\Type\ObjectType;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function count;
use function in_array;

Expand All @@ -30,10 +31,11 @@ class ClassPropertiesNode extends NodeAbstract implements VirtualNode

/**
* @param ClassPropertyNode[] $properties
* @param ClassPropertyNode[] $traitProperties
* @param array<int, PropertyRead|PropertyWrite> $propertyUsages
* @param array<int, MethodCall> $methodCalls
*/
public function __construct(private ClassLike $class, private array $properties, private array $propertyUsages, private array $methodCalls)
public function __construct(private ClassLike $class, private array $properties, private array $traitProperties, private array $propertyUsages, private array $methodCalls)
{
parent::__construct($class->getAttributes());
}
Expand All @@ -51,6 +53,14 @@ public function getProperties(): array
return $this->properties;
}

/**
* @return ClassPropertyNode[]
*/
public function getTraitProperties(): array
{
return $this->traitProperties;
}

/**
* @return array<int, PropertyRead|PropertyWrite>
*/
Expand Down Expand Up @@ -92,7 +102,7 @@ public function getUninitializedProperties(
$classReflection = $scope->getClassReflection();

$properties = [];
foreach ($this->getProperties() as $property) {
foreach (array_merge($this->getProperties(), $this->getTraitProperties()) as $property) {
if ($property->isStatic()) {
continue;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Node/ClassStatementsGatherer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class ClassStatementsGatherer
/** @var ClassPropertyNode[] */
private array $properties = [];

/** @var ClassPropertyNode[] */
private array $traitProperties = [];

/** @var Node\Stmt\ClassMethod[] */
private array $methods = [];

Expand Down Expand Up @@ -62,6 +65,14 @@ public function getProperties(): array
return $this->properties;
}

/**
* @return ClassPropertyNode[]
*/
public function getTraitProperties(): array
{
return $this->traitProperties;
}

/**
* @return Node\Stmt\ClassMethod[]
*/
Expand Down Expand Up @@ -127,6 +138,10 @@ private function gatherNodes(Node $node, Scope $scope): void
}
return;
}
if ($node instanceof ClassPropertyNode && $scope->isInTrait()) {
$this->traitProperties[] = $node;
herndlm marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if ($node instanceof Node\Stmt\ClassMethod && !$scope->isInTrait()) {
$this->methods[] = $node;
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ public function testRule(): void
'@readonly property MissingReadOnlyPropertyAssignPhpDoc\Immutable::$doubleAssigned is already assigned.',
135,
],
[
'Class MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass has an uninitialized @readonly property $unassigned. Assign it in the constructor.',
156,
],
[
'Class MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass has an uninitialized @readonly property $unassigned2. Assign it in the constructor.',
159,
],
[
'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass::$readBeforeAssigned.',
188,
],
[
'@readonly property MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass::$doubleAssigned is already assigned.',
192,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ public function testRule(): void
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$bar.',
87,
],
[
'Class MissingReadOnlyPropertyAssign\FooTraitClass has an uninitialized readonly property $unassigned. Assign it in the constructor.',
114,
],
[
'Class MissingReadOnlyPropertyAssign\FooTraitClass has an uninitialized readonly property $unassigned2. Assign it in the constructor.',
116,
],
[
'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\FooTraitClass::$readBeforeAssigned.',
145,
],
[
'Readonly property MissingReadOnlyPropertyAssign\FooTraitClass::$doubleAssigned is already assigned.',
149,
],
]);
}

Expand Down
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ public function testRule(): void
'Class UninitializedProperty\TestExtension has an uninitialized property $uninited. Give it default value or assign it in the constructor.',
122,
],
[
'Class UninitializedProperty\FooTraitClass has an uninitialized property $bar. Give it default value or assign it in the constructor.',
157,
],
[
'Class UninitializedProperty\FooTraitClass has an uninitialized property $baz. Give it default value or assign it in the constructor.',
159,
],
]);
}

Expand All @@ -87,4 +95,18 @@ public function testReadOnlyPhpDoc(): void
$this->analyse([__DIR__ . '/data/uninitialized-property-readonly-phpdoc.php'], []);
}

public function testBug7219(): void
{
$this->analyse([__DIR__ . '/data/bug-7219.php'], [
[
'Class Bug7219\Foo has an uninitialized property $id. Give it default value or assign it in the constructor.',
8,
],
[
'Class Bug7219\Foo has an uninitialized property $email. Give it default value or assign it in the constructor.',
15,
],
]);
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-7219.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1); // lint >= 7.4

namespace Bug7219;

class Foo
{

public int $id;
private ?string $val = null;
use EmailTrait;
}

trait EmailTrait
{
protected string $email; // not initialized

public function getEmail(): string
{
return $this->email;
}


public function setEmail(string $email): void
{
$this->email = $email;
}
}

$foo = new Foo();
echo $foo->getEmail(); // error Typed property must not be accessed before initialization
echo $foo->id;
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,58 @@ public function setUnassigned2(int $i): void
}

}

trait FooTrait
{

/** @readonly */
private int $assigned;

private int $unassignedButNotReadOnly;

private int $readBeforeAssignedNotReadOnly;

/** @readonly */
private int $unassigned;

/** @readonly */
private int $unassigned2;

/** @readonly */
private int $readBeforeAssigned;

/** @readonly */
private int $doubleAssigned;

private int $doubleAssignedNotReadOnly;

public function setUnassigned2(int $i): void
{
$this->unassigned2 = $i;
}

}

class FooTraitClass
{

use FooTrait;

public function __construct()
{
$this->assigned = 1;

echo $this->readBeforeAssignedNotReadOnly;
$this->readBeforeAssignedNotReadOnly = 1;

echo $this->readBeforeAssigned;
$this->readBeforeAssigned = 1;

$this->doubleAssigned = 1;
$this->doubleAssigned = 2;

$this->doubleAssignedNotReadOnly = 1;
$this->doubleAssignedNotReadOnly = 2;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,55 @@ public function __construct(int $foo)
}

}

trait FooTrait
{

private readonly int $assigned;

private int $unassignedButNotReadOnly;

private int $readBeforeAssignedNotReadOnly;

private readonly int $unassigned;

private readonly int $unassigned2;

private readonly int $readBeforeAssigned;

private readonly int $doubleAssigned;

private int $doubleAssignedNotReadOnly;

public function setUnassigned2(int $i): void
{
$this->unassigned2 = $i;
}

}

class FooTraitClass
{

use FooTrait;

public function __construct(
private readonly int $promoted,
)
{
$this->assigned = 1;

echo $this->readBeforeAssignedNotReadOnly;
$this->readBeforeAssignedNotReadOnly = 1;

echo $this->readBeforeAssigned;
$this->readBeforeAssigned = 1;

$this->doubleAssigned = 1;
$this->doubleAssigned = 2;

$this->doubleAssignedNotReadOnly = 1;
$this->doubleAssignedNotReadOnly = 2;
}

}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Properties/data/uninitialized-property.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,31 @@ private function __construct(string $message)
}

}

trait FooTrait
{

private int $foo;

private int $bar;

private int $baz;

public function setBaz()
{
$this->baz = 1;
}

}

class FooTraitClass
{

use FooTrait;

public function __construct()
{
$this->foo = 1;
}

}