Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1432188
phan_repl_helpers.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
20 KB
Referenced Files
None
Subscribers
None
phan_repl_helpers.php
View Options
<?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
=
"
\x
00extended_help"
):
void
{
phan_repl_help
(
$value
);
}
}
/* End function_exists('help') check */
/**
* Actual implementation of help()
*/
function
phan_repl_help
(
$value
=
"
\x
00extended_help"
):
void
{
if
(
$value
===
"
\x
00extended_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'
,
"
\x
00"
);
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
"
;
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 21:30 (1 d, 9 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
98/5b/a8cd0c4575bc83554f0e35379868
Default Alt Text
phan_repl_helpers.php (20 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment