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
4 changes: 2 additions & 2 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private function transformCommonType(Type $type): Type
|| (!$type->isExplicitMixed() && $this->checkImplicitMixed)
)
) {
return new StrictMixedType();
return new StrictMixedType($type->getSubtractedType());
Comment thread
staabm marked this conversation as resolved.
}

return $traverse($type);
Expand Down Expand Up @@ -208,7 +208,7 @@ private function findTypeToCheckImplementation(
return new FoundTypeResult(
$type instanceof TemplateMixedType
? $type->toStrictMixedType()
: new StrictMixedType(),
: new StrictMixedType($type->getSubtractedType()),
[],
[],
null,
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Generic/TemplateMixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function toStrictMixedType(): TemplateStrictMixedType
$this->strategy,
$this->variance,
$this->name,
new StrictMixedType(),
new StrictMixedType($this->getSubtractedType()),
$this->default,
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Type/Generic/TemplateStrictMixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public function __construct(
?Type $default,
)
{
parent::__construct();

$this->scope = $scope;
$this->strategy = $templateTypeStrategy;
$this->variance = $templateTypeVariance;
Expand Down
14 changes: 14 additions & 0 deletions src/Type/MixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ public function getConstantStrings(): array

public function accepts(Type $type, bool $strictTypes): AcceptsResult
{
if (
$this->subtractedType !== null
&& !$type instanceof NeverType
&& $this->subtractedType->isSuperTypeOf($type)->yes()
) {
return AcceptsResult::createNo([
sprintf(
'Type %s has already been eliminated from %s.',
$this->subtractedType->describe(VerbosityLevel::precise()),
$this->describe(VerbosityLevel::typeOnly()),
),
]);
}

return AcceptsResult::createYes();
}

Expand Down
44 changes: 41 additions & 3 deletions src/Type/StrictMixedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use PHPStan\Type\Traits\NonIterableTypeTrait;
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\SubstractableTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use function sprintf;

class StrictMixedType implements CompoundType
{
Expand All @@ -33,6 +35,16 @@ class StrictMixedType implements CompoundType
use NonIterableTypeTrait;
use NonRemoveableTypeTrait;
use NonGeneralizableTypeTrait;
use SubstractableTypeTrait;

public function __construct(private ?Type $subtractedType = null)
{
}

public function getSubtractedType(): ?Type
{
return $this->subtractedType;
}

public function getReferencedClasses(): array
{
Expand All @@ -56,6 +68,20 @@ public function getConstantStrings(): array

public function accepts(Type $type, bool $strictTypes): AcceptsResult
{
if (
$this->subtractedType !== null
&& !$type instanceof NeverType
&& $this->subtractedType->isSuperTypeOf($type)->yes()
) {
return AcceptsResult::createNo([
sprintf(
'Type %s has already been eliminated from %s.',
$this->subtractedType->describe(VerbosityLevel::precise()),
$this->describe(VerbosityLevel::typeOnly()),
),
]);
}

return AcceptsResult::createYes();
}

Expand Down Expand Up @@ -90,16 +116,28 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult

public function equals(Type $type): bool
{
return $type instanceof self;
if (!$type instanceof self) {
return false;
}

if ($this->subtractedType === null) {
return $type->subtractedType === null;
}

if ($type->subtractedType === null) {
return false;
}

return $this->subtractedType->equals($type->subtractedType);
}

public function describe(VerbosityLevel $level): string
{
return $level->handle(
static fn () => 'mixed',
static fn () => 'mixed',
static fn () => 'mixed',
static fn () => 'strict-mixed',
fn () => 'mixed' . $this->describeSubtractedType($this->subtractedType, $level),
fn () => 'strict-mixed' . $this->describeSubtractedType($this->subtractedType, $level),
);
}

Expand Down
25 changes: 24 additions & 1 deletion src/Type/VerbosityLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
{
$moreVerbose = false;
$veryVerbose = false;
$moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type {
$flagsCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type {
// stop deep traversal to not waste resources.
if ($veryVerbose) {
return $type;
Expand Down Expand Up @@ -188,6 +188,29 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
}
return $traverse($type);
};
$moreVerboseCallback = static function (Type $type, callable $traverse) use (&$veryVerbose, $flagsCallback): Type {
// stop deep traversal to not waste resources.
if ($veryVerbose) {
return $type;
}

// A subtracted mixed only makes sense in an error message when the subtraction
// is spelled out. Template subtrees switch to the plain flags callback -
// the subtraction there belongs to the bound, not to the type being described.
if ($type instanceof TemplateType) {
TypeTraverser::map($type, $flagsCallback);
return $type;
}
if (
($type instanceof MixedType || $type instanceof StrictMixedType)
&& $type->getSubtractedType() !== null
) {
$veryVerbose = true;
return $type;
}

return $flagsCallback($type, $traverse);
};

TypeTraverser::map($acceptingType, $moreVerboseCallback);

Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,14 @@ public function testStringableDynamicAccess(): void
'Class constant name for object must be a string, but mixed was given.',
39,
],
[
'Class constant name for ClassConstantDynamicStringableAccess\Baz must be a string, but mixed~null was given.',
53,
],
[
'Class constant name for ClassConstantDynamicStringableAccess\Baz must be a string, but T of mixed~null (method ClassConstantDynamicStringableAccess\Baz::testSubtractedTemplate(), argument) was given.',
66,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ public function testClassDynamic(DateTime|DateTimeImmutable $datetime, object $o
}

}

final class Baz
{

public function testSubtractedMixed(mixed $mixed): void
{
if ($mixed === null) {
return;
}

echo self::{$mixed};
}

/**
* @template T
* @param T $name
*/
public function testSubtractedTemplate($name): void
{
if ($name === null) {
return;
}

echo self::{$name};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function testRule(): void
38,
],
[
'Value \'installed\' in the haystack passed to in_array() can never be identical to the needle type mixed.',
'Value \'installed\' in the haystack passed to in_array() can never be identical to the needle type mixed~\'installed\'.',
99,
'Type \'installed\' has already been eliminated from mixed.',
],
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3018,6 +3018,56 @@ public function testBug11894(): void
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
}

public static function dataNonEmptyMixedParameter(): iterable
{
yield [false];
yield [true];
}

#[DataProvider('dataNonEmptyMixedParameter')]
public function testNonEmptyMixedParameter(bool $checkExplicitMixed): void
{
$this->checkExplicitMixed = $checkExplicitMixed;
$this->checkImplicitMixed = $checkExplicitMixed;
$this->analyse([__DIR__ . '/data/non-empty-mixed-parameter.php'], [
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), \'\' given.',
17,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), \'0\' given.',
18,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), 0 given.',
19,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), 0.0 given.',
20,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), array{} given.',
21,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), false given.',
22,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Parameter #1 $value of function NonEmptyMixedParameter\acceptsNonEmptyMixed expects mixed~(0|0.0|\'\'|\'0\'|array{}|false|null), null given.',
23,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
]);
}

public function testBug11494(): void
{
$this->analyse([__DIR__ . '/data/bug-11494.php'], [
Expand Down
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
Expand Down Expand Up @@ -438,6 +439,36 @@ public function testBug14428(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14428.php'], []);
}

public static function dataNonEmptyMixedReturn(): iterable
{
yield [false];
yield [true];
}

#[DataProvider('dataNonEmptyMixedReturn')]
public function testNonEmptyMixedReturn(bool $checkExplicitMixed): void
{
$this->checkNullables = true;
$this->checkExplicitMixed = $checkExplicitMixed;
$this->analyse([__DIR__ . '/data/non-empty-mixed-return.php'], [
[
'Function NonEmptyMixedReturn\returnsEmptyString() should return mixed~(0|0.0|\'\'|\'0\'|array{}|false|null) but returns \'\'.',
8,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Function NonEmptyMixedReturn\returnsNull() should return mixed~(0|0.0|\'\'|\'0\'|array{}|false|null) but returns null.',
14,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
[
'Function NonEmptyMixedReturn\returnsEmptyArray() should return mixed~(0|0.0|\'\'|\'0\'|array{}|false|null) but returns array{}.',
20,
'Type 0|0.0|\'\'|\'0\'|array{}|false|null has already been eliminated from mixed.',
],
]);
}

public function testBug13565(): void
{
$this->checkNullables = true;
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Functions/data/non-empty-mixed-parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace NonEmptyMixedParameter;

/** @param non-empty-mixed $value */
function acceptsNonEmptyMixed($value): void
{
}

/** @param mixed $value */
function acceptsPlainMixed($value): void
{
}

function doFoo(string $string, int $int, bool $bool): void
{
acceptsNonEmptyMixed('');
acceptsNonEmptyMixed('0');
acceptsNonEmptyMixed(0);
acceptsNonEmptyMixed(0.0);
acceptsNonEmptyMixed([]);
acceptsNonEmptyMixed(false);
acceptsNonEmptyMixed(null);

acceptsNonEmptyMixed('x');
acceptsNonEmptyMixed(1);
acceptsNonEmptyMixed(true);
acceptsNonEmptyMixed([1]);
acceptsNonEmptyMixed($string);
acceptsNonEmptyMixed($int);
acceptsNonEmptyMixed($bool);

acceptsPlainMixed('');
acceptsPlainMixed(null);
}

/** @param mixed $value */
function forwardsSubtractedMixed($value): void
{
if ($value === null) {
return;
}

acceptsPlainMixed($value);
}
Loading
Loading