internal/fuzz_test.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare(strict_types=1);

/**
 * Utilities to fuzz test Phan when tokens are missing
 */
class FuzzTest
{
    /** @var string */
    private static $basename;

    /**
     * @return array<string,string>
     */
    private static function readFileContents(string $basename): array
    {
        $files = glob("$basename/*.php");
        $result = [];
        foreach ($files as $file) {
            if (strpos($file, '0493_') !== false) {
                // TODO: Fix https://github.com/phan/phan/issues/1988
                continue;
            }
            $contents = file_get_contents($file);
            if (!is_string($contents)) {
                throw new RuntimeException("Failed to read $file");
            }
            $result[$file] = $contents;
        }
        return $result;
    }

    /**
     * @param list<array|string> $tokens
     * @return ?list<array|string>
     */
    private static function mutateTokensByRemoval(string $path, array $tokens, int $i)
    {
        if ($i >= count($tokens)) {
            return null;
        }
        $j = ($i + crc32($path) + 71155) % count($tokens);
        unset($tokens[$j]);
        return array_values($tokens);
    }

    /**
     * @param list<array|string> $tokens
     * @return ?list<array|string>
     */
    private static function mutateTokens(string $path, array $tokens, int $i)
    {
        return self::mutateTokensByRemoval($path, $tokens, $i);
    }

    /**
     * @return void
     */
    public static function main()
    {
        self::$basename = dirname(realpath(__DIR__));
        $file_contents = array_merge(
            self::readFileContents(self::$basename . '/tests/files/src'),
            self::readFileContents(self::$basename . '/tests/php80_files/src')
        );
        $tokens_for_files = array_map('token_get_all', $file_contents);
        for ($i = 0; true; $i++) {
            $new_tokens_for_files = [];
            foreach ($tokens_for_files as $path => $tokens) {
                $new_tokens = self::mutateTokens($path, $tokens, $i);
                if ($new_tokens) {
                    $new_tokens_for_files[$path] = $new_tokens;
                }
            }
            if (!$new_tokens_for_files) {
                // No mutations left to analyze
                return;
            }

            self::analyzeTemporaryDirectory($i, $new_tokens_for_files);
        }
    }

    private static function tokensToString(array $tokens): string
    {
        $result = '';
        foreach ($tokens as $token) {
            if (is_array($token)) {
                $result .= $token[1];
            } else {
                $result .= $token;
            }
        }
        return $result;
    }

    /**
     * @return void
     */
    private static function analyzeTemporaryDirectory(int $i, array $new_tokens_for_files)
    {
        $tmp_dir = self::$basename . "/tmp/mutate$i";
        mkdir("$tmp_dir/.phan", 0766, true);
        mkdir("$tmp_dir/src", 0766, true);
        file_put_contents("$tmp_dir/.phan/config.php", <<<'EOT'
<?php
return [
    'directory_list' => ['src'],

    'check_docblock_signature_return_type_match' => true,

    'check_docblock_signature_param_type_match' => true,

    'prefer_narrowed_phpdoc_param_type' => true,

    'redundant_condition_detection' => true,

    'unused_variable_detection' => true,
    'plugins' => [
        'AlwaysReturnPlugin',
        'DemoPlugin',
        'DollarDollarPlugin',
        'UnreachableCodePlugin',
        'DuplicateArrayKeyPlugin',
        'PregRegexCheckerPlugin',
        'PrintfCheckerPlugin',
        'UnknownElementTypePlugin',
        'DuplicateExpressionPlugin',
        'NoAssertPlugin',
        'HasPHPDocPlugin',
    ],
];
EOT
        );

        foreach ($new_tokens_for_files as $file => $tokens) {
            $contents = self::tokensToString($tokens);
            $tmp_path = $tmp_dir . '/src/' . basename($file);
            file_put_contents($tmp_path, $contents);
        }

        // TODO: Use proc_open
        $cmd = self::$basename . '/phan --use-fallback-parser --always-exit-successfully-after-analysis --no-progress-bar --project-root-directory ' . $tmp_dir;
        echo "Running $cmd\n";
        system($cmd, $out_status);
        if ($out_status) {
            echo "FAILED TO RUN in $tmp_dir\n";
        }
    }
}
FuzzTest::main();