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
2 changes: 2 additions & 0 deletions conf/config.level4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ services:
class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
bleedingEdge: %featureToggles.bleedingEdge%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
bleedingEdge: %featureToggles.bleedingEdge%
tags:
- phpstan.rules.rule

Expand Down
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ services:
class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck
arguments:
reportMaybes: %reportMaybes%
bleedingEdge: %featureToggles.bleedingEdge%

-
class: PHPStan\Rules\ClassCaseSensitivityCheck
Expand Down
37 changes: 26 additions & 11 deletions src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
class NonexistentOffsetInArrayDimFetchCheck
{

public function __construct(private RuleLevelHelper $ruleLevelHelper, private bool $reportMaybes)
public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private bool $reportMaybes,
private bool $bleedingEdge,
)
{
}

Expand All @@ -44,9 +48,19 @@ public function check(
return $typeResult->getUnknownClassErrors();
}

$report = $type->hasOffsetValueType($dimType)->no();
if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) {
return [];
}

if ($type->hasOffsetValueType($dimType)->no()) {
return [
RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(),
];
}

if ($this->reportMaybes) {
$report = false;

if (!$report && $this->reportMaybes) {
if ($type instanceof BenevolentUnionType) {
$flattenedTypes = [$type];
} else {
Expand All @@ -67,16 +81,17 @@ public function check(
}
}
}
}

if ($report) {
if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) {
return [];
if ($report) {
if ($this->bleedingEdge) {
return [
RuleErrorBuilder::message(sprintf('Offset %s might not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(),
];
}
return [
RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(),
];
}

return [
RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(),
];
}

return [];
Expand Down
11 changes: 8 additions & 3 deletions src/Rules/Comparison/BooleanAndConstantConditionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class BooleanAndConstantConditionRule implements Rule
public function __construct(
private ConstantConditionRuleHelper $helper,
private bool $treatPhpDocTypesAsCertain,
private bool $bleedingEdge,
)
{
}
Expand All @@ -36,6 +37,7 @@ public function processNode(
{
$errors = [];
$originalNode = $node->getOriginalNode();
$nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '&&';
$leftType = $this->helper->getBooleanType($scope, $originalNode->left);
$tipText = '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 ($leftType instanceof ConstantBooleanType) {
Expand All @@ -52,7 +54,8 @@ public function processNode(
return $ruleErrorBuilder->tip($tipText);
};
$errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf(
'Left side of && is always %s.',
'Left side of %s is always %s.',
$nodeText,
$leftType->getValue() ? 'true' : 'false',
)))->line($originalNode->left->getLine())->build();
}
Expand Down Expand Up @@ -81,7 +84,8 @@ public function processNode(

if (!$scope->isInFirstLevelStatement()) {
$errors[] = $addTipRight(RuleErrorBuilder::message(sprintf(
'Right side of && is always %s.',
'Right side of %s is always %s.',
$nodeText,
$rightType->getValue() ? 'true' : 'false',
)))->line($originalNode->right->getLine())->build();
}
Expand All @@ -105,7 +109,8 @@ public function processNode(

if (!$scope->isInFirstLevelStatement()) {
$errors[] = $addTip(RuleErrorBuilder::message(sprintf(
'Result of && is always %s.',
'Result of %s is always %s.',
$nodeText,
$nodeType->getValue() ? 'true' : 'false',
)))->build();
}
Expand Down
11 changes: 8 additions & 3 deletions src/Rules/Comparison/BooleanOrConstantConditionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class BooleanOrConstantConditionRule implements Rule
public function __construct(
private ConstantConditionRuleHelper $helper,
private bool $treatPhpDocTypesAsCertain,
private bool $bleedingEdge,
)
{
}
Expand All @@ -35,6 +36,7 @@ public function processNode(
): array
{
$originalNode = $node->getOriginalNode();
$nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '||';
$messages = [];
$leftType = $this->helper->getBooleanType($scope, $originalNode->left);
$tipText = '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%</>.';
Expand All @@ -52,7 +54,8 @@ public function processNode(
return $ruleErrorBuilder->tip($tipText);
};
$messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf(
'Left side of || is always %s.',
'Left side of %s is always %s.',
$nodeText,
$leftType->getValue() ? 'true' : 'false',
)))->line($originalNode->left->getLine())->build();
}
Expand Down Expand Up @@ -81,7 +84,8 @@ public function processNode(

if (!$scope->isInFirstLevelStatement()) {
$messages[] = $addTipRight(RuleErrorBuilder::message(sprintf(
'Right side of || is always %s.',
'Right side of %s is always %s.',
$nodeText,
$rightType->getValue() ? 'true' : 'false',
)))->line($originalNode->right->getLine())->build();
}
Expand All @@ -105,7 +109,8 @@ public function processNode(

if (!$scope->isInFirstLevelStatement()) {
$messages[] = $addTip(RuleErrorBuilder::message(sprintf(
'Result of || is always %s.',
'Result of %s is always %s.',
$nodeText,
$nodeType->getValue() ? 'true' : 'false',
)))->build();
}
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Levels/data/stringOffsetAccess-7.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[
{
"message": "Offset 'foo' does not exist on array|string.",
"message": "Offset 'foo' might not exist on array|string.",
"line": 27,
"ignorable": true
},
{
"message": "Offset 12.34 does not exist on array|string.",
"message": "Offset 12.34 might not exist on array|string.",
"line": 31,
"ignorable": true
},
Expand All @@ -14,4 +14,4 @@
"line": 35,
"ignorable": true
}
]
]
4 changes: 3 additions & 1 deletion tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
class ArrayDestructuringRuleTest extends RuleTestCase
{

private bool $bleedingEdge = false;

protected function getRule(): Rule
{
$ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false);

return new ArrayDestructuringRule(
$ruleLevelHelper,
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true),
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase

private bool $checkExplicitMixed = false;

private bool $bleedingEdge = false;

protected function getRule(): Rule
{
$ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false);

return new NonexistentOffsetInArrayDimFetchRule(
$ruleLevelHelper,
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true),
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge),
true,
);
}
Expand Down Expand Up @@ -170,6 +172,151 @@ public function testRule(): void
]);
}

public function testRuleBleedingEdge(): void
{
$this->bleedingEdge = true;
$this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [
[
'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.',
17,
],
[
'Offset 1 does not exist on array{a: stdClass, 0: 2}.',
18,
],
[
'Offset \'a\' does not exist on array{b: 1}.',
55,
],
[
'Access to offset \'bar\' on an unknown class NonexistentOffset\Bar.',
101,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Access to an offset on an unknown class NonexistentOffset\Bar.',
102,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Offset 0 does not exist on array<string, string>.',
111,
],
[
'Offset \'0\' does not exist on array<string, string>.',
112,
],
[
'Offset int does not exist on array<string, string>.',
114,
],
[
'Offset \'test\' does not exist on null.',
126,
],
[
'Cannot access offset 42 on int.',
142,
],
[
'Cannot access offset 42 on float.',
143,
],
[
'Cannot access offset 42 on bool.',
144,
],
[
'Cannot access offset 42 on resource.',
145,
],
[
'Offset \'c\' might not exist on array{c: false}|array{c: true}|array{e: true}.',
171,
],
[
'Offset int might not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.',
190,
],
[
'Offset int might not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.',
193,
],
[
'Offset \'b\' does not exist on array{a: \'blabla\'}.',
225,
],
[
'Offset \'b\' does not exist on array{a: \'blabla\'}.',
228,
],
[
'Offset string does not exist on array<int, mixed>.',
240,
],
[
'Cannot access offset \'a\' on Closure(): void.',
253,
],
[
'Offset string does not exist on array<int, string>.',
308,
],
[
'Offset null does not exist on array<int, string>.',
310,
],
[
'Offset int does not exist on array<string, string>.',
312,
],
[
'Offset \'baz\' might not exist on array{bar: 1, baz?: 2}.',
344,
],
[
'Offset \'foo\' does not exist on ArrayAccess<int, stdClass>.',
411,
],
[
'Cannot access offset \'foo\' on stdClass.',
423,
],
[
'Cannot access offset \'foo\' on true.',
426,
],
[
'Cannot access offset \'foo\' on false.',
429,
],
[
'Cannot access offset \'foo\' on resource.',
433,
],
[
'Cannot access offset \'foo\' on 42.',
436,
],
[
'Cannot access offset \'foo\' on 4.141.',
439,
],
[
'Cannot access offset \'foo\' on array|int.',
443,
],
[
'Offset \'feature_pretty…\' might not exist on array{version: non-falsy-string, commit: string|null, pretty_version: string|null, feature_version: non-falsy-string, feature_pretty_version?: string|null}.',
504,
],
[
"Cannot access offset 'foo' on bool.",
517,
],
]);
}

public function testStrings(): void
{
$this->analyse([__DIR__ . '/data/strings-offset-access.php'], [
Expand Down
Loading