Skip to content

Support string assertions resulting in non-empty-string #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 21, 2022
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,26 @@ This extension specifies types of values passed to:
* `Assert::methodExists`
* `Assert::propertyExists`
* `Assert::isArrayAccessible`
* `Assert::contains`
* `Assert::startsWith`
* `Assert::startsWithLetter`
* `Assert::endsWith`
* `Assert::unicodeLetters`
* `Assert::alpha`
* `Assert::digits`
* `Assert::alnum`
* `Assert::lower`
* `Assert::upper`
* `Assert::length`
* `Assert::minLength`
* `Assert::maxLength`
* `Assert::lengthBetween`
* `Assert::uuid`
* `Assert::ip`
* `Assert::ipv4`
* `Assert::ipv6`
* `Assert::email`
* `Assert::notWhitespaceOnly`
* `nullOr*`, `all*` and `allNullOr*` variants of the above methods


Expand Down
133 changes: 111 additions & 22 deletions src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IterableType;
use PHPStan\Type\MixedType;
Expand All @@ -58,6 +59,7 @@
use function array_reduce;
use function array_shift;
use function count;
use function is_array;
use function lcfirst;
use function substr;

Expand Down Expand Up @@ -104,7 +106,7 @@ public function isStaticMethodSupported(
}

$resolver = $resolvers[$trimmedName];
$resolverReflection = new ReflectionObject($resolver);
$resolverReflection = new ReflectionObject(Closure::fromCallable($resolver));

return count($node->getArgs()) >= count($resolverReflection->getMethod('__invoke')->getParameters()) - 1;
}
Expand Down Expand Up @@ -156,50 +158,62 @@ static function (Type $type) {
);
}

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

return $this->typeSpecifier->specifyTypesInCondition(
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
$scope,
$expression,
TypeSpecifierContext::createTruthy()
$expr,
TypeSpecifierContext::createTruthy(),
$rootExpr
);

return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes);
}

/**
* @param Arg[] $args
* @return array{?Expr, ?Expr}
*/
private static function createExpression(
Scope $scope,
string $name,
array $args
): ?Expr
): array
{
$trimmedName = self::trimName($name);
$resolvers = self::getExpressionResolvers();
$resolver = $resolvers[$trimmedName];
$expression = $resolver($scope, ...$args);
if ($expression === null) {
return null;

$resolverResult = $resolver($scope, ...$args);
if (is_array($resolverResult)) {
[$expr, $rootExpr] = $resolverResult;
} else {
$expr = $resolverResult;
$rootExpr = null;
}

if ($expr === null) {
return [null, null];
}

if (substr($name, 0, 6) === 'nullOr') {
$expression = new BooleanOr(
$expression,
$expr = new BooleanOr(
$expr,
new Identical(
$args[0]->value,
new ConstFetch(new Name('null'))
)
);
}

return $expression;
return [$expr, $rootExpr];
}

/**
* @return Closure[]
* @return array<string, callable(Scope, Arg...): (Expr|array{?Expr, ?Expr}|null)>
*/
private static function getExpressionResolvers(): array
{
Expand Down Expand Up @@ -723,6 +737,38 @@ private static function getExpressionResolvers(): array
);
},
];

foreach (['contains', 'startsWith', 'endsWith'] as $name) {
self::$resolvers[$name] = static function (Scope $scope, Arg $value, Arg $subString) use ($name): array {
if ($scope->getType($subString->value)->isNonEmptyString()->yes()) {
return self::createIsNonEmptyStringAndSomethingExprPair($name, [$value, $subString]);
}

return [self::$resolvers['string']($scope, $value), null];
};
}

$assertionsResultingAtLeastInNonEmptyString = [
'startsWithLetter',
'unicodeLetters',
'alpha',
'digits',
'alnum',
'lower',
'upper',
'uuid',
'ip',
'ipv4',
'ipv6',
'email',
'notWhitespaceOnly',
];
foreach ($assertionsResultingAtLeastInNonEmptyString as $name) {
self::$resolvers[$name] = static function (Scope $scope, Arg $value) use ($name): array {
return self::createIsNonEmptyStringAndSomethingExprPair($name, [$value]);
};
}

}

return self::$resolvers;
Expand Down Expand Up @@ -790,15 +836,16 @@ private function handleAll(
{
$args = $node->getArgs();
$args[0] = new Arg(new ArrayDimFetch($args[0]->value, new LNumber(0)));
$expression = self::createExpression($scope, $methodName, $args);
if ($expression === null) {
[$expr, $rootExpr] = self::createExpression($scope, $methodName, $args);
if ($expr === null) {
return new SpecifiedTypes();
}

$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition(
$scope,
$expression,
TypeSpecifierContext::createTruthy()
$expr,
TypeSpecifierContext::createTruthy(),
$rootExpr
);

$sureNotTypes = $specifiedTypes->getSureNotTypes();
Expand All @@ -817,7 +864,8 @@ private function handleAll(
$node->getArgs()[0]->value,
static function () use ($type): Type {
return $type;
}
},
$rootExpr
);
}

Expand All @@ -827,7 +875,8 @@ static function () use ($type): Type {
private function arrayOrIterable(
Scope $scope,
Expr $expr,
Closure $typeCallback
Closure $typeCallback,
?Expr $rootExpr = null
): SpecifiedTypes
{
$currentType = TypeCombinator::intersect($scope->getType($expr), new IterableType(new MixedType(), new MixedType()));
Expand All @@ -854,13 +903,16 @@ private function arrayOrIterable(
return new SpecifiedTypes([], []);
}

return $this->typeSpecifier->create(
$specifiedTypes = $this->typeSpecifier->create(
$expr,
$specifiedType,
TypeSpecifierContext::createTruthy(),
false,
$scope
$scope,
$rootExpr
);

return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes);
}

/**
Expand Down Expand Up @@ -900,4 +952,41 @@ static function (?ArrayItem $item) use ($scope, $value, $resolver) {
return self::implodeExpr($resolvers, BooleanOr::class);
}

/**
* @param Arg[] $args
* @return array{Expr, Expr}
*/
private static function createIsNonEmptyStringAndSomethingExprPair(string $name, array $args): array
{
$expr = new BooleanAnd(
new FuncCall(
new Name('is_string'),
[$args[0]]
),
new NotIdentical(
$args[0]->value,
new String_('')
)
);

$rootExpr = new BooleanAnd(
$expr,
new FuncCall(new Name('FAUX_FUNCTION_ ' . $name), $args)
);

return [$expr, $rootExpr];
}

private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specifiedTypes): SpecifiedTypes
{
if ($rootExpr === null) {
return $specifiedTypes;
}

// Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true
return $specifiedTypes->unionWith(
$this->typeSpecifier->create($rootExpr, new ConstantBooleanType(true), TypeSpecifierContext::createTruthy())
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ public function testExtension(): void
'Call to static method Webmozart\Assert\Assert::allCount() with array<non-empty-array> and 2 will always evaluate to true.',
76,
],
[
'Call to static method Webmozart\Assert\Assert::uuid() with non-empty-string will always evaluate to true.',
84,
],
[
'Call to static method Webmozart\Assert\Assert::contains() with non-empty-string and \'foo\' will always evaluate to true.',
88,
],
[
'Call to static method Webmozart\Assert\Assert::allUuid() with array<non-empty-string> will always evaluate to true.',
94,
],
[
'Call to static method Webmozart\Assert\Assert::allContains() with array<non-empty-string> and \'foo\' will always evaluate to true.',
98,
],
]);
}

Expand Down
24 changes: 24 additions & 0 deletions tests/Type/WebMozartAssert/data/collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ public function allStringNotEmpty(array $a, iterable $b, $c): void
assertType('iterable<non-empty-string>', $c);
}

public function allContains(array $a, iterable $b, $c): void
{
Assert::allContains($a, 'foo');
assertType('array<non-empty-string>', $a);

Assert::allContains($b, 'foo');
assertType('iterable<non-empty-string>', $b);

Assert::allContains($c, 'foo');
assertType('iterable<non-empty-string>', $c);
}

public function allNullOrContains(array $a, iterable $b, $c): void
{
Assert::allNullOrContains($a, 'foo');
assertType('array<non-empty-string|null>', $a);

Assert::allNullOrContains($b, 'foo');
assertType('iterable<non-empty-string|null>', $b);

Assert::allNullOrContains($c, 'foo');
assertType('iterable<non-empty-string|null>', $c);
}

public function allInteger(array $a, iterable $b, $c): void
{
Assert::allInteger($a);
Expand Down
23 changes: 23 additions & 0 deletions tests/Type/WebMozartAssert/data/impossible-check.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ public function allCount(array $a): void
Assert::allCount($a, 2);
}

public function nonEmptyStringAndSomethingUnknownNarrow($a, string $b, array $c, array $d): void
{
Assert::string($a);
Assert::stringNotEmpty($a);
Assert::uuid($a);
Assert::uuid($a); // only this should report

Assert::stringNotEmpty($b);
Assert::contains($b, 'foo');
Assert::contains($b, 'foo'); // only this should report
Assert::contains($b, 'bar');

Assert::allString($c);
Assert::allStringNotEmpty($c);
Assert::allUuid($c);
Assert::allUuid($c); // only this should report

Assert::allStringNotEmpty($d);
Assert::allContains($d, 'foo');
Assert::allContains($d, 'foo'); // only this should report
Assert::allContains($d, 'bar');
}

}

interface Bar {};
Expand Down
Loading