AST търсене
AST режимът на Greph търси в изходен код на PHP по структура, а не по текст. Шаблоните се записват като обикновен код на PHP с метапроменливи $VARIABLE и $$$VARIADIC. Шаблонът се анализира от nikic/php-parser до същата форма на AST като изходния код, в който се търси, след което се съпоставя възел по възел.
Това е същият модел като при ast-grep, а съвместимият обвиващ слой sg предоставя повърхността на CLI на ast-grep върху същия механизъм.
Кратки примери
# Find every constructor call
./vendor/bin/greph -p 'new $CLASS()' src
# Find every method call with any number of arguments
./vendor/bin/greph -p '$obj->$method($$$ARGS)' src
# Find old-style array() calls
./vendor/bin/greph -p 'array($$$ITEMS)' src
# Find void-returning functions
./vendor/bin/greph -p 'function $name($$$PARAMS): void {}' src
# Find isset ternaries that should be ??
./vendor/bin/greph -p 'isset($x) ? $x : $default' src
Синтаксис на шаблоните
Шаблонът е валиден изходен код на PHP. Навсякъде, където може да стои истински израз или оператор, можете да замените възела с метапроменлива, която да го улови.
| Синтаксис | Съвпада с | Пример |
|---|---|---|
$VAR |
Всеки единичен AST възел, уловен като VAR |
$x + $y съвпада с foo() + bar |
$_ |
Всеки единичен AST възел, без улавяне | $_->method() съвпада с всеки получател |
$$$ARGS |
Нула или повече възли (вариадично), уловени като ARGS |
func($$$ARGS) съвпада с func(), func(1), func(1, 2, 3) |
$VAR (повторено) |
Същото $VAR трябва да съвпадне със същата структура и двата пъти |
$x == $x съвпада с a == a, но не и с a == b |
По правило името на метапроменливата е с главни букви, но това не е задължително. $_ е заместващ символ без улавяне.
$$$VARIADIC работи навсякъде, където PHP допуска списък от възли: аргументи на функции, елементи на масиви, списъци с параметри и тела на оператори. Улавянето съхранява цялата съвпаднала поредица (включително при нула елемента).
Метапроменливи за идентификатори
Имената на класове, функции, интерфейси, трейтове и изброени типове са идентификатори, а не изрази, затова PHP обикновено не би приел $Name след class. Greph пренаписва тези шаблони преди анализирането, така че метапроменливата да продължи да работи:
./vendor/bin/greph -p 'class $NAME extends BaseController {}' src
./vendor/bin/greph -p 'function $NAME(): void {}' src
./vendor/bin/greph -p 'interface $NAME {}' src
Същото пренаписване се извършва за class, interface, trait, enum и function.
Повтарящи се метапроменливи
Повторното използване на метапроменлива в един и същ шаблон означава, че и двете ѝ срещания трябва структурно да съвпадат със същия възел. Това е полезно за откриване на излишен код:
# Tautological comparisons
./vendor/bin/greph -p '$X == $X' src
# Redundant ternaries
./vendor/bin/greph -p '$X ? $X : null' src
Същото правило важи за $$$VARIADIC: двете срещания на $$$ARGS трябва да съвпадат с една и съща поредица.
Филтриране на файлове
AST режимът обхожда дървото с файлове чрез същия обходчик като текстовия режим и по подразбиране използва филтъра за типове файлове php. Заменете го с --type, --type-not, --glob, --no-ignore и --hidden. Обходчикът продължава да спазва .gitignore и .grephignore и пропуска двоичните файлове.
./vendor/bin/greph -p 'new $CLASS()' --glob 'src/**/*.php' .
./vendor/bin/greph -p 'new $CLASS()' --type-not phpt src
Резултат
Форматът на изхода по подразбиране е същият като file:line:content в текстов режим, като съвпадналият код е сведен до един ред:
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();
Подайте --json, за да извеждате структурирани съвпадения:
[
{
"file": "src/Greph.php",
"start_line": 38,
"end_line": 38,
"start_file_pos": 1234,
"end_file_pos": 1252,
"code": "new FileWalker()"
}
]
Грешки при анализиране
По подразбиране Greph тихомълком пропуска файлове, които не могат да бъдат анализирани. Това е подходящото поведение при работа с голяма кодова база, съдържаща експериментален или генериран PHP. Подайте --strict-parse (за greph-index ast-index search и greph-index ast-cache search), за да се изведе вместо това грешка при анализиране, или задайте skipParseErrors: false в AstSearchOptions, когато използвате директно фасадата.
Програмно използване
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
$matches = Greph::searchAst(
'$obj->$method($$$ARGS)',
'src',
new AstSearchOptions(jobs: 4),
);
foreach ($matches as $match) {
echo "{$match->file}:{$match->startLine}\n";
echo $match->code . "\n";
foreach ($match->captures as $name => $node) {
echo " {$name} = " . get_debug_type($node) . "\n";
}
}
$match->node е уловеният PhpParser\Node (така че можете да го обхождате с NodeVisitor на PHP-Parser), а $match->captures съпоставя всяко име на метапроменлива с уловения възел или списъка от възли.
Как работи
Конвейерът е следният:
- Анализиране на шаблона:
Greph\Ast\PatternParserпренаписва метапроменливите за идентификатори, след което анализира шаблона с PHP-Parser. - Анализиране на изходния код: всеки файл кандидат се анализира веднъж. В режим
ast-cacheтази стъпка се заменя с десериализация от кеш хранилището. - Филтриране на кандидатите:
Greph\Ast\AstPatternPrefilterизвлича груб подпис от шаблона (име на извикване, име на клас, тип оператор) и го използва, за да пропуска файлове, които очевидно не могат да съдържат съвпадение. В режимast-indexтази стъпка вместо това прави заявка към предварително изградено хранилище на факти. - Съпоставяне:
Greph\Ast\PatternMatcherобхожда AST на изходния код и го сравнява с AST на шаблона, като по пътя присвоява стойности на метапроменливите и налага съгласуваност между повторните срещания. - Извеждане на резултатите: съвпадналите възли се преобразуват в обекти
AstMatchс позиции на редовете и байтовете и оригиналния откъс от изходния код.
При повтарящи се натоварвания индексираното AST търсене и търсенето в кеширан AST пропускат повечето от стъпки 2 и 3.