Vai al contenuto

Ricerca AST

La modalità AST di Greph cerca nel codice sorgente PHP in base alla struttura, non al testo. I pattern sono scritti come normale codice PHP con metavariabili $VARIABLE e $$$VARIADIC. Il pattern viene analizzato da nikic/php-parser ottenendo la stessa struttura AST del sorgente in cui si cerca, poi viene confrontato nodo per nodo.

È lo stesso modello di ast-grep, e il wrapper di compatibilità sg espone l’interfaccia CLI di ast-grep sullo stesso motore.

Esempi rapidi

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

Sintassi dei pattern

Un pattern è codice sorgente PHP valido. Ovunque possa comparire un’espressione o un’istruzione reale, potete sostituire un nodo con una metavariabile per catturarlo.

Sintassi Corrispondenze Esempio
$VAR Qualsiasi singolo nodo AST, catturato come VAR $x + $y corrisponde a foo() + bar
$_ Qualsiasi singolo nodo AST, non catturato $_->method() corrisponde a qualsiasi ricevitore
$$$ARGS Zero o più nodi (variadici), catturati come ARGS func($$$ARGS) corrisponde a func(), func(1), func(1, 2, 3)
$VAR (ripetuta) La stessa $VAR deve corrispondere alla stessa struttura entrambe le volte $x == $x corrisponde a a == a, ma non a a == b

Per convenzione, il nome della metavariabile è in maiuscolo, ma non è obbligatorio. $_ è un carattere jolly che non acquisisce valori.

$$$VARIADIC funziona ovunque PHP ammetta un elenco di nodi: argomenti di funzione, elementi di array, elenchi di parametri e corpi delle istruzioni. La cattura memorizza l’intera sequenza corrispondente, anche se contiene zero elementi.

Metavariabili identificative

I nomi di classi, funzioni, interfacce, trait ed enum sono identificatori, non espressioni; quindi PHP normalmente non accetterebbe $Name dopo class. Greph riscrive questi pattern prima dell’analisi, così la metavariabile continua a funzionare:

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 stessa riscrittura avviene per class, interface, trait, enum e function.

Metavariabili ripetute

Riutilizzare una metavariabile nello stesso pattern significa che entrambe le occorrenze devono corrispondere strutturalmente allo stesso nodo. È utile per individuare codice ridondante:

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

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

La stessa regola si applica a $$$VARIADIC: due occorrenze di $$$ARGS devono corrispondere alla stessa sequenza.

Filtraggio dei file

La modalità AST attraversa l’albero dei file con lo stesso iteratore della modalità testo e, per impostazione predefinita, filtra i file di tipo php. Potete sovrascrivere il filtro con --type, --type-not, --glob, --no-ignore e --hidden. L’iteratore continua a rispettare .gitignore e .grephignore e salta i file binari.

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

Output

Il formato di output predefinito è lo stesso file:line:content della modalità testo, con il codice corrispondente compattato su una sola riga:

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

Passate --json per produrre corrispondenze strutturate:

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

Errori di analisi

Per impostazione predefinita, Greph salta senza avvisare i file che non riesce ad analizzare. È il comportamento giusto quando si opera su una base di codice ampia che contiene PHP sperimentali o generate. Passate --strict-parse (su greph-index ast-index search e greph-index ast-cache search) per generare invece l’errore di analisi, oppure impostate skipParseErrors: false su AstSearchOptions quando usate direttamente la facade.

Uso programmatico

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 è l’PhpParser\Node acquisito (quindi potete attraversarlo con NodeVisitor di PHP-Parser), mentre $match->captures associa ogni nome di metavariabile al nodo o all’elenco di nodi acquisito.

Come funziona

La pipeline è composta da:

  1. Analisi del pattern: Greph\Ast\PatternParser riscrive le metavariabili identificative, poi analizza il pattern con PHP-Parser.
  2. Analisi del sorgente: ogni file candidato viene analizzato una volta. In modalità ast-cache, questo passaggio viene sostituito dalla deserializzazione dall’archivio della cache.
  3. Filtraggio dei candidati: Greph\Ast\AstPatternPrefilter estrae dal pattern una firma approssimativa (nome della chiamata, nome della classe, tipo di istruzione) e la usa per saltare i file che chiaramente non possono contenere corrispondenze. In modalità ast-index, questo passaggio interroga invece un archivio di fatti precalcolato.
  4. Corrispondenza: Greph\Ast\PatternMatcher attraversa l’AST del sorgente e lo confronta con l’AST del pattern, associando le metavariabili ai nodi e verificando la coerenza delle variabili ripetute.
  5. Produzione dei risultati: i nodi corrispondenti vengono convertiti in oggetti AstMatch con posizioni di riga e byte e la porzione corrispondente del sorgente originale.

Per carichi di lavoro ripetuti, la ricerca AST indicizzata e la ricerca AST memorizzata nella cache saltano gran parte dei passaggi 2 e 3.