Aller au contenu

Recherche AST

Le mode AST de Greph recherche le code source PHP par structure, et non par texte. Les motifs s'écrivent comme du code PHP ordinaire avec des métavariables $VARIABLE et $$$VARIADIC. Le motif est analysé par nikic/php-parser pour obtenir la même forme d'AST que le code source recherché, puis les nœuds sont comparés un à un.

Ce modèle est le même que celui de ast-grep, et la couche de compatibilité sg expose l'interface CLI de ast-grep sur le même moteur.

Exemples rapides

Shell
# 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

Syntaxe des motifs

Un motif est du code source PHP valide. Partout où une expression ou une instruction réelle pourrait apparaître, vous pouvez substituer une métavariable pour capturer ce nœud.

Syntaxe Correspond à Exemple
$VAR Tout nœud AST unique, capturé sous le nom VAR $x + $y correspond à foo() + bar
$_ Tout nœud AST unique, sans capture $_->method() correspond à n'importe quel receveur
$$$ARGS Zéro ou plusieurs nœuds (variadique), capturés sous le nom ARGS func($$$ARGS) correspond à func(), func(1), func(1, 2, 3)
$VAR (répété) Le même $VAR doit correspondre à la même structure aux deux occurrences $x == $x correspond à a == a, mais pas à a == b

Par convention, le nom de la métavariable est en majuscules, mais ce n'est pas obligatoire. $_ est un joker sans capture.

$$$VARIADIC fonctionne partout où PHP autorise une liste de nœuds : arguments de fonction, éléments de tableau, listes de paramètres, corps d'instructions. La capture enregistre toute la séquence correspondante, même si elle est vide.

Métavariables d'identifiant

Les noms de classes, de fonctions, d'interfaces, de traits et d'énumérations sont des identifiants, et non des expressions ; PHP n'accepterait donc normalement pas $Name après class. Greph réécrit ces motifs avant l'analyse pour que la mét variable fonctionne tout de même :

Shell
./vendor/bin/greph -p 'class $NAME extends BaseController {}' src
./vendor/bin/greph -p 'function $NAME(): void {}' src
./vendor/bin/greph -p 'interface $NAME {}' src

La même réécriture s'applique à class, interface, trait, enum et function.

Métavariables répétées

Réutiliser une métavariable dans un même motif impose que ses deux occurrences correspondent structurellement au même nœud. C'est utile pour repérer du code redondant :

Shell
# Tautological comparisons
./vendor/bin/greph -p '$X == $X' src

# Redundant ternaries
./vendor/bin/greph -p '$X ? $X : null' src

La même règle s'applique à $$$VARIADIC : deux occurrences de $$$ARGS doivent correspondre à la même séquence.

Filtrer les fichiers

Le mode AST parcourt l'arborescence des fichiers avec le même outil que le mode textuel et applique par défaut le filtre de type de fichier php. Remplacez-le avec --type, --type-not, --glob, --no-ignore et --hidden. Le parcours respecte toujours .gitignore et .grephignore et ignore les fichiers binaires.

Shell
./vendor/bin/greph -p 'new $CLASS()' --glob 'src/**/*.php' .
./vendor/bin/greph -p 'new $CLASS()' --type-not phpt src

Sortie

Le format de sortie par défaut est le même que celui de file:line:content en mode textuel, avec le code correspondant regroupé sur une seule ligne :

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

Passez --json pour produire les correspondances structurées :

JSON
[
  {
    "file": "src/Greph.php",
    "start_line": 38,
    "end_line": 38,
    "start_file_pos": 1234,
    "end_file_pos": 1252,
    "code": "new FileWalker()"
  }
]

Erreurs d'analyse

Par défaut, Greph ignore silencieusement les fichiers dont l'analyse échoue. C'est le comportement adapté pour parcourir une grande base de code contenant du code PHP expérimental ou généré. Passez --strict-parse (avec greph-index ast-index search et greph-index ast-cache search) pour renvoyer l'erreur d'analyse à la place, ou définissez skipParseErrors: false sur AstSearchOptions en utilisant directement la façade.

Utilisation par programmation

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 correspond à l'PhpParser\Node capturée (vous pouvez donc la parcourir avec PHP-Parser de NodeVisitor), et $match->captures associe chaque nom de métavariable au nœud ou à la liste de nœuds capturés.

Fonctionnement

Le pipeline est le suivant :

  1. Analyse du motif : Greph\Ast\PatternParser réécrit les métavariables d'identifiant, puis analyse le motif avec PHP-Parser.
  2. Analyse du code source : chaque fichier candidat est analysé une seule fois. En mode ast-cache, cette étape est remplacée par une désérialisation depuis le cache.
  3. Filtrage des candidats : Greph\Ast\AstPatternPrefilter extrait une signature générale du motif (nom d'appel, nom de classe, type d'instruction) et s'en sert pour ignorer les fichiers qui ne peuvent manifestement pas contenir de correspondance. En mode ast-index, cette étape interroge plutôt un stockage de faits pré calculé.
  4. Comparaison : Greph\Ast\PatternMatcher parcourt l'AST du code source et le compare à l'AST du motif, en liant les métavariables au passage et en vérifiant la cohérence des variables répétées.
  5. Production des résultats : les nœuds correspondants sont convertis en objets AstMatch avec leurs positions en lignes et en octets ainsi que l'extrait source d'origine.

Pour les charges de travail répétées, la recherche AST indexée et la recherche AST avec cache ignorent la plupart des étapes 2 et 3.