-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #96 from boesing/bugfix/scanf-argument-count-mismatch
Bugfix: `scanf` and `fscanf` argument count mismatch
- Loading branch information
Showing
17 changed files
with
496 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\ArgumentValidator; | ||
|
||
final class ArgumentValidationResult | ||
{ | ||
/** @var 0|positive-int */ | ||
public int $requiredArgumentCount; | ||
|
||
/** @var 0|positive-int */ | ||
public int $actualArgumentCount; | ||
|
||
/** | ||
* @param 0|positive-int $requiredArgumentCount | ||
* @param 0|positive-int $actualArgumentCount | ||
*/ | ||
public function __construct( | ||
int $requiredArgumentCount, | ||
int $actualArgumentCount | ||
) { | ||
$this->requiredArgumentCount = $requiredArgumentCount; | ||
$this->actualArgumentCount = $actualArgumentCount; | ||
} | ||
|
||
public function valid(): bool | ||
{ | ||
return $this->requiredArgumentCount === $this->actualArgumentCount; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\ArgumentValidator; | ||
|
||
use Boesing\PsalmPluginStringf\Parser\TemplatedStringParser\TemplatedStringParser; | ||
use PhpParser\Node\Arg; | ||
use PhpParser\Node\VariadicPlaceholder; | ||
|
||
interface ArgumentValidator | ||
{ | ||
/** | ||
* @param array<Arg|VariadicPlaceholder> $arguments | ||
*/ | ||
public function validate(TemplatedStringParser $templatedStringParser, array $arguments): ArgumentValidationResult; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\ArgumentValidator; | ||
|
||
use Boesing\PsalmPluginStringf\Parser\TemplatedStringParser\TemplatedStringParser; | ||
|
||
final class ScanfArgumentValidator implements ArgumentValidator | ||
{ | ||
private ArgumentValidator $printfArgumentValidator; | ||
|
||
public function __construct() | ||
{ | ||
$this->printfArgumentValidator = new StringfArgumentValidator(2); | ||
} | ||
|
||
public function validate(TemplatedStringParser $templatedStringParser, array $arguments): ArgumentValidationResult | ||
{ | ||
$result = $this->printfArgumentValidator->validate($templatedStringParser, $arguments); | ||
if ($result->valid()) { | ||
return $result; | ||
} | ||
|
||
if ($result->actualArgumentCount !== 0) { | ||
return $result; | ||
} | ||
|
||
// sscanf and fscanf can return the arguments in case no arguments are passed | ||
return new ArgumentValidationResult( | ||
0, | ||
0 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\ArgumentValidator; | ||
|
||
use Boesing\PsalmPluginStringf\Parser\TemplatedStringParser\TemplatedStringParser; | ||
use PhpParser\Node\Arg; | ||
use PhpParser\Node\VariadicPlaceholder; | ||
use Webmozart\Assert\Assert; | ||
|
||
final class StringfArgumentValidator implements ArgumentValidator | ||
{ | ||
/** @var 0|positive-int */ | ||
private int $argumentsPriorPlaceholderArgumentsStart; | ||
|
||
/** | ||
* @param 0|positive-int $argumentsPriorPlaceholderArgumentsStart | ||
*/ | ||
public function __construct(int $argumentsPriorPlaceholderArgumentsStart) | ||
{ | ||
$this->argumentsPriorPlaceholderArgumentsStart = $argumentsPriorPlaceholderArgumentsStart; | ||
} | ||
|
||
public function validate(TemplatedStringParser $templatedStringParser, array $arguments): ArgumentValidationResult | ||
{ | ||
$requiredArgumentCount = $templatedStringParser->getPlaceholderCount(); | ||
$currentArgumentCount = $this->countArguments($arguments) - $this->argumentsPriorPlaceholderArgumentsStart; | ||
Assert::natural($currentArgumentCount); | ||
|
||
return new ArgumentValidationResult( | ||
$requiredArgumentCount, | ||
$currentArgumentCount | ||
); | ||
} | ||
|
||
/** | ||
* @param array<Arg|VariadicPlaceholder> $arguments | ||
*/ | ||
private function countArguments(array $arguments): int | ||
{ | ||
$argumentCount = 0; | ||
foreach ($arguments as $argument) { | ||
if ($argument instanceof VariadicPlaceholder) { | ||
continue; | ||
} | ||
|
||
$argumentCount++; | ||
} | ||
|
||
return $argumentCount; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\EventHandler; | ||
|
||
use Boesing\PsalmPluginStringf\ArgumentValidator\ArgumentValidator; | ||
use Boesing\PsalmPluginStringf\Parser\Psalm\PhpVersion; | ||
use Boesing\PsalmPluginStringf\Parser\TemplatedStringParser\TemplatedStringParser; | ||
use InvalidArgumentException; | ||
use PhpParser\Node\Arg; | ||
use PhpParser\Node\Expr\FuncCall; | ||
use PhpParser\Node\VariadicPlaceholder; | ||
use Psalm\CodeLocation; | ||
use Psalm\Context; | ||
use Psalm\Issue\ArgumentIssue; | ||
use Psalm\Issue\TooFewArguments; | ||
use Psalm\Issue\TooManyArguments; | ||
use Psalm\IssueBuffer; | ||
use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; | ||
use Psalm\Plugin\EventHandler\Event\AfterEveryFunctionCallAnalysisEvent; | ||
use Psalm\StatementsSource; | ||
|
||
use function assert; | ||
use function sprintf; | ||
|
||
/** | ||
* @psalm-consistent-constructor | ||
*/ | ||
abstract class FunctionArgumentValidator implements AfterEveryFunctionCallAnalysisInterface | ||
{ | ||
protected StatementsSource $statementsSource; | ||
|
||
protected CodeLocation $codeLocation; | ||
|
||
protected PhpVersion $phpVersion; | ||
|
||
protected FuncCall $functionCall; | ||
|
||
protected function __construct(StatementsSource $statementsSource, CodeLocation $codeLocation, PhpVersion $phpVersion, FuncCall $functionCall) | ||
{ | ||
$this->statementsSource = $statementsSource; | ||
$this->codeLocation = $codeLocation; | ||
$this->phpVersion = $phpVersion; | ||
$this->functionCall = $functionCall; | ||
} | ||
|
||
/** | ||
* @return 0|positive-int | ||
*/ | ||
abstract protected function getTemplateArgumentIndex(): int; | ||
|
||
/** | ||
* @return non-empty-string | ||
*/ | ||
abstract protected function getIssueTemplate(): string; | ||
|
||
abstract protected function getArgumentValidator(): ArgumentValidator; | ||
|
||
private function createCodeIssue( | ||
CodeLocation $codeLocation, | ||
string $functionName, | ||
int $argumentCount, | ||
int $requiredArgumentCount | ||
): ArgumentIssue { | ||
$message = $this->createIssueMessage( | ||
$functionName, | ||
$requiredArgumentCount, | ||
$argumentCount | ||
); | ||
|
||
if ($argumentCount < $requiredArgumentCount) { | ||
return new TooFewArguments($message, $codeLocation, $functionName); | ||
} | ||
|
||
return new TooManyArguments($message, $codeLocation, $functionName); | ||
} | ||
|
||
/** | ||
* @psalm-return non-empty-string | ||
*/ | ||
private function createIssueMessage(string $functionName, int $requiredArgumentCount, int $argumentCount): string | ||
{ | ||
$message = sprintf( | ||
$this->getIssueTemplate(), | ||
$functionName, | ||
$requiredArgumentCount, | ||
$argumentCount | ||
); | ||
|
||
assert($message !== ''); | ||
|
||
return $message; | ||
} | ||
|
||
/** | ||
* @param non-empty-string $functionId | ||
*/ | ||
abstract protected function canHandleFunction(string $functionId): bool; | ||
|
||
public static function afterEveryFunctionCallAnalysis(AfterEveryFunctionCallAnalysisEvent $event): void | ||
{ | ||
$functionId = $event->getFunctionId(); | ||
if ($functionId === '') { | ||
return; | ||
} | ||
|
||
$functionCall = $event->getExpr(); | ||
$arguments = $functionCall->args; | ||
|
||
$statementsSource = $event->getStatementsSource(); | ||
|
||
(new static($statementsSource, new CodeLocation($statementsSource, $functionCall), PhpVersion::fromCodebase($event->getCodebase()), $functionCall))->validate( | ||
$functionId, | ||
$arguments, | ||
$event->getContext() | ||
); | ||
} | ||
|
||
/** | ||
* @param non-empty-string $functionName | ||
* @param array<Arg|VariadicPlaceholder> $arguments | ||
*/ | ||
private function validate( | ||
string $functionName, | ||
array $arguments, | ||
Context $context | ||
): void { | ||
if (! $this->canHandleFunction($functionName)) { | ||
return; | ||
} | ||
|
||
$templateArgumentIndex = $this->getTemplateArgumentIndex(); | ||
$template = null; | ||
|
||
foreach ($arguments as $index => $argument) { | ||
if ($index < $templateArgumentIndex) { | ||
continue; | ||
} | ||
|
||
if ($argument instanceof VariadicPlaceholder) { | ||
continue; | ||
} | ||
|
||
$template = $argument; | ||
break; | ||
} | ||
|
||
// Unable to detect template argument | ||
if ($template === null) { | ||
return; | ||
} | ||
|
||
try { | ||
$parsed = TemplatedStringParser::fromArgument( | ||
$functionName, | ||
$template, | ||
$context, | ||
$this->phpVersion->versionId, | ||
false, | ||
$this->statementsSource | ||
); | ||
} catch (InvalidArgumentException $exception) { | ||
return; | ||
} | ||
|
||
$validator = $this->getArgumentValidator(); | ||
$validationResult = $validator->validate($parsed, $arguments); | ||
|
||
if ($validationResult->valid()) { | ||
return; | ||
} | ||
|
||
IssueBuffer::add($this->createCodeIssue( | ||
$this->codeLocation, | ||
$functionName, | ||
$validationResult->actualArgumentCount, | ||
$validationResult->requiredArgumentCount | ||
)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Boesing\PsalmPluginStringf\EventHandler; | ||
|
||
use Boesing\PsalmPluginStringf\ArgumentValidator\ArgumentValidator; | ||
use Boesing\PsalmPluginStringf\ArgumentValidator\StringfArgumentValidator; | ||
|
||
use function in_array; | ||
|
||
final class PrintfFunctionArgumentValidator extends FunctionArgumentValidator | ||
{ | ||
private const FUNCTIONS = [ | ||
'sprintf', | ||
'printf', | ||
]; | ||
|
||
private const TEMPLATE_ARGUMENT_INDEX = 0; | ||
|
||
private const ISSUE_TEMPLATE = 'Template passed to function `%s` requires %d specifier but %d are passed.'; | ||
|
||
protected function getTemplateArgumentIndex(): int | ||
{ | ||
return self::TEMPLATE_ARGUMENT_INDEX; | ||
} | ||
|
||
protected function getIssueTemplate(): string | ||
{ | ||
return self::ISSUE_TEMPLATE; | ||
} | ||
|
||
protected function canHandleFunction(string $functionId): bool | ||
{ | ||
return in_array($functionId, self::FUNCTIONS, true); | ||
} | ||
|
||
protected function getArgumentValidator(): ArgumentValidator | ||
{ | ||
return new StringfArgumentValidator(1); | ||
} | ||
} |
Oops, something went wrong.