diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 76dd0d8904..9a945a7c63 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -5,3 +5,4 @@ parameters: skipCheckGenericClasses!: [] stricterFunctionMap: true reportPreciseLineForUnusedFunctionParameter: true + reportPossiblyNonexistentStringOffset: true diff --git a/conf/config.neon b/conf/config.neon index d77e889531..54277ed99e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -26,6 +26,7 @@ parameters: skipCheckGenericClasses: [] stricterFunctionMap: false reportPreciseLineForUnusedFunctionParameter: false + reportPossiblyNonexistentStringOffset: false fileExtensions: - php checkAdvancedIsset: false @@ -837,6 +838,7 @@ services: reportMaybes: %reportMaybes% reportPossiblyNonexistentGeneralArrayOffset: %reportPossiblyNonexistentGeneralArrayOffset% reportPossiblyNonexistentConstantArrayOffset: %reportPossiblyNonexistentConstantArrayOffset% + reportPossiblyNonexistentStringOffset: %featureToggles.reportPossiblyNonexistentStringOffset% - class: PHPStan\Rules\ClassNameCheck diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index f7328f7863..facabc44b9 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -32,6 +32,7 @@ parametersSchema: skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() reportPreciseLineForUnusedFunctionParameter: bool() + reportPossiblyNonexistentStringOffset: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index 5b81f82f1a..7b15273295 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -10,6 +10,7 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; @@ -25,6 +26,7 @@ public function __construct( private bool $reportMaybes, private bool $reportPossiblyNonexistentGeneralArrayOffset, private bool $reportPossiblyNonexistentConstantArrayOffset, + private bool $reportPossiblyNonexistentStringOffset, ) { } @@ -65,6 +67,16 @@ public function check( if ($this->reportMaybes) { $report = false; + $zeroOrMore = IntegerRangeType::fromInterval(0, null); + if ( + $this->reportPossiblyNonexistentStringOffset + && $type->isString()->yes() + && $dimType instanceof IntegerRangeType + && !$zeroOrMore->isSuperTypeOf($dimType)->yes() + ) { + $report = true; + } + if ($type instanceof BenevolentUnionType) { $flattenedTypes = [$type]; } else { diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 4605c92efe..64e5ade4e4 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -63,7 +63,8 @@ public function isOffsetAccessLegal(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + $zeroOrMore = IntegerRangeType::fromInterval(0, null); + return $zeroOrMore->isSuperTypeOf($offsetType)->result->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index d37f09084b..8e2696da10 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -19,7 +19,7 @@ protected function getRule(): Rule return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, false, false, false), ); } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index e212c65218..a57adc7da4 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -21,13 +21,15 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $reportPossiblyNonexistentConstantArrayOffset = false; + private bool $reportPossiblyNonexistentStringOffset = false; + protected function getRule(): Rule { $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset, $this->reportPossiblyNonexistentStringOffset), true, ); } @@ -780,4 +782,60 @@ public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid-php84.php'], []); } + public function testBug11946(): void + { + $this->reportPossiblyNonexistentStringOffset = true; + + $this->analyse([__DIR__ . '/data/bug-11946.php'], [ + [ + 'Offset -1 does not exist on string.', + 21, + ], + [ + 'Offset -1 does not exist on numeric-string.', + 22, + ], + [ + 'Offset -1 does not exist on non-empty-string.', + 23, + ], + [ + 'Offset -1 does not exist on non-falsy-string.', + 24, + ], + [ + 'Offset -1 does not exist on string.', + 25, + ], + [ + "Offset 10 does not exist on 'hi'.", + 29, + ], + [ + 'Offset int<-5, 5> might not exist on string.', + 49, + ], + [ + 'Offset int<-5, 5> might not exist on numeric-string.', + 50, + ], + [ + 'Offset int<-5, 5> might not exist on non-empty-string.', + 51, + ], + [ + 'Offset int<-5, 5> might not exist on non-falsy-string.', + 52, + ], + [ + 'Offset int<-5, 5> might not exist on string.', + 53, + ], + [ + "Offset int<-5, 5> might not exist on 'hia'.", + 56, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11946.php b/tests/PHPStan/Rules/Arrays/data/bug-11946.php new file mode 100644 index 0000000000..e0479377d2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11946.php @@ -0,0 +1,61 @@ + $maybeWrong + */ + public function maybeNonExistentStringOffset( + string $s, + string $numericS, + string $nonEmpty, + string $nonFalsy, + string $lowerCase, + int $maybeWrong, int $oneToTwo + ) + { + echo $s[$maybeWrong]; + echo $numericS[$maybeWrong]; + echo $nonEmpty[$maybeWrong]; + echo $nonFalsy[$maybeWrong]; + echo $lowerCase[$maybeWrong]; + + $s = 'hia'; + echo $s[$maybeWrong]; + if ($maybeWrong >= 1 && $maybeWrong < 3) { + echo $s[$maybeWrong]; + } + } +}