Skip to content

Instantly share code, notes, and snippets.

@Nex-Otaku
Created March 2, 2023 15:11
Show Gist options
  • Save Nex-Otaku/d4efa31059831dc864c7fe8bf58c0dc0 to your computer and use it in GitHub Desktop.
Save Nex-Otaku/d4efa31059831dc864c7fe8bf58c0dc0 to your computer and use it in GitHub Desktop.
Вычисляем связи в PHP классе с помощью парсера Никиты Попова и визуализируем в Orb
<?php
require './../vendor/autoload.php';
use NexOtaku\MinimalFilesystem\Filesystem;
use PhpParser\ParserFactory;
use PhpParser\{Node, NodeTraverser, NodeVisitorAbstract};
class CallMap
{
private array $nodes = [];
private array $nodeNames = [];
private array $nodeIdsByName = [];
private int $nodeCount = 0;
private array $edges = [];
private int $edgeCount = 0;
public function addNode(string $name): void
{
if (in_array($name, $this->nodeNames)) {
return;
}
$this->nodeNames []= $name;
$nodeId = $this->nodeCount + 1;
$this->nodeIdsByName[$name] = $nodeId;
if (
str_starts_with($name, '$this->get')
|| str_starts_with($name, '$this->is')
|| str_starts_with($name, '$this->has')
|| str_starts_with($name, '$this->query')
) {
$sideEffect = 'query';
} else if (
str_starts_with($name, '$this->update')
|| str_starts_with($name, '$this->set')
|| str_starts_with($name, '$this->clear')
|| str_starts_with($name, '$this->command')
|| str_starts_with($name, '$this->switch')
) {
$sideEffect = 'command';
} else {
$sideEffect = 'default';
}
if (
str_starts_with($name, '$this->query')
|| str_starts_with($name, '$this->command')
) {
$visibility = 'public';
} else {
$visibility = 'private';
}
$this->nodes []= [
'id' => $nodeId,
'name' => $name,
'sideEffect' => $sideEffect,
'visibility' => $visibility,
];
$this->nodeCount++;
}
public function addEdge(string $from, string $to): void
{
$fromId = $this->getNodeId($from);
$toId = $this->getNodeId($to);
$edgeId = $this->edgeCount + 1;
$this->edges []= [
'id' => $edgeId,
'start' => $fromId,
'end' => $toId,
];
$this->edgeCount++;
}
private function getNodeId(string $name): int
{
if (!array_key_exists($name, $this->nodeIdsByName)) {
$this->addNode($name);
}
return $this->nodeIdsByName[$name];
}
public function getNodes(): array
{
return $this->nodes;
}
public function getEdges(): array
{
return $this->edges;
}
}
function getAst()
{
$fs = new Filesystem();
$code = $fs->readFile('./../app/Module/Demo/Xo/Xo.php');
try {
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$stmts = $parser->parse($code);
$json = json_encode($stmts, JSON_PRETTY_PRINT);
$fs->writeFile('./xo-ast.json', $json);
echo $json, "\n";
} catch (PhpParser\Error $e) {
die('Parse Error: ' . $e->getMessage());
}
return $stmts;
}
class MethodCallsVisitor extends NodeVisitorAbstract
{
private string $method = '';
private CallMap $callMap;
public function __construct()
{
$this->callMap = new CallMap();
}
public function enterNode(Node $node)
{
if ($node->getType() === 'Stmt_ClassMethod') {
$this->method = '$this->' . $node->name->name;
$this->callMap->addNode($this->method);
}
return null;
}
public function leaveNode(Node $node) {
if ($node->getType() === 'Expr_MethodCall') {
$this->callMap->addEdge($this->method, '$' . $node->var->name . '->' . $node->name->name);
}
return null;
}
public function getMap(): CallMap
{
return $this->callMap;
}
}
function writeJson(array $jsonData, string $filename): void
{
$json = json_encode($jsonData, JSON_PRETTY_PRINT);
$fs = new Filesystem();
$fs->writeFile('./' . $filename, $json);
}
$visitor = new MethodCallsVisitor();
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$stmts = getAst();
$traverser->traverse($stmts);
$map = $visitor->getMap();
writeJson($map->getNodes(), 'nodes.json');
writeJson($map->getEdges(), 'edges.json');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment