Browse Source

chore: Implement PHPStan `Preg::match()` extensions (#8103)

Co-authored-by: Greg Korba <greg@codito.dev>
Markus Staab 5 months ago
parent
commit
3e921f240c

+ 1 - 0
composer.json

@@ -73,6 +73,7 @@
     },
     "autoload-dev": {
         "psr-4": {
+            "PhpCsFixer\\PHPStan\\": "dev-tools/phpstan/src/",
             "PhpCsFixer\\Tests\\": "tests/"
         },
         "exclude-from-classmap": [

+ 9 - 201
dev-tools/phpstan/baseline.php

@@ -135,7 +135,7 @@ $ignoreErrors[] = [
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'major\' might not exist on array\\<string\\>\\.$#',
+	'message' => '#^Offset \'major\' might not exist on array\\{0\\?\\: string, major\\?\\: numeric\\-string, 1\\?\\: numeric\\-string\\}\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Console/Command/SelfUpdateCommand.php',
 ];
@@ -355,18 +355,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/DocBlock/DocBlock.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/DocBlock/Line.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 2 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/DocBlock/Line.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset \'_array_shape_inner\' might not exist on array\\<string\\>\\.$#',
@@ -517,12 +505,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/DocBlock/TypeExpression.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/../../src/DocBlock/TypeExpression.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int\\<0, max\\> might not exist on non\\-empty\\-list\\<array\\{start_index\\: int\\<0, max\\>, value\\: string, next_glue\\: string\\|null, next_glue_raw\\: string\\|null\\}\\>\\.$#',
@@ -685,24 +667,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/AttributeNotation/OrderedAttributesFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Basic/BracesPositionFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Basic/CurlyBracesPositionFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Basic/NoMultipleStatementsPerLineFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset 0 might not exist on list\\<string\\>\\.$#',
@@ -1093,18 +1057,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/ClassNotation/ProtectedToPrivateFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 0 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/ClassNotation/SingleClassElementPerStatementFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Comment/CommentToPhpdocFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: foreach.nonIterable
 	'message' => '#^Argument of an invalid type array\\<int\\<0, max\\>, PhpCsFixer\\\\Tokenizer\\\\Token\\>\\|PhpCsFixer\\\\Tokenizer\\\\Token supplied for foreach, only iterables are supported\\.$#',
@@ -1113,22 +1065,16 @@ $ignoreErrors[] = [
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
+	'message' => '#^Offset 1 might not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: non\\-falsy\\-string\\}\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/ControlStructure/NoBreakCommentFixer.php',
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 2 might not exist on array\\<string\\>\\.$#',
+	'message' => '#^Offset 2 might not exist on array\\{0\\?\\: string, 1\\?\\: string, 2\\?\\: non\\-falsy\\-string\\}\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/ControlStructure/NoBreakCommentFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/ControlStructure/NoSuperfluousElseifFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int\\|null might not exist on array\\<int\\|string, bool\\|null\\>\\.$#',
@@ -1183,18 +1129,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<list\\<string\\>\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/FullyQualifiedStrictTypesFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/FullyQualifiedStrictTypesFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset string might not exist on array\\<string, PhpCsFixer\\\\Tokenizer\\\\Analyzer\\\\Analysis\\\\ArgumentAnalysis\\>\\.$#',
@@ -1219,12 +1153,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Import/FullyQualifiedStrictTypesFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 0 might not exist on array\\<list\\<array\\{string, int\\}\\>\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/GlobalNamespaceImportFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset 0 might not exist on list\\<PhpCsFixer\\\\Tokenizer\\\\Analyzer\\\\Analysis\\\\NamespaceAnalysis\\>\\.$#',
@@ -1477,12 +1405,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/NamespaceNotation/BlankLineAfterNamespaceFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: plus.leftNonNumeric
 	'message' => '#^Only numeric types are allowed in \\+, int\\|false given on the left side\\.$#',
@@ -1747,12 +1669,6 @@ $ignoreErrors[] = [
 	'count' => 3,
 	'path' => __DIR__ . '/../../src/Fixer/PhpUnit/PhpUnitTestCaseStaticMethodCallsFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/AlignMultilineCommentFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset 1 might not exist on array\\.$#',
@@ -1772,15 +1688,15 @@ $ignoreErrors[] = [
 	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/NoBlankLinesAfterPhpdocFixer.php',
 ];
 $ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<list\\<string\\>\\>\\.$#',
+	// identifier: varTag.type
+	'message' => '#^PHPDoc tag @var with type non\\-empty\\-string is not subtype of type non\\-falsy\\-string\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php',
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 2,
+	'message' => '#^Offset 1 might not exist on array\\{0\\?\\: string, 1\\?\\: string\\}\\.$#',
+	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocAddMissingParamAnnotationFixer.php',
 ];
 $ignoreErrors[] = [
@@ -1939,30 +1855,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagCasingFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'inlined_tag\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagTypeFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'inlined_tag_name\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagTypeFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'tag\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagTypeFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'tag_name\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagTypeFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset 1\\|int\\<3, max\\> might not exist on array\\<int\\<0, max\\>, string\\>\\.$#',
@@ -1975,12 +1867,6 @@ $ignoreErrors[] = [
 	'count' => 2,
 	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocTagTypeFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<list\\<string\\>\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Phpdoc/PhpdocToCommentFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset 0 might not exist on array\\.$#',
@@ -2011,12 +1897,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/StringNotation/EscapeImplicitBackslashesFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 0 might not exist on array\\<string\\>\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/../../src/Fixer/StringNotation/NoTrailingWhitespaceInStringFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset \'end_index\' might not exist on non\\-empty\\-array\\<literal\\-string&non\\-falsy\\-string, int\\|string\\>\\.$#',
@@ -2041,12 +1921,6 @@ $ignoreErrors[] = [
 	'count' => 2,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/ArrayIndentationFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Whitespace/ArrayIndentationFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int might not exist on non\\-empty\\-list\\<non\\-empty\\-array\\<literal\\-string&non\\-falsy\\-string, int\\|string\\>\\>\\.$#',
@@ -2085,7 +1959,7 @@ $ignoreErrors[] = [
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 0 might not exist on array\\<string\\>\\.$#',
+	'message' => '#^Offset 0 might not exist on array\\{0\\?\\: string\\}\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/HeredocIndentationFixer.php',
 ];
@@ -2107,12 +1981,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/IndentationTypeFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Whitespace/MethodChainingIndentationFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int\\<1, max\\> might not exist on list\\<string\\>\\.$#',
@@ -2137,18 +2005,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/../../src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 2 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int\\<1, max\\> might not exist on array\\<int\\<0, max\\>, string\\>\\.$#',
@@ -2167,12 +2023,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/StatementIndentationFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Whitespace/StatementIndentationFixer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset int might not exist on array\\<int, int\\>\\.$#',
@@ -2191,12 +2041,6 @@ $ignoreErrors[] = [
 	'count' => 2,
 	'path' => __DIR__ . '/../../src/Fixer/Whitespace/StatementIndentationFixer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 3,
-	'path' => __DIR__ . '/../../src/FixerConfiguration/FixerConfigurationResolver.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset string might not exist on non\\-empty\\-array\\<string, array\\<int\\<0, max\\>, string\\>\\>\\.$#',
@@ -2263,18 +2107,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Preg.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<string\\>\\.$#',
-	'count' => 3,
-	'path' => __DIR__ . '/../../src/RuleSet/AbstractMigrationSetDescription.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 2 might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/RuleSet/AbstractMigrationSetDescription.php',
-];
 $ignoreErrors[] = [
 	// identifier: plus.rightNonNumeric
 	'message' => '#^Only numeric types are allowed in \\+, int\\|false given on the right side\\.$#',
@@ -2343,13 +2175,7 @@ $ignoreErrors[] = [
 ];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 0 might not exist on list\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Runner/Runner.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<list\\<string\\>\\>\\.$#',
+	'message' => '#^Offset 0 might not exist on list\\<non\\-empty\\-string\\>\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Runner/Runner.php',
 ];
@@ -2407,12 +2233,6 @@ $ignoreErrors[] = [
 	'count' => 2,
 	'path' => __DIR__ . '/../../src/Tokenizer/Analyzer/ControlCaseStructuresAnalyzer.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset 1 might not exist on array\\<list\\<string\\>\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Tokenizer/Analyzer/DataProviderAnalyzer.php',
-];
 $ignoreErrors[] = [
 	// identifier: offsetAccess.notFound
 	'message' => '#^Offset non\\-falsy\\-string might not exist on array\\<string, list\\<int\\>\\>\\.$#',
@@ -2551,18 +2371,6 @@ $ignoreErrors[] = [
 	'count' => 1,
 	'path' => __DIR__ . '/../../tests/AutoReview/CiConfigurationTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'major\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../tests/AutoReview/CiConfigurationTest.php',
-];
-$ignoreErrors[] = [
-	// identifier: offsetAccess.notFound
-	'message' => '#^Offset \'minor\' might not exist on array\\<string\\>\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../tests/AutoReview/CiConfigurationTest.php',
-];
 $ignoreErrors[] = [
 	// identifier: argument.type
 	'message' => '#^Parameter \\#1 \\$code of static method PhpCsFixer\\\\Tokenizer\\\\Tokens\\:\\:fromCode\\(\\) expects string, string\\|false given\\.$#',

+ 68 - 0
dev-tools/phpstan/src/Extension/PregMatchParameterOutExtension.php

@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\PHPStan\Extension;
+
+use PhpCsFixer\Preg;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\Reflection\ParameterReflection;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Php\RegexArrayShapeMatcher;
+use PHPStan\Type\StaticMethodParameterOutTypeExtension;
+use PHPStan\Type\Type;
+
+final class PregMatchParameterOutExtension implements StaticMethodParameterOutTypeExtension
+{
+    private RegexArrayShapeMatcher $regexShapeMatcher;
+
+    public function __construct(
+        RegexArrayShapeMatcher $regexShapeMatcher
+    ) {
+        $this->regexShapeMatcher = $regexShapeMatcher;
+    }
+
+    public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
+    {
+        return
+            Preg::class === $methodReflection->getDeclaringClass()->getName()
+            && \in_array($methodReflection->getName(), ['match', 'matchAll'], true)
+            && 'matches' === $parameter->getName();
+    }
+
+    public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
+    {
+        $args = $methodCall->getArgs();
+        $patternArg = $args[0] ?? null;
+        $matchesArg = $args[2] ?? null;
+        $flagsArg = $args[3] ?? null;
+
+        if (
+            null === $patternArg || null === $matchesArg
+        ) {
+            return null;
+        }
+
+        $flagsType = null;
+        if (null !== $flagsArg) {
+            $flagsType = $scope->getType($flagsArg->value);
+        }
+
+        $matcherMethod = ('match' === $methodReflection->getName()) ? 'matchExpr' : 'matchAllExpr';
+
+        // @phpstan-ignore method.dynamicName
+        return $this->regexShapeMatcher->{$matcherMethod}($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
+    }
+}

+ 103 - 0
dev-tools/phpstan/src/Extension/PregMatchTypeSpecifyingExtension.php

@@ -0,0 +1,103 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\PHPStan\Extension;
+
+use PhpCsFixer\Preg;
+use PhpParser\Node\Expr\StaticCall;
+use PHPStan\Analyser\Scope;
+use PHPStan\Analyser\SpecifiedTypes;
+use PHPStan\Analyser\TypeSpecifier;
+use PHPStan\Analyser\TypeSpecifierAwareExtension;
+use PHPStan\Analyser\TypeSpecifierContext;
+use PHPStan\Reflection\MethodReflection;
+use PHPStan\TrinaryLogic;
+use PHPStan\Type\Php\RegexArrayShapeMatcher;
+use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
+
+final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
+{
+    private RegexArrayShapeMatcher $regexShapeMatcher;
+
+    private TypeSpecifier $typeSpecifier;
+
+    public function __construct(
+        RegexArrayShapeMatcher $regexShapeMatcher
+    ) {
+        $this->regexShapeMatcher = $regexShapeMatcher;
+    }
+
+    public function getClass(): string
+    {
+        return Preg::class;
+    }
+
+    public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
+    {
+        $this->typeSpecifier = $typeSpecifier;
+    }
+
+    public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
+    {
+        return \in_array($methodReflection->getName(), ['match', 'matchAll'], true) && !$context->null();
+    }
+
+    public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
+    {
+        $args = $node->getArgs();
+        $patternArg = $args[0] ?? null;
+        $matchesArg = $args[2] ?? null;
+        $flagsArg = $args[3] ?? null;
+
+        if (
+            null === $patternArg || null === $matchesArg
+        ) {
+            return new SpecifiedTypes();
+        }
+
+        $flagsType = null;
+        if (null !== $flagsArg) {
+            $flagsType = $scope->getType($flagsArg->value);
+        }
+
+        $matcherMethod = ('match' === $methodReflection->getName()) ? 'matchExpr' : 'matchAllExpr';
+
+        /** @phpstan-ignore method.dynamicName */
+        $matchedType = $this->regexShapeMatcher->{$matcherMethod}(
+            $patternArg->value,
+            $flagsType,
+            TrinaryLogic::createFromBoolean($context->true()),
+            $scope
+        );
+
+        if (null === $matchedType) {
+            return new SpecifiedTypes();
+        }
+
+        $overwrite = false;
+        if ($context->false()) {
+            $overwrite = true;
+            $context = $context->negate();
+        }
+
+        return $this->typeSpecifier->create(
+            $matchesArg->value,
+            $matchedType,
+            $context,
+            $overwrite,
+            $scope,
+            $node,
+        );
+    }
+}

+ 12 - 0
phpstan.dist.neon

@@ -9,6 +9,7 @@ parameters:
     paths:
         - src
         - tests
+        - dev-tools/phpstan/src
     excludePaths:
         - tests/Fixtures
     polluteScopeWithLoopInitialAssignments: true # Do not enforce assignments outside of the loops
@@ -56,3 +57,14 @@ parameters:
             count: 266
     tipsOfTheDay: false
     tmpDir: dev-tools/phpstan/cache
+
+services:
+    -
+        class: PhpCsFixer\PHPStan\Extension\PregMatchParameterOutExtension
+        tags:
+            - phpstan.staticMethodParameterOutTypeExtension
+
+    -
+        class: PhpCsFixer\PHPStan\Extension\PregMatchTypeSpecifyingExtension
+        tags:
+            - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension