Toen ik voor het eerst hoorde over Abstract Syntax Trees (AST's) dacht ik dat het weer zo'n academisch concept was waar ik als webdeveloper toch niets mee zou doen. Tot ik ontdekte dat PHP's eigen tokenizer en tools zoals nikic/php-parser het mogelijk maken om code op een gestructureerde manier te analyseren en te transformeren. Dit opende een wereld van mogelijkheden voor geautomatiseerde refactoring die veel verder gaat dan wat je met reguliere expressies kunt bereiken.
De basis van Abstract Syntax Trees begrijpen
Abstract Syntax Trees zijn boomstructuren die de syntactische structuur van code representeren. Elke node in de boom staat voor een construct in de programmeertaal: een functie, een variabele, een if-statement, of een methodaanroep. Het verschil met gewone tekst is dat een AST de semantische betekenis van code behoudt terwijl het de syntactische details abstraheert.
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
$code = '<?php
function calculateTotal($price, $tax) {
return $price + ($price * $tax);
}';
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse($code);
$dumper = new NodeDumper;
echo $dumper->dump($ast);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
}
Deze code toont hoe je PHP-code kunt parsen naar een AST met de nikic/php-parser library. De output laat zien dat onze eenvoudige functie wordt omgezet naar een hiërarchische structuur van nodes, waarbij elke node type-informatie bevat over wat het precies representeert in de code.
Een AST-node voor een functiedeclaratie bevat bijvoorbeeld informatie over de functienaam, parameters, return type en de body van de functie. Deze gestructureerde representatie maakt het mogelijk om code te analyseren zonder je zorgen te maken over whitespace, commentaren of andere syntactische details die voor de compiler irrelevant zijn.
Patronen detecteren met AST-traversal
Het echte voordeel van AST's wordt duidelijk wanneer je code wilt analyseren op patronen die moeilijk te vangen zijn met reguliere expressies. Stel je wilt alle functies vinden die een bepaald antipatroon implementeren, zoals het direct returnen van een database query zonder error handling.
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
class DatabaseCallDetector extends NodeVisitorAbstract
{
private $suspiciousCalls = [];
public function leaveNode(Node $node)
{
if ($node instanceof Node\Stmt\Return_) {
$returnValue = $node->expr;
if ($returnValue instanceof Node\Expr\MethodCall) {
$methodName = $returnValue->name->name ?? null;
if (in_array($methodName, ['query', 'execute', 'get', 'first'])) {
$this->suspiciousCalls[] = [
'line' => $node->getLine(),
'method' => $methodName
];
}
}
}
}
public function getSuspiciousCalls()
{
return $this->suspiciousCalls;
}
}
$traverser = new NodeTraverser();
$detector = new DatabaseCallDetector();
$traverser->addVisitor($detector);
$traverser->traverse($ast);
$suspiciousCalls = $detector->getSuspiciousCalls();
Deze visitor implementatie doorloopt alle nodes in de AST en detecteert return statements die direct database methoden aanroepen. Door de AST-structuur te doorlopen kunnen we complexe patronen identificeren die contextafhankelijk zijn. De leaveNode methode wordt aangeroepen wanneer we een node verlaten tijdens de traversal, wat betekent dat alle child-nodes al zijn bezocht.
Door meerdere visitors te combineren kun je uitgebreide code analyses uitvoeren. Je kunt bijvoorbeeld detecteren waar deprecated functies worden aangeroepen, waar magic numbers voorkomen zonder constanten, of waar complexe nested if-statements vereenvoudigd kunnen worden.
Code transformaties automatiseren
Naast het analyseren van code kun je AST's ook inzetten om automatische transformaties uit te voeren. Dit gaat veel verder dan zoek-en-vervang operaties omdat je de context van de code behoudt en alleen transformeert waar het semantisch correct is.
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
class ArraySyntaxModernizer extends NodeVisitorAbstract
{
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\Array_) {
// Converteer array() syntax naar [] syntax
if ($node->getAttribute('kind') === Node\Expr\Array_::KIND_LONG) {
$node->setAttribute('kind', Node\Expr\Array_::KIND_SHORT);
}
}
return $node;
}
}
use PhpParser\PrettyPrinter;
$traverser = new NodeTraverser();
$modernizer = new ArraySyntaxModernizer();
$traverser->addVisitor($modernizer);
$modifiedAst = $traverser->traverse($ast);
$prettyPrinter = new PrettyPrinter\Standard();
$newCode = $prettyPrinter->prettyPrintFile($modifiedAst);
file_put_contents('modernized_code.php', $newCode);
Deze transformer zet automatisch alle oude array() declaraties om naar de moderne [] syntax. Het voordeel van deze aanpak is dat het alleen array declaraties transformeert en niet per ongeluk string literals of comments die toevallig "array()" bevatten.
Voor meer geavanceerde transformaties kun je complexere refactoring operaties implementeren. Ik heb bijvoorbeeld een transformer geschreven die automatisch dependency injection toevoegt aan Laravel controllers door constructor parameters te analyseren en de juiste type hints toe te voegen.
Praktische toepassingen in de development workflow
AST-based refactoring tools integreer ik in mijn dagelijkse workflow door ze te koppelen aan Git hooks of CI/CD pipelines. Dit zorgt ervoor dat bepaalde code quality checks automatisch worden uitgevoerd voordat code wordt gecommit of deployed.
class ComplexityAnalyzer extends NodeVisitorAbstract
{
private $complexity = 0;
private $functionName = '';
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Function_) {
$this->functionName = $node->name->name;
$this->complexity = 1; // Base complexity
}
// Verhoog complexity voor decision points
if ($node instanceof Node\Stmt\If_ ||
$node instanceof Node\Stmt\ElseIf_ ||
$node instanceof Node\Stmt\For_ ||
$node instanceof Node\Stmt\Foreach_ ||
$node instanceof Node\Stmt\While_ ||
$node instanceof Node\Stmt\Switch_) {
$this->complexity++;
}
// Tel ook ternary operators en null coalescing
if ($node instanceof Node\Expr\Ternary ||
$node instanceof Node\Expr\BinaryOp\Coalesce) {
$this->complexity++;
}
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Stmt\Function_) {
if ($this->complexity > 10) {
echo "Warning: Function {$this->functionName} has complexity {$this->complexity}\n";
}
}
}
}
Deze analyzer berekent de cyclomatic complexity van functies door decision points te tellen in de AST. Door dit toe te passen op een hele codebase krijg je inzicht in welke functies te complex zijn geworden en refactoring nodig hebben.
Een andere praktische toepassing is het automatisch genereren van documentatie. Door de AST te analyseren kun je automatisch PHPDoc blocks genereren voor functies die deze missen, of controleren of bestaande documentatie nog klopt met de daadwerkelijke functiesignatuur.
Het mooie van AST-based tools is dat ze language-aware zijn. Ze begrijpen PHP's syntax en semantiek, waardoor ze veel betrouwbaarder zijn dan tools die puur op string matching werken. Hierdoor kan ik met vertrouwen grootschalige refactoring operaties uitvoeren zonder bang te zijn dat ik per ongeluk werkende code breek.
Hoewel AST's in het begin overweldigend kunnen lijken, zijn ze een krachtige toevoeging aan elke developer's toolkit. Ze openen mogelijkheden voor geautomatiseerde code analyse en transformatie die anders uren handmatig werk zouden kosten, en ze doen dit met een precisie die handmatige refactoring vaak mist.