Skip to content

Commit

Permalink
additional magic for preg_match, fixes #46
Browse files Browse the repository at this point in the history
  • Loading branch information
shish committed Feb 9, 2025
1 parent c55cfa5 commit a38d64f
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 0 deletions.
4 changes: 4 additions & 0 deletions phpstan-safe-rule.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ services:
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchParameterOutTypeExtension
tags:
- phpstan.functionParameterOutTypeExtension
-
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
85 changes: 85 additions & 0 deletions src/Type/Php/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace TheCodingMachine\Safe\PHPStan\Type\Php;

use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use function in_array;
use function strtolower;

final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function __construct(
private RegexArrayShapeMatcher $regexShapeMatcher,
)
{
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return in_array(strtolower($functionReflection->getName()), ['safe\preg_match', 'safe\preg_match_all'], true) && !$context->null();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$args = $node->getArgs();
$patternArg = $args[0] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;

if (
$patternArg === null || $matchesArg === null
) {
return new SpecifiedTypes();
}

$flagsType = null;
if ($flagsArg !== null) {
$flagsType = $scope->getType($flagsArg->value);
}

if ($functionReflection->getName() === 'Safe\preg_match') {
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
} else {
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
}
if ($matchedType === null) {
return new SpecifiedTypes();
}

$overwrite = false;
if ($context->false()) {
$overwrite = true;
$context = $context->negate();
}

$types = $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
$scope,
)->setRootExpr($node);
if ($overwrite) {
$types = $types->setAlwaysOverwriteTypes();
}

return $types;
}

}
16 changes: 16 additions & 0 deletions tests/Type/Php/data/preg.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Checking that preg_match and Safe\preg_match are equivalent
$pattern = '/H(.)ll(o) (World)?/';
$string = 'Hello World';

// when return value isn't checked, we may-or-may-not have matches
$type = "array{0?: string, 1?: non-empty-string, 2?: 'o', 3?: 'World'}";

// @phpstan-ignore-next-line - use of unsafe is intentional
Expand All @@ -13,3 +15,17 @@

\Safe\preg_match($pattern, $string, $matches);
\PHPStan\Testing\assertType($type, $matches);


// when the return value is checked, we should have matches,
// unless the match-group itself is optional
$type = "array{0: string, 1: non-empty-string, 2: 'o', 3?: 'World'}";

// @phpstan-ignore-next-line - use of unsafe is intentional
if(\preg_match($pattern, $string, $matches)) {
\PHPStan\Testing\assertType($type, $matches);
}

if(\Safe\preg_match($pattern, $string, $matches)) {
\PHPStan\Testing\assertType($type, $matches);
}

0 comments on commit a38d64f

Please sign in to comment.