Dep: update
주로 PHAN
This commit is contained in:
Vendored
+42
-19
@@ -23,6 +23,7 @@ Shim::load();
|
||||
*
|
||||
* Configured stubs can be used in IDEs, analysis, etc. where extensions aren't installed or enabled (e.g. Xdebug)
|
||||
* @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod, PhanPluginNoCommentOnPublicMethod
|
||||
* @phan-file-suppress PhanPluginRemoveDebugAny
|
||||
*/
|
||||
class StubsGenerator
|
||||
{
|
||||
@@ -42,8 +43,15 @@ Usage: $prog_name [--opts]
|
||||
-h, --help
|
||||
Print this help message
|
||||
|
||||
-e, --extension
|
||||
-e, --extension extension_name
|
||||
Print stubs for only the given PECL or built in extension (e.g. 'ast', 'pcntl')
|
||||
|
||||
-d, --doc-comment
|
||||
Include doc comments from the comment summaries bundled with Phan.
|
||||
|
||||
-o, --output path/to/file
|
||||
Save the stubs to a file instead of printing them to stdout.
|
||||
|
||||
EOT;
|
||||
exit($exit_code);
|
||||
}
|
||||
@@ -54,22 +62,33 @@ EOT;
|
||||
*/
|
||||
public static function main() : void
|
||||
{
|
||||
$options = getopt('he:', ['help', 'extension:']);
|
||||
$options = getopt('he:o:d', ['help', 'extension:', 'output:', 'doc-comment']);
|
||||
if (isset($options['h']) || isset($options['help'])) {
|
||||
self::printHelpAndExit('', 0);
|
||||
}
|
||||
if (isset($options['d']) || isset($options['doc-comment'])) {
|
||||
$_ENV['PHAN_STUB_INCLUDE_DOC'] = '1';
|
||||
}
|
||||
|
||||
$code_base = require_once(dirname(__DIR__) . '/src/codebase.php');
|
||||
$code_base = require(dirname(__DIR__) . '/src/codebase.php');
|
||||
|
||||
$extension_name = $options['e'] ?? $options['extension'] ?? null;
|
||||
$output_path = $options['o'] ?? $options['output'] ?? null;
|
||||
|
||||
if (is_string($extension_name)) {
|
||||
self::printStubsForExtension($extension_name, $code_base);
|
||||
return;
|
||||
$output = self::generateStubsForExtension($extension_name, $code_base);
|
||||
} else {
|
||||
$output = self::generateAllStubs($code_base);
|
||||
}
|
||||
if (is_string($output_path)) {
|
||||
file_put_contents($output_path, $output);
|
||||
fwrite(STDERR, "Saved stubs for $extension_name to $output_path\n");
|
||||
} else {
|
||||
echo $output;
|
||||
}
|
||||
self::printAllStubs($code_base);
|
||||
}
|
||||
|
||||
private static function printStubsForExtension(string $extension_name, CodeBase $code_base) : void
|
||||
private static function generateStubsForExtension(string $extension_name, CodeBase $code_base): string
|
||||
{
|
||||
$stub_collection = new StubCollection($code_base);
|
||||
$reflection_extension = new ReflectionExtension($extension_name);
|
||||
@@ -79,12 +98,14 @@ EOT;
|
||||
self::recordGlobalFunctionStubsForExtension($stub_collection, $reflection_extension, $code_base);
|
||||
self::recordGlobalConstantStubsForExtension($stub_collection, $reflection_extension, $code_base);
|
||||
|
||||
echo "<" . "?php\n";
|
||||
echo "// These stubs were generated by the phan stub generator.\n";
|
||||
echo "// @phan-stub-for-extension $extension_name@$extension_version\n";
|
||||
echo "\n";
|
||||
$result = '';
|
||||
$result .= "<" . "?php\n";
|
||||
$result .= "// These stubs were generated by the phan stub generator.\n";
|
||||
$result .= "// @phan-stub-for-extension $extension_name@$extension_version\n";
|
||||
$result .= "\n";
|
||||
|
||||
echo $stub_collection->toString();
|
||||
$result .= $stub_collection->toString();
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function recordClassStubsForExtension(
|
||||
@@ -150,14 +171,15 @@ EOT;
|
||||
}
|
||||
}
|
||||
|
||||
public static function printAllStubs(CodeBase $code_base) : void
|
||||
public static function generateAllStubs(CodeBase $code_base): string
|
||||
{
|
||||
$code_base->eagerlyLoadAllSignatures();
|
||||
$stub_collection = new StubCollection($code_base);
|
||||
$class_map = $code_base->getInternalClassMap();
|
||||
|
||||
echo "<" . "?php\n";
|
||||
echo "// These stubs were generated by the phan stub generator.\n";
|
||||
$result = '';
|
||||
$result .= "<" . "?php\n";
|
||||
$result .= "// These stubs were generated by the phan stub generator.\n";
|
||||
|
||||
foreach ($class_map as $class) {
|
||||
$stub_collection->addClazz($class);
|
||||
@@ -176,7 +198,8 @@ EOT;
|
||||
foreach ($const_map as $const) {
|
||||
$stub_collection->addGlobalConstant($const);
|
||||
}
|
||||
echo $stub_collection->toString();
|
||||
$result .= $stub_collection->toString();
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,19 +227,19 @@ class StubCollection
|
||||
|
||||
public function addClazz(Clazz $class) : void
|
||||
{
|
||||
list($namespace, $name) = $class->toStubInfo($this->code_base);
|
||||
[$namespace, $name] = $class->toStubInfo($this->code_base);
|
||||
$this->class_stubs[$namespace][(string)$class->getFQSEN()] = $name;
|
||||
}
|
||||
|
||||
public function addGlobalConstant(GlobalConstant $global_constant) : void
|
||||
{
|
||||
list($namespace, $name) = $global_constant->toStubInfo();
|
||||
[$namespace, $name] = $global_constant->toStubInfo();
|
||||
$this->global_constant_stubs[$namespace][(string)$global_constant->getFQSEN()] = $name;
|
||||
}
|
||||
|
||||
public function addFunc(Func $function) : void
|
||||
{
|
||||
list($namespace, $name) = $function->toStubInfo();
|
||||
[$namespace, $name] = $function->toStubInfo();
|
||||
$this->function_stubs[$namespace][(string)$function->getFQSEN()] = $name;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -1,6 +1,7 @@
|
||||
#!/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;
|
||||
@@ -113,7 +114,6 @@ call_user_func(static function () : void {
|
||||
return;
|
||||
}
|
||||
|
||||
$code_base = require_once __DIR__ . '/../src/codebase.php';
|
||||
require_once __DIR__ . '/../src/Phan/Bootstrap.php';
|
||||
|
||||
$cli_builder = new CLIBuilder();
|
||||
@@ -180,7 +180,6 @@ call_user_func(static function () : void {
|
||||
|
||||
$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');
|
||||
@@ -196,6 +195,8 @@ call_user_func(static function () : void {
|
||||
$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);
|
||||
|
||||
+490
@@ -0,0 +1,490 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phan\CLI;
|
||||
use Phan\Language\Element\Comment;
|
||||
use Phan\Language\Element\MarkupDescription;
|
||||
use Phan\Library\StringUtil;
|
||||
|
||||
// On the off chance that php or an extension ever provides a global function called 'help',
|
||||
// check for this so that other utilities will work.
|
||||
if (!function_exists('help')) {
|
||||
|
||||
/**
|
||||
* tool/phan_repl_helpers.php is a utility that can be loaded after `php -a` is started.
|
||||
*
|
||||
* It provides the following:
|
||||
* - A prototype replacement for PHP's code completion, on platforms where readline was installed.
|
||||
* **This does not take advantage of Phan's inference and only reads the last line of multi-line expressions/statements.**
|
||||
* - A prototype global function `help()` which will dump information about constants/functions/objects/classes.
|
||||
*
|
||||
* The format of this will probably change.
|
||||
* - Access to an environment where Phan's bootstrapping and the project's autoloader already ran and Phan's classes can be autoloaded.
|
||||
*
|
||||
* Examples of how this can be loaded and used from a PHP shell:
|
||||
*
|
||||
* ```
|
||||
* php > require_once 'tool/phan_repl_helpers.php';
|
||||
*
|
||||
* php > help(\Phan\CLI::class);
|
||||
* Help for class Phan\CLI defined at /path/to/phan/src/Phan/CLI.php:79.
|
||||
*
|
||||
* Contains methods for parsing CLI arguments to Phan,
|
||||
* outputting to the CLI, as well as helper methods to retrieve files/folders
|
||||
* for the analyzed project.
|
||||
*
|
||||
* php > help('ast\AST_BINARY_OP');
|
||||
* Help for global constant ast\AST_BINARY_OP
|
||||
*
|
||||
* Value: 520
|
||||
*
|
||||
* A binary operation of the form `left op right`.
|
||||
* The operation is determined by the flags `ast\flags\BINARY_*`
|
||||
* (children: left, right)
|
||||
* ```
|
||||
*
|
||||
* tool/phan_repl_helpers.php also replaces the code completion capabilities
|
||||
* of `php -a` with an alternative with a different feature set.
|
||||
*
|
||||
* ```
|
||||
* php > require_once 'tool/phan_repl_helpers.php';
|
||||
* php > $object = new ArrayObject();
|
||||
* php > help($object);
|
||||
* Help for class ArrayObject defined by module SPL.
|
||||
*
|
||||
* This class allows objects to work as arrays.
|
||||
*
|
||||
* php > $object->a<TAB>
|
||||
* append asort
|
||||
* ```
|
||||
*
|
||||
* @suppress PhanUnreferencedFunction this is meant to be used interactively and is currently untested
|
||||
*/
|
||||
function help($value = "\x00extended_help"): void
|
||||
{
|
||||
phan_repl_help($value);
|
||||
}
|
||||
} /* End function_exists('help') check */
|
||||
|
||||
/**
|
||||
* Actual implementation of help()
|
||||
*/
|
||||
function phan_repl_help($value = "\x00extended_help"): void
|
||||
{
|
||||
if ($value === "\x00extended_help") {
|
||||
echo "Phan " . CLI::PHAN_VERSION . " CLI autocompletion utilities.\n";
|
||||
echo "Type help(\$value); or help('function or constant or class name'); for help.\n";
|
||||
echo "Type help('help'); for extended help.\n";
|
||||
return;
|
||||
}
|
||||
if ($value instanceof Closure || (is_string($value) && function_exists($value)) || $value instanceof ReflectionFunction) {
|
||||
$reflection_function = $value instanceof ReflectionFunction ? $value : new ReflectionFunction($value);
|
||||
$function_name = $reflection_function->getName();
|
||||
$doc_comment = $reflection_function->getDocComment();
|
||||
if ($reflection_function->isUserDefined()) {
|
||||
$details = 'defined at ' . $reflection_function->getFileName() . ':' . $reflection_function->getStartLine();
|
||||
} else {
|
||||
$details = 'defined by module ' . $reflection_function->getExtensionName();
|
||||
}
|
||||
echo "Help for function $function_name $details.\n\n";
|
||||
// TODO: Use Phan's stub generation code and handle any issues caused by inheritance?
|
||||
// echo "$reflection_function\n";
|
||||
$description = '';
|
||||
if (is_string($doc_comment)) {
|
||||
$description = MarkupDescription::extractDocComment($doc_comment, Comment::ON_FUNCTION);
|
||||
}
|
||||
if (strlen($description) > 0) {
|
||||
echo rtrim($description) . "\n\n";
|
||||
return;
|
||||
}
|
||||
$function_documentation = MarkupDescription::loadFunctionDescriptionMap()[strtolower($function_name)] ?? '';
|
||||
if ($function_documentation !== '') {
|
||||
echo rtrim($function_documentation) . "\n\n";
|
||||
return;
|
||||
}
|
||||
echo "Could not find info on $function_name\n\n";
|
||||
return;
|
||||
}
|
||||
if (is_object($value) || (is_string($value) && (class_exists($value) || trait_exists($value) || interface_exists($value)))) {
|
||||
$class_name = is_string($value) ? $value : get_class($value);
|
||||
$reflection_class = new ReflectionClass($class_name);
|
||||
$class_name = ltrim($class_name, '\\');
|
||||
if ($reflection_class->isUserDefined()) {
|
||||
$details = 'defined at ' . $reflection_class->getFileName() . ':' . $reflection_class->getStartLine();
|
||||
} else {
|
||||
$details = 'defined by module ' . $reflection_class->getExtensionName();
|
||||
}
|
||||
echo "Help for class $class_name $details.\n\n";
|
||||
$doc_comment = $reflection_class->getDocComment();
|
||||
$description = '';
|
||||
if (is_string($doc_comment)) {
|
||||
$description = MarkupDescription::extractDocComment($doc_comment, Comment::ON_CLASS);
|
||||
}
|
||||
if (strlen($description) > 0) {
|
||||
echo rtrim($description) . "\n\n";
|
||||
return;
|
||||
}
|
||||
$class_documentation = MarkupDescription::loadClassDescriptionMap()[strtolower(ltrim($class_name, '\\'))] ?? '';
|
||||
if ($class_documentation !== '') {
|
||||
echo rtrim($class_documentation) . "\n\n";
|
||||
return;
|
||||
}
|
||||
echo "Could not find info on $class_name\n\n";
|
||||
return;
|
||||
}
|
||||
if (is_string($value) && defined($value)) {
|
||||
// TODO: Make this properly case sensitive for names but not namespaces
|
||||
// TODO: Support class constants
|
||||
echo "Help for global constant $value\n\n";
|
||||
echo "Value: " . StringUtil::jsonEncode(constant($value)) . "\n\n";
|
||||
$constant_documentation = MarkupDescription::loadConstantDescriptionMap()[strtolower($value)] ?? '';
|
||||
if ($constant_documentation !== '') {
|
||||
echo rtrim($constant_documentation) . "\n\n";
|
||||
return;
|
||||
}
|
||||
echo "Could not find info on $value\n\n";
|
||||
return;
|
||||
}
|
||||
echo "Unknown value for help(). Value was:\n";
|
||||
var_dump($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOs:
|
||||
* - Take advantage of Phan's static analysis compatibilities for generating
|
||||
* readline suggestions.
|
||||
* Currently, this only reads the last 3 tokens and doesn't take advantage of Phan's inference.
|
||||
* - Support help() for remaining element types
|
||||
* - Look into alternative approaches
|
||||
* - Look into ways to get the previous lines contents when the expression/statement to be evaluated contains newlines.
|
||||
* - Integrate with other tools such as tool/phoogle to create a useful debugging environment
|
||||
*/
|
||||
|
||||
// Currently used for signature info
|
||||
require_once(__DIR__ . '/../src/Phan/Bootstrap.php');
|
||||
|
||||
// Phan's error handler terminates the process when there's an unexpected notice. This isn't helpful in an interactive shell.
|
||||
restore_error_handler();
|
||||
|
||||
/**
|
||||
* Utilities such as completions to be added to `php -a` after launching it.
|
||||
*
|
||||
* This is written as a class with public/protected methods to make it easier to extend or to unit test.
|
||||
*
|
||||
* TODO: When possible, take advantage of the code that already exists
|
||||
* in Phan to generate completion for files, keywords, etc.
|
||||
*
|
||||
* TODO: Add unit tests
|
||||
* @phan-file-suppress PhanPluginRemoveDebugAny this is a debugging utility
|
||||
* @phan-file-suppress PhanAccessMethodInternal this is bundled with phan
|
||||
*/
|
||||
class PhanPhpShellUtils
|
||||
{
|
||||
/** @var bool whether to emit debugging code */
|
||||
private $debug;
|
||||
|
||||
public function __construct(bool $debug = false)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a line to a logging file
|
||||
*/
|
||||
public function appendToLogFile(string $line): void
|
||||
{
|
||||
if (!$this->debug) {
|
||||
return;
|
||||
}
|
||||
@file_put_contents('/tmp/phan_repl_helpers.php', $line, FILE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for the current token
|
||||
*
|
||||
* @param list<int|string> $candidates
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateCompletionsFromCandidates(array $candidates, string $prefix, string $prefix_to_add_to_completion): array
|
||||
{
|
||||
$prefix_len = strlen($prefix);
|
||||
$completions = [];
|
||||
foreach ($candidates as $candidate) {
|
||||
if (!is_string($candidate)) {
|
||||
continue;
|
||||
}
|
||||
$candidate_len = strlen($candidate);
|
||||
if ($candidate_len >= $prefix_len && strncmp($prefix, $candidate, $prefix_len) === 0) {
|
||||
$completions[] = $prefix_to_add_to_completion . $candidate;
|
||||
}
|
||||
}
|
||||
return $completions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for a variable. TODO: Account for local variables
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateVariableCompletions(string $last_token_str): array
|
||||
{
|
||||
$prefix = ltrim($last_token_str, '${');
|
||||
$keys = array_keys($GLOBALS);
|
||||
$keys[] = 'GLOBALS';
|
||||
$completions = self::generateCompletionsFromCandidates($keys, $prefix, '$');
|
||||
$this->appendToLogFile("generateVariableCompletions for $last_token_str = " . StringUtil::jsonEncode($completions) . "\n");
|
||||
|
||||
return $completions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a token to a string
|
||||
* @param array{0:int,1:string,2:int}|string|false $token
|
||||
*/
|
||||
public static function tokenToString($token): string
|
||||
{
|
||||
return is_array($token) ? $token[1] : (string)$token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for accessing instance property or methods where the object instance is known
|
||||
*
|
||||
* @return list<string>
|
||||
* @suppress PhanCompatibleObjectTypePHP71
|
||||
*/
|
||||
public function generateCompletionsForInstancePropertyOfObject(object $object, string $instance_element_prefix): array
|
||||
{
|
||||
// Gets the accessible non-static properties of the given object according to scope.
|
||||
$property_candidates = array_keys(get_object_vars($object));
|
||||
$property_completions = self::generateCompletionsFromCandidates($property_candidates, $instance_element_prefix, '');
|
||||
$reflection_object = new ReflectionClass($object);
|
||||
$method_candidates = [];
|
||||
foreach ($reflection_object->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$method_candidates[] = $method->getName(); // . '('; seems to cause extra whitespace to get added
|
||||
}
|
||||
$method_completions = self::generateCompletionsFromCandidates($method_candidates, $instance_element_prefix, '');
|
||||
$completions = array_merge($property_completions, $method_completions);
|
||||
if ($method_completions && !$property_completions) {
|
||||
$this->setReadlineConfig('completion_append_character', "(");
|
||||
}
|
||||
$this->appendToLogFile("generateCompletionsForInstancePropertyOfObject completions = " . StringUtil::jsonEncode($completions) . "\n");
|
||||
return $completions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for accessing instance property or methods ($obj->prefix)
|
||||
*
|
||||
* @param list<array{0:int,1:string,2:int}|string> $tokens
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateInstanceObjectCompletions(array $tokens): array
|
||||
{
|
||||
$i = count($tokens) - 1;
|
||||
$this->appendToLogFile("generateInstanceObjectCompletions tokens = " . StringUtil::jsonEncode($tokens) . "\n");
|
||||
while (!is_array($tokens[$i]) || $tokens[$i][0] !== T_OBJECT_OPERATOR) {
|
||||
$i--;
|
||||
if ($i <= 0) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
$instance_element_prefix = self::tokenToString($tokens[$i + 1] ?? '');
|
||||
// Not definitely the expression - tolerant-php-parser would be a better way to fetch this.
|
||||
$expression = $tokens[$i - 1];
|
||||
$expression_str = self::tokenToString($expression);
|
||||
if (is_array($expression) && $expression[0] === T_VARIABLE) {
|
||||
$var_name = substr($expression_str, 1);
|
||||
$global_var = $GLOBALS[$var_name] ?? null;
|
||||
if (!is_object($global_var)) {
|
||||
return [];
|
||||
}
|
||||
return $this->generateCompletionsForInstancePropertyOfObject($global_var, $instance_element_prefix);
|
||||
}
|
||||
$this->appendToLogFile("instance_element_prefix = '$instance_element_prefix' expression=$expression_str\n");
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for SomeClass::$prefix or SomeClass::prefix
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateStaticElementSuggestionsForClass(string $class, string $instance_element_prefix): array
|
||||
{
|
||||
// TODO support ::class
|
||||
if (!class_exists($class)) {
|
||||
return [];
|
||||
}
|
||||
$reflection_class = new ReflectionClass($class);
|
||||
$property_completions = [];
|
||||
if (($instance_element_prefix[0] ?? '$') === '$') {
|
||||
// Generate completions for static properties
|
||||
$property_candidates = [];
|
||||
foreach ($reflection_class->getProperties(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_PUBLIC) as $prop) {
|
||||
$property_candidates[] = '$' . $prop->getName();
|
||||
}
|
||||
$property_completions = $this->generateCompletionsFromCandidates($property_candidates, $instance_element_prefix, '');
|
||||
if ($instance_element_prefix !== '') {
|
||||
return $property_completions;
|
||||
}
|
||||
}
|
||||
// TODO: PHP adds filtering by ReflectionClassConstant::IS_PUBLIC in 8.0
|
||||
// TODO: Make some of these case insensitive?
|
||||
|
||||
$constant_candidates = ['class'];
|
||||
foreach ($reflection_class->getReflectionConstants() as $reflection_constant) {
|
||||
if (!$reflection_constant->isPublic()) {
|
||||
continue;
|
||||
}
|
||||
$constant_candidates[] = $reflection_constant->getName();
|
||||
}
|
||||
$constant_completions = $this->generateCompletionsFromCandidates($constant_candidates, $instance_element_prefix, '');
|
||||
|
||||
$method_candidates = [];
|
||||
foreach ($reflection_class->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC) as $reflection_method) {
|
||||
if (!$reflection_method->isPublic()) {
|
||||
continue;
|
||||
}
|
||||
$method_candidates[] = $reflection_method->getName();
|
||||
}
|
||||
$method_completions = $this->generateCompletionsFromCandidates($method_candidates, $instance_element_prefix, '');
|
||||
|
||||
return array_merge(
|
||||
$property_completions,
|
||||
$constant_completions,
|
||||
$method_completions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate completions for accessing class constants, static properties or methods ($obj::prefix)
|
||||
*
|
||||
* @param list<array{0:int,1:string,2:int}|string> $tokens
|
||||
* @param string $completed_text this is the `Foo::prefix` that returned values need to begin with
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateStaticObjectCompletions(array $tokens, string $completed_text): array
|
||||
{
|
||||
$i = count($tokens) - 1;
|
||||
$this->appendToLogFile("generateStaticObjectCompletions tokens = " . StringUtil::jsonEncode($tokens) . "\n");
|
||||
while (!is_array($tokens[$i]) || $tokens[$i][0] !== T_DOUBLE_COLON) {
|
||||
$i--;
|
||||
if ($i <= 0) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
$instance_element_prefix = self::tokenToString($tokens[$i + 1] ?? '');
|
||||
// Not definitely the expression - tolerant-php-parser would be a better way to fetch this.
|
||||
$expression = $tokens[$i - 1];
|
||||
$expression_str = self::tokenToString($expression);
|
||||
if (is_array($expression) && $expression[0] === T_STRING) {
|
||||
// TODO: Check if this snippet is within a namespace block with uses, etc.
|
||||
// Or just reuse Phan's real completion abilities.
|
||||
$class_name = $expression[1];
|
||||
$pos = strrpos($completed_text, '::');
|
||||
if (!is_int($pos)) {
|
||||
return [];
|
||||
}
|
||||
$new_prefix = substr($completed_text, 0, $pos + 2);
|
||||
$completions = [];
|
||||
foreach ($this->generateStaticElementSuggestionsForClass($class_name, $instance_element_prefix) as $element_name) {
|
||||
$completions[] = $new_prefix . $element_name;
|
||||
}
|
||||
return $completions;
|
||||
}
|
||||
$this->appendToLogFile("instance_element_prefix = '$instance_element_prefix' expression=$expression_str\n");
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> a list of completions for a generic identifier
|
||||
*/
|
||||
public function generateCompletionsForGlobalName(string $prefix): array
|
||||
{
|
||||
$function_candidates = array_values(array_merge(...array_values(get_defined_functions(true))));
|
||||
$function_completions = $this->generateCompletionsFromCandidates($function_candidates, $prefix, '');
|
||||
// @phan-suppress-next-line PhanRedundantArrayValuesCall
|
||||
$other_candidates = array_values(array_merge(
|
||||
get_declared_classes(),
|
||||
get_declared_traits(),
|
||||
get_declared_interfaces(),
|
||||
array_keys(get_defined_constants())
|
||||
));
|
||||
$other_completions = $this->generateCompletionsFromCandidates($other_candidates, $prefix, '');
|
||||
if ($function_completions && !$other_completions) {
|
||||
$this->setReadlineConfig('completion_append_character', "(");
|
||||
}
|
||||
$result = array_merge($function_completions, $other_completions);
|
||||
$prefix_len = strlen($prefix);
|
||||
foreach ($result as &$val) {
|
||||
$i = strrpos(substr($val, 0, $prefix_len), '\\');
|
||||
if ($i !== false) {
|
||||
$val = substr($val, $i + 1);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|bool|int $value
|
||||
*/
|
||||
protected function setReadlineConfig(string $key, $value): void
|
||||
{
|
||||
readline_info($key, $value);
|
||||
}
|
||||
|
||||
/** Workaround to make readline not print any suggestions. Not sure how if this will work on all versions. */
|
||||
public const NO_AVAILABLE_COMPLETIONS = [''];
|
||||
|
||||
/**
|
||||
* Generate completion for any token
|
||||
* @return list<string>
|
||||
*/
|
||||
public function generateCompletions(string $text, int $start, int $end): array
|
||||
{
|
||||
$this->setReadlineConfig('completion_append_character', "\x00");
|
||||
try {
|
||||
// TODO: PHP's API only allows us to fetch the most recent line.
|
||||
$line_buffer = readline_info('line_buffer');
|
||||
$tokens = (@token_get_all('<' . '?php ' . $line_buffer)) ?: ['']; // Split up to fix vim syntax highlighting.
|
||||
$last_token = end($tokens);
|
||||
$last_token_str = self::tokenToString($last_token);
|
||||
$this->appendToLogFile("text='''$text''' start=$start end=$end line_buffer='''$line_buffer''' last_token_str='''$last_token_str'''\n");
|
||||
$c = $last_token_str[0] ?? '';
|
||||
$prev_token = prev($tokens);
|
||||
$prev_token_str = self::tokenToString($prev_token);
|
||||
if ($last_token_str === '::' || $prev_token_str === '::') {
|
||||
// Complete static members
|
||||
// (Must check if this is completing a static property instead of a variable)
|
||||
return $this->generateStaticObjectCompletions($tokens, $text) ?: self::NO_AVAILABLE_COMPLETIONS;
|
||||
} elseif ($c === '$') {
|
||||
return $this->generateVariableCompletions($last_token_str) ?: self::NO_AVAILABLE_COMPLETIONS;
|
||||
} elseif ($last_token_str === '->' || $prev_token_str === '->') {
|
||||
// TODO: Actually infer types for expressions other than variables
|
||||
return $this->generateInstanceObjectCompletions($tokens) ?: self::NO_AVAILABLE_COMPLETIONS;
|
||||
}
|
||||
if ($last_token_str === '\\') {
|
||||
if (is_array($prev_token)) {
|
||||
$prev_token_kind = $prev_token[0];
|
||||
// TODO: T_NAME_RELATIVE for namespace\
|
||||
if ($prev_token_kind === T_STRING || PHP_VERSION_ID >= 80000 && in_array($prev_token_kind, [T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) {
|
||||
$last_token_str = ltrim($prev_token[1], '\\') . $last_token_str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle completions when the text is incorrectly tokenized (e.g. 'ast\parse_')
|
||||
// That would benefit from using tolerant-php-parser to identify identifiers that contain multiple tokens (e.g. `$x = ast\parse_<TAB>`)
|
||||
// Alternately, just look for T_STRING and T_BACKSLASH and T_WHITESPACE combinations
|
||||
return $this->generateCompletionsForGlobalName($last_token_str) ?: self::NO_AVAILABLE_COMPLETIONS;
|
||||
// TODO: $c === '#' for ini completions for ini_set().
|
||||
} catch (Throwable $e) {
|
||||
$this->appendToLogFile("Caught $e");
|
||||
}
|
||||
return self::NO_AVAILABLE_COMPLETIONS;
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('readline_completion_function')) {
|
||||
readline_completion_function([new PhanPhpShellUtils(true), 'generateCompletions']);
|
||||
} else {
|
||||
echo __FILE__ . "could not install a readline_completion_function - the readline extension is unavailable\n";
|
||||
}
|
||||
Vendored
+4
-4
@@ -17,6 +17,8 @@
|
||||
* - Add ways to integrate this with building a phar file (unsafe)
|
||||
* - Integrate other optimizations Phan can already do, such as --automatic-fix for NotFullyQualifiedUsagePlugin.
|
||||
* - Use Phan's signature map/documentation map information about which constants, classes, and functions/methods are internal
|
||||
*
|
||||
* @phan-file-suppress PhanPluginRemoveDebugAny
|
||||
*/
|
||||
|
||||
use Phan\CLI;
|
||||
@@ -74,16 +76,12 @@ call_user_func(static function () : void {
|
||||
return;
|
||||
}
|
||||
|
||||
$code_base = require_once(__DIR__ . '/../src/codebase.php');
|
||||
require_once(__DIR__ . '/../src/Phan/Bootstrap.php');
|
||||
|
||||
$cli_builder = new CLIBuilder();
|
||||
if ($has_any_option('p', 'progress-bar')) {
|
||||
$cli_builder->setOption('progress-bar');
|
||||
}
|
||||
if ($has_any_option('p', 'progress-bar')) {
|
||||
$cli_builder->setOption('progress-bar');
|
||||
}
|
||||
$cli_builder->setOption('force-polyfill-parser-with-original-tokens');
|
||||
$cli_builder->setOption('plugin', dirname(__DIR__) . '/src/Phan/Plugin/Internal/PhantasmPlugin.php');
|
||||
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
|
||||
@@ -113,6 +111,8 @@ call_user_func(static function () : void {
|
||||
CLI::printErrorToStderr("Must either pass --output-directory <dir> or --in-place\n");
|
||||
phantasm_usage(1);
|
||||
}
|
||||
// Generate codebase info after parsing configs (e.g. included_extension_subset)
|
||||
$code_base = require(__DIR__ . '/../src/codebase.php');
|
||||
|
||||
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
|
||||
Phan::analyzeFileList($code_base, /** @return string[] */ static function () use($cli) : array {
|
||||
|
||||
Vendored
+7
-2
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/** phoogle is a tool to locate functions based on their signatures. */
|
||||
/**
|
||||
* phoogle is a tool to locate functions based on their signatures.
|
||||
*
|
||||
* @phan-file-suppress PhanPluginRemoveDebugAny
|
||||
*/
|
||||
|
||||
use Phan\CLIBuilder;
|
||||
use Phan\Phan;
|
||||
@@ -74,7 +78,6 @@ call_user_func(static function () : void {
|
||||
phoogle_usage(1);
|
||||
}
|
||||
|
||||
$code_base = require_once(__DIR__ . '/../src/codebase.php');
|
||||
require_once(__DIR__ . '/../src/Phan/Bootstrap.php');
|
||||
|
||||
$cli_builder = new CLIBuilder();
|
||||
@@ -95,6 +98,8 @@ call_user_func(static function () : void {
|
||||
$cli_builder->setOption('find-signature', $remaining_argv[0]);
|
||||
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
|
||||
$cli = $cli_builder->build();
|
||||
// Generate codebase info after parsing configs (e.g. included_extension_subset)
|
||||
$code_base = require(__DIR__ . '/../src/codebase.php');
|
||||
|
||||
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
|
||||
Phan::analyzeFileList($code_base, /** @return string[] */ static function () use($cli) : array {
|
||||
|
||||
Reference in New Issue
Block a user