diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index 9b35420..ce490f7 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -754,18 +754,17 @@ private function getExpressionResolvers(): array ) ); }, + 'contains' => static function (Scope $scope, Arg $value, Arg $subString): array { + return self::createContainsResolver($scope, $value, $subString); + }, + 'startsWith' => static function (Scope $scope, Arg $value, Arg $subString): array { + return self::createStartsWithResolver($scope, $value, $subString); + }, + 'endsWith' => static function (Scope $scope, Arg $value, Arg $subString): array { + return self::createEndsWithResolver($scope, $value, $subString); + }, ]; - foreach (['contains', 'startsWith', 'endsWith'] as $name) { - $this->resolvers[$name] = function (Scope $scope, Arg $value, Arg $subString) use ($name): array { - if ($scope->getType($subString->value)->isNonEmptyString()->yes()) { - return self::createIsNonEmptyStringAndSomethingExprPair($name, [$value, $subString]); - } - - return [$this->resolvers['string']($scope, $value), null]; - }; - } - $assertionsResultingAtLeastInNonEmptyString = [ 'startsWithLetter', 'unicodeLetters', @@ -1022,4 +1021,138 @@ private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specified ); } + /** + * @return array{Expr, Expr} + */ + private static function createContainsResolver(Scope $scope, Arg $value, Arg $subString): array + { + $stringExpr = $scope->getType($subString->value)->isNonEmptyString()->yes() + ? new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value] + ), + new NotIdentical( + $value->value, + new String_('') + ) + ) + : new FuncCall( + new Name('is_string'), + [$value] + ); + + $expr = new BooleanOr( + $stringExpr, + new BooleanAnd( + $stringExpr, + new GreaterOrEqual( + new FuncCall( + new Name('strpos'), + [$value] + ), + new LNumber(0) + ) + ) + ); + + $rootExpr = new BooleanAnd( + $expr, + new FuncCall(new Name('FAUX_FUNCTION_ contains'), [$value, $subString]) + ); + + return [$expr, $rootExpr]; + } + + /** + * @return array{Expr, Expr} + */ + private static function createStartsWithResolver(Scope $scope, Arg $string, Arg $subString): array + { + $stringExpr = $scope->getType($subString->value)->isNonEmptyString()->yes() + ? new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$string] + ), + new NotIdentical( + $string->value, + new String_('') + ) + ) + : new FuncCall( + new Name('is_string'), + [$string] + ); + + $expr = new BooleanOr( + $stringExpr, + new Identical( + new FuncCall( + new Name('strpos'), + [$string, $subString] + ), + new LNumber(0) + ) + ); + + $rootExpr = new BooleanAnd( + $expr, + new FuncCall(new Name('FAUX_FUNCTION_ startsWith'), [$string, $subString]) + ); + + return [$expr, $rootExpr]; + } + + /** + * @return array{Expr, Expr} + */ + private static function createEndsWithResolver(Scope $scope, Arg $value, Arg $subString): array + { + if ($scope->getType($subString->value)->isNonEmptyString()->yes()) { + $stringExpr = new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value] + ), + new NotIdentical( + $value->value, + new String_('') + ) + ); + } else { + $stringExpr = new FuncCall( + new Name('is_string'), + [$value] + ); + } + + $expr = new BooleanOr( + $stringExpr, + new Identical( + new FuncCall( + new Name('strpos'), + [$value, $subString] + ), + new BinaryOp\Minus( + new FuncCall( + new Name('strlen'), + [$value] + ), + new FuncCall( + new Name('strlen'), + [$subString] + ) + ) + ) + ); + + $rootExpr = new BooleanAnd( + $expr, + new FuncCall(new Name('FAUX_FUNCTION_ endsWith'), [$value, $subString]) + ); + + return [$expr, $rootExpr]; + } + } diff --git a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php index 95b1dfc..aa860de 100644 --- a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php @@ -105,6 +105,10 @@ public function testExtension(): void 'Call to static method Webmozart\Assert\Assert::isInstanceOf() with Exception and class-string will always evaluate to true.', 119, ], + [ + 'Call to static method Webmozart\Assert\Assert::startsWith() with \'value\' and string will always evaluate to true.', + 126, + ], ]); } diff --git a/tests/Type/WebMozartAssert/data/impossible-check.php b/tests/Type/WebMozartAssert/data/impossible-check.php index 271efc3..0e00dcc 100644 --- a/tests/Type/WebMozartAssert/data/impossible-check.php +++ b/tests/Type/WebMozartAssert/data/impossible-check.php @@ -119,6 +119,14 @@ public function testInstanceOfClassString(\Exception $e, string $name): void Assert::isInstanceOf($e, $name); } + public function testStartsWith(string $a): void + { + Assert::startsWith("value", "val"); + Assert::startsWith("value", $a); + Assert::startsWith("value", $a); + Assert::startsWith("value", "bix"); + } + } interface Bar {}; diff --git a/tests/Type/WebMozartAssert/data/string.php b/tests/Type/WebMozartAssert/data/string.php index 7ad6007..48ce019 100644 --- a/tests/Type/WebMozartAssert/data/string.php +++ b/tests/Type/WebMozartAssert/data/string.php @@ -29,7 +29,7 @@ public function startsWith(string $a, string $b): void assertType('string', $a); Assert::startsWith($a, $b); - assertType('non-empty-string', $a); + assertType('string', $a); } public function startsWithLetter(string $a): void @@ -47,7 +47,7 @@ public function endsWith(string $a, string $b): void assertType('string', $a); Assert::endsWith($a, $b); - assertType('non-empty-string', $a); + assertType('string', $a); } public function unicodeLetters($a): void