Skip to content

Commit

Permalink
Implement array shapes for preg_match() $matches by-ref parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored Jun 21, 2024
1 parent a8ededf commit 721a0a6
Show file tree
Hide file tree
Showing 26 changed files with 807 additions and 26 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
"composer/ca-bundle": [
"patches/cloudflare-ca.patch"
],
"hoa/regex": [
"patches/Grammar.patch"
],
"hoa/iterator": [
"patches/Buffer.patch",
"patches/Lookahead.patch"
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ parameters:
paramOutType: true
pure: true
checkParameterCastableToStringFunctions: true
narrowPregMatches: true
stubFiles:
- ../stubs/bleedingEdge/Rule.stub
14 changes: 14 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ parameters:
paramOutType: false
pure: false
checkParameterCastableToStringFunctions: false
narrowPregMatches: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -284,6 +285,10 @@ conditionalTags:
phpstan.parser.richParserNodeVisitor: %featureToggles.curlSetOptTypes%
PHPStan\Parser\TypeTraverserInstanceofVisitor:
phpstan.parser.richParserNodeVisitor: %featureToggles.instanceofType%
PHPStan\Type\Php\PregMatchTypeSpecifyingExtension:
phpstan.typeSpecifier.functionTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
PHPStan\Type\Php\PregMatchParameterOutTypeExtension:
phpstan.functionParameterOutTypeExtension: %featureToggles.narrowPregMatches%

services:
-
Expand Down Expand Up @@ -1465,6 +1470,15 @@ services:
tags:
- phpstan.dynamicFunctionThrowTypeExtension

-
class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension

-
class: PHPStan\Type\Php\PregMatchParameterOutTypeExtension

-
class: PHPStan\Type\Php\RegexArrayShapeMatcher

-
class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension
tags:
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ parametersSchema:
paramOutType: bool()
pure: bool()
checkParameterCastableToStringFunctions: bool()
narrowPregMatches: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
37 changes: 37 additions & 0 deletions patches/Grammar.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--- Grammar.pp 2024-05-18 12:15:53
+++ Grammar.pp.fix 2024-05-18 12:15:05
@@ -109,7 +109,7 @@
// Please, see PCRESYNTAX(3), General Category properties, PCRE special category
// properties and script names for \p{} and \P{}.
%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+})
-%token anchor \\(bBAZzG)|\^|\$
+%token anchor \\([bBAZzG])|\^|\$
%token match_point_reset \\K
%token literal \\.|.

@@ -168,7 +168,7 @@
::negative_class_:: #negativeclass
| ::class_::
)
- ( range() | literal() )+
+ ( <class_> | range() | literal() )+
::_class::

#range:
@@ -178,7 +178,7 @@
capturing()
| literal()

-capturing:
+#capturing:
::comment_:: <comment>? ::_comment:: #comment
| (
::named_capturing_:: <capturing_name> ::_named_capturing:: #namedcapturing
@@ -191,6 +191,7 @@

literal:
<character>
+ | <range>
| <dynamic_character>
| <character_type>
| <anchor>
16 changes: 16 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,22 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
$unwrappedRightExpr = $rightExpr->getExpr();
}
$rightType = $scope->getType($rightExpr);

if (
$context->true()
&& $unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
&& (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
) {
return $this->specifyTypesInCondition(
$scope,
$leftExpr,
$context,
$rootExpr,
);
}

if (
$context->true()
&& $unwrappedLeftExpr instanceof FuncCall
Expand Down
8 changes: 8 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,12 @@ public function supportsNeverReturnTypeInArrowFunction(): bool
return $this->versionId >= 80200;
}

// see https://www.php.net/manual/en/migration74.incompatible.php#migration74.incompatible.pcre
public function returnsPregUnmatchedCapturingGroups(): bool
{
// When PREG_UNMATCHED_AS_NULL mode is used, trailing unmatched capturing groups will now also be set to null (or [null, -1] if offset capture is enabled).
// This means that the size of the $matches will always be the same.
return $this->versionId >= 70400;
}

}
51 changes: 51 additions & 0 deletions src/Type/Php/PregMatchParameterOutTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\FunctionParameterOutTypeExtension;
use PHPStan\Type\Type;
use function in_array;
use function strtolower;

final class PregMatchParameterOutTypeExtension implements FunctionParameterOutTypeExtension
{

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

public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
{
return in_array(strtolower($functionReflection->getName()), ['preg_match'], true) && $parameter->getName() === 'matches';
}

public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $funcCall->getArgs();
$patternArg = $args[0] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;

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

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

return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe());
}

}
78 changes: 78 additions & 0 deletions src/Type/Php/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

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()), ['preg_match'], 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();
}

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

$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
if ($matchedType === null) {
return new SpecifiedTypes();
}

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

return $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
$overwrite,
$scope,
$node,
);
}

}
Loading

0 comments on commit 721a0a6

Please sign in to comment.