PHP-Parser 利用之扫描发现代码中的打印、输入构造语句

PHP-Parser 是由 nikic 开发的一个 PHP 形象语法树(AST)解析器,可不便的将代码与形象语法树相互转换。工程上罕用来生成模板代码(如 rector)、生成形象语法树进行动态剖析(如 phpstan)。最近学习利用(动态剖析)了一下,编写了一个简略的扫描发现代码中的打印、输入构造语句的命令(FindDumpStatementCommand)。

成果

FindDumpStatementCommand

<?php/** * This file is part of the guanguans/laravel-skeleton. * * (c) guanguans <ityaozm@gmail.com> * * This source file is subject to the MIT license that is bundled. * * @see https://github.com/guanguans/laravel-skeleton */namespace App\Console\Commands;use Composer\XdebugHandler\XdebugHandler;use Illuminate\Console\Command;use Illuminate\Support\Str;use Illuminate\Support\Stringable;use PhpParser\Error;use PhpParser\Node;use PhpParser\NodeFinder;use PhpParser\ParserFactory;use PhpParser\PrettyPrinter\Standard;use SebastianBergmann\Timer\ResourceUsageFormatter;use SebastianBergmann\Timer\Timer;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;use Symfony\Component\Finder\Finder;use Symfony\Component\Finder\SplFileInfo;class FindDumpStatementCommand extends Command{    /** @var string */    protected $signature = '        find:dump-statement        {--dir=* : The directories to search for files}        {--path=* : The paths to search for files}        {--name=* : The names to search for files}        {--not-path=* : The paths to exclude from the search}        {--not-name=* : The names to exclude from the search}        {--s|struct=* : The structs to search}        {--f|func=* : The functions to search}        {--m|parse-mode=1 : The mode(1,2,3,4) to use for the PHP parser}        {--M|memory-limit= : The memory limit to use for the PHP parser}';    /** @var string */    protected $description = 'Find dump statements in PHP files.';    /** @var \string[][] */    private $statements = [        'struct' => [            'echo',            'print',            'die',            'exit',        ],        'func' => [            'printf',            'vprintf',            'var_dump',            'dump',            'dd',            'print_r',            'var_export'        ]    ];    /** @var \Symfony\Component\Finder\Finder */    private $fileFinder;    /** @var \PhpParser\Parser */    private $parser;    /** @var \PhpParser\NodeFinder */    private $nodeFinder;    /** @var \PhpParser\PrettyPrinter\Standard */    private $prettyPrinter;    /** @var \SebastianBergmann\Timer\ResourceUsageFormatter */    private $resourceUsageFormatter;    protected function initialize(InputInterface $input, OutputInterface $output)    {        $this->checkOptions();        $this->initializeEnvs();        $this->initializeProperties();    }    public function handle(Timer $timer)    {        $timer->start();        $this->withProgressBar($this->fileFinder, function (SplFileInfo $fileInfo) use (&$findInfos, &$odd) {            try {                $nodes = $this->parser->parse($fileInfo->getContents());            } catch (Error $e) {                $this->newLine();                $this->error(sprintf("The file of %s parse error: %s.", $fileInfo->getRealPath(), $e->getMessage()));                return;            }            $dumpNodes = $this->nodeFinder->find($nodes, function (Node $node) {                if (                    $node instanceof Node\Stmt\Expression                    && $node->expr instanceof Node\Expr\FuncCall                    && $node->expr->name instanceof Node\Name                    && in_array($node->expr->name->toString(), $this->statements['func'])                ) {                    return true;                }                return Str::of(class_basename(get_class($node)))                    ->lower()                    ->replaceLast('_', '')                    ->is($this->statements['struct']);            });            if (empty($dumpNodes)) {                return;            }            $findInfos[] = array_map(function (Node $dumpNode) use ($fileInfo, $odd) {                if ($dumpNode instanceof Node\Stmt\Expression && $dumpNode->expr instanceof Node\Expr\FuncCall) {                    $name = "<fg=cyan>{$dumpNode->expr->name->parts[0]}</>";                    $type = '<fg=cyan>func</>';                } else {                    $name = Str::of(class_basename(get_class($dumpNode)))->lower()->replaceLast('_', '')->pipe(function (Stringable $name) {                        return "<fg=red>$name</>";                    });                    $type = '<fg=red>struct</>';                }                $file = Str::of($fileInfo->getRealPath())->replace(base_path().DIRECTORY_SEPARATOR, '')->pipe(function (Stringable $file) use ($odd) {                    return $odd ? "<fg=green>$file</>" : "<fg=blue>$file</>";                });                $line = Str::of($dumpNode->getAttribute('startLine'))->pipe(function (Stringable $line) use ($odd) {                    return $odd ? "<fg=green>$line</>" : "<fg=blue>$line</>";                });                $formattedCode = Str::of($this->prettyPrinter->prettyPrint([$dumpNode]))->pipe(function (Stringable $formattedCode) use ($odd) {                    return $odd ? "<fg=green>$formattedCode</>" : "<fg=blue>$formattedCode</>";                });                return [                    'index' => null,                    'name' => $name,                    'type' => $type,                    'file' => $file,                    'line' => $line,                    'formatted_code' => $formattedCode,                ];            }, $dumpNodes);            $odd = ! $odd;        });        $this->newLine();        if (empty($findInfos)) {            $this->info('The print statement was not found.');            $this->info($this->resourceUsageFormatter->resourceUsage($timer->stop()));            return static::INVALID;        }        $findInfos = array_map(function ($info, $index) {            $index++;            $info['index'] = "<fg=yellow>$index</>";            return $info;        }, $findInfos = array_merge([], ...$findInfos), array_keys($findInfos));        $this->table(array_map(function ($name) {            return Str::of($name)->snake()->replace('_', ' ')->title();        }, array_keys($findInfos[0])), $findInfos);        $this->info($this->resourceUsageFormatter->resourceUsage($timer->stop()));        return self::SUCCESS;    }    protected function checkOptions()    {        if (! in_array($this->option('parse-mode'), [            ParserFactory::PREFER_PHP7,            ParserFactory::PREFER_PHP5,            ParserFactory::ONLY_PHP7,            ParserFactory::ONLY_PHP5])        ) {            $this->error('The parse-mode option is not valid(1,2,3,4).');            exit(1);        }        if ($this->option('struct')) {            $this->statements['struct'] = array_intersect($this->statements['struct'], $this->option('struct'));        }        if ($this->option('func')) {            $this->statements['func'] = array_intersect($this->statements['func'], $this->option('func'));        }    }    protected function initializeEnvs()    {        $xdebug = new XdebugHandler(__CLASS__);        $xdebug->check();        unset($xdebug);        extension_loaded('xdebug') and ini_set('xdebug.max_nesting_level', 2048);        ini_set('zend.assertions', 0);        $this->option('memory-limit') and ini_set('memory_limit', $this->option('memory-limit'));    }    protected function initializeProperties()    {        $this->fileFinder = tap(Finder::create()->files()->ignoreDotFiles(true)->ignoreVCS(true), function (Finder $finder) {            $methods = [                'in' => $this->option('dir') ?: [base_path()],                'path' => $this->option('path') ?: [],                'notPath' => $this->option('not-path') ?: ['vendor', 'storage'],                'name' => $this->option('name') ?: ['*.php'],                'notName' => $this->option('not-name') ?: [],            ];            foreach ($methods as $method => $parameters) {                $finder->{$method}($parameters);            }        });        $this->parser = (new ParserFactory())->create((int)$this->option('parse-mode'));        $this->nodeFinder = new NodeFinder();        $this->prettyPrinter = new Standard();        $this->resourceUsageFormatter = new ResourceUsageFormatter();    }}

原文链接

  • https://github.com/guanguans/guanguans.github.io/issues/49