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
2 changes: 1 addition & 1 deletion 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());

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.

should these code-sites also get the subtracted type?

grafik

}

return $traverse($type);
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
24 changes: 24 additions & 0 deletions src/Type/VerbosityLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,30 @@ public function isCache(): bool
*/
public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self
{
// A subtracted mixed only makes sense in an error message when the subtraction
// is spelled out. Template bounds are skipped - the subtraction there belongs
// to the bound, not to the type being described.
$hasSubtractedMixed = false;
TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$hasSubtractedMixed): Type {
if ($hasSubtractedMixed || $type instanceof TemplateType) {
return $type;
}

if (
($type instanceof MixedType || $type instanceof StrictMixedType)
&& $type->getSubtractedType() !== null
) {
$hasSubtractedMixed = true;
return $type;
}

return $traverse($type);
});

if ($hasSubtractedMixed) {
return self::precise();
}

$moreVerbose = false;
$veryVerbose = false;
$moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type {
Expand Down
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);
}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Functions/data/non-empty-mixed-return.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace NonEmptyMixedReturn;

/** @return non-empty-mixed */
function returnsEmptyString()
{
return '';
}

/** @return non-empty-mixed */
function returnsNull()
{
return null;
}

/** @return non-empty-mixed */
function returnsEmptyArray()
{
return [];
}

/** @return non-empty-mixed */
function returnsNonEmptyString()
{
return 'x';
}

/**
* @param string $string
* @return non-empty-mixed
*/
function returnsGeneralString($string)
{
return $string;
}

/**
* @param mixed $value
* @return non-empty-mixed
*/
function returnsPlainMixed($value)
{
return $value;
}
Loading
Loading