tool/phantasm

Summary

Maintainability
Test Coverage
#!/usr/bin/env php
<?php
/**
 * phantasm is a tool to assemble information about the codebase and then aggressively optimize it.
 * This modifies the files that would be analyzed by Phan.
 *
 * Currently, the only optimization step this performs is replacing class constants with simple values.
 * (Opcache can only replace class constants within the class declaring the constant, so that helps with that)
 *
 * See tests/phantasm_test for examples of the output.
 *
 * Future plans:
 *
 * - Expand the supported types of expressions that can be replaced or used as replacements
 * - Inline instance and method calls across files (e.g. methods returning constants or simple expressions)
 * - Add option to inline getters/setters, where the real type is known and there are no subclasses (by default, assume the original won't throw a TypeError)
 * - Add ways to integrate this with building a phar file (unsafe)
 * - Integrate other optimizations Phan can already do, such as --automatic-fix for NotFullyQualifiedUsagePlugin.
 * - Use Phan's signature map/documentation map information about which constants, classes, and functions/methods are internal
 *
 * @phan-file-suppress PhanPluginRemoveDebugAny
 */

use Phan\CLI;
use Phan\CLIBuilder;
use Phan\Config;
use Phan\Phan;

/**
 * Print usage for phantasm and exit.
 */
function phantasm_usage(int $status) : void {
    global $argv;
    $program = $argv[0];
    fwrite($status !== 0 ? STDERR : STDOUT, <<<EOT
Usage: $program [--in-place|--output-directory <dir>]

Options:
  --in-place: Modify files in place. This may result in loss of uncommitted changes.
  --output-directory <dir>: Save new files to a different directory.
  -h, --help: Output this help message.

EOT
    );
    exit($status);
}
call_user_func(static function () : void {
    global $argv;
    $options = getopt(
        "hop:",
        [
            'help',
            'in-place',
            'progress-bar',
            'output-directory:',
        ],
        $optind
    );
    $has_any_option = static function (string ...$arg_names) use ($options) : bool {
        foreach ($arg_names as $arg) {
            if (array_key_exists($arg, $options)) {
                return true;
            }
        }
        return false;
    };

    if ($has_any_option('h', 'help')) {
        phantasm_usage(0);
        return;
    }
    $remaining_argv = array_slice($argv, $optind);
    if (count($remaining_argv) !== 0) {
        fwrite(STDERR, "ERROR: unexpected arguments: " . json_encode($remaining_argv) . "\n");
        phantasm_usage(1);
        return;
    }

    require_once(__DIR__ . '/../src/Phan/Bootstrap.php');

    $cli_builder = new CLIBuilder();
    if ($has_any_option('p', 'progress-bar')) {
        $cli_builder->setOption('progress-bar');
    }
    $cli_builder->setOption('force-polyfill-parser-with-original-tokens');
    $cli_builder->setOption('plugin', dirname(__DIR__) . '/src/Phan/Plugin/Internal/PhantasmPlugin.php');
    // @phan-suppress-next-line PhanThrowTypeAbsentForCall
    $cli = $cli_builder->build();
    if ($has_any_option('o', 'output-directory')) {
        $dir = $options['output-directory'] ?? $options['o'] ?? null;
        if (!is_string($dir) || $dir === '') {
            CLI::printErrorToStderr("Invalid --output-directory\n");
            phantasm_usage(1);
            return;
        }
        if (isset($options['in-place'])) {
            CLI::printErrorToStderr("Cannot combine --output-directory and --in-place\n");
            phantasm_usage(1);
        }
        $resolved_dir = Config::projectPath($dir);
        if ($resolved_dir === __DIR__ || realpath($resolved_dir) === realpath(__DIR__)) {
            fwrite(STDERR, "--output-directory cannot be the same directory as the project directory. Use --in-place to overwrite a project in place\n");
            phantasm_usage(1);
        }
        fwrite(STDERR, "phantasm: Going to output new contents to '$resolved_dir'\n");

        $_ENV['PHANTASM_OUTPUT_DIRECTORY'] = $resolved_dir;
    } else if (isset($options['in-place'])) {
        $_ENV['PHANTASM_MODIFY_IN_PLACE'] = '1';
    } else {
        CLI::printErrorToStderr("Must either pass --output-directory <dir> or --in-place\n");
        phantasm_usage(1);
    }
    // Generate codebase info after parsing configs (e.g. included_extension_subset)
    $code_base = require(__DIR__ . '/../src/codebase.php');

    // @phan-suppress-next-line PhanThrowTypeAbsentForCall
    Phan::analyzeFileList($code_base, /** @return string[] */ static function () use($cli) : array {
        return $cli->getFileList();
    });
});