Skip to content
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

Implement array shapes for preg_match() $matches by-ref parameter #2589

Merged
merged 55 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8a6bc34
Implement array shapes for preg_match()
staabm Aug 13, 2023
633a661
added tests
staabm May 15, 2024
d922a1b
fix regex pattern
staabm May 15, 2024
1775cce
fix group name
staabm May 15, 2024
32b3a9c
Fix PHP < 7.4
staabm May 16, 2024
a15d5bd
fix tests per php version
staabm May 16, 2024
6ae9639
add more tests
staabm May 16, 2024
3df46e8
added more tests
staabm May 16, 2024
c58a990
moved php 8.2 only test
staabm May 16, 2024
68ada66
patch Grammer.pp to support capturing groups
staabm May 18, 2024
4f03514
ast
staabm May 18, 2024
53ec814
fix
staabm May 18, 2024
d4b7cd0
fix
staabm May 18, 2024
43d496d
fix
staabm May 18, 2024
887e6a5
fix
staabm May 18, 2024
c361855
fix
staabm May 18, 2024
25e7940
refactor
staabm May 18, 2024
8c61968
Update preg_match_shapes_php82.php
staabm May 18, 2024
1c52274
fix build
staabm May 18, 2024
96780ce
Update composer.lock
staabm May 18, 2024
d782ab3
fix hoa/regex parseable named captures
staabm May 18, 2024
3dd7fc5
Prevent "Call to function preg_match() with arguments '/^(?<name>\\S+…
staabm May 18, 2024
5387535
Fix hoa/regex bug31
staabm May 18, 2024
e03f6b1
read the grammar only once
staabm May 18, 2024
45f9ab3
simplify
staabm May 18, 2024
cce33a5
remove outdated comment
staabm May 18, 2024
71f409d
fix test typo
staabm May 18, 2024
25de902
test more quantifiers
staabm May 18, 2024
e1e33be
test hoa/regex unsupported regex syntax
staabm May 19, 2024
5afdf6f
enable only in bleeding edge
staabm May 19, 2024
85cff74
use nette/utils
staabm May 19, 2024
1650b00
refactor into service
staabm May 19, 2024
6e246e2
fix dash outside of capturing group
staabm May 19, 2024
1c977c8
decouple RegexArrayShapeMatcher from TypeSpecifierContext
staabm May 21, 2024
6cae0ac
use separate feature flag
staabm May 24, 2024
7fca860
move NodeScopeResolverTest-tests into "nsrt"
staabm Jun 20, 2024
11512bb
Implement additional PregMatchParameterOutTypeExtension
staabm Jun 20, 2024
5334cfd
utlize feature flag
staabm Jun 20, 2024
bf66341
Update composer.lock
staabm Jun 20, 2024
f85da99
Update PregMatchParameterOutTypeExtension.php
staabm Jun 20, 2024
da8653d
Update composer.lock
staabm Jun 20, 2024
eb76681
Update composer.lock
staabm Jun 20, 2024
f8b9b8a
cs
staabm Jun 20, 2024
ae0b208
use conditionalTags
staabm Jun 20, 2024
6ae7dda
Update preg_match_shapes.php
staabm Jun 20, 2024
84a4ca1
Improve negative context inference
staabm Jun 21, 2024
28103f8
separate php 7.x expectations
staabm Jun 21, 2024
891a631
php 7.2/7.3 expectation fixes
staabm Jun 21, 2024
c073c6a
fix phpstan
staabm Jun 21, 2024
db824cd
Update LegacyNodeScopeResolverTest.php
staabm Jun 21, 2024
d74d107
move test into separate file
staabm Jun 21, 2024
1ccfcfe
turn test into a php 7.3 only test
staabm Jun 21, 2024
71f4223
Update preg_match_shapes_php73.php
staabm Jun 21, 2024
328fcb9
TypeSpecifier - understand `preg_match() === 1` same way as `preg_mat…
ondrejmirtes Jun 21, 2024
cf3fc13
PHPStan fix
ondrejmirtes Jun 21, 2024
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
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])|\^|\$
Comment on lines +7 to +8
Copy link
Contributor Author

@staabm staabm May 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreleased hoa/regex upstream fix
hoaproject/Regex@5f670af

%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 on lines +25 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

::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
Loading