Skip to content

Commit 1c6a7b8

Browse files
committed
Support string assertions resulting in non-empty-string
1 parent 64c0042 commit 1c6a7b8

File tree

5 files changed

+258
-20
lines changed

5 files changed

+258
-20
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,26 @@ This extension specifies types of values passed to:
8585
* `Assert::methodExists`
8686
* `Assert::propertyExists`
8787
* `Assert::isArrayAccessible`
88+
* `Assert::contains`
89+
* `Assert::startsWith`
90+
* `Assert::startsWithLetter`
91+
* `Assert::endsWith`
92+
* `Assert::unicodeLetters`
93+
* `Assert::alpha`
94+
* `Assert::digits`
95+
* `Assert::alnum`
96+
* `Assert::lower`
97+
* `Assert::upper`
8898
* `Assert::length`
8999
* `Assert::minLength`
90100
* `Assert::maxLength`
91101
* `Assert::lengthBetween`
102+
* `Assert::uuid`
103+
* `Assert::ip`
104+
* `Assert::ipv4`
105+
* `Assert::ipv6`
106+
* `Assert::email`
107+
* `Assert::notWhitespaceOnly`
92108
* `nullOr*`, `all*` and `allNullOr*` variants of the above methods
93109

94110

src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php

+94-20
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use function array_reduce;
5959
use function array_shift;
6060
use function count;
61+
use function is_array;
6162
use function lcfirst;
6263
use function substr;
6364

@@ -104,7 +105,7 @@ public function isStaticMethodSupported(
104105
}
105106

106107
$resolver = $resolvers[$trimmedName];
107-
$resolverReflection = new ReflectionObject($resolver);
108+
$resolverReflection = new ReflectionObject(Closure::fromCallable($resolver));
108109

109110
return count($node->getArgs()) >= count($resolverReflection->getMethod('__invoke')->getParameters()) - 1;
110111
}
@@ -156,50 +157,60 @@ static function (Type $type) {
156157
);
157158
}
158159

159-
$expression = self::createExpression($scope, $staticMethodReflection->getName(), $node->getArgs());
160-
if ($expression === null) {
160+
[$expr, $rootExpr] = self::createExpression($scope, $staticMethodReflection->getName(), $node->getArgs());
161+
if ($expr === null) {
161162
return new SpecifiedTypes([], []);
162163
}
163164

164165
return $this->typeSpecifier->specifyTypesInCondition(
165166
$scope,
166-
$expression,
167-
TypeSpecifierContext::createTruthy()
167+
$expr,
168+
TypeSpecifierContext::createTruthy(),
169+
$rootExpr
168170
);
169171
}
170172

171173
/**
172174
* @param Arg[] $args
175+
* @return array{?Expr, ?Expr}
173176
*/
174177
private static function createExpression(
175178
Scope $scope,
176179
string $name,
177180
array $args
178-
): ?Expr
181+
): array
179182
{
180183
$trimmedName = self::trimName($name);
181184
$resolvers = self::getExpressionResolvers();
182185
$resolver = $resolvers[$trimmedName];
183-
$expression = $resolver($scope, ...$args);
184-
if ($expression === null) {
185-
return null;
186+
187+
$resolverResult = $resolver($scope, ...$args);
188+
if (is_array($resolverResult)) {
189+
[$expr, $rootExpr] = $resolverResult;
190+
} else {
191+
$expr = $resolverResult;
192+
$rootExpr = null;
193+
}
194+
195+
if ($expr === null) {
196+
return [null, null];
186197
}
187198

188199
if (substr($name, 0, 6) === 'nullOr') {
189-
$expression = new BooleanOr(
190-
$expression,
200+
$expr = new BooleanOr(
201+
$expr,
191202
new Identical(
192203
$args[0]->value,
193204
new ConstFetch(new Name('null'))
194205
)
195206
);
196207
}
197208

198-
return $expression;
209+
return [$expr, $rootExpr];
199210
}
200211

201212
/**
202-
* @return Closure[]
213+
* @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
203214
*/
204215
private static function getExpressionResolvers(): array
205216
{
@@ -723,6 +734,38 @@ private static function getExpressionResolvers(): array
723734
);
724735
},
725736
];
737+
738+
foreach (['contains', 'startsWith', 'endsWith'] as $name) {
739+
self::$resolvers[$name] = static function (Scope $scope, Arg $value, Arg $subString): array {
740+
if ($scope->getType($subString->value)->isNonEmptyString()->yes()) {
741+
return self::createIsNonEmptyStringAndSomethingExprPair([$value]);
742+
}
743+
744+
return [self::$resolvers['string']($scope, $value), null];
745+
};
746+
}
747+
748+
$assertionsResultingAtLeastInNonEmptyString = [
749+
'startsWithLetter',
750+
'unicodeLetters',
751+
'alpha',
752+
'digits',
753+
'alnum',
754+
'lower',
755+
'upper',
756+
'uuid',
757+
'ip',
758+
'ipv4',
759+
'ipv6',
760+
'email',
761+
'notWhitespaceOnly',
762+
];
763+
foreach ($assertionsResultingAtLeastInNonEmptyString as $name) {
764+
self::$resolvers[$name] = static function (Scope $scope, Arg $value): array {
765+
return self::createIsNonEmptyStringAndSomethingExprPair([$value]);
766+
};
767+
}
768+
726769
}
727770

728771
return self::$resolvers;
@@ -790,15 +833,16 @@ private function handleAll(
790833
{
791834
$args = $node->getArgs();
792835
$args[0] = new Arg(new ArrayDimFetch($args[0]->value, new LNumber(0)));
793-
$expression = self::createExpression($scope, $methodName, $args);
794-
if ($expression === null) {
836+
[$expr, $rootExpr] = self::createExpression($scope, $methodName, $args);
837+
if ($expr === null) {
795838
return new SpecifiedTypes();
796839
}
797840

798841
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
799842
$scope,
800-
$expression,
801-
TypeSpecifierContext::createTruthy()
843+
$expr,
844+
TypeSpecifierContext::createTruthy(),
845+
$rootExpr
802846
);
803847

804848
$sureNotTypes = $specifiedTypes->getSureNotTypes();
@@ -817,7 +861,8 @@ private function handleAll(
817861
$node->getArgs()[0]->value,
818862
static function () use ($type): Type {
819863
return $type;
820-
}
864+
},
865+
$rootExpr
821866
);
822867
}
823868

@@ -827,7 +872,8 @@ static function () use ($type): Type {
827872
private function arrayOrIterable(
828873
Scope $scope,
829874
Expr $expr,
830-
Closure $typeCallback
875+
Closure $typeCallback,
876+
?Expr $rootExpr = null
831877
): SpecifiedTypes
832878
{
833879
$currentType = TypeCombinator::intersect($scope->getType($expr), new IterableType(new MixedType(), new MixedType()));
@@ -859,7 +905,8 @@ private function arrayOrIterable(
859905
$specifiedType,
860906
TypeSpecifierContext::createTruthy(),
861907
false,
862-
$scope
908+
$scope,
909+
$rootExpr
863910
);
864911
}
865912

@@ -900,4 +947,31 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
900947
return self::implodeExpr($resolvers, BooleanOr::class);
901948
}
902949

950+
/**
951+
* @param Arg[] $args
952+
* @return array{Expr, Expr}
953+
*/
954+
private static function createIsNonEmptyStringAndSomethingExprPair(array $args): array
955+
{
956+
$expr = new BooleanAnd(
957+
new FuncCall(
958+
new Name('is_string'),
959+
[$args[0]]
960+
),
961+
new NotIdentical(
962+
$args[0]->value,
963+
new String_('')
964+
)
965+
);
966+
967+
$rootExpr = new BooleanAnd(
968+
$expr,
969+
new FuncCall(new Name('FAUX_FUNCTION'), [
970+
new Arg($args[0]->value),
971+
])
972+
);
973+
974+
return [$expr, $rootExpr];
975+
}
976+
903977
}

tests/Type/WebMozartAssert/data/collection.php

+24
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ public function allStringNotEmpty(array $a, iterable $b, $c): void
3030
assertType('iterable<non-empty-string>', $c);
3131
}
3232

33+
public function allContains(array $a, iterable $b, $c): void
34+
{
35+
Assert::allContains($a, 'foo');
36+
assertType('array<non-empty-string>', $a);
37+
38+
Assert::allContains($b, 'foo');
39+
assertType('iterable<non-empty-string>', $b);
40+
41+
Assert::allContains($c, 'foo');
42+
assertType('iterable<non-empty-string>', $c);
43+
}
44+
45+
public function allNullOrContains(array $a, iterable $b, $c): void
46+
{
47+
Assert::allNullOrContains($a, 'foo');
48+
assertType('array<non-empty-string|null>', $a);
49+
50+
Assert::allNullOrContains($b, 'foo');
51+
assertType('iterable<non-empty-string|null>', $b);
52+
53+
Assert::allNullOrContains($c, 'foo');
54+
assertType('iterable<non-empty-string|null>', $c);
55+
}
56+
3357
public function allInteger(array $a, iterable $b, $c): void
3458
{
3559
Assert::allInteger($a);

tests/Type/WebMozartAssert/data/impossible-check.php

+10
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ public function allCount(array $a): void
7676
Assert::allCount($a, 2);
7777
}
7878

79+
public function nonEmptyStringAndSomethingUnknownNarrow($a, string $b): void
80+
{
81+
Assert::string($a);
82+
Assert::stringNotEmpty($a);
83+
Assert::uuid($a);
84+
85+
Assert::stringNotEmpty($b);
86+
Assert::contains($b, 'foo');
87+
}
88+
7989
}
8090

8191
interface Bar {};

0 commit comments

Comments
 (0)