From 4ab4c634a1f1876097a8a236f6e0e453d94f8f52 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Sat, 26 Aug 2023 16:10:28 +0200 Subject: [PATCH] Add phpstan --- composer.json | 3 ++- phpstan-baseline.neon | 26 ++++++++++++++++++++++++++ phpstan.neon | 7 +++++++ src/Config.php | 3 +++ src/Corpus.php | 17 ++++++++--------- src/CorpusEntry.php | 7 ++++++- src/CoverageRenderer.php | 10 ++++++++-- src/DictionaryParser.php | 5 ++++- src/Fuzzer.php | 24 +++++++++++++++--------- src/FuzzingContext.php | 12 ++++++++++-- src/Instrumentation/FileInfo.php | 3 ++- src/Instrumentation/MutableString.php | 2 +- src/Instrumentation/Visitor.php | 2 +- src/Mutation/Dictionary.php | 6 +++++- src/Mutation/Mutator.php | 10 +++++++--- src/Mutation/RNG.php | 9 +++++++-- 16 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 844a944..875d7e9 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "ulrichsg/getopt-php": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^9" + "phpunit/phpunit": "^9", + "phpstan/phpstan": "^1.10" }, "suggest": { "ext-pcntl": "Needed for timeout support" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..400a186 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,26 @@ +parameters: + ignoreErrors: + - + message: "#^Method PhpFuzzer\\\\Fuzzer\\:\\:handleFuzzCommand\\(\\) has parameter \\$getOpt with no value type specified in iterable type GetOpt\\\\GetOpt\\.$#" + count: 1 + path: src/Fuzzer.php + + - + message: "#^Method PhpFuzzer\\\\Fuzzer\\:\\:handleMinimizeCrashCommand\\(\\) has parameter \\$getOpt with no value type specified in iterable type GetOpt\\\\GetOpt\\.$#" + count: 1 + path: src/Fuzzer.php + + - + message: "#^Method PhpFuzzer\\\\Fuzzer\\:\\:handleReportCoverage\\(\\) has parameter \\$getOpt with no value type specified in iterable type GetOpt\\\\GetOpt\\.$#" + count: 1 + path: src/Fuzzer.php + + - + message: "#^Method PhpFuzzer\\\\Fuzzer\\:\\:handleRunSingleCommand\\(\\) has parameter \\$getOpt with no value type specified in iterable type GetOpt\\\\GetOpt\\.$#" + count: 1 + path: src/Fuzzer.php + + - + message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$stmts\\.$#" + count: 1 + path: src/Instrumentation/Visitor.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b8875eb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 6 + paths: + - src diff --git a/src/Config.php b/src/Config.php index d91519f..2016574 100644 --- a/src/Config.php +++ b/src/Config.php @@ -11,6 +11,7 @@ class Config { public \Closure $target; public Dictionary $dictionary; + /** @var list> */ public array $allowedExceptions = [\Exception::class]; public int $maxLen = PHP_INT_MAX; @@ -28,6 +29,8 @@ public function setTarget(\Closure $target): void { /** * Set which exceptions are not considered as fuzzing failures. * Defaults to just "Exception", considering all "Errors" failures. + * + * @param list> $allowedExceptions */ public function setAllowedExceptions(array $allowedExceptions): void { $this->allowedExceptions = $allowedExceptions; diff --git a/src/Corpus.php b/src/Corpus.php index 6cccb80..ddfc48a 100644 --- a/src/Corpus.php +++ b/src/Corpus.php @@ -9,16 +9,16 @@ final class Corpus { private array $entriesByHash = []; /** @var CorpusEntry[] Only used to get a random element. */ private array $entriesByIndex = []; + /** @var array */ private array $seenFeatures = []; - /** @var CorpusEntry[] $crashEntries */ - private array $crashEntries = []; + /** @var array */ private array $seenCrashFeatures = []; private int $totalLen = 0; private int $maxLen = 0; - public function computeUniqueFeatures(CorpusEntry $entry) { + public function computeUniqueFeatures(CorpusEntry $entry): void { $entry->uniqueFeatures = []; foreach ($entry->features as $feature => $_) { if (!isset($this->seenFeatures[$feature])) { @@ -78,6 +78,9 @@ public function getMaxLen(): int { return $this->maxLen; } + /** + * @return array + */ public function getSeenBlockMap(): array { $blocks = []; foreach ($this->seenFeatures as $feature => $_) { @@ -96,10 +99,6 @@ public function addCrashEntry(CorpusEntry $entry): bool { $this->seenCrashFeatures[$feature] = true; } } - if ($hasNewFeature) { - $this->crashEntries[] = $entry; - return true; - } - return false; + return $hasNewFeature; } -} \ No newline at end of file +} diff --git a/src/CorpusEntry.php b/src/CorpusEntry.php index ebfc994..b189f43 100644 --- a/src/CorpusEntry.php +++ b/src/CorpusEntry.php @@ -5,11 +5,16 @@ final class CorpusEntry { public string $input; public string $hash; + /** @var array */ public array $features; public ?string $crashInfo; public ?string $path; + /** @var array */ public array $uniqueFeatures; + /** + * @param array $features + */ public function __construct(string $input, array $features, ?string $crashInfo) { $this->input = $input; $this->hash = \md5($input); @@ -33,4 +38,4 @@ public function storeAtPath(string $path): void { $result = file_put_contents($this->path, $this->input); assert($result === \strlen($this->input)); } -} \ No newline at end of file +} diff --git a/src/CoverageRenderer.php b/src/CoverageRenderer.php index 9be5c4e..5a0ea86 100644 --- a/src/CoverageRenderer.php +++ b/src/CoverageRenderer.php @@ -11,7 +11,10 @@ public function __construct(string $outDir) { $this->outDir = $outDir; } - /** @param FileInfo[] $fileInfos */ + /** + * @param FileInfo[] $fileInfos + * @param array $seenBlocks + */ public function render(array $fileInfos, array $seenBlocks): void { @mkdir($this->outDir); @@ -56,6 +59,9 @@ public function render(array $fileInfos, array $seenBlocks): void { file_put_contents($this->outDir . '/index.html', $overview); } + /** + * @param list $strings + */ private function getCommonPrefix(array $strings): string { if (empty($strings)) { return ''; @@ -68,4 +74,4 @@ private function getCommonPrefix(array $strings): string { } return $prefix; } -} \ No newline at end of file +} diff --git a/src/DictionaryParser.php b/src/DictionaryParser.php index 278e4f2..20f68e6 100644 --- a/src/DictionaryParser.php +++ b/src/DictionaryParser.php @@ -3,6 +3,9 @@ namespace PhpFuzzer; final class DictionaryParser { + /** + * @return list + */ public function parse(string $code): array { $lines = explode("\n", $code); $dictionary = []; @@ -32,4 +35,4 @@ public function parse(string $code): array { } return $dictionary; } -} \ No newline at end of file +} diff --git a/src/Fuzzer.php b/src/Fuzzer.php index 3c4108e..cb7c2f9 100644 --- a/src/Fuzzer.php +++ b/src/Fuzzer.php @@ -27,6 +27,7 @@ final class Fuzzer { public ?string $targetPath = null; private ?string $coverageDir = null; + /** @var array */ private array $fileInfos = []; private ?string $lastInput = null; @@ -180,7 +181,7 @@ private function isAllowedException(\Throwable $e): bool { return false; } - private function runInput(string $input) { + private function runInput(string $input): CorpusEntry { $this->runs++; if (\extension_loaded('pcntl')) { \pcntl_alarm($this->timeout); @@ -206,6 +207,10 @@ private function runInput(string $input) { return new CorpusEntry($input, $features, $crashInfo); } + /** + * @param array $edgeCounts + * @return array + */ private function edgeCountsToFeatures(array $edgeCounts): array { $features = []; foreach ($edgeCounts as $edge => $count) { @@ -269,7 +274,7 @@ private function loadCorpus(): bool { return true; } - private function printAction(string $action, CorpusEntry $entry) { + private function printAction(string $action, CorpusEntry $entry): void { $time = microtime(true) - $this->startTime; $mem = memory_get_usage(); $numFeatures = $this->corpus->getNumFeatures(); @@ -298,12 +303,12 @@ private function formatBytes(int $bytes): string { } } - private function printCrash(string $prefix, CorpusEntry $entry) { + private function printCrash(string $prefix, CorpusEntry $entry): void { echo "$prefix in $entry->path!\n"; echo $entry->crashInfo . "\n"; } - public function renderCoverage() { + public function renderCoverage(): void { if ($this->coverageDir === null) { throw new FuzzerException('Missing coverage directory'); } @@ -312,7 +317,7 @@ public function renderCoverage() { $renderer->render($this->fileInfos, $this->corpus->getSeenBlockMap()); } - private function minimizeCrash(string $path) { + private function minimizeCrash(string $path): void { if (!is_file($path)) { throw new FuzzerException("Crash input \"$path\" does not exist"); } @@ -391,6 +396,7 @@ public function handleCliArgs(): int { return 0; } + /** @var Command|null $command The CommandInterface is missing the getHandler() method. */ $command = $getOpt->getCommand(); if (!$command) { echo 'Missing command' . PHP_EOL; @@ -437,7 +443,7 @@ private function createTemporaryCorpusDirectory(): string { return $corpusDir; } - private function handleFuzzCommand(GetOpt $getOpt) { + private function handleFuzzCommand(GetOpt $getOpt): void { $corpusDir = $getOpt->getOperand('corpus'); if ($corpusDir === null) { $corpusDir = $this->createTemporaryCorpusDirectory(); @@ -447,7 +453,7 @@ private function handleFuzzCommand(GetOpt $getOpt) { $this->fuzz(); } - private function handleRunSingleCommand(GetOpt $getOpt) { + private function handleRunSingleCommand(GetOpt $getOpt): void { $inputPath = $getOpt->getOperand('input'); if (!is_file($inputPath)) { throw new FuzzerException('Input "' . $inputPath . '" does not exist'); @@ -461,14 +467,14 @@ private function handleRunSingleCommand(GetOpt $getOpt) { } } - private function handleMinimizeCrashCommand(GetOpt $getOpt) { + private function handleMinimizeCrashCommand(GetOpt $getOpt): void { if ($this->maxRuns === PHP_INT_MAX) { $this->maxRuns = 100000; } $this->minimizeCrash($getOpt->getOperand('input')); } - private function handleReportCoverage(GetOpt $getOpt) { + private function handleReportCoverage(GetOpt $getOpt): void { $this->setCorpusDir($getOpt->getOperand('corpus')); $this->setCoverageDir($getOpt->getOperand('coverage-dir')); $this->loadCorpus(); diff --git a/src/FuzzingContext.php b/src/FuzzingContext.php index d639338..9ac5cb4 100644 --- a/src/FuzzingContext.php +++ b/src/FuzzingContext.php @@ -3,17 +3,25 @@ namespace PhpFuzzer; final class FuzzingContext { + /** @var int */ public static $prevBlock = 0; + /** @var array */ public static $edges = []; - public static function reset() { + public static function reset(): void { self::$prevBlock = 0; self::$edges = []; } + /** + * @template T + * @param int $blockIndex + * @param T $returnValue + * @return T + */ public static function traceBlock($blockIndex, $returnValue) { $key = self::$prevBlock << 28 | $blockIndex; self::$edges[$key] = (self::$edges[$key] ?? 0) + 1; return $returnValue; } -} \ No newline at end of file +} diff --git a/src/Instrumentation/FileInfo.php b/src/Instrumentation/FileInfo.php index 8c227a8..53da9f2 100644 --- a/src/Instrumentation/FileInfo.php +++ b/src/Instrumentation/FileInfo.php @@ -3,5 +3,6 @@ namespace PhpFuzzer\Instrumentation; final class FileInfo { + /** @var array */ public array $blockIndexToPos = []; -} \ No newline at end of file +} diff --git a/src/Instrumentation/MutableString.php b/src/Instrumentation/MutableString.php index 89437a9..d4b2785 100644 --- a/src/Instrumentation/MutableString.php +++ b/src/Instrumentation/MutableString.php @@ -5,7 +5,7 @@ /** String that can be modified without invalidating offsets into it */ final class MutableString { private string $string; - // [[pos, len, newString, order]] + /** @var list [[pos, len, newString, order]] */ private array $modifications = []; public function __construct(string $string) { diff --git a/src/Instrumentation/Visitor.php b/src/Instrumentation/Visitor.php index 3dfc2a7..ed71f98 100644 --- a/src/Instrumentation/Visitor.php +++ b/src/Instrumentation/Visitor.php @@ -138,7 +138,7 @@ private function insertInlineBlockStubInStmts(Node $node): void { } else if ($endChar === ':') { $this->context->code->insert($endPos + 1, " $stub"); } else { - assert(false); + throw new \Error("Unexpected end char '$endChar'"); } } diff --git a/src/Mutation/Dictionary.php b/src/Mutation/Dictionary.php index fdc814d..bf9230b 100644 --- a/src/Mutation/Dictionary.php +++ b/src/Mutation/Dictionary.php @@ -3,13 +3,17 @@ namespace PhpFuzzer\Mutation; final class Dictionary { + /** @var list */ public array $dict = []; public function isEmpty(): bool { return empty($this->dict); } + /** + * @param list $words + */ public function addWords(array $words): void { $this->dict = [...$this->dict, ...$words]; } -} \ No newline at end of file +} diff --git a/src/Mutation/Mutator.php b/src/Mutation/Mutator.php index b1d83ff..b00f3a2 100644 --- a/src/Mutation/Mutator.php +++ b/src/Mutation/Mutator.php @@ -8,6 +8,7 @@ final class Mutator { private RNG $rng; private Dictionary $dictionary; + /** @var list */ private array $mutators; private ?string $crossOverWith = null; // TODO: Get rid of this @@ -29,6 +30,9 @@ public function __construct(RNG $rng, Dictionary $dictionary) { ]; } + /** + * @return list + */ public function getMutators(): array { return $this->mutators; } @@ -151,7 +155,7 @@ public function mutateChangeASCIIInt(string $str, int $maxLen): ?string { $int <<= 1; break; default: - assert(false); + throw new \Error("Cannot happen"); } $intStr = (string) $int; @@ -287,7 +291,7 @@ public function mutateCrossOver(string $str, int $maxLen): ?string { case 2: return $this->copyPartOf($this->crossOverWith, $str); default: - assert(false); + throw new \Error("Cannot happen"); } } @@ -336,4 +340,4 @@ public function mutate(string $str, int $maxLen, ?string $crossOverWith): string } } } -} \ No newline at end of file +} diff --git a/src/Mutation/RNG.php b/src/Mutation/RNG.php index 6d2a4cb..9b6f7bc 100644 --- a/src/Mutation/RNG.php +++ b/src/Mutation/RNG.php @@ -7,7 +7,7 @@ public function randomInt(int $maxExclusive): int { return \mt_rand(0, $maxExclusive - 1); } - public function randomIntRange(int $minInclusive, $maxInclusive): int { + public function randomIntRange(int $minInclusive, int $maxInclusive): int { return \mt_rand($minInclusive, $maxInclusive); } @@ -28,6 +28,11 @@ public function randomPosOrEnd(string $str): int { return $this->randomInt(\strlen($str) + 1); } + /** + * @template T + * @param list $array + * @return T + */ public function randomElement(array $array) { return $array[$this->randomInt(\count($array))]; } @@ -43,4 +48,4 @@ public function randomString(int $len): string { } return $result; } -} \ No newline at end of file +}