MbstringFunctionCallRule.php 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the league/commonmark package.
  5. *
  6. * (c) Colin O'Dell <colinodell@gmail.com>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace League\CommonMark\Tests\PHPStan;
  12. use PHPStan\Analyser\Scope;
  13. use PHPStan\Rules\Rule;
  14. use PhpParser\Node;
  15. /**
  16. * Custom phpstan rule that:
  17. *
  18. * 1. Disallows the use of certain mbstring functions that could be problematic
  19. * 2. Requires an explicit encoding be provided to all `mb_*()` functions that support it
  20. */
  21. final class MbstringFunctionCallRule implements Rule
  22. {
  23. private array $disallowedFunctionsThatAlterGlobalSettings = [
  24. 'mb_internal_encoding',
  25. 'mb_regex_encoding',
  26. 'mb_detect_order',
  27. 'mb_language',
  28. ];
  29. private array $encodingParamPositionCache = [];
  30. public function getNodeType(): string
  31. {
  32. return Node\Expr\FuncCall::class;
  33. }
  34. public function processNode(Node $node, Scope $scope): array
  35. {
  36. if (! $node instanceof Node\Expr\FuncCall) {
  37. return [];
  38. }
  39. if (! $node->name instanceof Node\Name) {
  40. return [];
  41. }
  42. $functionName = $node->name->toString();
  43. if (! str_starts_with($functionName, 'mb_')) {
  44. return [];
  45. }
  46. if (\in_array($functionName, $this->disallowedFunctionsThatAlterGlobalSettings, true)) {
  47. return [\sprintf('Use of %s() is not allowed in this library because it alters global settings', $functionName)];
  48. }
  49. $encodingParamPosition = $this->getEncodingParamPosition($functionName);
  50. if ($encodingParamPosition === null) {
  51. return [];
  52. }
  53. $arg = $node->args[$encodingParamPosition] ?? null;
  54. if ($arg === null) {
  55. return [\sprintf('%s() is missing the $encoding param (should be "UTF-8")', $functionName)];
  56. }
  57. if (! $arg instanceof Node\Arg) {
  58. return [];
  59. }
  60. $encodingArg = $arg->value;
  61. if (! ($encodingArg instanceof Node\Scalar\String_)) {
  62. return [\sprintf('%s() must define the $encoding as "UTF-8"', $functionName)];
  63. }
  64. if (! \in_array($encodingArg->value, ['UTF-8', 'ASCII'], true)) {
  65. return [\sprintf('%s() must define the $encoding as "UTF-8" or "ASCII", not "%s"', $functionName, $encodingArg->value)];
  66. }
  67. return [];
  68. }
  69. private function getEncodingParamPosition(string $function): ?int
  70. {
  71. if (isset($this->encodingParamPositionCache[$function])) {
  72. return $this->encodingParamPositionCache[$function];
  73. }
  74. $reflection = new \ReflectionFunction($function);
  75. $params = $reflection->getParameters();
  76. $encodingParamPosition = null;
  77. foreach ($params as $i => $param) {
  78. if ($param->getName() === 'encoding') {
  79. $encodingParamPosition = $i;
  80. break;
  81. }
  82. }
  83. $this->encodingParamPositionCache[$function] = $encodingParamPosition;
  84. return $encodingParamPosition;
  85. }
  86. }