PHP-Parser 应用之扫描发现代码中的打印、输出结构语句

PHP-Parser 是由 nikic 开发的一个 PHP 抽象语法树(AST)解析器,可方便的将代码与抽象语法树互相转换。工程上常用来生成模板代码(如 rector)、生成抽象语法树进行静态分析(如 phpstan)。最近学习应用(静态分析)了一下,编写了一个简单的扫描发现代码中的打印、输出结构语句的命令(FindDumpStatementCommand)。

效果

finded-dump-statements

流程概述

  1. 扫描拿到指定的 PHP 文件结果集
  2. 提取文件内容转化为抽象语法树
  3. 遍历抽象语法树节点,匹配符合要求的节点,暂存符合要求的节点信息
  4. 输出节点结果集信息

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();
    }
}

原文链接

本作品采用《CC 协议》,转载必须注明作者和本文链接
No practice, no gain in one's wit. 我的 Gitub
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

有时候确实在一些地方进行了打印,也忘记了添加一些标志,再去找真的很烦

1年前 评论
guanguans (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
56
粉丝
126
喜欢
971
收藏
1333
排名:46
访问:15.3 万
私信
所有博文
社区赞助商