UnsafeStrictGroupsCallRule.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. <?php declare(strict_types=1);
  2. namespace Composer\Pcre\PHPStan;
  3. use Composer\Pcre\Preg;
  4. use Composer\Pcre\Regex;
  5. use PhpParser\Node;
  6. use PhpParser\Node\Expr\StaticCall;
  7. use PhpParser\Node\Name\FullyQualified;
  8. use PHPStan\Analyser\Scope;
  9. use PHPStan\Analyser\SpecifiedTypes;
  10. use PHPStan\Rules\Rule;
  11. use PHPStan\Rules\RuleErrorBuilder;
  12. use PHPStan\TrinaryLogic;
  13. use PHPStan\Type\ObjectType;
  14. use PHPStan\Type\Type;
  15. use PHPStan\Type\TypeCombinator;
  16. use PHPStan\Type\Php\RegexArrayShapeMatcher;
  17. use function sprintf;
  18. /**
  19. * @implements Rule<StaticCall>
  20. */
  21. final class UnsafeStrictGroupsCallRule implements Rule
  22. {
  23. /**
  24. * @var RegexArrayShapeMatcher
  25. */
  26. private $regexShapeMatcher;
  27. public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
  28. {
  29. $this->regexShapeMatcher = $regexShapeMatcher;
  30. }
  31. public function getNodeType(): string
  32. {
  33. return StaticCall::class;
  34. }
  35. public function processNode(Node $node, Scope $scope): array
  36. {
  37. if (!$node->class instanceof FullyQualified) {
  38. return [];
  39. }
  40. $isRegex = $node->class->toString() === Regex::class;
  41. $isPreg = $node->class->toString() === Preg::class;
  42. if (!$isRegex && !$isPreg) {
  43. return [];
  44. }
  45. if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) {
  46. return [];
  47. }
  48. $args = $node->getArgs();
  49. if (!isset($args[0])) {
  50. return [];
  51. }
  52. $patternArg = $args[0] ?? null;
  53. if ($isPreg) {
  54. if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway
  55. return [];
  56. }
  57. $flagsArg = $args[3] ?? null;
  58. } else {
  59. $flagsArg = $args[2] ?? null;
  60. }
  61. if ($patternArg === null) {
  62. return [];
  63. }
  64. $flagsType = PregMatchFlags::getType($flagsArg, $scope);
  65. if ($flagsType === null) {
  66. return [];
  67. }
  68. $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
  69. if ($matchedType === null) {
  70. return [
  71. RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name))
  72. ->identifier('composerPcre.maybeUnsafeStrictGroups')
  73. ->build(),
  74. ];
  75. }
  76. if (count($matchedType->getConstantArrays()) === 1) {
  77. $matchedType = $matchedType->getConstantArrays()[0];
  78. $nullableGroups = [];
  79. foreach ($matchedType->getValueTypes() as $index => $type) {
  80. if (TypeCombinator::containsNull($type)) {
  81. $nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue();
  82. }
  83. }
  84. if (\count($nullableGroups) > 0) {
  85. return [
  86. RuleErrorBuilder::message(sprintf(
  87. 'The %s call is unsafe as match group%s "%s" %s optional and may be null.',
  88. $node->name->name,
  89. \count($nullableGroups) > 1 ? 's' : '',
  90. implode('", "', $nullableGroups),
  91. \count($nullableGroups) > 1 ? 'are' : 'is'
  92. ))->identifier('composerPcre.unsafeStrictGroups')->build(),
  93. ];
  94. }
  95. }
  96. return [];
  97. }
  98. }