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
4 changes: 3 additions & 1 deletion src/Analyser/Fiber/FiberScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function filterByTruthyValue(Expr $expr): self
/** @var self $scope */
$scope = parent::filterByTruthyValue($expr);
$scope->truthyValueExprs = $this->truthyValueExprs;
$scope->falseyValueExprs = $this->falseyValueExprs;
$scope->truthyValueExprs[] = $expr;

return $scope;
Expand All @@ -144,7 +145,8 @@ public function filterByTruthyValue(Expr $expr): self
public function filterByFalseyValue(Expr $expr): self
{
/** @var self $scope */
$scope = parent::filterByTruthyValue($expr);
$scope = parent::filterByFalseyValue($expr);
$scope->truthyValueExprs = $this->truthyValueExprs;
$scope->falseyValueExprs = $this->falseyValueExprs;
$scope->falseyValueExprs[] = $expr;

Expand Down
18 changes: 14 additions & 4 deletions src/Rules/Comparison/FunctionCallConstantConditionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,26 @@ public function getNodeType(): string
public function processNode(Node $node, Scope $scope): array
{
$reportedMarkers = [];
foreach ($node->get(ImpossibleCheckTypeReportedCollector::class) as $fileData) {
$reportedMarkersByFile = [];
foreach ($node->get(ImpossibleCheckTypeReportedCollector::class) as $filePath => $fileData) {
foreach ($fileData as $data) {
$reportedMarkers[$data[0]] = true;
$reportedMarkersByFile[$filePath . "\0" . $data[0]] = true;
}
}

$errorsByRuleTraitExprValue = [];
foreach ($node->get(FunctionCallConstantConditionCollector::class) as $fileData) {
foreach ($node->get(FunctionCallConstantConditionCollector::class) as $filePath => $fileData) {
foreach ($fileData as $data) {
$ruleName = $data[0];
$traitName = $data[1];
$traitKey = $traitName ?? self::NULL_TRAIT_KEY;
$exprString = $data[2];
// A non-trait call site is per-file: the same printed condition at
// the same line in two different files must neither merge its
// deferred errors nor be suppressed by the other file's marker.
// Trait call sites keep merging across the analysed contexts -
// trait names are unique project-wide.
$exprString = $traitName === null ? $filePath . "\0" . $data[2] : $data[2];
$value = $data[3];
$valueKey = var_export($value, true);
if ($data[3] === null) {
Expand All @@ -68,7 +75,10 @@ public function processNode(Node $node, Scope $scope): array
foreach ($ruleData as $traitKey => $traitData) {
$isTrait = $traitKey !== self::NULL_TRAIT_KEY;
foreach ($traitData as $exprString => $valueData) {
if (array_key_exists($exprString, $reportedMarkers)) {
// non-trait keys carry their file, so only the same file's
// marker suppresses; trait entries match markers from any
// analysed context
if (array_key_exists($exprString, $isTrait ? $reportedMarkers : $reportedMarkersByFile)) {
// the ImpossibleCheckType* rule owns this call site
continue;
}
Expand Down
8 changes: 7 additions & 1 deletion src/Rules/Comparison/MatchExpressionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
foreach ($armConditions as $armCondition) {
$armConditionScope = $armCondition->getScope();
$rawCondition = $armCondition->getCondition();
$isTypeCheckCandidate = $this->functionCallConstantConditionHelper->isTypeCheckCandidate($rawCondition);
// Only for a match(true)-style subject is the arm comparison the
// same fact as the call's own constant truthiness - the site the
// ImpossibleCheckType* rules own. For any other subject the
// comparison ("int is never true") is an independent finding and
// must not be deduplicated away against their markers.
$isTypeCheckCandidate = ($matchConditionType->isTrue()->yes() || $matchConditionType->isFalse()->yes())
&& $this->functionCallConstantConditionHelper->isTypeCheckCandidate($rawCondition);
$armConditionExpr = new Node\Expr\BinaryOp\Identical(
$matchCondition,
$rawCondition,
Expand Down
10 changes: 8 additions & 2 deletions src/Rules/PhpDoc/VarTagTypeRuleHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException;
use PHPStan\PhpDoc\Tag\VarTag;
use PHPStan\PhpDoc\TypeNodeResolver;
Expand Down Expand Up @@ -78,7 +78,13 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra
$dimExpr = $arrayItem->key;
}

$itemErrors = $this->checkVarType($scope, $arrayItem->value, new TypeExpr($scope->getType($expr)->getOffsetValueType($scope->getType($dimExpr))), $varTags, $assignedVariables);
// carry both flavours so the native-type check reads the native
// offset type, not the phpdoc one (mirrors the foreach key/value
// sites in WrongVariableNameInVarTagRule)
$itemErrors = $this->checkVarType($scope, $arrayItem->value, new NativeTypeExpr(
$scope->getType($expr)->getOffsetValueType($scope->getType($dimExpr)),
$scope->getNativeType($expr)->getOffsetValueType($scope->getNativeType($dimExpr)),
), $varTags, $assignedVariables);
foreach ($itemErrors as $error) {
$errors[] = $error;
}
Expand Down
80 changes: 80 additions & 0 deletions tests/PHPStan/Analyser/FiberScopeFilterByValueRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function count;
use function is_string;
use function sprintf;

/**
* Exercises the rule-facing Scope::filterByTruthyValue()/filterByFalseyValue()
* API the way a third-party rule would: filtering the scope it received and
* reading state directly off the filtered scope.
*
* @implements Rule<FuncCall>
*/
class FiberScopeFilterByValueRule implements Rule
{

public function getNodeType(): string
{
return FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}

$functionName = $node->name->getLast();
$args = $node->getArgs();

if ($functionName === 'probeFilter') {
if (count($args) < 2) {
return [];
}
$var = $args[1]->value;
if (!$var instanceof Variable || !is_string($var->name)) {
return [];
}

$truthyType = $scope->filterByTruthyValue($args[0]->value)->getVariableType($var->name);
$falseyType = $scope->filterByFalseyValue($args[0]->value)->getVariableType($var->name);

return [
RuleErrorBuilder::message(sprintf(
'truthy: %s, falsey: %s',
$truthyType->describe(VerbosityLevel::precise()),
$falseyType->describe(VerbosityLevel::precise()),
))->identifier('tests.fiberScopeFilter')->build(),
];
}

if ($functionName === 'probeChainedFilter') {
if (count($args) < 3) {
return [];
}

$chainedType = $scope->filterByTruthyValue($args[0]->value)
->filterByFalseyValue($args[1]->value)
->getType($args[2]->value);

return [
RuleErrorBuilder::message(sprintf(
'chained: %s',
$chainedType->describe(VerbosityLevel::precise()),
))->identifier('tests.fiberScopeFilter')->build(),
];
}

return [];
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Analyser/FiberScopeFilterByValueRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<FiberScopeFilterByValueRule>
*/
class FiberScopeFilterByValueRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new FiberScopeFilterByValueRule();
}

public function testFilterByValue(): void
{
$this->analyse([__DIR__ . '/data/fiber-scope-filter-by-value.php'], [
[
'truthy: int, falsey: null',
15,
],
[
'chained: int<min, 4>|int<6, max>',
20,
],
]);
}

}
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/fiber-scope-filter-by-value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace FiberScopeFilterByValue;

function probeFilter(bool $condition, ?int $subject): void
{
}

function probeChainedFilter(bool $conditionA, bool $conditionB, ?int $subject): void
{
}

function testFilter(?int $x): void
{
probeFilter($x !== null, $x);
}

function testChainedFilter(?int $a): void
{
probeChainedFilter($a !== null, $a === 5, $a);
}
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,42 @@ public function testConstantConditionFunctionCall(): void
]);
}

public function testDeferredErrorsAreNotCollapsedAcrossFiles(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([
__DIR__ . '/data/call-condition-cross-file-a.php',
__DIR__ . '/data/call-condition-cross-file-b.php',
], [
[
'If condition is always true.',
18,
'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%</>.',
],
[
'If condition is always true.',
18,
'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 testMarkerFromAnotherFileDoesNotSuppress(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([
__DIR__ . '/data/call-condition-cross-file-marker-a.php',
__DIR__ . '/data/call-condition-cross-file-marker-b.php',
], [
[
'Call to function is_int() with int will always evaluate to true.',
7,
],
[
'If condition is always true.',
7,
],
]);
}

}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,23 @@ public function testInTrait(): void
]);
}

public function testMatchArmComparisonNotSuppressedByImpossibleCheck(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/match-arm-type-check-call.php'], [
[
'Call to function is_int() with int will always evaluate to true.',
8,
],
[
'Match arm comparison between int and true is always false.',
8,
],
[
'Call to function is_int() with int will always evaluate to true.',
16,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace CallConditionCrossFileA;

class Check
{

/** @return true */
public function ok(): bool
{
return true;
}

}

function doFoo(Check $c): void
{
if ($c->ok()) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace CallConditionCrossFileB;

class Check
{

/** @return true */
public function ok(): bool
{
return true;
}

}

function doFoo(Check $c): void
{
if ($c->ok()) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types = 1);

namespace CallConditionCrossFileMarkerA;

function doFoo(int $x): void
{
if (is_int($x)) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace CallConditionCrossFileMarkerB;

function doBar(int $x): void
{
if (is_int($x)) {
}
}

function is_int($value): \stdClass
{
return new \stdClass();
}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/match-arm-type-check-call.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php // lint >= 8.0

namespace MatchArmTypeCheckCall;

function doFoo(int $i, int $y): string
{
return match ($i) {
is_int($y) => 'a',
default => 'b',
};
}

function doBar(int $y): string
{
return match (true) {
is_int($y) => 'a',
default => 'b',
};
}
Loading
Loading