tool/pdep
#!/usr/bin/env php
<?php
// pdep is a tool to help explore dependencies of classes.
// @phan-file-suppress PhanPluginRemoveDebugEcho
use Phan\CLIBuilder;
use Phan\Phan;
define("PDEP_IGNORE_STATIC", 1 << 0);
define("PDEP_HIDE_LABELS", 1 << 1);
/** Prints a usage message for 'pdep' and exits. */
function pdep_usage(int $status) : void {
global $argv;
echo <<<EOT
Usage: {$argv[0]} [options] [files or classes...]
-c, --find-classes Find classes that depend on the passed files or classes
-f, --find-files Find files that depend on the passed files or classes
-i, --import Import class graph from json file previously generated
-j, --json JSON output of entire class and file graphs plus metadata
unless a depth and starting node is given, then you get
a json file with just the dependency list for that node
-g, --graph Graphviz dot output
-m, --graphml GraphML output
--hide-labels Labels are hidden - hover in yEd to see them
--ignore-static Don't include static calls and static var dependencies
-d, --depth <depth_level>
When walking the dependency graph, limit it to this depth. For
example,
{$argv[0]} -f -d 1 MyClass
would show only the files that directly depend on MyClass.
-l, --file-list <filelist.txt>
-q, --quick Uses Phan's --quick mode
-p, --progress-bar Show progress bar
-h, --help This help
If no filenames or classnames are provided, it will generate the
full dependency tree.
Note that this tool will read your local .phan/config.php and pick out
the list of files to scan/not scan from there. Or, you can provide it
with a file list.
Examples:
{$argv[0]} -f src/Phan/PluginV3/PluginAwarePreAnalysisVisitor.php
{$argv[0]} -c -g '\Phan\PluginV3\PluginAwarePreAnalysisVisitor' | dot -Tpng > graph.png
{$argv[0]} -c -d 2 -g '\Phan\Language\Type\ClassStringType' | dot -Kfdp -Tpng > graph.png
EOT;
exit($status);
}
call_user_func(static function () : void {
global $argv;
$tool_dir = dirname($argv[0]);
$depth = 0;
$cmd = '';
$graph_flags = 0;
$graph_file = '';
$options = getopt(
"cfjgmhpqd:l:i:",
[
'import',
'json',
'graph',
'graphml',
'ignore-static',
'hide-labels',
'find-classes',
'find-files',
'file-list:',
'progress-bar',
'depth:',
'help',
],
$optind
);
if (isset($options['find-classes'])) {
$options['c'] = false;
}
if (isset($options['find-files'])) {
$options['f'] = false;
}
if (isset($options['depth'])) {
$options['d'] = false;
}
if (isset($options['json'])) {
$options['j'] = false;
}
if (isset($options['import'])) {
$options['i'] = false;
}
$mode = null;
if (isset($options['c'])) {
if (isset($options['f'])) {
echo "ERROR: Cannot pass both -c and -f\n";
pdep_usage(1);
}
$mode = 'class';
} elseif (isset($options['f'])) {
$mode = 'file';
}
if (isset($options['d'])) {
if (empty($mode)) {
echo "ERROR: You must specify either -c or -f\n";
pdep_usage(1);
}
}
if (isset($options['h']) || isset($options['help'])) {
pdep_usage(0);
return;
}
require_once __DIR__ . '/../src/Phan/Bootstrap.php';
$cli_builder = new CLIBuilder();
foreach ($options as $opt => $value) {
switch ($opt) {
case 'j':
case 'json':
$cmd = 'json';
break;
case 'g':
case 'graph':
$cmd = "graph";
break;
case 'm':
case 'graphml':
$cmd = "graphml";
break;
case 'ignore-static':
$graph_flags |= \PDEP_IGNORE_STATIC;
break;
case 'hide-labels':
$graph_flags |= \PDEP_HIDE_LABELS;
break;
case 'p':
case 'progress-bar':
$cli_builder->setOption('progress-bar');
break;
case 'q':
case 'quick':
$cli_builder->setOption('quick');
break;
case 'd':
case 'depth':
$depth = filter_var($value, FILTER_VALIDATE_INT);
if ($depth === false) {
echo "ERROR: Invalid depth '$value' (expected int)\n";
pdep_usage(1);
}
break;
case 'l':
case 'file-list':
// @phan-suppress-next-line PhanPossiblyNullTypeArgument
$cli_builder->setOption('file-list', $value);
break;
case 'i':
case 'import':
$cli_builder->setOption('no-progress-bar');
$graph_file = (string)$value;
break;
}
}
if ($cmd === 'json' && $depth === 0) {
$mode = '';
}
// Args for PDEP
$arg_string = implode(' ', array_slice($argv, $optind));
if (empty($arg_string) && $depth) {
echo "ERROR: You must specify a starting node when specifying a depth\n";
pdep_usage(1);
}
$cwd = \getcwd();
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$cli_builder->setOption('allow-polyfill-parser');
$cli_builder->setOption('processes', '1');
$cli_builder->setOption('plugin', dirname($tool_dir) . '/src/Phan/Plugin/Internal/DependencyGraphPlugin.php');
$cli_builder->setOption('config-file', "$cwd/.phan/pdep_config.php");
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$cli = $cli_builder->build();
$putenv = static function (string $key, string $value) : void {
putenv("$key=$value");
$_ENV[$key] = $value;
};
$putenv("PDEP_CMD", $cmd);
$putenv("PDEP_MODE", (string)$mode);
$putenv("PDEP_DEPTH", (string)$depth);
$putenv("PDEP_ARGS", $arg_string);
$putenv("PDEP_GRAPH_FLAGS", (string)$graph_flags);
// Generate codebase info after parsing configs (e.g. included_extension_subset)
$code_base = require(__DIR__ . '/../src/codebase.php');
if (isset($options['i'])) {
$data = @file_get_contents($graph_file);
if (!is_string($data)) {
echo "Unable to read graph file '$graph_file'\n";
exit(1);
}
$cached_graph = @json_decode($data, true);
if (!is_array($cached_graph)) {
echo "Invalid JSON contents of graph file '$graph_file': " . (json_last_error() !== JSON_ERROR_NONE ? json_last_error_msg() : 'expected array, got ' . gettype($cached_graph)) . "\n";
exit(1);
}
require_once __DIR__ . '/../src/Phan/Plugin/Internal/DependencyGraphPlugin.php';
(new \Phan\Plugin\Internal\DependencyGraphPlugin)->processGraph($cached_graph);
} else {
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
Phan::analyzeFileList($code_base, /** @return string[] */ static function () use($cli) : array {
return $cli->getFileList();
});
}
});