PHP-Parser 是由 nikic 开发的一个 PHP 抽象语法树(AST)解析器,可方便的将代码与抽象语法树互相转换。工程上常用来生成模板代码(如 rector)、生成抽象语法树进行静态分析(如 phpstan)。最近学习应用(静态分析)了一下,编写了一个简单的扫描发现代码中的打印、输出结构语句的命令(FindDumpStatementCommand)。
效果
流程概述
- 扫描拿到指定的 PHP 文件结果集
- 提取文件内容转化为抽象语法树
- 遍历抽象语法树节点,匹配符合要求的节点,暂存符合要求的节点信息
- 输出节点结果集信息
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();
}
}