Перейти до вмісту

Пошук AST

Режим AST у Greph шукає вихідний код PHP за структурою, а не за текстом. Шаблони записуються як звичайний код PHP із метазмінними $VARIABLE і $$$VARIADIC. Шаблон розбирається за допомогою nikic/php-parserпарсера у ту саму форму AST, що й код, у якому виконується пошук, а потім зіставляється вузол за вузлом.

Це та сама модель, що й ast-grep, а обгортка сумісності sg надає інтерфейс ast-grepCLI поверх того самого рушія.

Короткі приклади

Командна оболонка
# 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, а відповідний код згорнуто в один рядок:

Text
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();

Передайте --json, щоб вивести структуровані збіги:

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 під час безпосереднього використання фасаду.

Програмне використання

PHP
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 (тож його можна обійти за допомогою PHP-Parser із NodeVisitor), а $match->captures зіставляє назву кожної метазмінної із захопленим вузлом або списком вузлів.

Як це працює

Послідовність етапів:

  1. Розбір шаблону: Greph\Ast\PatternParser переписує метазмінні ідентифікаторів, а потім розбирає шаблон за допомогою PHP-Parser.
  2. Розбір вихідного коду: кожен файл-кандидат розбирається один раз. У режимі ast-cache цей етап замінює десеріалізація зі сховища кешу.
  3. Фільтрування кандидатів: Greph\Ast\AstPatternPrefilter витягує із шаблону грубий підпис (назва виклику, назва класу, тип інструкції) і пропускає файли, які очевидно не можуть містити збіг. У режимі ast-index цей етап натомість виконує запит до попередньо обчисленого сховища фактів.
  4. Зіставлення: Greph\Ast\PatternMatcher обходить AST вихідного коду та порівнює його з AST шаблону, прив'язуючи метазмінні й перевіряючи узгодженість повторних змінних.
  5. Виведення результатів: відповідні вузли перетворюються на об'єкти AstMatch із позиціями рядків і байтів та вихідним фрагментом коду.

Для повторюваних завдань індексований пошук AST і кешований пошук AST пропускають більшість етапів 2 і 3.