Skip to content

Commit

Permalink
Implement PrintfArrayParametersRule
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Jun 4, 2024
1 parent 04b8403 commit faf6099
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 66 deletions.
4 changes: 4 additions & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ rules:
- PHPStan\Rules\Functions\InvalidLexicalVariablesInClosureUseRule
- PHPStan\Rules\Functions\ParamAttributesRule
- PHPStan\Rules\Functions\PrintfParametersRule
- PHPStan\Rules\Functions\PrintfArrayParametersRule
- PHPStan\Rules\Functions\RedefinedParametersRule
- PHPStan\Rules\Functions\ReturnNullsafeByRefRule
- PHPStan\Rules\Ignore\IgnoreParseErrorRule
Expand Down Expand Up @@ -293,3 +294,6 @@ services:

-
class: PHPStan\Rules\Constants\MagicConstantContextRule

-
class: PHPStan\Rules\Functions\PrintfHelper
111 changes: 111 additions & 0 deletions src/Rules/Functions/PrintfArrayParametersRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use function array_key_exists;
use function count;
use function sprintf;
use function strtolower;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class PrintfArrayParametersRule implements Rule
{

public function __construct(private PrintfHelper $printfHelper)
{
}

public function getNodeType(): string
{
return FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Node\Name)) {
return [];
}

$functionsArgumentPositions = [
'vprintf' => 0,
'vsprintf' => 0,
];
$minimumNumberOfArguments = [
'vprintf' => 1,
'vsprintf' => 1,
];

$name = strtolower((string) $node->name);
if (!array_key_exists($name, $functionsArgumentPositions)) {
return [];
}

$formatArgumentPosition = $functionsArgumentPositions[$name];

$args = $node->getArgs();
foreach ($args as $arg) {
if ($arg->unpack) {
return [];
}
}
$argsCount = count($args);
if ($argsCount < $minimumNumberOfArguments[$name]) {
return []; // caught by CallToFunctionParametersRule
}

$formatArgType = $scope->getType($args[$formatArgumentPosition]->value);
$maxPlaceHoldersCount = null;
foreach ($formatArgType->getConstantStrings() as $formatString) {
$format = $formatString->getValue();
$tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format);
if ($maxPlaceHoldersCount === null) {
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
} elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) {
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
}
}

if ($maxPlaceHoldersCount === null) {
return [];
}

$formatArgsCount = 0;
if (isset($args[1])) {
$formatArgs = $scope->getType($args[1]->value);

foreach ($formatArgs->getConstantArrays() as $constantArray) {
$size = $constantArray->getArraySize();
if (!$size instanceof ConstantIntegerType) {
return [];
}
$formatArgsCount = $size->getValue();
}
}

if ($formatArgsCount !== $maxPlaceHoldersCount) {
return [
RuleErrorBuilder::message(sprintf(
sprintf(
'%s, %s.',
$maxPlaceHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
$formatArgsCount === 1 ? '%d value given' : '%d values given',
),
$name,
$maxPlaceHoldersCount,
$formatArgsCount,
))->identifier(sprintf('argument.%s', $name))->build(),
];
}

return [];
}

}
75 changes: 75 additions & 0 deletions src/Rules/Functions/PrintfHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use Nette\Utils\Strings;
use PHPStan\Php\PhpVersion;
use function array_filter;
use function count;
use function max;
use function sprintf;
use function strlen;
use const PREG_SET_ORDER;

final class PrintfHelper
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function getPrintfPlaceholdersCount(string $format): int
{
return $this->getPlaceholdersCount('(?:[bs%s]|l?[cdeEgfFGouxX])', $format);
}

public function getScanfPlaceholdersCount(string $format): int
{
return $this->getPlaceholdersCount('(?:[cdDeEfinosuxX%s]|\[[^\]]+\])', $format);
}

private function getPlaceholdersCount(string $specifiersPattern, string $format): int
{
$addSpecifier = '';
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
$addSpecifier .= 'hH';
}

$specifiers = sprintf($specifiersPattern, $addSpecifier);

$pattern = '~(?<before>%*)%(?:(?<position>\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?<width>\*)?-?\d*(?:\.(?:\d+|(?<precision>\*))?)?' . $specifiers . '~';

$matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER);

if (count($matches) === 0) {
return 0;
}

$placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0);

if (count($placeholders) === 0) {
return 0;
}

$maxPositionedNumber = 0;
$maxOrdinaryNumber = 0;
foreach ($placeholders as $placeholder) {
if (isset($placeholder['width']) && $placeholder['width'] !== '') {
$maxOrdinaryNumber++;
}

if (isset($placeholder['precision']) && $placeholder['precision'] !== '') {
$maxOrdinaryNumber++;
}

if (isset($placeholder['position']) && $placeholder['position'] !== '') {
$maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber);
} else {
$maxOrdinaryNumber++;
}
}

return max($maxPositionedNumber, $maxOrdinaryNumber);
}

}
85 changes: 20 additions & 65 deletions src/Rules/Functions/PrintfParametersRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,24 @@

namespace PHPStan\Rules\Functions;

use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_filter;
use function array_key_exists;
use function count;
use function in_array;
use function max;
use function sprintf;
use function strlen;
use function strtolower;
use const PREG_SET_ORDER;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class PrintfParametersRule implements Rule
{

public function __construct(private PhpVersion $phpVersion)
public function __construct(private PrintfHelper $printfHelper)
{
}

Expand All @@ -40,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$functionsArgumentPositions = [
$functionsFormatArgumentPositions = [
'printf' => 0,
'sprintf' => 0,
'sscanf' => 1,
Expand All @@ -54,11 +48,11 @@ public function processNode(Node $node, Scope $scope): array
];

$name = strtolower((string) $node->name);
if (!array_key_exists($name, $functionsArgumentPositions)) {
if (!array_key_exists($name, $functionsFormatArgumentPositions)) {
return [];
}

$formatArgumentPosition = $functionsArgumentPositions[$name];
$formatArgumentPosition = $functionsFormatArgumentPositions[$name];

$args = $node->getArgs();
foreach ($args as $arg) {
Expand All @@ -72,33 +66,39 @@ public function processNode(Node $node, Scope $scope): array
}

$formatArgType = $scope->getType($args[$formatArgumentPosition]->value);
$placeHoldersCount = null;
$maxPlaceHoldersCount = null;
foreach ($formatArgType->getConstantStrings() as $formatString) {
$format = $formatString->getValue();
$tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format);
if ($placeHoldersCount === null) {
$placeHoldersCount = $tempPlaceHoldersCount;
} elseif ($tempPlaceHoldersCount > $placeHoldersCount) {
$placeHoldersCount = $tempPlaceHoldersCount;

if (in_array($name, ['sprintf', 'printf'], true)) {
$tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format);
} else {
$tempPlaceHoldersCount = $this->printfHelper->getScanfPlaceholdersCount($format);
}

if ($maxPlaceHoldersCount === null) {
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
} elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) {
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
}
}

if ($placeHoldersCount === null) {
if ($maxPlaceHoldersCount === null) {
return [];
}

$argsCount -= $formatArgumentPosition;

if ($argsCount !== $placeHoldersCount + 1) {
if ($argsCount !== $maxPlaceHoldersCount + 1) {
return [
RuleErrorBuilder::message(sprintf(
sprintf(
'%s, %s.',
$placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
$maxPlaceHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
$argsCount - 1 === 1 ? '%d value given' : '%d values given',
),
$name,
$placeHoldersCount,
$maxPlaceHoldersCount,
$argsCount - 1,
))->identifier(sprintf('argument.%s', $name))->build(),
];
Expand All @@ -107,49 +107,4 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

private function getPlaceholdersCount(string $functionName, string $format): int
{
$specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '(?:[bs%s]|l?[cdeEgfFGouxX])' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])';
$addSpecifier = '';
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
$addSpecifier .= 'hH';
}

$specifiers = sprintf($specifiers, $addSpecifier);

$pattern = '~(?<before>%*)%(?:(?<position>\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?<width>\*)?-?\d*(?:\.(?:\d+|(?<precision>\*))?)?' . $specifiers . '~';

$matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER);

if (count($matches) === 0) {
return 0;
}

$placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0);

if (count($placeholders) === 0) {
return 0;
}

$maxPositionedNumber = 0;
$maxOrdinaryNumber = 0;
foreach ($placeholders as $placeholder) {
if (isset($placeholder['width']) && $placeholder['width'] !== '') {
$maxOrdinaryNumber++;
}

if (isset($placeholder['precision']) && $placeholder['precision'] !== '') {
$maxOrdinaryNumber++;
}

if (isset($placeholder['position']) && $placeholder['position'] !== '') {
$maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber);
} else {
$maxOrdinaryNumber++;
}
}

return max($maxPositionedNumber, $maxOrdinaryNumber);
}

}
Loading

0 comments on commit faf6099

Please sign in to comment.