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

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

作者: guanguans | 来源:发表于2022-09-07 13:10 被阅读0次

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

    效果

    dumps

    流程概述

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

    原文链接

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/wtjgnrtx.html