Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 5310c2e7f6
commit b306601c72
1098 changed files with 92137 additions and 33228 deletions
+52
View File
@@ -0,0 +1,52 @@
parameters:
configurationName: ''
phpVersion: ''
# A reference for testing PECL extensions using azure can be seen at https://github.com/microsoft/msphpsql/blob/master/azure-pipelines.yml
# (that extension also runs unit tests on Windows/Macs)
jobs:
- job: ${{ parameters.configurationName }}
# NOTE: This currently does not use containers. Doing so may be useful for testing on php zts/32-bit.
# Containers need to provide sudo apt-get in order to work, the php:x-cli images don't.
# Containers are slower to start up than the default vm images
pool:
vmImage: ${{ parameters.vmImage }}
steps:
- script: |
VER=${{ parameters.phpVersion }}
# Refresh the cache for the PPA repository, it can be out of date if it's updated recently
if [[ ! -f /usr/bin/phpize$VER ]]; then
sudo add-apt-repository -u ppa:ondrej/php
fi
# Silently try to install the php version if it's available.
# ondrej/php is a minimal install for php 8.0.
sudo apt-get install -y php$VER php$VER-dev php$VER-xml php$VER-mbstring
sudo update-alternatives --set php /usr/bin/php$VER
# Fail the build early if the php version isn't installed on this image
sudo update-alternatives --set phpize /usr/bin/phpize$VER || exit 1
sudo update-alternatives --set pecl /usr/bin/pecl$VER
sudo update-alternatives --set phar /usr/bin/phar$VER
sudo update-alternatives --set phpdbg /usr/bin/phpdbg$VER
sudo update-alternatives --set php-cgi /usr/bin/php-cgi$VER
sudo update-alternatives --set phar.phar /usr/bin/phar.phar$VER
sudo update-alternatives --set php-config /usr/bin/php-config$VER
displayName: Use PHP version ${{ parameters.phpVersion }}
- script: |
VER=${{ parameters.phpVersion }}
CONF_DIR=/etc/php/$VER/cli/conf.d
sudo pecl install ast-1.0.14
php --version
sudo rm -f $CONF_DIR/*xdebug.ini
echo 'extension=ast.so' | sudo tee $CONF_DIR/20-ast.ini
php -m
php --ini
composer validate
composer --prefer-dist --classmap-authoritative install
pushd internal/paratest; composer --prefer-dist --classmap-authoritative install; popd
displayName: 'Install dependencies'
- script: |
tests/run_all_tests || exit 1
php -d phar.readonly=0 internal/package.php || exit 1
displayName: 'Test phan'
+139 -48
View File
@@ -9,8 +9,8 @@ use Phan\Issue;
* default configuration. Command line arguments will be applied
* after this file is read.
*
* @see src/Phan/Config.php
* See Config for all configurable options.
* @see https://github.com/phan/phan/wiki/Phan-Config-Settings for all configurable options
* @see src/Phan/Config.php for the configurable options in this version of Phan
*
* A Note About Paths
* ==================
@@ -28,7 +28,13 @@ use Phan\Issue;
*/
return [
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`.
// The PHP version that the codebase will be checked for compatibility against.
// For best results, the PHP binary used to run Phan should have the same PHP version.
// (Phan relies on Reflection for some types, param counts,
// and checks for undefined classes/methods/functions)
//
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`,
// `'8.0'`, `'8.1'`, `null`.
// If this is set to `null`,
// then Phan assumes the PHP version which is closest to the minor version
// of the php executable used to execute Phan.
@@ -37,6 +43,16 @@ return [
// (See `backward_compatibility_checks` for additional options)
'target_php_version' => null,
// The PHP version that will be used for feature/syntax compatibility warnings.
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`,
// `'8.0'`, `'8.1'`, `null`.
// If this is set to `null`, Phan will first attempt to infer the value from
// the project's composer.json's `{"require": {"php": "version range"}}` if possible.
// If that could not be determined, then Phan assumes `target_php_version`.
//
// For analyzing Phan 3.x, this is determined to be `'7.2'` from `"version": "^7.2.0"`.
'minimum_target_php_version' => '7.2',
// Default: true. If this is set to true,
// and target_php_version is newer than the version used to run Phan,
// Phan will act as though functions added in newer PHP versions exist.
@@ -63,28 +79,30 @@ return [
// If null_casts_as_any_type is true, this has no effect.
'array_casts_as_null' => false,
// If enabled, Phan will warn if **any** type in a method's object expression
// If enabled, Phan will warn if **any** type in a method invocation's object
// is definitely not an object,
// or if **any** type in an invoked expression is not a callable.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_method_checking' => true,
// If enabled, Phan will warn if **any** type in the argument's type
// cannot be cast to a type in the parameter's expected type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check)
// If enabled, Phan will warn if **any** type in the argument's union type
// cannot be cast to a type in the parameter's expected union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_param_checking' => true,
// If enabled, Phan will warn if **any** type in a property assignment's type
// cannot be cast to a type in the property's expected type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// If enabled, Phan will warn if **any** type in a property assignment's union type
// cannot be cast to a type in the property's declared union type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
// (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check)
'strict_property_checking' => true,
// If enabled, Phan will warn if **any** type in the return statement's union type
// cannot be cast to a type in the method's declared return type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// If enabled, Phan will warn if **any** type in a returned value's union type
// cannot be cast to the declared return type.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
// (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check)
'strict_return_checking' => true,
@@ -94,20 +112,21 @@ return [
// If enabled, scalars (int, float, bool, string, null)
// are treated as if they can cast to each other.
// This does not affect checks of array keys. See scalar_array_key_cast.
// This does not affect checks of array keys. See `scalar_array_key_cast`.
'scalar_implicit_cast' => false,
// If enabled, any scalar array keys (int, string)
// are treated as if they can cast to each other.
// E.g. array<int,stdClass> can cast to array<string,stdClass> and vice versa.
// E.g. `array<int,stdClass>` can cast to `array<string,stdClass>` and vice versa.
// Normally, a scalar type such as int could only cast to/from int and mixed.
'scalar_array_key_cast' => false,
// If this has entries, scalars (int, float, bool, string, null)
// are allowed to perform the casts listed.
// E.g. ['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]
//
// E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]`
// allows casting null to a string, but not vice versa.
// (subset of scalar_implicit_cast)
// (subset of `scalar_implicit_cast`)
'scalar_implicit_partial' => [],
// If true, Phan will convert the type of a possibly undefined array offset to the nullable, defined equivalent.
@@ -115,9 +134,10 @@ return [
'convert_possibly_undefined_offset_to_nullable' => false,
// If true, seemingly undeclared variables in the global
// scope will be ignored. This is useful for projects
// with complicated cross-file globals that you have no
// hope of fixing.
// scope will be ignored.
//
// This is useful for projects with complicated cross-file
// globals that you have no hope of fixing.
'ignore_undeclared_variables_in_global_scope' => false,
// Backwards Compatibility Checking (This is very slow)
@@ -125,8 +145,7 @@ return [
// If true, check to make sure the return type declared
// in the doc-block (if any) matches the return type
// declared in the method signature. This process is
// slow.
// declared in the method signature.
'check_docblock_signature_return_type_match' => true,
// If true, check to make sure the param types declared
@@ -134,17 +153,21 @@ return [
// declared in the method signature.
'check_docblock_signature_param_type_match' => true,
// (*Requires check_docblock_signature_param_type_match to be true*)
// If true, make narrowed types from phpdoc params override
// the real types from the signature, when real types exist.
// (E.g. allows specifying desired lists of subclasses,
// or to indicate a preference for non-nullable types over nullable types)
//
// Affects analysis of the body of the method and the param types passed in by callers.
//
// (*Requires `check_docblock_signature_param_type_match` to be true*)
'prefer_narrowed_phpdoc_param_type' => true,
// (*Requires check_docblock_signature_return_type_match to be true*)
// (*Requires `check_docblock_signature_return_type_match` to be true*)
//
// If true, make narrowed types from phpdoc returns override
// the real types from the signature, when real types exist.
//
// (E.g. allows specifying desired lists of subclasses,
// or to indicate a preference for non-nullable types over nullable types)
// Affects analysis of return statements in the body of the method and the return types passed in by callers.
@@ -157,12 +180,6 @@ return [
// This will also check if final methods are overridden, etc.
'analyze_signature_compatibility' => true,
// Set this to true to allow contravariance in real parameter types of method overrides (Introduced in php 7.2)
// See https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening
// (Users may enable this if analyzing projects that support only php 7.2+)
// This is false by default. (Will warn if real parameter types are omitted in an override)
'allow_method_param_type_widening' => false,
// Set this to true to make Phan guess that undocumented parameter types
// (for optional parameters) have the same type as default values
// (Instead of combining that type with `mixed`).
@@ -187,10 +204,16 @@ return [
// Phan is slightly faster when these are disabled.
'enable_extended_internal_return_type_plugins' => true,
// This setting maps case insensitive strings to union types.
// This setting maps case-insensitive strings to union types.
//
// This is useful if a project uses phpdoc that differs from the phpdoc2 standard.
// If the corresponding value is the empty string, Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
// If the corresponding value is not empty, Phan will act as though it saw the corresponding union type when the keys show up in a UnionType of @param, @return, @var, @property, etc.
//
// If the corresponding value is the empty string,
// then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
//
// If the corresponding value is not empty,
// then Phan will act as though it saw the corresponding UnionTypes(s)
// when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc.
//
// This matches the **entire string**, not parts of the string.
// (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting)
@@ -198,7 +221,7 @@ return [
// (These are not aliases, this setting is ignored outside of doc comments).
// (Phan does not check if classes with these names exist)
//
// Example setting: ['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']
// Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']`
'phpdoc_type_mapping' => [ ],
// Set to true in order to attempt to detect dead
@@ -208,10 +231,15 @@ return [
// as variables (like `$class->$property` or
// `$class->$method()`) in ways that we're unable
// to make sense of.
//
// To more aggressively detect dead code,
// you may want to set `dead_code_detection_prefer_false_negative` to `false`.
'dead_code_detection' => false,
// Set to true in order to attempt to detect unused variables.
// dead_code_detection will also enable unused variable detection.
// `dead_code_detection` will also enable unused variable detection.
//
// This has a few known false positives, e.g. for loops or branches.
'unused_variable_detection' => true,
// Set to true in order to force tracking references to elements
@@ -321,8 +349,8 @@ return [
// Issue::SEVERITY_CRITICAL.
'minimum_severity' => Issue::SEVERITY_LOW,
// Add any issue types (such as 'PhanUndeclaredMethod')
// here to inhibit them from being reported
// Add any issue types (such as `'PhanUndeclaredMethod'`)
// to this list to inhibit them from being reported.
'suppress_issue_types' => [
'PhanUnreferencedClosure', // False positives seen with closures in arrays, TODO: move closure checks closer to what is done by unused variable plugin
'PhanPluginNoCommentOnProtectedMethod',
@@ -336,11 +364,13 @@ return [
'PhanPluginPossiblyStaticProtectedMethod',
// The types of ast\Node->children are all possibly unset.
'PhanTypePossiblyInvalidDimOffset',
// TODO: Fix PhanParamNameIndicatingUnusedInClosure instances (low priority)
'PhanParamNameIndicatingUnusedInClosure',
],
// If empty, no filter against issues types will be applied.
// If non-empty, only issues within the list will be emitted
// by Phan.
// If this list is empty, no filter against issues types will be applied.
// If this list is non-empty, only issues within the list
// will be emitted by Phan.
//
// See https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan
// for the full list of issues that Phan detects.
@@ -360,15 +390,15 @@ return [
'tool/pdep',
'tool/phantasm',
'tool/phoogle',
'tool/phan_repl_helpers.php',
'internal/dump_fallback_ast.php',
'internal/dump_html_styles.php',
'internal/extract_arg_info.php',
'internal/internalsignatures.php',
'internal/line_deleter.php',
'internal/package.php',
'internal/reflection_completeness_check.php',
'internal/sanitycheck.php',
'internal/update_wiki_config_types.php',
'internal/update_wiki_issue_types.php',
'vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlock.php',
@@ -432,6 +462,8 @@ return [
'vendor/psr/log/Psr',
'vendor/sabre/event/lib',
'vendor/symfony/console',
'vendor/symfony/polyfill-php80',
'vendor/tysonandre/var_representation_polyfill/src',
'.phan/plugins',
'.phan/stubs',
],
@@ -456,26 +488,70 @@ return [
],
// By default, Phan will log error messages to stdout if PHP is using options that slow the analysis.
// (e.g. PHP is compiled with --enable-debug or when using Xdebug)
// (e.g. PHP is compiled with `--enable-debug` or when using Xdebug)
'skip_slow_php_options_warning' => false,
// You can put paths to internal stubs in this config option.
// Phan will continue using its detailed type annotations, but load the constants, classes, functions, and classes (and their Reflection types) from these stub files (doubling as valid php files).
// Use a different extension from php to avoid accidentally loading these.
// The 'mkstubs' script can be used to generate your own stubs (compatible with php 7.0+ right now)
// The 'tool/mkstubs' script can be used to generate your own stubs (compatible with php 7.2+ right now)
//
// Also see `include_extension_subset` to configure Phan to analyze a codebase as if a certain extension is not available.
'autoload_internal_extension_signatures' => [
'ast' => '.phan/internal_stubs/ast.phan_php',
'ctype' => '.phan/internal_stubs/ctype.phan_php',
'igbinary' => '.phan/internal_stubs/igbinary.phan_php',
'mbstring' => '.phan/internal_stubs/mbstring.phan_php',
'pcntl' => '.phan/internal_stubs/pcntl.phan_php',
'phar' => '.phan/internal_stubs/phar.phan_php',
'posix' => '.phan/internal_stubs/posix.phan_php',
'readline' => '.phan/internal_stubs/readline.phan_php',
'simplexml' => '.phan/internal_stubs/simplexml.phan_php',
'sysvmsg' => '.phan/internal_stubs/sysvmsg.phan_php',
'sysvsem' => '.phan/internal_stubs/sysvsem.phan_php',
'sysvshm' => '.phan/internal_stubs/sysvshm.phan_php',
],
// This can be set to a list of extensions to limit Phan to using the reflection information of.
// If this is a list, then Phan will not use the reflection information of extensions outside of this list.
// The extensions loaded for a given php installation can be seen with `php -m` or `get_loaded_extensions(true)`.
//
// Note that this will only prevent Phan from loading reflection information for extensions outside of this set.
// If you want to add stubs, see `autoload_internal_extension_signatures`.
//
// If this is used, 'core', 'date', 'pcre', 'reflection', 'spl', and 'standard' will be automatically added.
//
// When this is an array, `ignore_undeclared_functions_with_known_signatures` will always be set to false.
// (because many of those functions will be outside of the configured list)
//
// Also see `ignore_undeclared_functions_with_known_signatures` to warn about using unknown functions.
// E.g. this is what Phan would use for self-analysis
/*
'included_extension_subset' => [
'core',
'standard',
'filter',
'json',
'tokenizer', // parsing php code
'ast', // parsing php code
'ctype', // misc uses, also polyfilled
'dom', // checkstyle output format
'iconv', // symfony mbstring polyfill
'igbinary', // serializing/unserializing polyfilled ASTs
'libxml', // internal tools for extracting stubs
'mbstring', // utf-8 support
'pcntl', // daemon/language server and parallel analysis
'phar', // packaging
'posix', // parallel analysis
'readline', // internal debugging utility, rarely used
'simplexml', // report generation
'sysvmsg', // parallelism
'sysvsem',
'sysvshm',
],
*/
// Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for,
// but aren't available in the codebase, or from Reflection.
// (may lead to false positives if an extension isn't loaded)
@@ -500,7 +576,7 @@ return [
// This may be temporarily higher if php_native_syntax_check_binaries has more elements than this process count.
'php_native_syntax_check_max_processes' => 4,
// blacklist of methods to warn about for HasPHPDocPlugin
// List of methods to suppress warnings about for HasPHPDocPlugin
'has_phpdoc_method_ignore_regex' => '@^Phan\\\\Tests\\\\.*::(test.*|.*Provider)$@',
// Warn about duplicate descriptions for methods and property groups within classes.
// (This skips over deprecated methods)
@@ -513,13 +589,17 @@ return [
// Automatically infer which methods are pure (i.e. should have no side effects) in UseReturnValuePlugin.
'infer_pure_methods' => true,
// Warn if newline is allowed before end of string for `$` (the default unless the `D` modifier (`PCRE_DOLLAR_ENDONLY`) is passed in).
// This is specific to coding styles.
'regex_warn_if_newline_allowed_at_end' => true,
],
// A list of plugin files to execute
// NOTE: values can be the base name without the extension for plugins bundled with Phan (E.g. 'AlwaysReturnPlugin')
// or relative/absolute paths to the plugin (Relative to the project root).
'plugins' => [
'AlwaysReturnPlugin',
'AlwaysReturnPlugin', // i.e. '.phan/plugin/AlwaysReturnPlugin.php' in phan itself
'DollarDollarPlugin',
'UnreachableCodePlugin',
'DuplicateArrayKeyPlugin',
@@ -561,10 +641,19 @@ return [
// These are specific to Phan's coding style
'StrictComparisonPlugin',
// Warn about `$var == SOME_INT_OR_STRING_CONST` due to unintuitive behavior such as `0 == 'a'`
'.phan/plugins/StrictLiteralComparisonPlugin.php',
'StrictLiteralComparisonPlugin',
'ShortArrayPlugin',
'SimplifyExpressionPlugin',
// 'UnknownClassElementAccessPlugin' is more useful with batch analysis than in an editor.
// It's used in tests/run_test __FakeSelfFallbackTest
// This checks that there are no accidental echos/printfs left inside Phan's code.
'RemoveDebugStatementPlugin',
'UnsafeCodePlugin',
'DeprecateAliasPlugin',
// Still have false positives to suppress
// '.phan/plugins/StaticVariableMisusePlugin.php',
////////////////////////////////////////////////////////////////////////
// End plugins for Phan's self-analysis
////////////////////////////////////////////////////////////////////////
@@ -576,6 +665,8 @@ return [
// 'PHPUnitNotDeadCodePlugin', // Marks PHPUnit test case subclasses and test cases as referenced code. This is only useful for runs when dead code detection is enabled.
// 'PHPDocInWrongCommentPlugin', // Useful to warn about using "/*" instead of ""/**" where phpdoc annotations are used. This is slow due to needing to tokenize files.
// NOTE: This plugin only produces correct results when
// Phan is run on a single core (-j1).
// 'UnusedSuppressionPlugin',
+25 -8
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ast@1.0.6
// @phan-stub-for-extension ast@1.0.12
namespace ast {
class Metadata {
@@ -22,15 +22,15 @@ class Node {
public $lineno;
// methods
public function __construct($kind = null, $flags = null, ?array $children = null, $lineno = null) {}
public function __construct(?int $kind = null, ?int $flags = null, ?array $children = null, ?int $lineno = null) {}
}
function get_kind_name($kind) {}
function get_metadata() {}
function get_supported_versions($exclude_deprecated = null) {}
function kind_uses_flags($kind) {}
function parse_code($code, $version, $filename = null) {}
function parse_file($filename, $version) {}
function get_kind_name(int $kind) : string {}
function get_metadata() : array {}
function get_supported_versions(bool $exclude_deprecated = unknown) : array {}
function kind_uses_flags(int $kind) : bool {}
function parse_code(string $code, int $version, string $filename = unknown) : \ast\Node {}
function parse_file(string $filename, int $version) : \ast\Node {}
const AST_ARG_LIST = 128;
const AST_ARRAY = 129;
const AST_ARRAY_ELEM = 525;
@@ -38,6 +38,9 @@ const AST_ARROW_FUNC = 71;
const AST_ASSIGN = 517;
const AST_ASSIGN_OP = 519;
const AST_ASSIGN_REF = 518;
const AST_ATTRIBUTE = 765;
const AST_ATTRIBUTE_GROUP = 251;
const AST_ATTRIBUTE_LIST = 253;
const AST_BINARY_OP = 520;
const AST_BREAK = 286;
const AST_CALL = 515;
@@ -47,6 +50,7 @@ const AST_CATCH_LIST = 135;
const AST_CLASS = 70;
const AST_CLASS_CONST = 516;
const AST_CLASS_CONST_DECL = 140;
const AST_CLASS_CONST_GROUP = 766;
const AST_CLASS_NAME = 276;
const AST_CLONE = 266;
const AST_CLOSURE = 68;
@@ -63,6 +67,7 @@ const AST_DO_WHILE = 533;
const AST_ECHO = 283;
const AST_EMPTY = 262;
const AST_ENCAPS_LIST = 130;
const AST_ENUM_CASE = 1022;
const AST_EXIT = 267;
const AST_EXPR_LIST = 131;
const AST_FOR = 1024;
@@ -80,14 +85,20 @@ const AST_ISSET = 263;
const AST_LABEL = 280;
const AST_LIST = 255;
const AST_MAGIC_CONST = 0;
const AST_MATCH = 764;
const AST_MATCH_ARM = 763;
const AST_MATCH_ARM_LIST = 252;
const AST_METHOD = 69;
const AST_METHOD_CALL = 768;
const AST_METHOD_REFERENCE = 540;
const AST_NAME = 2048;
const AST_NAMED_ARG = 762;
const AST_NAMESPACE = 541;
const AST_NAME_LIST = 141;
const AST_NEW = 526;
const AST_NULLABLE_TYPE = 2050;
const AST_NULLSAFE_METHOD_CALL = 1023;
const AST_NULLSAFE_PROP = 761;
const AST_PARAM = 773;
const AST_PARAM_LIST = 136;
const AST_POST_DEC = 274;
@@ -160,6 +171,7 @@ const BINARY_SPACESHIP = 170;
const BINARY_SUB = 2;
const CLASS_ABSTRACT = 64;
const CLASS_ANONYMOUS = 4;
const CLASS_ENUM = 4194304;
const CLASS_FINAL = 32;
const CLASS_INTERFACE = 1;
const CLASS_TRAIT = 2;
@@ -189,6 +201,9 @@ const MODIFIER_STATIC = 16;
const NAME_FQ = 0;
const NAME_NOT_FQ = 1;
const NAME_RELATIVE = 2;
const PARAM_MODIFIER_PRIVATE = 16;
const PARAM_MODIFIER_PROTECTED = 8;
const PARAM_MODIFIER_PUBLIC = 4;
const PARAM_REF = 1;
const PARAM_VARIADIC = 2;
const PARENTHESIZED_CONDITIONAL = 1;
@@ -200,6 +215,8 @@ const TYPE_DOUBLE = 5;
const TYPE_FALSE = 2;
const TYPE_ITERABLE = 18;
const TYPE_LONG = 4;
const TYPE_MIXED = 21;
const TYPE_NEVER = 22;
const TYPE_NULL = 1;
const TYPE_OBJECT = 8;
const TYPE_STATIC = 20;
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ctype@7.4.3-dev
// @phan-stub-for-extension ctype@7.4.19-dev
namespace {
function ctype_alnum($text) {}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension igbinary@3.1.2
// @phan-stub-for-extension igbinary@3.2.2
namespace {
function igbinary_serialize($value) {}
+4 -2
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension mbstring@7.3.8
// @phan-stub-for-extension mbstring@7.4.19-dev
namespace {
function mb_check_encoding($var = null, $encoding = null) {}
@@ -10,7 +10,7 @@ function mb_convert_encoding($str, $to, $from = null) {}
function mb_convert_kana($str, $option = null, $encoding = null) {}
function mb_convert_variables($to, $from, &...$vars) {}
function mb_decode_mimeheader($string) {}
function mb_decode_numericentity($string, $convmap, $encoding = null) {}
function mb_decode_numericentity($string, $convmap, $encoding = null, $is_hex = null) {}
function mb_detect_encoding($str, $encoding_list = null, $strict = null) {}
function mb_detect_order($encoding = null) {}
function mb_encode_mimeheader($str, $charset = null, $transfer = null, $linefeed = null, $indent = null) {}
@@ -44,6 +44,7 @@ function mb_regex_set_options($options = null) {}
function mb_scrub($str, $encoding = null) {}
function mb_send_mail($to, $subject, $message, $additional_headers = null, $additional_parameters = null) {}
function mb_split($pattern, $string, $limit = null) {}
function mb_str_split($str, $split_length = null, $encoding = null) {}
function mb_strcut($str, $start, $length = null, $encoding = null) {}
function mb_strimwidth($str, $start, $width, $trimmarker = null, $encoding = null) {}
function mb_stripos($haystack, $needle, $offset = null, $encoding = null) {}
@@ -83,6 +84,7 @@ const MB_CASE_TITLE = 2;
const MB_CASE_TITLE_SIMPLE = 6;
const MB_CASE_UPPER = 0;
const MB_CASE_UPPER_SIMPLE = 4;
const MB_ONIGURUMA_VERSION = '6.9.4';
const MB_OVERLOAD_MAIL = 1;
const MB_OVERLOAD_REGEX = 4;
const MB_OVERLOAD_STRING = 2;
+2 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension pcntl@7.4.3-dev
// @phan-stub-for-extension pcntl@7.4.19-dev
namespace {
function pcntl_alarm($seconds) {}
@@ -37,6 +37,7 @@ const CLD_EXITED = 1;
const CLD_KILLED = 2;
const CLD_STOPPED = 5;
const CLD_TRAPPED = 4;
const CLONE_NEWCGROUP = 33554432;
const CLONE_NEWIPC = 134217728;
const CLONE_NEWNET = 1073741824;
const CLONE_NEWNS = 131072;
+199
View File
@@ -0,0 +1,199 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension phar@7.4.19-dev
namespace {
class Phar extends \RecursiveDirectoryIterator implements \Countable, \ArrayAccess {
// constants
const CURRENT_MODE_MASK = 240;
const CURRENT_AS_PATHNAME = 32;
const CURRENT_AS_FILEINFO = 0;
const CURRENT_AS_SELF = 16;
const KEY_MODE_MASK = 3840;
const KEY_AS_PATHNAME = 0;
const FOLLOW_SYMLINKS = 512;
const KEY_AS_FILENAME = 256;
const NEW_CURRENT_AND_KEY = 256;
const OTHER_MODE_MASK = 12288;
const SKIP_DOTS = 4096;
const UNIX_PATHS = 8192;
const BZ2 = 8192;
const GZ = 4096;
const NONE = 0;
const PHAR = 1;
const TAR = 2;
const ZIP = 3;
const COMPRESSED = 61440;
const PHP = 0;
const PHPS = 1;
const MD5 = 1;
const OPENSSL = 16;
const SHA1 = 2;
const SHA256 = 3;
const SHA512 = 4;
// methods
public function __construct($filename, $flags = null, $alias = null) {}
public function __destruct() {}
public function addEmptyDir($dirname = null) {}
public function addFile($filename, $localname = null) {}
public function addFromString($localname, $contents = null) {}
public function buildFromDirectory($base_dir, $regex = null) {}
public function buildFromIterator($iterator, $base_directory = null) {}
public function compressFiles($compression_type) {}
public function decompressFiles() {}
public function compress($compression_type, $file_ext = null) {}
public function decompress($file_ext = null) {}
public function convertToExecutable($format = null, $compression_type = null, $file_ext = null) {}
public function convertToData($format = null, $compression_type = null, $file_ext = null) {}
public function copy($newfile, $oldfile) {}
public function count() {}
public function delete($entry) {}
public function delMetadata() {}
public function extractTo($pathto, $files = null, $overwrite = null) {}
public function getAlias() {}
public function getPath() {}
public function getMetadata() {}
public function getModified() {}
public function getSignature() {}
public function getStub() {}
public function getVersion() {}
public function hasMetadata() {}
public function isBuffering() {}
public function isCompressed() {}
public function isFileFormat($fileformat) {}
public function isWritable() {}
public function offsetExists($entry) {}
public function offsetGet($entry) {}
public function offsetSet($entry, $value) {}
public function offsetUnset($entry) {}
public function setAlias($alias) {}
public function setDefaultStub($index = null, $webindex = null) {}
public function setMetadata($metadata) {}
public function setSignatureAlgorithm($algorithm, $privatekey = null) {}
public function setStub($newstub, $maxlen = null) {}
public function startBuffering() {}
public function stopBuffering() {}
final public static function apiVersion() {}
final public static function canCompress($method = null) {}
final public static function canWrite() {}
final public static function createDefaultStub($index = null, $webindex = null) {}
final public static function getSupportedCompression() {}
final public static function getSupportedSignatures() {}
final public static function interceptFileFuncs() {}
final public static function isValidPharFilename($filename, $executable = null) {}
final public static function loadPhar($filename, $alias = null) {}
final public static function mapPhar($alias = null, $offset = null) {}
final public static function running($retphar = null) {}
final public static function mount($inphar, $externalfile) {}
final public static function mungServer($munglist) {}
final public static function unlinkArchive($archive) {}
final public static function webPhar($alias = null, $index = null, $f404 = null, $mimetypes = null, $rewrites = null) {}
}
class PharData extends \RecursiveDirectoryIterator implements \Countable, \ArrayAccess {
// constants
const CURRENT_MODE_MASK = 240;
const CURRENT_AS_PATHNAME = 32;
const CURRENT_AS_FILEINFO = 0;
const CURRENT_AS_SELF = 16;
const KEY_MODE_MASK = 3840;
const KEY_AS_PATHNAME = 0;
const FOLLOW_SYMLINKS = 512;
const KEY_AS_FILENAME = 256;
const NEW_CURRENT_AND_KEY = 256;
const OTHER_MODE_MASK = 12288;
const SKIP_DOTS = 4096;
const UNIX_PATHS = 8192;
// methods
public function __construct($filename, $flags = null, $alias = null, $fileformat = null) {}
public function __destruct() {}
public function addEmptyDir($dirname = null) {}
public function addFile($filename, $localname = null) {}
public function addFromString($localname, $contents = null) {}
public function buildFromDirectory($base_dir, $regex = null) {}
public function buildFromIterator($iterator, $base_directory = null) {}
public function compressFiles($compression_type) {}
public function decompressFiles() {}
public function compress($compression_type, $file_ext = null) {}
public function decompress($file_ext = null) {}
public function convertToExecutable($format = null, $compression_type = null, $file_ext = null) {}
public function convertToData($format = null, $compression_type = null, $file_ext = null) {}
public function copy($newfile, $oldfile) {}
public function count() {}
public function delete($entry) {}
public function delMetadata() {}
public function extractTo($pathto, $files = null, $overwrite = null) {}
public function getAlias() {}
public function getPath() {}
public function getMetadata() {}
public function getModified() {}
public function getSignature() {}
public function getStub() {}
public function getVersion() {}
public function hasMetadata() {}
public function isBuffering() {}
public function isCompressed() {}
public function isFileFormat($fileformat) {}
public function isWritable() {}
public function offsetExists($entry) {}
public function offsetGet($entry) {}
public function offsetSet($entry, $value) {}
public function offsetUnset($entry) {}
public function setAlias($alias) {}
public function setDefaultStub($index = null, $webindex = null) {}
public function setMetadata($metadata) {}
public function setSignatureAlgorithm($algorithm, $privatekey = null) {}
public function setStub($newstub, $maxlen = null) {}
public function startBuffering() {}
public function stopBuffering() {}
final public static function apiVersion() {}
final public static function canCompress($method = null) {}
final public static function canWrite() {}
final public static function createDefaultStub($index = null, $webindex = null) {}
final public static function getSupportedCompression() {}
final public static function getSupportedSignatures() {}
final public static function interceptFileFuncs() {}
final public static function isValidPharFilename($filename, $executable = null) {}
final public static function loadPhar($filename, $alias = null) {}
final public static function mapPhar($alias = null, $offset = null) {}
final public static function running($retphar = null) {}
final public static function mount($inphar, $externalfile) {}
final public static function mungServer($munglist) {}
final public static function unlinkArchive($archive) {}
final public static function webPhar($alias = null, $index = null, $f404 = null, $mimetypes = null, $rewrites = null) {}
}
class PharException extends \Exception {
// properties
protected $message;
protected $code;
protected $file;
protected $line;
}
class PharFileInfo extends \SplFileInfo {
// methods
public function __construct($filename) {}
public function __destruct() {}
public function chmod($perms) {}
public function compress($compression_type) {}
public function decompress() {}
public function delMetadata() {}
public function getCompressedSize() {}
public function getCRC32() {}
public function getContent() {}
public function getMetadata() {}
public function getPharFlags() {}
public function hasMetadata() {}
public function isCompressed($compression_type = null) {}
public function isCRCChecked() {}
public function setMetadata($metadata) {}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension posix@7.4.3-dev
// @phan-stub-for-extension posix@7.4.19-dev
namespace {
function posix_access($file, $mode = null) {}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension readline@7.4.3-dev
// @phan-stub-for-extension readline@7.4.19-dev
namespace {
function readline($prompt = null) {}
@@ -0,0 +1,43 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension simplexml@7.4.19-dev
namespace {
class SimpleXMLElement implements \Traversable, \Countable {
// methods
final public function __construct($data, $options = null, $data_is_url = null, $ns = null, $is_prefix = null) {}
public function asXML($filename = null) {}
public function saveXML($filename = null) {}
public function xpath($path) {}
public function registerXPathNamespace($prefix, $ns) {}
public function attributes($ns = null, $is_prefix = null) {}
public function children($ns = null, $is_prefix = null) {}
public function getNamespaces($recursve = null) {}
public function getDocNamespaces($recursve = null, $from_root = null) {}
public function getName() {}
public function addChild($name, $value = null, $ns = null) {}
public function addAttribute($name, $value = null, $ns = null) {}
public function __toString() {}
public function count() {}
}
class SimpleXMLIterator extends \SimpleXMLElement implements \RecursiveIterator, \Iterator {
// properties
public $name;
// methods
public function rewind() {}
public function valid() {}
public function current() {}
public function key() {}
public function next() {}
public function hasChildren() {}
public function getChildren() {}
}
function simplexml_import_dom($node, $class_name = null) {}
function simplexml_load_file($filename, $class_name = null, $options = null, $ns = null, $is_prefix = null) {}
function simplexml_load_string($data, $class_name = null, $options = null, $ns = null, $is_prefix = null) {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvmsg@7.4.3-dev
// @phan-stub-for-extension sysvmsg@7.4.19-dev
namespace {
function msg_get_queue($key, $perms = null) {}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvsem@7.4.3-dev
// @phan-stub-for-extension sysvsem@7.4.19-dev
namespace {
function sem_acquire($sem_identifier, $nowait = null) {}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvshm@7.4.3-dev
// @phan-stub-for-extension sysvshm@7.4.19-dev
namespace {
function shm_attach($key, $memsize = null, $perm = null) {}
+3 -1
View File
@@ -8,6 +8,7 @@ use Phan\CodeBase;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Type\NeverType;
use Phan\Language\Type\NullType;
use Phan\Language\Type\VoidType;
use Phan\PluginV3;
@@ -152,7 +153,8 @@ final class AlwaysReturnPlugin extends PluginV3 implements
}
$return_type = $func->getUnionType();
return ($return_type->isEmpty()
|| $return_type->containsNullable()
|| $return_type->containsNullableLabeled()
|| $return_type->hasType(NeverType::instance(false))
|| $return_type->hasType(VoidType::instance(false))
|| $return_type->hasType(NullType::instance(false)));
}
+2 -2
View File
@@ -3,7 +3,7 @@
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\ConditionVisitorUtil;
use Phan\Analysis\ConditionVisitor;
use Phan\AST\ASTReverter;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
@@ -85,7 +85,7 @@ class AvoidableGetterVisitor extends PluginAwarePostAnalysisVisitor
return;
// This only supports instance method getters, not static getters (AST_STATIC_CALL)
case ast\AST_METHOD_CALL:
if (!ConditionVisitorUtil::isThisVarNode($node->children['expr'])) {
if (!ConditionVisitor::isThisVarNode($node->children['expr'])) {
break;
}
$method_name = $node->children['method'];
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\PhanAnnotationAdder;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin detects variables with constant values
*/
class ConstantVariablePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return ConstantVariableVisitor::class;
}
}
/**
* This plugin checks if variable uses have constant values.
*/
class ConstantVariableVisitor extends PluginAwarePostAnalysisVisitor
{
/** @var Node[] the parent nodes of the analyzed node */
protected $parent_node_list;
// A plugin's visitors should not override visit() unless they need to.
/** @override */
public function visitVar(Node $node): void
{
// @phan-suppress-next-line PhanUndeclaredProperty
if ($node->flags & PhanAnnotationAdder::FLAG_INITIALIZES || isset($node->is_reference)) {
return;
}
$var_name = $node->children['name'];
if (!is_string($var_name)) {
return;
}
if ($this->context->isInLoop() || $this->context->isInGlobalScope()) {
return;
}
$parent_node = end($this->parent_node_list);
if ($parent_node instanceof Node) {
switch ($parent_node->kind) {
case ast\AST_IF_ELEM:
// Phan modifies type to match condition before plugins are called.
// --redundant-condition-detection would warn
return;
case ast\AST_ASSIGN_OP:
if ($parent_node->children['var'] === $node) {
return;
}
break;
}
}
$variable = $this->context->getScope()->getVariableByNameOrNull($var_name);
if (!$variable) {
return;
}
$type = $variable->getUnionType();
if ($type->isPossiblyUndefined()) {
return;
}
$value = $type->getRealUnionType()->asSingleScalarValueOrNullOrSelf();
if (is_object($value)) {
return;
}
// TODO: Account for methods expecting references
if (is_bool($value)) {
$issue_type = 'PhanPluginConstantVariableBool';
} elseif (is_null($value)) {
$issue_type = 'PhanPluginConstantVariableNull';
} else {
$issue_type = 'PhanPluginConstantVariableScalar';
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
$issue_type,
'Variable ${VARIABLE} is probably constant with a value of {TYPE}',
[$var_name, $type]
);
}
}
return new ConstantVariablePlugin();
+393
View File
@@ -0,0 +1,393 @@
<?php
declare(strict_types=1);
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\PluginV3;
use Phan\PluginV3\BeforeAnalyzePhaseCapability;
/**
* This plugin deprecates aliases of global functions.
*
* See https://www.php.net/manual/en/aliases.php
*/
class DeprecateAliasPlugin extends PluginV3 implements
BeforeAnalyzePhaseCapability
{
/**
* Source: https://www.php.net/manual/en/aliases.php
* TODO: Extract from php signatures instead?
*/
const KNOWN_ALIASES = [
// Deliberately not warning about `_` given how common it can be and unlikeliness of it being deprecated in the future.
// TODO: Provide ways to add or remove aliases to warn about?
//'_' => 'gettext',
'add' => 'swfmovie_add',
//'add' => 'swfsprite_add and others',
'addaction' => 'swfbutton_addAction',
'addcolor' => 'swfdisplayitem_addColor',
'addentry' => 'swfgradient_addEntry',
'addfill' => 'swfshape_addfill',
'addshape' => 'swfbutton_addShape',
'addstring' => 'swftext_addString and others',
//'addstring' => 'swftextfield_addString',
'align' => 'swftextfield_align',
'chop' => 'rtrim',
'close' => 'closedir',
'com_get' => 'com_propget',
'com_propset' => 'com_propput',
'com_set' => 'com_propput',
'die' => 'exit',
'diskfreespace' => 'disk_free_space',
'doubleval' => 'floatval',
'drawarc' => 'swfshape_drawarc',
'drawcircle' => 'swfshape_drawcircle',
'drawcubic' => 'swfshape_drawcubic',
'drawcubicto' => 'swfshape_drawcubicto',
'drawcurve' => 'swfshape_drawcurve',
'drawcurveto' => 'swfshape_drawcurveto',
'drawglyph' => 'swfshape_drawglyph',
'drawline' => 'swfshape_drawline',
'drawlineto' => 'swfshape_drawlineto',
'fbsql' => 'fbsql_db_query',
'fputs' => 'fwrite',
'getascent' => 'swffont_getAscent and others',
//'getascent' => 'swftext_getAscent',
'getdescent' => 'swffont_getDescent and others',
//'getdescent' => 'swftext_getDescent',
'getheight' => 'swfbitmap_getHeight',
'getleading' => 'swffont_getLeading and others and others',
//'getleading' => 'swftext_getLeading',
'getshape1' => 'swfmorph_getShape1',
'getshape2' => 'swfmorph_getShape2',
'getwidth' => 'swfbitmap_getWidth and others',
//'getwidth' => 'swffont_getWidth',
//'getwidth' => 'swftext_getWidth',
'gzputs' => 'gzwrite',
'i18n_convert' => 'mb_convert_encoding',
'i18n_discover_encoding' => 'mb_detect_encoding',
'i18n_http_input' => 'mb_http_input',
'i18n_http_output' => 'mb_http_output',
'i18n_internal_encoding' => 'mb_internal_encoding',
'i18n_ja_jp_hantozen' => 'mb_convert_kana',
'i18n_mime_header_decode' => 'mb_decode_mimeheader',
'i18n_mime_header_encode' => 'mb_encode_mimeheader',
'imap_create' => 'imap_createmailbox',
'imap_fetchtext' => 'imap_body',
'imap_getmailboxes' => 'imap_list_full',
'imap_getsubscribed' => 'imap_lsub_full',
'imap_header' => 'imap_headerinfo',
'imap_listmailbox' => 'imap_list',
'imap_listsubscribed' => 'imap_lsub',
'imap_rename' => 'imap_renamemailbox',
'imap_scan' => 'imap_listscan',
'imap_scanmailbox' => 'imap_listscan',
'ini_alter' => 'ini_set',
'is_double' => 'is_float',
'is_integer' => 'is_int',
'is_long' => 'is_int',
'is_real' => 'is_float',
'is_writeable' => 'is_writable',
'join' => 'implode',
'key_exists' => 'array_key_exists',
'labelframe' => 'swfmovie_labelFrame and others',
//'labelframe' => 'swfsprite_labelFrame',
'ldap_close' => 'ldap_unbind',
'magic_quotes_runtime' => 'set_magic_quotes_runtime',
'mbstrcut' => 'mb_strcut',
'mbstrlen' => 'mb_strlen',
'mbstrpos' => 'mb_strpos',
'mbstrrpos' => 'mb_strrpos',
'mbsubstr' => 'mb_substr',
'ming_setcubicthreshold' => 'ming_setCubicThreshold',
'ming_setscale' => 'ming_setScale',
'move' => 'swfdisplayitem_move',
'movepen' => 'swfshape_movepen',
'movepento' => 'swfshape_movepento',
'moveto' => 'swfdisplayitem_moveTo and others',
//'moveto' => 'swffill_moveTo',
//'moveto' => 'swftext_moveTo',
'msql' => 'msql_db_query',
'msql_createdb' => 'msql_create_db',
'msql_dbname' => 'msql_result',
'msql_dropdb' => 'msql_drop_db',
'msql_fieldflags' => 'msql_field_flags',
'msql_fieldlen' => 'msql_field_len',
'msql_fieldname' => 'msql_field_name',
'msql_fieldtable' => 'msql_field_table',
'msql_fieldtype' => 'msql_field_type',
'msql_freeresult' => 'msql_free_result',
'msql_listdbs' => 'msql_list_dbs',
'msql_listfields' => 'msql_list_fields',
'msql_listtables' => 'msql_list_tables',
'msql_numfields' => 'msql_num_fields',
'msql_numrows' => 'msql_num_rows',
'msql_regcase' => 'sql_regcase',
'msql_selectdb' => 'msql_select_db',
'msql_tablename' => 'msql_result',
'mssql_affected_rows' => 'sybase_affected_rows',
'mssql_close' => 'sybase_close',
'mssql_connect' => 'sybase_connect',
'mssql_data_seek' => 'sybase_data_seek',
'mssql_fetch_array' => 'sybase_fetch_array',
'mssql_fetch_field' => 'sybase_fetch_field',
'mssql_fetch_object' => 'sybase_fetch_object',
'mssql_fetch_row' => 'sybase_fetch_row',
'mssql_field_seek' => 'sybase_field_seek',
'mssql_free_result' => 'sybase_free_result',
'mssql_get_last_message' => 'sybase_get_last_message',
'mssql_min_client_severity' => 'sybase_min_client_severity',
'mssql_min_error_severity' => 'sybase_min_error_severity',
'mssql_min_message_severity' => 'sybase_min_message_severity',
'mssql_min_server_severity' => 'sybase_min_server_severity',
'mssql_num_fields' => 'sybase_num_fields',
'mssql_num_rows' => 'sybase_num_rows',
'mssql_pconnect' => 'sybase_pconnect',
'mssql_query' => 'sybase_query',
'mssql_result' => 'sybase_result',
'mssql_select_db' => 'sybase_select_db',
'multcolor' => 'swfdisplayitem_multColor',
'mysql' => 'mysql_db_query',
'mysql_createdb' => 'mysql_create_db',
'mysql_db_name' => 'mysql_result',
'mysql_dbname' => 'mysql_result',
'mysql_dropdb' => 'mysql_drop_db',
'mysql_fieldflags' => 'mysql_field_flags',
'mysql_fieldlen' => 'mysql_field_len',
'mysql_fieldname' => 'mysql_field_name',
'mysql_fieldtable' => 'mysql_field_table',
'mysql_fieldtype' => 'mysql_field_type',
'mysql_freeresult' => 'mysql_free_result',
'mysql_listdbs' => 'mysql_list_dbs',
'mysql_listfields' => 'mysql_list_fields',
'mysql_listtables' => 'mysql_list_tables',
'mysql_numfields' => 'mysql_num_fields',
'mysql_numrows' => 'mysql_num_rows',
'mysql_selectdb' => 'mysql_select_db',
'mysql_tablename' => 'mysql_result',
'nextframe' => 'swfmovie_nextFrame and others',
//'nextframe' => 'swfsprite_nextFrame',
'ociassignelem' => 'OCI-Collection::assignElem',
'ocibindbyname' => 'oci_bind_by_name',
'ocicancel' => 'oci_cancel',
'ocicloselob' => 'OCI-Lob::close',
'ocicollappend' => 'OCI-Collection::append',
'ocicollassign' => 'OCI-Collection::assign',
'ocicollmax' => 'OCI-Collection::max',
'ocicollsize' => 'OCI-Collection::size',
'ocicolltrim' => 'OCI-Collection::trim',
'ocicolumnisnull' => 'oci_field_is_null',
'ocicolumnname' => 'oci_field_name',
'ocicolumnprecision' => 'oci_field_precision',
'ocicolumnscale' => 'oci_field_scale',
'ocicolumnsize' => 'oci_field_size',
'ocicolumntype' => 'oci_field_type',
'ocicolumntyperaw' => 'oci_field_type_raw',
'ocicommit' => 'oci_commit',
'ocidefinebyname' => 'oci_define_by_name',
'ocierror' => 'oci_error',
'ociexecute' => 'oci_execute',
'ocifetch' => 'oci_fetch',
'ocifetchinto' => 'oci_fetch_array,',
'ocifetchstatement' => 'oci_fetch_all',
'ocifreecollection' => 'OCI-Collection::free',
'ocifreecursor' => 'oci_free_statement',
'ocifreedesc' => 'oci_free_descriptor',
'ocifreestatement' => 'oci_free_statement',
'ocigetelem' => 'OCI-Collection::getElem',
'ociinternaldebug' => 'oci_internal_debug',
'ociloadlob' => 'OCI-Lob::load',
'ocilogon' => 'oci_connect',
'ocinewcollection' => 'oci_new_collection',
'ocinewcursor' => 'oci_new_cursor',
'ocinewdescriptor' => 'oci_new_descriptor',
'ocinlogon' => 'oci_new_connect',
'ocinumcols' => 'oci_num_fields',
'ociparse' => 'oci_parse',
'ocipasswordchange' => 'oci_password_change',
'ociplogon' => 'oci_pconnect',
'ociresult' => 'oci_result',
'ocirollback' => 'oci_rollback',
'ocisavelob' => 'OCI-Lob::save',
'ocisavelobfile' => 'OCI-Lob::import',
'ociserverversion' => 'oci_server_version',
'ocisetprefetch' => 'oci_set_prefetch',
'ocistatementtype' => 'oci_statement_type',
'ociwritelobtofile' => 'OCI-Lob::export',
'ociwritetemporarylob' => 'OCI-Lob::writeTemporary',
'odbc_do' => 'odbc_exec',
'odbc_field_precision' => 'odbc_field_len',
'output' => 'swfmovie_output',
'pdf_add_outline' => 'pdf_add_bookmark',
'pg_clientencoding' => 'pg_client_encoding',
'pg_setclientencoding' => 'pg_set_client_encoding',
'pos' => 'current',
'recode' => 'recode_string',
'remove' => 'swfmovie_remove and others',
// 'remove' => 'swfsprite_remove',
'rotate' => 'swfdisplayitem_rotate',
'rotateto' => 'swfdisplayitem_rotateTo and others',
// 'rotateto' => 'swffill_rotateTo',
'save' => 'swfmovie_save',
'savetofile' => 'swfmovie_saveToFile',
'scale' => 'swfdisplayitem_scale',
'scaleto' => 'swfdisplayitem_scaleTo and others',
// 'scaleto' => 'swffill_scaleTo',
'setaction' => 'swfbutton_setAction',
'setbackground' => 'swfmovie_setBackground',
'setbounds' => 'swftextfield_setBounds',
'setcolor' => 'swftext_setColor and others',
// 'setcolor' => 'swftextfield_setColor',
'setdepth' => 'swfdisplayitem_setDepth',
'setdimension' => 'swfmovie_setDimension',
'setdown' => 'swfbutton_setDown',
'setfont' => 'swftext_setFont and others',
// 'setfont' => 'swftextfield_setFont',
'setframes' => 'swfmovie_setFrames and others',
// 'setframes' => 'swfsprite_setFrames',
'setheight' => 'swftext_setHeight and others',
// 'setheight' => 'swftextfield_setHeight',
'sethit' => 'swfbutton_setHit',
'setindentation' => 'swftextfield_setIndentation',
'setleftfill' => 'swfshape_setleftfill',
'setleftmargin' => 'swftextfield_setLeftMargin',
'setline' => 'swfshape_setline',
'setlinespacing' => 'swftextfield_setLineSpacing',
'setmargins' => 'swftextfield_setMargins',
'setmatrix' => 'swfdisplayitem_setMatrix',
'setname' => 'swfdisplayitem_setName and others',
// 'setname' => 'swftextfield_setName',
'setover' => 'swfbutton_setOver',
'setrate' => 'swfmovie_setRate',
'setratio' => 'swfdisplayitem_setRatio',
'setrightfill' => 'swfshape_setrightfill',
'setrightmargin' => 'swftextfield_setRightMargin',
'setspacing' => 'swftext_setSpacing',
'setup' => 'swfbutton_setUp',
'show_source' => 'highlight_file',
'sizeof' => 'count',
'skewx' => 'swfdisplayitem_skewX',
'skewxto' => 'swfdisplayitem_skewXTo',
// 'skewxto' => 'swffill_skewXTo',
'skewy' => 'swfdisplayitem_skewY and others',
'skewyto' => 'swfdisplayitem_skewYTo and others',
// 'skewyto' => 'swffill_skewYTo',
'snmpwalkoid' => 'snmprealwalk',
'strchr' => 'strstr',
'streammp3' => 'swfmovie_streamMp3',
'swfaction' => 'swfaction_init',
'swfbitmap' => 'swfbitmap_init',
'swfbutton' => 'swfbutton_init',
'swffill' => 'swffill_init',
'swffont' => 'swffont_init',
'swfgradient' => 'swfgradient_init',
'swfmorph' => 'swfmorph_init',
'swfmovie' => 'swfmovie_init',
'swfshape' => 'swfshape_init',
'swfsprite' => 'swfsprite_init',
'swftext' => 'swftext_init',
'swftextfield' => 'swftextfield_init',
'xptr_new_context' => 'xpath_new_context',
// miscellaneous
'bzclose' => 'fclose',
'bzflush' => 'fflush',
'bzwrite' => 'fwrite',
'checkdnsrr' => 'dns_check_record',
'dir' => 'getdir',
'ftp_quit' => 'ftp_close',
'getmxrr' => 'dns_get_mx',
// 'getrandmax' => 'mt_getrandmax', // confusing because rand is not an alias of mt_rand
'get_required_files' => 'get_included_files',
'gmp_div' => 'gmp_div_q',
// This may change in the future
// 'gzclose' => 'fclose',
// 'gzeof' => 'feof',
// 'gzgetc' => 'fgetc',
// 'gzgets' => 'fgets',
// 'gzpassthru' => 'fpassthru',
// 'gzread' => 'fread',
// 'gzrewind' => 'rewind',
// 'gzseek' => 'fseek',
// 'gztell' => 'ftell',
// 'gzwrite' => 'fwrite',
'ldap_get_values' => 'ldap_get_values_len',
'ldap_modify' => 'ldap_mod_replace',
'mysqli_escape_string' => 'mysqli_real_escape_string',
'mysqli_execute' => 'mysqli_stmt_execute',
'mysqli_set_opt' => 'mysqli_options',
'oci_free_cursor' => 'oci_free_statement',
'openssl_get_privatekey' => 'openssl_pkey_get_private',
'openssl_get_publickey' => 'openssl_pkey_get_public',
'pcntl_errno' => 'pcntl_get_last_error',
'pg_cmdtuples' => 'pg_affected_rows',
'pg_errormessage' => 'pg_last_error',
'pg_exec' => 'pg_query',
'pg_fieldisnull' => 'pg_field_is_null',
'pg_fieldname' => 'pg_field_name',
'pg_fieldnum' => 'pg_field_num',
'pg_fieldprtlen' => 'pg_field_prtlen',
'pg_fieldsize' => 'pg_field_size',
'pg_fieldtype' => 'pg_field_type',
'pg_freeresult' => 'pg_free_result',
'pg_getlastoid' => 'pg_last_oid',
'pg_loclose' => 'pg_lo_close',
'pg_locreate' => 'pg_lo_create',
'pg_loexport' => 'pg_lo_export',
'pg_loimport' => 'pg_lo_import',
'pg_loopen' => 'pg_lo_open',
'pg_loreadall' => 'pg_lo_read_all',
'pg_loread' => 'pg_lo_read',
'pg_lounlink' => 'pg_lo_unlink',
'pg_lowrite' => 'pg_lo_write',
'pg_numfields' => 'pg_num_fields',
'pg_numrows' => 'pg_num_rows',
'pg_result' => 'pg_fetch_result',
'posix_errno' => 'posix_get_last_error',
'session_commit' => 'session_write_close',
'set_file_buffer' => 'stream_set_write_buffer',
'snmp_set_oid_numeric_print' => 'snmp_set_oid_output_format',
'socket_getopt' => 'socket_get_option',
'socket_get_status' => 'stream_get_meta_data',
'socket_set_blocking' => 'stream_set_blocking',
'socket_setopt' => 'socket_set_option',
'socket_set_timeout' => 'stream_set_timeout',
'sodium_crypto_scalarmult_base' => 'sodium_crypto_box_publickey_from_secretkey',
'srand' => 'mt_srand',
'stream_register_wrapper' => 'stream_wrapper_register',
'user_error' => 'trigger_error',
];
public function beforeAnalyzePhase(CodeBase $code_base): void
{
foreach (self::KNOWN_ALIASES as $alias => $original_name) {
try {
$fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($alias);
} catch (Exception $_) {
continue;
}
if (!$code_base->hasFunctionWithFQSEN($fqsen)) {
continue;
}
$function = $code_base->getFunctionByFQSEN($fqsen);
if (!$function->isPHPInternal()) {
continue;
}
$function->setIsDeprecated(true);
if (!$function->getDocComment()) {
$function->setDocComment('/** @deprecated DeprecateAliasPlugin marked this as an alias of ' .
$original_name . (strpos($original_name, ' ') === false ? '()' : '') . '*/');
}
}
}
}
if (Config::isIssueFixingPluginEnabled()) {
require_once __DIR__ . '/DeprecateAliasPlugin/fixers.php';
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DeprecateAliasPlugin();
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
use Microsoft\PhpParser\Node\Expression\CallExpression;
use Microsoft\PhpParser\Node\QualifiedName;
use Phan\AST\TolerantASTConverter\NodeUtils;
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Library\FileCacheEntry;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEdit;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
use Phan\Plugin\Internal\IssueFixingPlugin\IssueFixer;
/**
* Implements --automatic-fix for NotFullyQualifiedUsagePlugin
*
* This is a prototype, there are various features it does not implement.
*/
call_user_func(static function (): void {
/**
* @param $code_base @unused-param
* @return ?FileEditSet a representation of the edit to make to replace a call to a function alias with a call to the original function
*/
$fix = static function (CodeBase $code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet {
$line = $instance->getLine();
$reason = (string)$instance->getTemplateParameters()[1];
if (!preg_match('/Deprecated because: DeprecateAliasPlugin marked this as an alias of (\w+)\(\)/', $reason, $match)) {
return null;
}
$new_name = (string)$match[1];
$function_repr = (string)$instance->getTemplateParameters()[0];
if (!preg_match('/\\\\(\w+)\(\)/', $function_repr, $match)) {
return null;
}
$expected_name = $match[1];
$edits = [];
foreach ($contents->getNodesAtLine($line) as $node) {
if (!$node instanceof QualifiedName) {
continue;
}
$is_actual_call = $node->parent instanceof CallExpression;
if (!$is_actual_call) {
continue;
}
$file_contents = $contents->getContents();
$actual_name = strtolower((new NodeUtils($file_contents))->phpParserNameToString($node));
if ($actual_name !== $expected_name) {
continue;
}
//fwrite(STDERR, "name is: " . get_class($node->parent) . "\n");
// They are case-sensitively identical.
// Generate a fix.
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$start = $node->getStartPosition();
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$end = $node->getEndPosition();
$edits[] = new FileEdit($start, $end, (($file_contents[$start] ?? '') === '\\' ? '\\' : '') . $new_name);
}
if ($edits) {
return new FileEditSet($edits);
}
return null;
};
IssueFixer::registerFixerClosure(
'PhanDeprecatedFunctionInternal',
$fix
);
});
+71 -8
View File
@@ -7,6 +7,7 @@ use Phan\AST\ASTHasher;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\Issue;
use Phan\Parse\ParseVisitor;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
@@ -68,10 +69,10 @@ class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
}
// Skip array entries without literal keys. (Do it before resolving the key value)
if (!is_scalar($case_cond)) {
$original_case_cond = $case_cond;
$case_cond = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $case_cond)->asSingleScalarValueOrNullOrSelf();
if (is_object($case_cond)) {
// Skip non-literal keys.
continue;
$case_cond = $original_case_cond;
}
}
if (is_string($case_cond)) {
@@ -80,14 +81,18 @@ class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
} elseif (is_int($case_cond)) {
$cond_key = $case_cond;
$values_to_check[$i] = $case_cond;
} elseif (is_bool($case_cond)) {
$cond_key = $case_cond ? "T" : "F";
$values_to_check[$i] = $case_cond;
} else {
$cond_key = json_encode($case_cond);
if (is_scalar($case_cond)) {
// could be literal null?
$cond_key = ASTHasher::hash($case_cond);
if (!is_object($case_cond)) {
$values_to_check[$i] = $case_cond;
}
}
if (isset($case_constant_set[$cond_key])) {
$normalized_case_cond = self::normalizeSwitchKey($case_cond);
$normalized_case_cond = is_object($case_cond) ? ASTReverter::toShortString($case_cond) : self::normalizeSwitchKey($case_cond);
$this->emitPluginIssue(
$this->code_base,
clone($this->context)->withLineNumberStart($case_node->lineno),
@@ -166,7 +171,7 @@ class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
if ($old_index !== null) {
$this->emitPluginIssue(
$this->code_base,
clone($this->context)->withLineNumberStart($children[$i]->lineno),
(clone($this->context))->withLineNumberStart($children[$i]->lineno),
'PhanPluginDuplicateSwitchCaseLooseEquality',
"Switch case({STRING_LITERAL}) is loosely equivalent (==) to an earlier case ({STRING_LITERAL}) in switch statement - the earlier entry may be chosen instead.",
[self::normalizeSwitchKey($values_to_check[$i]), self::normalizeSwitchKey($values_to_check[$old_index])],
@@ -178,6 +183,64 @@ class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
}
}
/**
* @param Node $node
* A match expressions's arms list (AST_MATCH_ARM_LIST) node to analyze
* @override
* @suppress PhanPossiblyUndeclaredProperty
*/
public function visitMatchArmList(Node $node): void
{
$children = $node->children;
if (!$children) {
// This plugin will never emit errors if there are 0 elements.
return;
}
$arm_expr_constant_set = [];
foreach ($children as $arm_node) {
foreach ($arm_node->children['cond']->children ?? [] as $arm_expr_cond) {
if ($arm_expr_cond === null) {
continue; // This is `default:`. php --syntax-check already checks for duplicates.
}
$lineno = $arm_expr_cond->lineno ?? $arm_node->lineno;
// Skip array entries without literal keys. (Do it before resolving the key value)
if (is_object($arm_expr_cond) && ParseVisitor::isConstExpr($arm_expr_cond, ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
// Only infer the value for values not affected by conditions - that will change after the expressions are analyzed
$original_cond = $arm_expr_cond;
$arm_expr_cond = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $arm_expr_cond)->asSingleScalarValueOrNullOrSelf();
if (is_object($arm_expr_cond)) {
$arm_expr_cond = $original_cond;
}
}
if (is_string($arm_expr_cond)) {
$cond_key = "s$arm_expr_cond";
} elseif (is_int($arm_expr_cond)) {
$cond_key = $arm_expr_cond;
} elseif (is_bool($arm_expr_cond)) {
$cond_key = $arm_expr_cond ? "T" : "F";
} else {
// TODO: This seems like it'd be flaky with ast\Node->flags and lineno?
$cond_key = ASTHasher::hash($arm_expr_cond);
}
if (isset($arm_expr_constant_set[$cond_key])) {
$normalized_arm_expr_cond = ASTReverter::toShortString($arm_expr_cond);
$this->emitPluginIssue(
$this->code_base,
clone($this->context)->withLineNumberStart($lineno),
'PhanPluginDuplicateMatchArmExpression',
"Duplicate match arm expression({STRING_LITERAL}) detected in match expression - the later entry will be ignored in favor of expression {CODE} at line {LINE}.",
[$normalized_arm_expr_cond, ASTReverter::toShortString($arm_expr_constant_set[$cond_key][0]), $arm_expr_constant_set[$cond_key][1]],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15071
);
}
$arm_expr_constant_set[$cond_key] = [$arm_expr_cond, $arm_node->lineno];
}
}
}
/**
* @param Node $node
* An array literal(AST_ARRAY) node to analyze
@@ -199,11 +262,11 @@ class DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
}
$key = $entry->children['key'] ?? null;
// Skip array entries without literal keys. (Do it before resolving the key value)
if ($key === null) {
if (is_null($key)) {
$has_entry_without_key = true;
continue;
}
if (!is_scalar($key)) {
if (is_object($key)) {
$key = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $key)->asSingleScalarValueOrNullOrSelf();
if (is_object($key)) {
$key = self::HASH_PREFIX . ASTHasher::hash($entry->children['key']);
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for duplicate constant declarations within a statement list.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DuplicateConstantPlugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class DuplicateConstantPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return DuplicateConstantVisitor::class;
}
}
/**
* When __invoke on this class is called with a node, a method
* will be dispatched based on the `kind` of the given node.
*
* Visitors such as this are useful for defining lots of different
* checks on a node based on its kind.
*/
class DuplicateConstantVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node
* A node to analyze of kind ast\AST_STMT_LIST
* @override
*/
public function visitStmtList(Node $node): void
{
if (count($node->children) <= 1) {
return;
}
$declarations = [];
foreach ($node->children as $child) {
if (!$child instanceof Node) {
continue;
}
if ($child->kind === ast\AST_CONST_DECL) {
foreach ($child->children as $const) {
if (!$const instanceof Node) {
continue;
}
$name = (string) $const->children['name'];
if (isset($declarations[$name])) {
$this->warnDuplicateConstant($name, $declarations[$name], $const);
} else {
$declarations[$name] = $const;
}
}
} elseif ($child->kind === ast\AST_CALL) {
$expr = $child->children['expr'];
if ($expr instanceof Node && $expr->kind === ast\AST_NAME && strcasecmp((string) $expr->children['name'], 'define') === 0) {
$name = $child->children['args']->children[0] ?? null;
if (is_string($name)) {
if (isset($declarations[$name])) {
$this->warnDuplicateConstant($name, $declarations[$name], $expr);
} else {
$declarations[$name] = $expr;
}
}
}
}
}
}
private function warnDuplicateConstant(string $name, Node $original_def, Node $new_def): void
{
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($new_def->lineno),
'PhanPluginDuplicateConstant',
'Constant {CONST} was previously declared at line {LINE} - the previous declaration will be used instead',
[$name, $original_def->lineno]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DuplicateConstantPlugin();
+109 -1
View File
@@ -8,6 +8,7 @@ use Phan\Analysis\PostOrderAnalysisVisitor;
use Phan\AST\ASTHasher;
use Phan\AST\ASTReverter;
use Phan\AST\InferValue;
use Phan\Config;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PluginAwarePreAnalysisVisitor;
@@ -191,6 +192,22 @@ class RedundantNodePostAnalysisVisitor extends PluginAwarePostAnalysisVisitor
$this->visitAssign($node);
}
private const ASSIGN_OP_FLAGS = [
flags\BINARY_BITWISE_OR => '|',
flags\BINARY_BITWISE_AND => '&',
flags\BINARY_BITWISE_XOR => '^',
flags\BINARY_CONCAT => '.',
flags\BINARY_ADD => '+',
flags\BINARY_SUB => '-',
flags\BINARY_MUL => '*',
flags\BINARY_DIV => '/',
flags\BINARY_MOD => '%',
flags\BINARY_POW => '**',
flags\BINARY_SHIFT_LEFT => '<<',
flags\BINARY_SHIFT_RIGHT => '>>',
flags\BINARY_COALESCE => '??',
];
/**
* @param Node $node
* An assignment operation node to analyze
@@ -198,8 +215,37 @@ class RedundantNodePostAnalysisVisitor extends PluginAwarePostAnalysisVisitor
*/
public function visitAssign(Node $node): void
{
$var = $node->children['var'];
$expr = $node->children['expr'];
if (!$expr instanceof Node) {
// Guaranteed not to contain duplicate expressions in valid php assignments.
return;
}
$var = $node->children['var'];
if ($expr->kind === ast\AST_BINARY_OP) {
$op_str = self::ASSIGN_OP_FLAGS[$expr->flags] ?? null;
if (is_string($op_str) && ASTHasher::hash($var) === ASTHasher::hash($expr->children['left'])) {
$message = 'Can simplify this assignment to {CODE} {OPERATOR} {CODE}';
if ($expr->flags === ast\flags\BINARY_COALESCE) {
if (Config::get_closest_minimum_target_php_version_id() < 70400) {
return;
}
$message .= ' (requires php version 7.4 or newer)';
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDuplicateExpressionAssignmentOperation',
$message,
[
ASTReverter::toShortString($var),
$op_str . '=',
ASTReverter::toShortString($expr->children['right']),
]
);
}
return;
}
if (ASTHasher::hash($var) === ASTHasher::hash($expr)) {
$this->emitPluginIssue(
$this->code_base,
@@ -288,6 +334,33 @@ class RedundantNodePostAnalysisVisitor extends PluginAwarePostAnalysisVisitor
}
}
/**
* @param Node $node
* A statement list of kind ast\AST_STMT_LIST to analyze.
* @override
*/
public function visitStmtList(Node $node): void
{
$children = $node->children;
if (count($children) < 2) {
return;
}
$prev_hash = null;
foreach ($children as $child) {
$hash = ASTHasher::hash($child);
if ($hash === $prev_hash) {
$this->emitPluginIssue(
$this->code_base,
(clone($this->context))->withLineNumberStart($child->lineno ?? $node->lineno),
'PhanPluginDuplicateAdjacentStatement',
"Statement {CODE} is a duplicate of the statement on the above line. Suppress this issue instance if there's a good reason for this.",
[ASTReverter::toShortString($child)]
);
}
$prev_hash = $hash;
}
}
/**
* @param int|string $true_node_hash
*/
@@ -426,6 +499,41 @@ class RedundantNodePreAnalysisVisitor extends PluginAwarePreAnalysisVisitor
}
}
/**
* Visit a node of kind ast\AST_TRY, to check for adjacent catch blocks
*
* @override
* @suppress PhanPossiblyUndeclaredProperty
*/
public function visitTry(Node $node): void
{
if (Config::get_closest_target_php_version_id() < 70100) {
return;
}
$catches = $node->children['catches']->children ?? [];
$n = count($catches);
if ($n <= 1) {
// There can't be any duplicates.
return;
}
$prev_hash = ASTHasher::hash($catches[0]->children['stmts']) . ASTHasher::hash($catches[0]->children['var']);
for ($i = 1; $i < $n; $prev_hash = $cur_hash, $i++) {
$cur_hash = ASTHasher::hash($catches[$i]->children['stmts']) . ASTHasher::hash($catches[$i]->children['var']);
if ($prev_hash === $cur_hash) {
$this->emitPluginIssue(
$this->code_base,
clone($this->context)->withLineNumberStart($catches[$i]->lineno),
'PhanPluginDuplicateCatchStatementBody',
'The implementation of catch({CODE}) and catch({CODE}) are identical, and can be combined if the application only needs to supports php 7.1 and newer',
[
ASTReverter::toShortString($catches[$i - 1]->children['class']),
ASTReverter::toShortString($catches[$i]->children['class']),
]
);
}
}
}
/**
* @param Node $node a node of kind ast\AST_IF
* @return list<Node> the list of AST_IF_ELEM nodes making up the chain of if/elseif/else if conditions.
@@ -7,6 +7,7 @@ use Phan\Issue;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Element\Parameter;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
@@ -34,26 +35,36 @@ final class EmptyMethodAndFunctionPlugin extends PluginV3 implements PostAnalyze
final class EmptyMethodAndFunctionVisitor extends PluginAwarePostAnalysisVisitor
{
/** @param Node $node a node of kind ast\AST_METHOD */
public function visitMethod(Node $node): void
{
$stmts_node = $node->children['stmts'] ?? null;
if ($stmts_node && !$stmts_node->children) {
$method = $this->context->getFunctionLikeInScope($this->code_base);
if (!($method instanceof Method)) {
throw new AssertionError("Expected $method to be a method");
if (!$stmts_node || $stmts_node->children) {
return;
}
$method = $this->context->getFunctionLikeInScope($this->code_base);
if (!($method instanceof Method)) {
throw new AssertionError("Expected $method to be a method");
}
if ($method->isNewConstructor()) {
foreach ($node->children['params']->children as $param) {
if ($param instanceof Node && ($param->flags & Parameter::PARAM_MODIFIER_VISIBILITY_FLAGS)) {
// This uses constructor property promotion
return;
}
}
}
if (!$method->isOverriddenByAnother()
&& !$method->isOverride()
&& !$method->isDeprecated()
) {
$this->emitIssue(
self::getIssueTypeForEmptyMethod($method),
$node->lineno,
$method->getName()
);
}
if (!$method->isOverriddenByAnother()
&& !$method->isOverride()
&& !$method->isDeprecated()
) {
$this->emitIssue(
self::getIssueTypeForEmptyMethod($method),
$node->lineno,
$method->getRepresentationForIssue()
);
}
}
@@ -81,19 +92,12 @@ final class EmptyMethodAndFunctionVisitor extends PluginAwarePostAnalysisVisitor
throw new AssertionError("Expected $function to be Func\n");
}
if (! $function->isDeprecated()) {
if (!$function->isClosure()) {
$this->emitIssue(
Issue::EmptyFunction,
$node->lineno,
$function->getName()
);
} else {
$this->emitIssue(
Issue::EmptyClosure,
$node->lineno
);
}
if (!$function->isDeprecated()) {
$this->emitIssue(
$function->isClosure() ? Issue::EmptyClosure : Issue::EmptyFunction,
$node->lineno,
$function->getRepresentationForIssue()
);
}
}
}
@@ -79,6 +79,10 @@ final class EmptyStatementListVisitor extends PluginAwarePostAnalysisVisitor
{
// @phan-suppress-next-line PhanUndeclaredProperty set by ASTSimplifier
if (isset($node->is_simplified)) {
$first_child = end($node->children);
if (!$first_child instanceof Node || $first_child->children['cond'] === null) {
return;
}
$last_if_elem = reset($node->children);
} else {
$last_if_elem = end($node->children);
@@ -338,7 +342,7 @@ final class EmptyStatementListVisitor extends PluginAwarePostAnalysisVisitor
}
}
}
if (!ParseVisitor::isConstExpr($c->children['cond'])) {
if (!ParseVisitor::isConstExpr($c->children['cond'], ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
return;
}
}
+6 -1
View File
@@ -13,6 +13,7 @@ use Phan\PluginV3;
use Phan\PluginV3\AfterAnalyzeFileCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
use Phan\PluginV3\UnloadablePluginException;
/**
* This plugin checks for accidental whitespace in regular php files.
@@ -160,9 +161,10 @@ class InlineHTMLVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @override
* @param Node $node @unused-param
* @return void
*/
public function visitEcho(Node $_)
public function visitEcho(Node $node)
{
InlineHTMLPlugin::$file_set_to_analyze[$this->context->getFile()] = true;
}
@@ -170,4 +172,7 @@ class InlineHTMLVisitor extends PluginAwarePostAnalysisVisitor
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
if (!function_exists('token_get_all')) {
throw new UnloadablePluginException("InlineHTMLPlugin requires the tokenizer extension, which is not enabled (this plugin uses token_get_all())");
}
return new InlineHTMLPlugin();
@@ -7,6 +7,7 @@ use Phan\AST\Parser;
use Phan\CLI;
use Phan\CodeBase;
use Phan\Config;
use Phan\Issue;
use Phan\Language\Context;
use Phan\PluginV3;
use Phan\PluginV3\AfterAnalyzeFileCapability;
@@ -36,7 +37,7 @@ class InvokePHPNativeSyntaxCheckPlugin extends PluginV3 implements
BeforeAnalyzeFileCapability,
FinalizeProcessCapability
{
private const LINE_NUMBER_REGEX = "@ on line ([1-9][0-9]*)$@";
private const LINE_NUMBER_REGEX = "@ on line ([1-9][0-9]*)$@D";
private const STDIN_FILENAME_REGEX = "@ in (Standard input code|-)@";
/**
@@ -141,7 +142,6 @@ class InvokePHPNativeSyntaxCheckPlugin extends PluginV3 implements
}
$check_error_message = preg_replace(self::STDIN_FILENAME_REGEX, '', $check_error_message);
self::emitIssue(
$code_base,
clone($context)->withLineNumberStart($lineno),
@@ -151,7 +151,8 @@ class InvokePHPNativeSyntaxCheckPlugin extends PluginV3 implements
$binary === PHP_BINARY ? 'php' : $binary,
json_encode($check_error_message),
]
],
Issue::SEVERITY_CRITICAL
);
}
}
@@ -242,7 +243,7 @@ class InvokeExecutionPromise
['pipe', 'wb'],
];
$this->binary = $binary;
// @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal
// @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal PHP 7.3 does not accept arrays
$process = proc_open($cmd, $descriptorspec, $pipes);
if (!is_resource($process)) {
$this->done = true;
+1 -199
View File
@@ -2,204 +2,6 @@
declare(strict_types=1);
use ast\Node;
use Phan\Language\Context;
use Phan\Language\Element\Variable;
use Phan\Plugin\Internal\RedundantConditionLoopCheck;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin detects reuse of loop variables
*/
class LoopVariableReusePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return LoopVariableReuseVisitor::class;
}
}
/**
* This visitor implements the checks for reuse of loop variables.
*/
class LoopVariableReuseVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @var list<Node> set by plugin framework
* @suppress PhanReadOnlyProtectedProperty
*/
protected $parent_node_list;
/**
* @override checks for reuse of variables in a node of kind ast\AST_FOREACH
*/
public function visitForeach(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfForeach($node));
return $this->context;
}
/**
* @return array<string|int,Node>
*/
private function extractLoopVariablesOfForeach(Node $node): array
{
return $this->extractVariables($node->children['key']) + $this->extractVariables($node->children['value']);
}
/**
* @override checks for reuse of variables in a node of kind ast\AST_FOR
*/
public function visitFor(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfFor($node));
return $this->context;
}
/**
* @param Node $node a node of kind ast\AST_FOR
* @return array<string|int,Node>
* @suppress PhanAccessMethodInternal
*/
private function extractLoopVariablesOfFor(Node $node): array
{
$directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']) +
RedundantConditionLoopCheck::extractIncrementDirections($this->code_base, $this->context, $node->children['loop']);
if (!$directions) {
return [];
}
$variables = self::extractVariables($node->children['cond']) + self::extractVariables($node->children['loop']);
return array_intersect_key($variables, $directions);
}
/**
* @override checks for reuse of variables in a node of kind ast\AST_WHILE
*/
public function visitWhile(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfWhile($node));
return $this->context;
}
/**
* @param Node $node a node of kind ast\AST_WHILE
* @return array<string|int,Node>
* @suppress PhanAccessMethodInternal
*/
private function extractLoopVariablesOfWhile(Node $node): array
{
$directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']);
if (!$directions) {
return [];
}
return array_intersect_key(self::extractVariables($node->children['cond']), $directions);
}
/**
* @param array<string|int,Node> $variables
*/
private function findVariableReuse(array $variables): void
{
if (!$variables) {
return;
}
for ($i = count($this->parent_node_list) - 1; $i >= 0; $i--) {
$parent_node = $this->parent_node_list[$i];
$outer_variables = [];
switch ($parent_node->kind) {
case ast\AST_FOREACH:
$outer_variables = $this->extractLoopVariablesOfForeach($parent_node);
break;
case ast\AST_FOR:
$outer_variables = $this->extractLoopVariablesOfFor($parent_node);
break;
case ast\AST_WHILE:
$outer_variables = $this->extractLoopVariablesOfWhile($parent_node);
break;
case ast\AST_FUNC_DECL:
case ast\AST_CLOSURE:
case ast\AST_ARROW_FUNC:
case ast\AST_METHOD:
case ast\AST_CLASS:
return;
default:
continue 2;
}
$common_outer_variables = array_intersect_key($outer_variables, $variables);
if ($common_outer_variables) {
$this->warnCommonOuterVariables($variables, $common_outer_variables);
return;
}
}
}
/**
* @param array<string|int,Node> $variables
* @param array<string|int,Node> $common_outer_variables
*/
private function warnCommonOuterVariables(array $variables, array $common_outer_variables): void
{
foreach ($common_outer_variables as $variable_name => $node) {
$inner_node = $variables[$variable_name];
$this->emitPluginIssue(
$this->code_base,
(clone($this->context))->withLineNumberStart($inner_node->lineno),
'PhanPluginLoopVariableReuse',
'Variable ${VARIABLE} used in loop was also used in an outer loop on line {LINE}',
[$variable_name, $node->lineno]
);
}
}
/**
* @param Node|string|int|float|null $node
* @return array<int|string,Node> a list of all variable nodes in this foreach
*/
private function extractVariables($node): array
{
if (!$node instanceof Node) {
return [];
}
switch ($node->kind) {
case ast\AST_VAR:
if ($node->kind === ast\AST_VAR) {
$var_name = $node->children['name'];
if (is_string($var_name)) {
if (in_array($var_name, ['this', '_'], true) || Variable::isHardcodedVariableInScopeWithName($var_name, $this->context->isInGlobalScope())) {
return [];
}
return [$var_name => $node];
}
}
break;
// Kinds of nodes we don't bother checking
case ast\AST_STATIC_PROP:
case ast\AST_PROP:
// Kinds of declarations creating a new scope.
case ast\AST_FUNC_DECL:
case ast\AST_CLOSURE:
case ast\AST_ARROW_FUNC:
case ast\AST_METHOD:
case ast\AST_CLASS:
// FUNC_DECL and METHOD are probably unreachable.
return [];
}
$result = [];
foreach ($node->children as $child_node) {
$result += self::extractVariables($child_node);
}
return $result;
}
}
use Phan\Plugin\Internal\LoopVariableReusePlugin;
return new LoopVariableReusePlugin();
@@ -44,6 +44,8 @@ use Phan\PluginV3\PostAnalyzeNodeCapability;
* add them to the corresponding section of README.md
*
* TODO: Account for methods in traits being possibly overrides
*
* TODO: This does not support intersection types
*/
class MoreSpecificElementTypePlugin extends PluginV3 implements
PostAnalyzeNodeCapability,
@@ -99,7 +101,7 @@ class MoreSpecificElementTypePlugin extends PluginV3 implements
return true;
}
if ($declared_return_type->typeCount() === 1) {
if ($declared_return_type->getTypeSet()[0]->isObjectWithKnownFQSEN()) {
if ($declared_return_type->getTypeSet()[0]->hasObjectWithKnownFQSEN()) {
if ($actual_type->typeCount() >= 2) {
// Don't warn about Subclass1|Subclass2 being more specific than BaseClass
return false;
@@ -109,7 +111,7 @@ class MoreSpecificElementTypePlugin extends PluginV3 implements
if ($declared_return_type->isStrictSubtypeOf($code_base, $actual_type)) {
return false;
}
if (!$actual_type->asExpandedTypes($code_base)->canCastToUnionType($declared_return_type)) {
if (!$actual_type->canCastToUnionType($declared_return_type, $code_base)) {
// Don't warn here about type mismatches such as int->string or object->array, but do warn about SubClass->BaseClass.
// Phan should warn elsewhere about those mismatches
return false;
@@ -128,7 +130,7 @@ class MoreSpecificElementTypePlugin extends PluginV3 implements
private static function containsObjectWithKnownFQSEN(UnionType $union_type): bool
{
foreach ($union_type->getTypesRecursively() as $type) {
if ($type->isObjectWithKnownFQSEN()) {
if ($type->hasObjectWithKnownFQSEN()) {
return true;
}
}
@@ -71,7 +71,7 @@ call_user_func(static function (): void {
// They are case-sensitively identical.
// Generate a fix.
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$start = $node->getStart();
$start = $node->getStartPosition();
$edits[] = new FileEdit($start, $start, '\\');
}
if ($edits) {
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin contains examples of checks for code that would be incompatible with php 5.3.
* This goes beyond what `backward_compatibility_checks` checks for.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* PHP53CompatibilityPlugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class PHP53CompatibilityPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return PHP53CompatibilityVisitor::class;
}
}
/**
* When __invoke on this class is called with a node, a method
* will be dispatched based on the `kind` of the given node.
*
* Visitors such as this are useful for defining lots of different
* checks on a node based on its kind.
*/
class PHP53CompatibilityVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node
* A node to analyze of kind ast\AST_ARRAY
* @override
*/
public function visitArray(Node $node): void
{
if ($node->flags === ast\flags\ARRAY_SYNTAX_SHORT) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginCompatibilityShortArray',
"Short arrays ({CODE}) require support for php 5.4+",
[ASTReverter::toShortString($node)]
);
}
}
/**
* @param Node $node
* A node to analyze of kind ast\AST_ARG_LIST
* @override
*/
public function visitArgList(Node $node): void
{
$lastArg = end($node->children);
if ($lastArg instanceof Node && $lastArg->kind === ast\AST_UNPACK) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginCompatibilityArgumentUnpacking',
"Argument unpacking ({CODE}) requires support for php 5.6+",
[ASTReverter::toShortString($lastArg)]
);
}
}
/**
* @param Node $node
* A node to analyze of kind ast\AST_PARAM
* @override
*/
public function visitParam(Node $node): void
{
if ($node->flags & ast\flags\PARAM_VARIADIC) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginCompatibilityVariadicParam',
"Variadic functions ({CODE}) require support for php 5.6+",
[ASTReverter::toShortString($node)]
);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHP53CompatibilityPlugin();
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\Language\Context;
use Phan\Language\Element\Comment;
use Phan\Language\Element\Comment\NullComment;
use Phan\Library\StringUtil;
use Phan\PluginV3;
use Phan\PluginV3\AfterAnalyzeFileCapability;
use Phan\PluginV3\UnloadablePluginException;
/**
* This plugin checks for the use of phpdoc annotations in non-phpdoc comments
* (e.g. starting with `/*` or `//`)
*
* Note that this is slow due to needing token_get_all.
*
* TODO: Cache and reuse the results
*/
class PHPDocInWrongCommentPlugin extends PluginV3 implements
AfterAnalyzeFileCapability
{
/**
* @param CodeBase $code_base
* The code base in which the node exists
*
* @param Context $context @phan-unused-param
* A context with the file name for $file_contents and the scope after analyzing $node.
*
* @param string $file_contents the unmodified file contents @phan-unused-param
* @param Node $node the node @phan-unused-param
* @override
* @throws Error if a process fails to shut down
*/
public function afterAnalyzeFile(
CodeBase $code_base,
Context $context,
string $file_contents,
Node $node
): void {
$tokens = @token_get_all($file_contents);
foreach ($tokens as $token) {
if (!is_array($token)) {
continue;
}
if ($token[0] !== T_COMMENT) {
continue;
}
// This is a comment, not T_DOC_COMMENT
$comment_string = $token[1];
if (strncmp($comment_string, '/*', 2) !== 0) {
if ($comment_string[0] === '#' && substr($comment_string, 1, 1) !== '[') {
$this->emitIssue(
$code_base,
(clone($context))->withLineNumberStart($token[2]),
'PhanPluginPHPDocHashComment',
'Saw comment starting with {COMMENT} in {COMMENT} - consider using {COMMENT} instead to avoid confusion with php 8.0 {COMMENT} attributes',
['#', StringUtil::jsonEncode(self::truncate(trim($comment_string))), '//', '#[']
);
}
continue;
}
if (strpos($comment_string, '@') === false) {
continue;
}
$lineno = $token[2];
// @phan-suppress-next-line PhanAccessClassConstantInternal
$comment = Comment::fromStringInContext("/**" . $comment_string, $code_base, $context, $lineno, Comment::ON_ANY);
if ($comment instanceof NullComment) {
continue;
}
$this->emitIssue(
$code_base,
(clone($context))->withLineNumberStart($token[2]),
'PhanPluginPHPDocInWrongComment',
'Saw possible phpdoc annotation in ordinary block comment {COMMENT}. PHPDoc comments should start with "/**" (followed by whitespace), not "/*"',
[StringUtil::jsonEncode(self::truncate($comment_string))]
);
}
}
private static function truncate(string $token): string
{
if (strlen($token) > 200) {
return mb_substr($token, 0, 200) . "...";
}
return $token;
}
}
if (!function_exists('token_get_all')) {
throw new UnloadablePluginException("PHPDocInWrongCommentPlugin requires the tokenizer extension, which is not enabled (this plugin uses token_get_all())");
}
return new PHPDocInWrongCommentPlugin();
@@ -29,9 +29,10 @@ class Fixers
{
/**
* Remove a redundant phpdoc return type from the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixRedundantFunctionLikeComment(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
@@ -98,9 +99,10 @@ class Fixers
/**
* Add a missing return type to the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixRedundantReturnComment(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
@@ -143,7 +145,7 @@ class Fixers
$leadingTriviaTokens = PhpTokenizer::getTokensArrayFromContent(
$leadingTriviaText,
ParseContext::SourceElements,
$node->getFullStart(),
$node->getFullStartPosition(),
false
);
for ($i = \count($leadingTriviaTokens) - 1; $i >= 0; $i--) {
+4 -1
View File
@@ -62,7 +62,10 @@ class PHPDocToRealTypesPlugin extends PluginV3 implements
self::analyzeFunctionLike($code_base, $function);
}
public function analyzeMethod(CodeBase $unused_code_base, Method $method): void
/**
* @param CodeBase $code_base @unused-param
*/
public function analyzeMethod(CodeBase $code_base, Method $method): void
{
if ($method->isFromPHPDoc() || $method->isMagic() || $method->isPHPInternal()) {
return;
@@ -25,9 +25,10 @@ class Fixers
/**
* Add a missing return type to the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixReturnType(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
@@ -44,9 +45,10 @@ class Fixers
/**
* Add a missing param type to the real signature
* @unused-param $code_base
*/
public static function fixParamType(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
+4 -8
View File
@@ -37,18 +37,14 @@ class PHPUnitAssertionPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
$assert_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('PHPUnit\Framework\Assert');
if (!$code_base->hasClassWithFQSEN($assert_class_fqsen)) {
if (!getenv('PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET')) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
fwrite(STDERR, "PHPUnitAssertionPlugin failed to find class PHPUnit\Framework\Assert, giving up (set environment variable PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET=1 to ignore this)\n");
}
return [];
}
$result = [];
foreach ($code_base->getMethodSet() as $method) {
$method_fqsen = $method->getDefiningFQSEN();
$class_fqsen = $method_fqsen->getFullyQualifiedClassName();
if ($class_fqsen !== $assert_class_fqsen) {
continue;
}
$closure = $this->createClosureForMethod($code_base, $method, $method_fqsen->getName());
foreach ($code_base->getClassByFQSEN($assert_class_fqsen)->getMethodMap($code_base) as $method) {
$closure = $this->createClosureForMethod($code_base, $method, $method->getName());
if (!$closure) {
continue;
}
@@ -184,7 +180,7 @@ class PHPUnitAssertionPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
return $result;
case 'callable':
$result = $original_type->callableTypes();
$result = $original_type->callableTypes($code_base);
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('callable');
}
@@ -54,8 +54,9 @@ class PHPUnitNotDeadPluginVisitor extends PluginAwarePostAnalysisVisitor
/**
* This is called after the parse phase is completely finished, so $this->code_base contains all class definitions
* @override
* @unused-param $node
*/
public function visitClass(Node $unused_node): void
public function visitClass(Node $node): void
{
if (!Config::get_track_references()) {
return;
@@ -63,6 +64,7 @@ class PHPUnitNotDeadPluginVisitor extends PluginAwarePostAnalysisVisitor
$code_base = $this->code_base;
if (!$code_base->hasClassWithFQSEN(self::$phpunit_test_case_fqsen)) {
if (!self::$did_warn_missing_class) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
fprintf(STDERR, "Using plugin %s but could not find PHPUnit\Framework\TestCase\n", self::class);
self::$did_warn_missing_class = true;
}
@@ -111,7 +113,7 @@ class PHPUnitNotDeadPluginVisitor extends PluginAwarePostAnalysisVisitor
{
if (preg_match('/@dataProvider\s+' . self::WORD_REGEX . '/', $method->getNode()->children['docComment'] ?? '', $match)) {
$data_provider_name = $match[1];
if ($class->hasMethodWithName($this->code_base, $data_provider_name)) {
if ($class->hasMethodWithName($this->code_base, $data_provider_name, true)) {
$class->getMethodByName($this->code_base, $data_provider_name)->addReference($this->context);
}
}
@@ -208,15 +208,16 @@ final class PossiblyStaticMethodPlugin extends PluginV3 implements
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
*
* @override
*/
public function analyzeMethod(
CodeBase $unused_code_base,
CodeBase $code_base,
Method $method
): void {
// 1. Perform any checks that can be done immediately to rule out being able
@@ -253,7 +254,7 @@ final class PossiblyStaticMethodPlugin extends PluginV3 implements
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the function exists
*
* @param Func $function
@@ -261,7 +262,7 @@ final class PossiblyStaticMethodPlugin extends PluginV3 implements
* @override
*/
public function analyzeFunction(
CodeBase $unused_code_base,
CodeBase $code_base,
Func $function
): void {
if (!$function->isClosure()) {
@@ -9,7 +9,6 @@ use Microsoft\PhpParser\FunctionLike;
use Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Statement\FunctionDeclaration;
use Microsoft\PhpParser\Token;
use Phan\AST\TolerantASTConverter\NodeUtils;
use Phan\CodeBase;
use Phan\IssueInstance;
@@ -25,9 +24,10 @@ class Fixers
/**
* Generate an edit to replace a fully qualified return type with a shorter equivalent representation.
* @unused-param $code_base
*/
public static function fixReturnType(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
@@ -44,9 +44,10 @@ class Fixers
/**
* Generate an edit to replace a fully qualified param type with a shorter equivalent representation.
* @unused-param $code_base
*/
public static function fixParamType(
CodeBase $unused_code_base,
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
@@ -76,7 +77,7 @@ class Fixers
// Generate an edit to replace the long return type with the shorter return type
// Long return types are always Nodes instead of Tokens.
$file_edit = new FileEdit(
$return_type_node->getStart(),
$return_type_node->getStartPosition(),
$return_type_node->getEndPosition(),
$shorter_return_type
);
@@ -104,12 +105,13 @@ class Fixers
if ($declaration_name !== $param_name) {
continue;
}
$token = $param->typeDeclaration;
$token = $param->typeDeclarationList;
if (!$token) {
return null;
}
// @phan-suppress-next-line PhanThrowTypeAbsentForCall php-parser is not expected to throw here
$start = $token instanceof Token ? $token->start : $token->getStart();
$start = $token->getStartPosition();
// @phan-suppress-next-line PhanThrowTypeAbsentForCall php-parser is not expected to throw here
$file_edit = new FileEdit($start, $token->getEndPosition(), $shorter_param_type);
return new FileEditSet([$file_edit]);
}
+104 -5
View File
@@ -6,6 +6,7 @@ use ast\Node;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\Language\Type\IterableType;
@@ -68,6 +69,96 @@ class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
);
return;
}
if (strpos($pattern, '$') !== false && (Config::getValue('plugin_config')['regex_warn_if_newline_allowed_at_end'] ?? false)) {
foreach (self::checkForSuspiciousRegexPatterns($pattern) as [$issue_type, $issue_template]) {
self::emitIssue(
$code_base,
$context,
$issue_type,
$issue_template,
[$function->getFQSEN(), StringUtil::encodeValue($pattern)]
);
}
}
}
/**
* @return Generator<array{0:string, 1:string}>
*/
private static function checkForSuspiciousRegexPatterns(string $pattern): Generator
{
$pattern = \trim($pattern);
$start_chr = $pattern[0] ?? '/';
// @phan-suppress-next-line PhanParamSuspiciousOrder this is deliberate
$i = \strpos('({[', $start_chr);
if ($i !== false) {
$end_chr = ')}]'[$i];
} else {
$end_chr = $start_chr;
}
// TODO: Reject characters that preg_match would reject
$end_pos = \strrpos($pattern, $end_chr);
if ($end_pos === false) {
return;
}
$inner = (string)\substr($pattern, 1, $end_pos - 1);
if ($i !== false) {
// Unescape '/x\/y/' as 'x/y'
$inner = \str_replace('\\' . $start_chr, $start_chr, $inner);
}
foreach (self::tokenizeRegexParts($inner) as $part) {
// If special handling of newlines is given, don't warn.
// If PCRE_EXTENDED is given, this was likely a false positive (E.g. # can be a comment)
if ($part === '$' && !preg_match('/[mDx]/', (string) substr($pattern, $end_pos + 1))) {
yield ['PhanPluginPregRegexDollarAllowsNewline', 'Call to {FUNCTION} used \'$\' in {STRING_LITERAL}, which allows a newline character \'\n\' before the end of the string. Add D to qualifiers to forbid the newline, m to match any newline, or suppress this issue if this is deliberate'];
}
}
}
/**
* Tokenize the regex, using imperfect heuristics to split up the parts of a regular expression.
*/
private static function tokenizeRegexParts(string $inner): Generator
{
$inner_len = strlen($inner);
for ($j = 0; $j < $inner_len;) {
switch ($c = $inner[$j]) {
case '\\':
// TODO: https://www.php.net/manual/en/regexp.reference.escape.php for alphanumeric characters
yield substr($inner, $j, $j + 2);
$j += 2;
break;
case '[':
// TODO: Handle escaped ]. This is a heuristic that is usually good enough.
$end = strpos($inner, ']', $j + 1);
if ($end === false) {
yield substr($inner, $j);
return;
}
yield substr($inner, $j, $end);
$j = $end;
break;
case '{':
$end = strpos($inner, '}', $j + 1);
if ($end === false) {
yield substr($inner, $j);
return;
}
yield substr($inner, $j, $end);
$j = $end;
break;
// case '(':
// case '}':
// case ')':
// case ']':
default:
yield $c;
$j++;
break;
}
}
}
/**
@@ -176,18 +267,20 @@ class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
/**
* @param CodeBase $code_base @phan-unused-param
* @return array<string, Closure(CodeBase,Context,Func,array):void>
* @return array<string, Closure(CodeBase,Context,Func,array,?Node):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
* @unused-param $node
*/
$preg_pattern_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
array $args,
?Node $node = null
): void {
if (count($args) < 1) {
return;
@@ -203,12 +296,14 @@ class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
/**
* @param list<Node|int|string|float> $args
* @unused-param $node
*/
$preg_pattern_or_array_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
array $args,
?Node $node = null
): void {
if (count($args) < 1) {
return;
@@ -221,12 +316,14 @@ class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
/**
* @param list<Node|int|string|float> $args
* @unused-param $node
*/
$preg_pattern_and_replacement_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
array $args,
?Node $node = null
): void {
if (count($args) < 1) {
return;
@@ -247,12 +344,14 @@ class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapa
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
* @unused-param $node
*/
$preg_replace_callback_array_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
array $args,
?Node $node = null
): void {
if (count($args) < 1) {
return;
+13 -10
View File
@@ -190,7 +190,10 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
return new PrimitiveValue($str);
}
public function getReturnTypeOverrides(CodeBase $unused_code_base): array
/**
* @unused-param $code_base
*/
public function getReturnTypeOverrides(CodeBase $code_base): array
{
$string_union_type = StringType::instance(false)->asPHPDocUnionType();
/**
@@ -468,7 +471,7 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
// emit issues with 1-based offsets
$emit_issue(
'PhanPluginPrintfNonexistentArgument',
'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}. This will be an ArgumentCountError in PHP 8',
'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}. This will be an ArgumentCountError in PHP 8.',
[self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)],
Issue::SEVERITY_CRITICAL,
self::ERR_UNTRANSLATED_NONEXISTENT
@@ -505,9 +508,9 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
// emit issues with 1-based offsets
$emit_issue(
'PhanPluginPrintfNonexistentArgument',
'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}',
'Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}. This will be an ArgumentCountError in PHP 8.',
[self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)],
Issue::SEVERITY_NORMAL,
Issue::SEVERITY_CRITICAL,
self::ERR_UNTRANSLATED_NONEXISTENT
);
} elseif ($largest_positional < count($arg_nodes)) {
@@ -592,7 +595,7 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
// @phan-suppress-next-line PhanThrowTypeAbsentForCall getExpectedUnionTypeName should only return valid union types
$expected_union_type = $expected_union_type->withType(Type::fromFullyQualifiedString($type_name));
}
if ($actual_union_type->canCastToUnionType($expected_union_type)) {
if ($actual_union_type->canCastToUnionType($expected_union_type, $code_base)) {
continue;
}
if (isset($expected_set['string'])) {
@@ -600,8 +603,8 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
// Allow passing objects with __toString() to printf whether or not strict types are used in the caller.
// TODO: Move into a common helper method?
try {
foreach ($actual_union_type->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, '__toString')) {
foreach ($actual_union_type->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, '__toString', true)) {
$can_cast_to_string = true;
break;
}
@@ -615,7 +618,7 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
}
$expected_union_type_string = (string)$expected_union_type;
if (self::canWeakCast($actual_union_type, $expected_set)) {
if (self::canWeakCast($actual_union_type, $expected_set, $code_base)) {
// This can be resolved by casting the arg to (string) manually in printf.
$emit_issue(
'PhanPluginPrintfIncompatibleArgumentTypeWeak',
@@ -673,14 +676,14 @@ class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapabil
/**
* @param array<string,true> $expected_set the types being checked for the ability to weakly cast to
*/
private static function canWeakCast(UnionType $actual_union_type, array $expected_set): bool
private static function canWeakCast(UnionType $actual_union_type, array $expected_set, CodeBase $code_base): bool
{
if (isset($expected_set['string'])) {
static $string_weak_types;
if ($string_weak_types === null) {
$string_weak_types = UnionType::fromFullyQualifiedPHPDocString('int|string|float');
}
return $actual_union_type->canCastToUnionType($string_weak_types);
return $actual_union_type->canCastToUnionType($string_weak_types, $code_base);
}
// We already allow int->float conversion
return false;
+87 -1
View File
@@ -82,6 +82,8 @@ This plugin is able to resolve literals, global constants, and class constants a
- **PhanPluginInvalidPregRegex**: The provided regex is invalid, according to PHP.
- **PhanPluginInvalidPregRegexReplacement**: The replacement string template of `preg_replace` refers to a match group that doesn't exist. (e.g. `preg_replace('/x(a)/', 'y$2', $strVal)`)
- **PhanPluginRegexDollarAllowsNewline**: `Call to {FUNCTION} used \'$\' in {STRING_LITERAL}, which allows a newline character \'\n\' before the end of the string. Add D to qualifiers to forbid the newline, m to match any newline, or suppress this issue if this is deliberate`
(This issue type is specific to coding style, and only checked for when configuration includes `['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`)
#### PrintfCheckerPlugin
@@ -151,12 +153,20 @@ Configuration settings can be added to `.phan/config.php`:
If you wish to make sure that analyzed files would be accepted by those PHP versions
(Requires that php72, php70, and php56 be locatable with the `$PATH` environment variable)
As of Phan 2.7.2, it is also possible to locally configure the PHP binary (or binaries) to run syntax checks with.
e.g. `phan --native-syntax-check php --native-syntax-check /usr/bin/php7.4` would run checks both with `php` (resolved with `$PATH`)
and the absolute path `/usr/bin/php7.4`. (see `phan --extended-help`)
#### UseReturnValuePlugin.php
This plugin warns when code fails to use the return value of internal functions/methods such as `sprintf` or `array_merge` or `Exception->getCode()`.
(functions/methods where the return value should almost always be used)
- **PhanPluginUseReturnValueInternalKnown**: `Expected to use the return value of the internal function/method {FUNCTION}`,
This also warns when using a return value of a function that returns the type `never`.
- **PhanPluginUseReturnValueInternalKnown**: `Expected to use the return value of the internal function/method {FUNCTION}` (and similar issues),
- **PhanPluginUseReturnValueGenerator**: `Expected to use the return value of the function/method {FUNCTION} returning a generator of type {TYPE}`,
- **PhanUseReturnValueOfNever**: `Saw use of value of expression {CODE} which likely uses the function {FUNCTIONLIKE} with a return type of '{TYPE}' - this will not return normally`,
`'plugin_config' => ['infer_pure_method' => true]` will make this plugin automatically infer which methods are pure, recursively.
This is a best-effort heuristic.
@@ -177,6 +187,7 @@ Note that this prevents the hardcoded checks from working.
- **PhanPluginUseReturnValue**: `Expected to use the return value of the user-defined function/method {FUNCTION} - {SCALAR}%% of calls use it in the rest of the codebase`,
- **PhanPluginUseReturnValueInternal**: `Expected to use the return value of the internal function/method {FUNCTION} - {SCALAR}%% of calls use it in the rest of the codebase`,
- **PhanPluginUseReturnValueGenerator**: `Expected to use the return value of the function/method {FUNCTION} returning a generator of type {TYPE}`,
See [UseReturnValuePlugin.php](./UseReturnValuePlugin.php) for configuration options.
@@ -253,6 +264,14 @@ This uses the following heuristics to reduce the number of false positives.
- Avoids warning when the actual return type contains multiple types and the declared return type is a single FQSEN
(e.g. don't warn about `Subclass1|Subclass2` being more specific than `BaseClass`)
#### UnsafeCodePlugin.php
This warns about code constructs that may be unsafe and prone to being used incorrectly in general.
- **PhanPluginUnsafeEval**: `eval() is often unsafe and may have better alternatives such as closures and is unanalyzable. Suppress this issue if you are confident that input is properly escaped for this use case and there is no better way to do this.`
- **PhanPluginUnsafeShellExec**: `This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling. Consider proc_open() instead.`
- **PhanPluginUnsafeShellExecDynamic**: `This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling, and is used with a non-constant. Consider proc_open() instead.`
### 3. Plugins Specific to Code Styles
These plugins may be useful to enforce certain code styles,
@@ -301,6 +320,14 @@ The warning types for methods are below:
- **PhanPluginDuplicatePropertyDescription**: `Property {PROPERTY} has the same description as the property {PROPERTY} on line {LINE}: {COMMENT}`
- **PhanPluginDuplicateMethodDescription**: `Method {METHOD} has the same description as the method {METHOD} on line {LINE}: {COMMENT}`
#### PHPDocInWrongCommentPlugin
This plugin warns about using phpdoc annotations such as `@param` in block comments(`/*`) instead of phpdoc comments(`/**`).
This also warns about using `#` instead of `//` for line comments, because `#[` is used for php 8.0 attributes and will cause confusion.
- **PhanPluginPHPDocInWrongComment**: `Saw possible phpdoc annotation in ordinary block comment {COMMENT}. PHPDoc comments should start with "/**", not "/*"`
- **PhanPluginPHPDocHashComment**: `Saw comment starting with # in {COMMENT} - consider using // instead to avoid confusion with php 8.0 #[ attributes`
#### InvalidVariableIssetPlugin.php
Warns about invalid uses of `isset`. This README documentation may be inaccurate for this plugin.
@@ -378,12 +405,18 @@ Warns about elements containing unknown types (function/method/closure return ty
This plugin checks for duplicate expressions in a statement
that are likely to be a bug. (e.g. `expr1 == expr`)
This will significantly increase the memory used by Phan, but that's rarely an issue in small projects.
- **PhanPluginDuplicateExpressionAssignment**: `Both sides of the assignment {OPERATOR} are the same: {CODE}`
- **PhanPluginDuplicateExpressionBinaryOp**: `Both sides of the binary operator {OPERATOR} are the same: {CODE}`
- **PhanPluginDuplicateConditionalTernaryDuplication**: `"X ? X : Y" can usually be simplified to "X ?: Y". The duplicated expression X was {CODE}`
- **PhanPluginDuplicateConditionalNullCoalescing**: `"isset(X) ? X : Y" can usually be simplified to "X ?? Y" in PHP 7. The duplicated expression X was {CODE}`
- **PhanPluginBothLiteralsBinaryOp**: `Suspicious usage of a binary operator where both operands are literals. Expression: {CODE} {OPERATOR} {CODE} (result is {CODE})` (e.g. warns about `null == 'a literal` in `$x ?? null == 'a literal'`)
- **PhanPluginDuplicateConditionalUnnecessary**: `"X ? Y : Y" results in the same expression Y no matter what X evaluates to. Y was {CODE}`
- **PhanPluginDuplicateCatchStatementBody**: `The implementation of catch({CODE}) and catch({CODE}) are identical, and can be combined if the application only needs to supports php 7.1 and newer`
- **PhanPluginDuplicateAdjacentStatement**: `Statement {CODE} is a duplicate of the statement on the above line. Suppress this issue instance if there's a good reason for this.`
Note that equivalent catch statements may be deliberate or a coding style choice, and this plugin does not check for TODOs.
#### WhitespacePlugin.php
@@ -496,6 +529,32 @@ Checks for complex variable access expressions `$$x`, which may be hard to read,
- **PhanPluginDollarDollar**: Warns about the use of $$x, ${(expr)}, etc.
### DeprecateAliasPlugin.php
Makes Phan analyze aliases of global functions (e.g. `join()`, `sizeof()`) as if they were deprecated.
Supports `--automatic-fix`.
#### PHP53CompatibilityPlugin.php
Catches common incompatibilities from PHP 5.3 to 5.6.
**This plugin does not aim to be comprehensive - read the guides on https://www.php.net/manual/en/appendices.php if you need to migrate from php versions older than 5.6**
`InvokePHPNativeSyntaxCheckPlugin` with `'php_native_syntax_check_binaries' => [PHP_BINARY, '/path/to/php53']` in the `'plugin_config'` is a better but slower way to check that syntax used does not cause errors in PHP 5.3.
`backward_compatibility_checks` should also be enabled if migrating a project from php 5 to php 7.
Emitted issue types:
- **PhanPluginCompatibilityShortArray**: `Short arrays ({CODE}) require support for php 5.4+`
- **PhanPluginCompatibilityArgumentUnpacking**: `Argument unpacking ({CODE}) requires support for php 5.6+`
- **PhanPluginCompatibilityVariadicParam**: `Variadic functions ({CODE}) require support for php 5.6+`
#### DuplicateConstantPlugin.php
Checks for duplicate constant names for calls to `define()` or `const X =` within the same statement list.
- **PhanPluginDuplicateConstant**: `Constant {CONST} was previously declared at line {LINE} - the previous declaration will be used instead`
#### AvoidableGetterPlugin.php
This plugin checks for uses of getters on `$this` that can be avoided inside of a class.
@@ -510,6 +569,33 @@ or hurt the readability of code.
This will also remove runtime type checks that were enforced by the getter's return type.
#### ConstantVariablePlugin.php
This plugin warns about using variables when they probably have only one possible scalar value (or the only inferred type is `null`).
This may catch some logic errors such as `echo($result === null ? json_encode($result) : 'default')`, or indicate places where it may or may not be clearer to use the constant itself.
Most of the reported issues will likely not be worth fixing, or be false positives due to references/loops.
- **PhanPluginConstantVariableBool**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}`
- **PhanPluginConstantVariableNull**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}`
- **PhanPluginConstantVariableScalar**: `Variable ${VARIABLE} is probably constant with a value of {TYPE}`
#### ShortArrayPlugin.php
This suggests using shorter array syntaxes if supported by the `minimum_target_php_version`.
- **PhanPluginLongArray**: `Should use [] instead of array()`
- **PhanPluginLongArrayList**: `Should use [] instead of list()`
#### RemoveDebugStatementPlugin.php
This suggests removing debugging output statements such as `echo`, `print`, `printf`, fwrite(STDERR)`, `var_export()`, inline html, etc.
This is only useful in applications or libraries that print output in only a few places, as a sanity check that debugging statements are not accidentally left in code.
- **PhanPluginRemoveDebugEcho**: `Saw output expression/statement in {CODE}`
- **PhanPluginRemoveDebugCall**: `Saw call to {FUNCTION} for debugging`
Suppression comments can use the issue name `PhanPluginRemoveDebugAny` to suppress all issue types emitted by this plugin.
### 4. Demo plugins:
These files demonstrate plugins for Phan.
@@ -86,7 +86,7 @@ class RedundantAssignmentPreAnalysisVisitor extends PluginAwarePreAnalysisVisito
return;
}
$expr = $node->children['expr'];
if (!ParseVisitor::isConstExpr($expr)) {
if (!ParseVisitor::isConstExpr($expr, ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
return;
}
try {
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\ContextNode;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for possible debugging statements.
*/
class RemoveDebugStatementPlugin extends PluginV3 implements
AnalyzeFunctionCallCapability,
PostAnalyzeNodeCapability
{
const ISSUE_GROUP = 'PhanPluginRemoveDebugAny';
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return RemoveDebugStatementVisitor::class;
}
/**
* @param CodeBase $code_base @phan-unused-param
* @return array<string, Closure(CodeBase,Context,Func,array,?Node=):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
$warn_remove_debug_call = static function (CodeBase $code_base, Context $context, FunctionInterface $function): void {
self::emitIssue(
$code_base,
$context,
'PhanPluginRemoveDebugCall',
'Saw call to {FUNCTION} for debugging',
[(string)$function->getFQSEN()]
);
};
/**
* @param list<Node|string|int|float> $unused_args the nodes for the arguments to the invocation
*/
$always_debug_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $unused_args,
?Node $unused_node = null
) use ($warn_remove_debug_call): void {
if (self::shouldSuppressDebugIssues($code_base, $context)) {
return;
}
$warn_remove_debug_call($code_base, $context, $function);
};
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
* Based on DependentReturnTypeOverridePlugin check
*/
$var_export_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args,
?Node $unused_node = null
) use ($warn_remove_debug_call): void {
if (self::shouldSuppressDebugIssues($code_base, $context)) {
return;
}
if (count($args) >= 2) {
$result = (new ContextNode($code_base, $context, $args[1]))->getEquivalentPHPScalarValue();
// @phan-suppress-next-line PhanSuspiciousTruthyString
if (is_object($result) || $result) {
return;
}
}
$warn_remove_debug_call($code_base, $context, $function);
};
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$fwrite_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args,
?Node $unused_node = null
) use ($warn_remove_debug_call): void {
$file = $args[0] ?? null;
if (!$file instanceof Node || $file->kind !== ast\AST_CONST || !in_array($file->children['name']->children['name'] ?? null, ['STDOUT', 'STDERR'], true)) {
// Could resolve the constant, but low priority
return;
}
if (self::shouldSuppressDebugIssues($code_base, $context)) {
return;
}
$warn_remove_debug_call($code_base, $context, $function);
};
return [
'var_dump' => $always_debug_callback,
'printf' => $always_debug_callback,
'debug_print_backtrace' => $always_debug_callback,
'debug_zval_dump' => $always_debug_callback,
// Warn for these functions unless the second argument is false
'var_export' => $var_export_callback,
'print_r' => $var_export_callback,
// check for STDOUT/STDERR
'fwrite' => $fwrite_callback,
'fprintf' => $fwrite_callback,
];
}
/**
* Returns true if any debug issue should be suppressed
*/
public static function shouldSuppressDebugIssues(CodeBase $code_base, Context $context): bool
{
return Issue::shouldSuppressIssue($code_base, $context, RemoveDebugStatementPlugin::ISSUE_GROUP, $context->getLineNumberStart(), []);
}
}
/**
* Analyzes node kinds that are associated with debugging
*/
class RemoveDebugStatementVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node a node of kind ast\AST_ECHO
*/
public function visitPrint(Node $node): void
{
$this->visitEcho($node);
}
/**
* @param Node $node a node which echoes or prints
*/
public function visitEcho(Node $node): void
{
if (RemoveDebugStatementPlugin::shouldSuppressDebugIssues($this->code_base, $this->context)) {
return;
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginRemoveDebugEcho',
"Saw output expression/statement in {CODE}",
[ASTReverter::toShortString($node)]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new RemoveDebugStatementPlugin();
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Config;
use Phan\Issue;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* Demo plugin to suggest using short array syntax.
*
* TODO: Implement a fixer if possible, e.g. base it on token_get_all()
*/
class ShortArrayPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return ShortArrayVisitor::class;
}
}
/**
* This class has visitArray called on all array literals in files to suggest using short arrays instead
*/
class ShortArrayVisitor extends PluginAwarePostAnalysisVisitor
{
// Do not define the visit() method unless a plugin has code and needs to visit most/all node types.
/**
* @param Node $node
* An array literal(AST_ARRAY) node to analyze
* @override
*/
public function visitArray(Node $node): void
{
switch ($node->flags) {
case \ast\flags\ARRAY_SYNTAX_LONG:
$this->emit(
'PhanPluginShortArray',
'Should use [] instead of array()',
[],
Issue::SEVERITY_LOW,
Issue::REMEDIATION_A
);
return;
case \ast\flags\ARRAY_SYNTAX_LIST:
if (Config::get_closest_minimum_target_php_version_id() >= 70100) {
$this->emit(
'PhanPluginShortArrayList',
'Should use [] instead of list()',
[],
Issue::SEVERITY_LOW,
Issue::REMEDIATION_A
);
}
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new ShortArrayPlugin();
@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
use ast\flags;
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\Language\Type\BoolType;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for expressions that can be simplified based on the union types.
* This is similar to `DuplicateExpressionPlugin`, which generally does not check union types.
*
* - E.g. `$x > 0 ? true : false` can be simplified to `$x > 0`
*
* Note that in PHP 7, many functions did not yet have real return types
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DuplicateExpressionPlugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class SimplifyExpressionPlugin extends PluginV3 implements
PostAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return SimplifyExpressionVisitor::class;
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* that can be simplified, and is called on nodes in post-order.
*/
class SimplifyExpressionVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* Returns true if all types are strictly subtypes of `bool`
*/
protected static function isDefinitelyBool(UnionType $union_type): bool
{
$real_type_set = $union_type->getRealTypeSet();
if (!$real_type_set) {
return false;
}
foreach ($real_type_set as $type) {
if (!$type->isInBoolFamily() || $type->isNullable()) {
return false;
}
if (count($real_type_set) === 1) {
// If the expression is `true` or `false`, assume that ExtendedDependentReturnPlugin or some other plugin
// inferred a literal value instead of the expression being guaranteed to be a boolean.
// (e.g. `strpos(SOME_CONST, 'val') === false`)
//
// TODO: Could check if the expression is a call and what the getRealReturnType is for that function.
return $type instanceof BoolType;
}
}
return true;
}
/**
* @param Node|string|int|float|null $node
* @return ?bool if this is the name of a boolean, the value. Otherwise, returns null.
*/
private static function getBoolConst($node): ?bool
{
if (!$node instanceof Node) {
return null;
}
if ($node->kind !== ast\AST_CONST) {
return null;
}
// @phan-suppress-next-line PhanPartialTypeMismatchArgumentInternal
switch (strtolower($node->children['name']->children['name'] ?? '')) {
case 'false':
return false;
case 'true':
return true;
}
return null;
}
/**
* @param Node $node
* A ternary operation node of kind ast\AST_CONDITIONAL to analyze
* @override
*/
public function visitConditional(Node $node): void
{
// Detect conditions such as`$bool ?: null` or `$bool ? true : false`
$true_node = $node->children['true'];
$value_if_true = $true_node !== null ? self::getBoolConst($true_node) : true;
if (!is_bool($value_if_true)) {
return;
}
$value_if_false = self::getBoolConst($node->children['false']);
if ($value_if_false !== !$value_if_true) {
return;
}
$this->suggestBoolSimplification($node, $node->children['cond'], !$value_if_true);
}
/**
* @param Node|string|int|float $inner_expr
*/
private function suggestBoolSimplification(Node $node, $inner_expr, bool $negate): void
{
if (!self::isDefinitelyBool(UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $inner_expr))) {
return;
}
// TODO: Use redundant condition detection helper methods to handle loops
$new_inner_repr = ASTReverter::toShortString($inner_expr);
if ($negate) {
$new_inner_repr = "!($new_inner_repr)";
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginSimplifyExpressionBool',
'{CODE} can probably be simplified to {CODE}',
[
ASTReverter::toShortString($node),
$new_inner_repr,
]
);
}
/**
* @param Node $node
* A binary op node of kind ast\AST_BINARY_OP to analyze
* @override
*/
public function visitBinaryOp(Node $node): void
{
$is_negated_assertion = false;
switch ($node->flags) {
case flags\BINARY_IS_NOT_IDENTICAL:
case flags\BINARY_IS_NOT_EQUAL:
case flags\BINARY_BOOL_XOR:
$is_negated_assertion = true;
case flags\BINARY_IS_EQUAL:
case flags\BINARY_IS_IDENTICAL:
['left' => $left_node, 'right' => $right_node] = $node->children;
$left_const = self::getBoolConst($left_node);
if (is_bool($left_const)) {
// E.g. `$x === true` can be simplified to `$x`
$this->suggestBoolSimplification($node, $right_node, $left_const === $is_negated_assertion);
return;
}
$right_const = self::getBoolConst($right_node);
if (is_bool($right_const)) {
$this->suggestBoolSimplification($node, $left_node, $right_const === $is_negated_assertion);
}
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new SimplifyExpressionPlugin();
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\Issue;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* NOTE: This is automatically loaded by phan. Do not include it in a config.
*
* Checks for potentially misusing static variables
*/
final class StaticVariableMisusePlugin extends PluginV3 implements
PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return StaticVariableMisuseVisitor::class;
}
}
/**
* Checks node kinds that can be used to access the inherited class
* for conflicts with uses of static variables.
*/
final class StaticVariableMisuseVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @override
*/
public function visitVar(Node $node): void
{
$name = $node->children['name'];
if ($name !== 'this') {
return;
}
$this->analyzeStaticAccessCommon($node);
}
/**
* @override
*/
public function visitName(Node $node): void
{
$context = $this->context;
if (!$context->isInClassScope() || !$context->isInFunctionLikeScope()) {
return;
}
$name = $node->children['name'];
if (!is_string($name)) {
return;
}
if (strcasecmp($name, 'static') !== 0) {
return;
}
$this->analyzeStaticAccessCommon($node);
}
private function analyzeStaticAccessCommon(Node $node): void
{
$context = $this->context;
if (!$context->isInClassScope() || !$context->isInFunctionLikeScope()) {
return;
}
$function = $context->getFunctionLikeInScope($this->code_base);
if (!$function->hasStaticVariable()) {
return;
}
$class = $context->getClassInScope($this->code_base);
if ($class->isFinal()) {
return;
}
$this->emitIssue(
Issue::StaticClassAccessWithStaticVariable,
$node->lineno,
ASTReverter::toShortString($node)
);
}
}
return new StaticVariableMisusePlugin();
+5 -3
View File
@@ -38,22 +38,24 @@ class StrictComparisonPlugin extends PluginV3 implements
/**
* @param CodeBase $code_base @phan-unused-param
* @return array<string, Closure(CodeBase,Context,Func,array):void>
* @return array<string, Closure(CodeBase,Context,Func,array,?Node=):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @return Closure(CodeBase,Context,Func,array):void
* @return Closure(CodeBase,Context,Func,array,?Node=):void
*/
$make_callback = static function (int $index, string $index_name, int $min_args): Closure {
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
* @unused-param $node
*/
return static function (
CodeBase $code_base,
Context $context,
Func $func,
array $args
array $args,
?Node $node = null
) use (
$index,
$index_name,
@@ -58,8 +58,8 @@ class StrictLiteralComparisonVisitor extends PluginAwarePostAnalysisVisitor
private function analyzeEqualityCheck(Node $node): void
{
['left' => $left, 'right' => $right] = $node->children;
$left_is_const = ParseVisitor::isConstExpr($left);
$right_is_const = ParseVisitor::isConstExpr($right);
$left_is_const = ParseVisitor::isConstExpr($left, ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION);
$right_is_const = ParseVisitor::isConstExpr($right, ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION);
if ($left_is_const === $right_is_const) {
return;
}
+112 -10
View File
@@ -36,6 +36,8 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
// this is deliberate for issue names
private const SuspiciousParamOrderInternal = 'PhanPluginSuspiciousParamOrderInternal';
private const SuspiciousParamOrder = 'PhanPluginSuspiciousParamOrder';
private const SuspiciousParamPosition = 'PhanPluginSuspiciousParamPosition';
private const SuspiciousParamPositionInternal = 'PhanPluginSuspiciousParamPositionInternal';
// phpcs:enable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
/**
@@ -45,8 +47,9 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
public function visitCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 2) {
// Can't have a suspicious param order if there are less than 2 params
if (count($args) < 1) {
// Can't have a suspicious param order/position if there are no params
// (or for AST_CALLABLE_CONVERT)
return;
}
$expression = $node->children['expr'];
@@ -113,7 +116,7 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
}
/**
* @param list<Node|string|int|float|null> $args
* @param list<Node|string|int|float> $args
*/
private function checkCall(FunctionInterface $function, array $args, Node $node): void
{
@@ -121,11 +124,14 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
foreach ($args as $i => $arg_node) {
$name = self::extractName($arg_node);
if (!is_string($name)) {
return;
continue;
}
$arg_names[$i] = strtolower($name);
}
if (count($arg_names) < 2) {
if (count($arg_names) === 1) {
$this->checkMovedArg($function, $args, $node, $arg_names);
}
return;
}
$parameters = $function->getParameterList();
@@ -138,6 +144,8 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
$parameter_names[$i] = strtolower($parameters[$i]->getName());
}
if (count($arg_names) < 2) {
// $arg_names and $parameter_names have the same keys
$this->checkMovedArg($function, $args, $node, $arg_names);
return;
}
$best_destination_map = [];
@@ -166,18 +174,23 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
}
}
if (count($best_destination_map) < 2) {
$this->checkMovedArg($function, $args, $node, $arg_names);
return;
}
$places_set = [];
foreach (self::findCycles($best_destination_map) as $cycle) {
// To reduce false positives, don't warn unless we know the parameter $j would be compatible with what was used at $i
foreach ($cycle as $array_index => $i) {
$j = $cycle[($array_index + 1) % count($cycle)];
$type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $args[$i]);
// echo "Checking if $type can cast to $parameters[$j]\n";
if (!$type->asExpandedTypes($this->code_base)->canCastToUnionType($parameters[$j]->getUnionType())) {
if (!$type->canCastToUnionType($parameters[$j]->getUnionType(), $this->code_base)) {
continue 2;
}
}
foreach ($cycle as $i) {
$places_set[$i] = true;
}
$arg_details = implode(' and ', array_map(static function (int $i) use ($args): string {
return self::extractName($args[$i]) ?? 'unknown';
}, $cycle));
@@ -213,6 +226,85 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
);
}
}
$this->checkMovedArg($function, $args, $node, $arg_names, $places_set);
}
/**
* @param FunctionInterface $function the function being called
* @param list<Node|string|int|float> $args
* @param Node $node
* @param associative-array<int,string> $arg_names
* @param associative-array<int,true> $places_set the places that were already warned about being transposed.
*/
private function checkMovedArg(FunctionInterface $function, array $args, Node $node, array $arg_names, array $places_set = []): void
{
$real_parameters = $function->getRealParameterList();
$parameters = $function->getParameterList();
/** @var associative-array<string,?int> maps lowercase param names to their unique index, or null */
$parameter_names = [];
foreach ($real_parameters as $i => $param) {
if (isset($places_set[$i])) {
continue;
}
$name_key = str_replace('_', '', strtolower($param->getName()));
if (array_key_exists($name_key, $parameter_names)) {
$parameter_names[$name_key] = null;
} else {
$parameter_names[$name_key] = $i;
}
}
foreach ($arg_names as $i => $name) {
$other_i = $parameter_names[str_replace('_', '', strtolower($name))] ?? null;
if ($other_i === null || $other_i === $i) {
continue;
}
$real_param = $real_parameters[$other_i];
if ($real_param->isVariadic()) {
// Skip warning about signatures such as var_dump($var, ...$args) or array_unshift($values, $arg, $arg2)
//
// NOTE: For internal functions, some functions such as implode() have alternate signatures where the real parameter is in a different place,
// which is why this checks both $real_param and $param
//
// For user-defined functions, alternates are not supported.
continue;
}
$param = $parameters[$other_i] ?? null;
if ($param && $param->getName() === $real_param->getName()) {
if ($param->isVariadic()) {
continue;
}
$real_param = $param;
}
$real_param_details = '#' . ($other_i + 1) . ' (' . trim($real_param->getUnionType() . ' $' . $real_param->getName()) . ')';
$arg_details = self::extractName($args[$i]) ?? 'unknown';
if ($function->isPHPInternal()) {
$this->emitPluginIssue(
$this->code_base,
(clone($this->context))->withLineNumberStart($args[$i]->lineno ?? $node->lineno),
self::SuspiciousParamPositionInternal,
'Suspicious order for argument {DETAILS} - This is getting passed to parameter {DETAILS} of {FUNCTION}',
[
$arg_details,
$real_param_details,
$function->getRepresentationForIssue(true),
]
);
} else {
$this->emitPluginIssue(
$this->code_base,
clone($this->context)->withLineNumberStart($args[$i]->lineno ?? $node->lineno),
self::SuspiciousParamPosition,
'Suspicious order for argument {DETAILS} - This is getting passed to parameter {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}',
[
$arg_details,
$real_param_details,
$function->getRepresentationForIssue(true),
$function->getContext()->getFile(),
$function->getContext()->getLineNumberStart(),
]
);
}
}
}
/**
@@ -265,6 +357,15 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
return $result;
}
/**
* @param Node $node a node of type AST_NULLSAFE_METHOD_CALL
* @override
*/
public function visitNullsafeMethodCall(Node $node): void
{
$this->visitMethodCall($node);
}
/**
* @param Node $node a node of type AST_METHOD_CALL
* @override
@@ -272,8 +373,9 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
public function visitMethodCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 2) {
// Can't have a suspicious param order if there are less than 2 params
if (count($args) < 1) {
// Can't have a suspicious param order/position if there are no params
// (or for AST_CALLABLE_CONVERT)
return;
}
@@ -287,7 +389,7 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
$this->code_base,
$this->context,
$node
))->getMethod($method_name, false);
))->getMethod($method_name, false, true);
} catch (Exception $_) {
return;
}
@@ -302,8 +404,8 @@ class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
public function visitStaticCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 2) {
// Can't have a suspicious param order if there are less than 2 params
if (count($args) < 1) {
// Can't have a suspicious param order/position if there are no params
return;
}
@@ -88,7 +88,7 @@ class UnknownClassElementAccessPlugin extends PluginV3 implements
/**
* Prevent this plugin from warning about $node_string at this file and line
*/
public static function blacklistMethodIssue(Context $context, Node $node): void
public static function preventMethodIssueWarning(Context $context, Node $node): void
{
$node_string = ASTReverter::toShortString($node);
$key = self::generateKey($context, $node->lineno, $node_string);
@@ -121,6 +121,14 @@ class UnknownClassElementAccessPlugin extends PluginV3 implements
*/
class UnknownClassElementAccessVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node a node of kind ast\AST_NULLSAFE_METHOD_CALL, representing a call to an instance method
*/
public function visitNullsafeMethodCall(Node $node): void
{
$this->visitMethodCall($node);
}
/**
* @param Node $node a node of kind ast\AST_METHOD_CALL, representing a call to an instance method
*/
@@ -135,8 +143,8 @@ class UnknownClassElementAccessVisitor extends PluginAwarePostAnalysisVisitor
return;
}
foreach ($union_type->getTypeSet() as $type) {
if ($type->isObjectWithKnownFQSEN()) {
UnknownClassElementAccessPlugin::blacklistMethodIssue($this->context, $node);
if ($type->hasObjectWithKnownFQSEN()) {
UnknownClassElementAccessPlugin::preventMethodIssueWarning($this->context, $node);
return;
}
}
@@ -355,7 +355,7 @@ class UnknownElementTypePlugin extends PluginV3 implements
}
/**
* @param CodeBase $_
* @param CodeBase $code_base @unused-param
* The code base in which the property exists
*
* @param Property $property
@@ -363,7 +363,7 @@ class UnknownElementTypePlugin extends PluginV3 implements
* @override
*/
public function analyzeProperty(
CodeBase $_,
CodeBase $code_base,
Property $property
): void {
if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) {
@@ -380,8 +380,13 @@ class UnknownElementTypePlugin extends PluginV3 implements
public function finalizeProcess(CodeBase $code_base): void
{
foreach ($this->deferred_checks as $check) {
$check($code_base);
try {
foreach ($this->deferred_checks as $check) {
$check($code_base);
}
} finally {
// There were errors in unit tests if this wasn't cleared.
$this->deferred_checks = [];
}
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\Parse\ParseVisitor;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for occurrences of unsafe constructs such as shell_exec, eval(), etc.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* UnsafeCodePlugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
*
* - End by returning an instance of that class.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class UnsafeCodePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return UnsafeCodeVisitor::class;
}
}
/**
* When __invoke on this class is called with a node, a method
* will be dispatched based on the `kind` of the given node.
*
* Visitors such as this are useful for defining lots of different
* checks on a node based on its kind.
*/
class UnsafeCodeVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node a
* A node of kind ast\AST_INCLUDE_OR_EVAL to analyze
* @override
*/
public function visitIncludeOrEval(Node $node): void
{
if ($node->flags !== ast\flags\EXEC_EVAL) {
return;
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginUnsafeEval',
'eval() is often unsafe and may have better alternatives such as closures and is unanalyzable. Suppress this issue if you are confident that input is properly escaped for this use case and there is no better way to do this.',
[]
);
}
/**
* @param Node $node a
* A node of kind ast\AST_SHELL_EXEC to analyze
* @override
*/
public function visitShellExec(Node $node): void
{
if (!ParseVisitor::isConstExpr($node->children['expr'], ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginUnsafeShellExecDynamic',
'This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling, and is used with a non-constant. Consider proc_open() instead.',
[ASTReverter::toShortString($node)]
);
return;
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginUnsafeShellExec',
'This syntax for shell_exec() ({CODE}) is easily confused for a string and does not allow proper exit code/stderr handling. Consider proc_open() instead.',
[ASTReverter::toShortString($node)]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnsafeCodePlugin();
+21 -13
View File
@@ -60,7 +60,7 @@ class UnusedSuppressionPlugin extends PluginV3 implements
* issue type to
* unique list of line numbers of suppressions
*/
private $plugin_active_suppression_list;
private $plugin_active_suppression_list = [];
/**
* @param CodeBase $code_base
@@ -111,7 +111,7 @@ class UnusedSuppressionPlugin extends PluginV3 implements
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the class exists
*
* @param Clazz $class
@@ -119,14 +119,14 @@ class UnusedSuppressionPlugin extends PluginV3 implements
* @override
*/
public function analyzeClass(
CodeBase $unused_code_base,
CodeBase $code_base,
Clazz $class
): void {
$this->postponeAnalysisOfElement($class);
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the method exists
*
* @param Method $method
@@ -134,12 +134,12 @@ class UnusedSuppressionPlugin extends PluginV3 implements
* @override
*/
public function analyzeMethod(
CodeBase $unused_code_base,
CodeBase $code_base,
Method $method
): void {
// Ignore methods inherited by subclasses
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) {
return;
}
@@ -147,7 +147,7 @@ class UnusedSuppressionPlugin extends PluginV3 implements
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the function exists
*
* @param Func $function
@@ -155,14 +155,14 @@ class UnusedSuppressionPlugin extends PluginV3 implements
* @override
*/
public function analyzeFunction(
CodeBase $unused_code_base,
CodeBase $code_base,
Func $function
): void {
$this->postponeAnalysisOfElement($function);
}
/**
* @param CodeBase $unused_code_base
* @param CodeBase $code_base @unused-param
* The code base in which the property exists
*
* @param Property $property
@@ -170,9 +170,12 @@ class UnusedSuppressionPlugin extends PluginV3 implements
* @override
*/
public function analyzeProperty(
CodeBase $unused_code_base,
CodeBase $code_base,
Property $property
): void {
if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) {
return;
}
$this->elements_for_postponed_analysis[] = $property;
}
@@ -273,11 +276,16 @@ class UnusedSuppressionPlugin extends PluginV3 implements
return;
}
/**
* @unused-param $code_base
* @unused-param $file_contents
* @unused-param $node
*/
public function beforeAnalyzeFile(
CodeBase $unused_code_base,
CodeBase $code_base,
Context $context,
string $unused_file_contents,
Node $unused_node
string $file_contents,
Node $node
): void {
$file = $context->getFile();
$this->files_for_postponed_analysis[$file] = $file;
+2 -2
View File
@@ -46,7 +46,7 @@ class WhitespacePlugin extends PluginV3 implements
string $file_contents,
Node $node
): void {
if (!preg_match('/[\r\t]|[ \t]\r?$/m', $file_contents)) {
if (!preg_match('/[\r\t]|[ \t]\r?$/mS', $file_contents)) {
// Typical case: no errors
return;
}
@@ -68,7 +68,7 @@ class WhitespacePlugin extends PluginV3 implements
'The first occurrence of a tab was seen here. Running "expand" can fix that.'
);
}
if (preg_match('/[ \t]\r?$/m', $file_contents, $match, PREG_OFFSET_CAPTURE)) {
if (preg_match('/[ \t]\r?$/mS', $file_contents, $match, PREG_OFFSET_CAPTURE)) {
self::emitIssue(
$code_base,
clone($context)->withLineNumberStart(self::calculateLine($file_contents, $match[0][1])),
+1 -1
View File
@@ -77,7 +77,7 @@ return [
foreach (explode("\n", $raw_contents) as $line_contents) {
$new_byte_offset = $byte_offset + strlen($line_contents) + 1;
$line_contents = rtrim($line_contents, "\r");
if (preg_match('/\s+$/', $line_contents, $matches)) {
if (preg_match('/\s+$/D', $line_contents, $matches)) {
$len = strlen($matches[0]);
$offset = $byte_offset + strlen($line_contents) - $len;
// Remove 1 or more bytes of trailing whitespace from each line
+851 -4
View File
@@ -1,5 +1,852 @@
Phan NEWS
Aug 01 2021, Phan 5.0.0
-----------------------
New Features (Analysis):
- Warn about implicitly nullable parameter intersection types (`function(A&B $paramName = null)`) being a compile error.
New issue type: `PhanTypeMismatchDefaultIntersection`
- Emit `PhanTypeMismatchArgumentSuperType` instead of `PhanTypeMismatchArgument` when passing in an object supertype (e.g. ancestor class) of an object instead of a subtype.
Emit `PhanTypeMismatchReturnSuperType` instead of `PhanTypeMismatchReturn` when returning an object supertype (e.g. ancestor class) of an object instead of a subtype.
Phan 5 starts warning about ancestor classes being incompatible argument or return types in cases where it previously allowed it. (#4413)
Jul 24 2021, Phan 5.0.0a4
-------------------------
New Features (Analysis):
- Use the enum class declaration type (int, string, or absent) from AST version 85 to check if enum cases are valid. (#4313)
New issue types: `PhanSyntaxEnumCaseExpectedValue`, `PhanSyntaxEnumCaseUnexpectedValue`, `PhanTypeUnexpectedEnumCaseType`
Backwards incompatible changes:
- Bump the minimum required AST version from 80 to 85 (Required to analyze php 8.1 enum classes - 'type' was added in AST version 85).
- In php 8.1, require php-ast 1.0.14 to natively parse AST version 85.
Maintenance:
- Upgrade tolerant-php-parser from 0.1.0 to 0.1.1 to prepare to support new php syntax in the polyfill/fallback parser. (#4449)
Bug fixes:
- Fix extraction of reflection attribute target type bitmask from internal attributes such as PHP 8.1's `ReturnTypeWillChange`
Jul 15 2021, Phan 5.0.0a3
-------------------------
New Features (Analysis):
+ Support parsing php 8.1 intersection types in php-ast 1.0.13+ (#4469)
(not yet supported in polyfill)
+ Support parsing php 8.1 first-class callable syntax in unreleased php-ast version (#4464)
+ Support parsing php 8.1 readonly property modifier (#4463)
+ Support allowing `new` expressions in php 8.1 readonly property modifier (#4460)
+ Emit `PhanTypeInvalidArrayKey` and `PhanTypeInvalidArrayKeyValue` for invalid array key literal types or values.
+ Fix false positive `PhanTypeMissingReturn`/`PhanPluginAlwaysReturnMethod` for method with phpdoc return type of `@return never`
+ Warn about direct access to static methods or properties on traits (instead of classes using those methods/properties) being deprecated in php 8.1 (#4396)
+ Add `Stringable` to allowed types for sprintf variadic arguments. This currently requires explicitly implementing Stringable. (#4466)
Bug fixes:
- Fix a crash when analyzing array literals with invalid key literal values in php 8.1.
- Fix a crash due to deprecation notices for accessing trait methods/properties directly in php 8.1
Jun 26 2021, Phan 5.0.0a2
-------------------------
New Features (Analysis):
- Improve accuracy of checks for weak type overlap for redundant condition warnings on `<=`
- Emit `PhanAccessOverridesFinalConstant` when overriding a final class constant. (#4436)
- Emit `PhanCompatibleFinalClassConstant` if class constants have the final modifier in codebases supporting a minimum target php version older than 8.1 (#4436)
- Analyze class constants declared in interfaces as if they were final in php versions prior to 8.1. (#4436)
- Warn about using $this or superglobals as a parameter or closure use. (#4336)
New Features (CLI)
- Use `var_representation`/polyfill for generating representations of values in issue messages.
Maintenance:
- Upgrade tolerant-php-parser from 0.0.23 to 0.1.0 to prepare to support new php syntax in the polyfill/fallback parser. (#4449)
Bug fixes:
- Properly warn about referencing $this from a `static fn` declared in an instance method. (#4336)
- Fix a crash getting template parameters of intersection types
May 30 2021, Phan 5.0.0a1
-------------------------
Phan 5 introduces support for intersection types, and improves the accuracy of type casting checks and type inference to catch more issues.
This is the unstable branch for alpha releases of Phan 5. Planned/remaining work is described in https://github.com/phan/phan/issues/4413
If you are migrating from Phan 4, it may be useful to set up or update a Phan [baseline file](https://github.com/phan/phan/wiki/Phan-Config-Settings#baseline_path) to catch issues such as nullable type mismatches.
https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base has other advice on setting up suppressions.
For example, Phan is now more consistently warning about nullable arguments (i.e. both `\X|null` and `?\X`) in a few cases where it may have not warned about passing `\X|null` to a function that expects a non-null type.
If you are using plugins that are not part of Phan itself, they may have issues in Phan 5 due
to additional required methods being added to many of Phan's methods.
New Features (Analysis):
+ Support parsing intersection types in phpdoc and checking if intersection types satisfy type comparisons
+ Support inferring intersection types from conditions such as `instanceof`
+ Warn about impossible type combinations in phpdoc intersection types.
New issue types: `PhanImpossibleIntersectionType`
+ Improve type checking precision for whether a type can cast to another type.
+ Improve precision of checking if a type is a subtype of another type.
+ Split out warnings about possibly invalid types for property access (non-object) and possibly invalid classes for property access
New issue types: `PhanPossiblyUndeclaredPropertyOfClass`
+ Also check for partially invalid expressions for instance properties during assignment (`PhanPossiblyUndeclaredProperty*`)
+ Treat `@template-covariant T` as an alias of `@template T` - Previously, that tag was not parsed and `T` would be treated like a (probably undeclared) classlike name. (#4432)
Bug fixes:
+ Fix wrong expression in issue message for PhanPossiblyNullTypeMismatchProperty (#4427)
Breaking Changes:
+ Many internal methods now require a mandatory `CodeBase` instance. This will affect third party plugins.
+ Remove `--language-server-min-diagnostic-delay-ms`.
May 19 2021, Phan 4.0.7 (dev)
-----------------------
Language Server/Daemon mode:
+ Fix an uncaught exception sometimes seen checking for issue suppressions when pcntl is unavailable.
Bug fixes:
+ Don't emit `PhanCompatibleNonCapturingCatch` when `minimum_target_php_version` is `'8.0'` or newer. (#4433)
+ Stop ignoring `@return null` and `@param null $paramName` in phpdoc. (#4453)
Stop special casing `@param null` now that Phan allows many other literal types in param types.
May 19 2021, Phan 4.0.6
-----------------------
New Features (Analysis):
+ Partially support php 8.1 enums (#4313)
(infer the real type is the class type, that they cannot be instantiated, that enum values cannot be reused, and that class constants will exist for enum cases)
New issue types: `PhanReusedEnumCaseValue`, `PhanTypeInstantiateEnum`, `PhanTypeInvalidEnumCaseType`, `PhanSyntaxInconsistentEnum`,
`PhanInstanceMethodWithNoEnumCases`, `PhanInstanceMethodWithNoEnumCases`, `PhanEnumCannotHaveProperties`, `PhanUnreferencedEnumCase`,
`PhanEnumForbiddenMagicMethod`.
+ Support php 7.4 covariant return types and contravariant parameter types when the configured or inferred `minimum_target_php_version` is `'7.4'` or newer (#3795)
+ Add initial support for the php 8.1 `never` type (in real return types and phpdoc). (#4380)
Also add support for the phpdoc aliases `no-return`, `never-return`, and `never-returns`
+ Support casting `iterable<K, V>` to `Traversable<K, V>` with `is_object` or `!is_array` checks
+ Detect more types of expressions that never return when inferring types (e.g. when analyzing `?:`, `??` operators)
+ Use php 8.1's tentative return types from reflection (`hasTentativeReturnType`, `getTentativeReturnType`) to assume real return types of internal functions/methods (#4400)
This can be disabled by setting `use_tentative_return_type` to `false` (e.g. when using subclasses of internal classes that return incompatible types).
+ Warn about modifying properties of classes that are immutable at runtime (enums, internal classes such as `\Closure` and `\WeakRef`, etc.) (#4313)
New issue type: `PhanTypeModifyImmutableObjectProperty`
Dead code detection:
+ Infer that functions with a return type of `never` (or phpdoc aliases such as `no-return`) are unreachable when performing control flow analysis.
This can be disabled by setting `dead_code_detection_treat_never_type_as_unreachable` to false
Note that control flow is only affected when `UseReturnValuePlugin` is enabled.
Plugins:
+ In `UseReturnValuePlugin`, also start warning about when using the result of an expression that evaluates to `never`
New issue types: `PhanUseReturnValueOfNever`
Bug fixes:
+ As part of the work on php 7.4 contravariant parameter types,
don't automatically inherit inferred parameter types from ancestor classlikes when (1) there is no `@param` tag with a type for the parameter on the overriding method and (2) the ancestor parameter types are a subtype of the real parameter types unless
1. `@inheritDoc` is used.
2. This is a generic array type such as `array<string,mixed>` that is a specialization of an array type.
If you want to indicate that the overriding method can be any array type, add `@param array $paramName`.
+ Change composer.json dependency on `composer/xdebug-handler` from `^2.0` to `^1.1|2.0` to avoid conflicting with other libraries or applications that depend on xdebug-handler 1.x (#4382)
+ Support parsing multiple declare directives in the polyfill/fallback parser (#4160)
Apr 29 2021, Phan 4.0.5
-----------------------
New Features (Analysis):
+ Fix handling of some redundant condition checks involving `non-null-mixed` and `null` (#4388, #4391)
+ Emit `PhanCompatibleSerializeInterfaceDeprecated` when a class implements Serializable without also implementing the `__serialize` and `__unserialize` methods as well. (#4387)
PHP 8.1 deprecates the `Serializable` interface when `__serialize` and `__unserialize` aren't also implemented to be used instead of `serialize`/`unserialize`.
Maintenance:
+ Warn about running phan with multiple processes without pcntl before the analysis phase starts.
+ Start implementing `__serialize`/`__unserialize` in Phan itself in places that use `Serializable`.
+ Use different static variables in different subclasses of `Phan\Language\Type` to account for changes in static variable inheritance in php 8.1. (#4379)
Bug fixes:
+ Allow `?T` to be used in parameter/property types with `@template T` (#4388)
Apr 14 2021, Phan 4.0.4
-----------------------
New Features (CLI, Config):
+ Support `--doc-comment` flag on `tool/make_stubs` to emit the doc comments Phan
is using for internal elements along with the stubs.
(these are the doc comments Phan would use for hover text in the language server)
+ Allow `target_php_version` and `minimum_target_php_version` to be 8.1 or newer.
New Features (Analysis):
+ Support the php 8.1 array unpacking with string keys RFC (#4358).
Don't emit warnings about array unpacking with string keys when `minimum_target_php_version` is '8.1' or newer.
+ Support php 8.1 `array_is_list(array $array): bool` conditional and its negation. (#4348)
+ Fix some false positive issues when trying to eagerly evaluate expressions without emitting issues (#4377)
Bug fixes:
+ Fix crash analyzing union type in trait (#4383)
Maintenance:
+ Update from xdebug-handler 1.x to 2.0.0 to support Xdebug 3 (#4382)
Plugins:
+ Cache plugin instances in `ConfigPluginSet`. This is useful for unit testing stateless plugins which declare the plugin class in the same file returning the plugin instance. (#4352)
Jan 29 2021, Phan 4.0.3
-----------------------
New Features:
+ Support inferring iterable value types/keys from `getIterator` returning an ordinary `Iterator<X>` (previously only inferred types for subclasses of Iterator)
Bug fixes:
+ Fix crash when rendering `[...$x]` in an issue message (#4351)
+ Infer that `if ($x)` `converts non-null-mixed` to `non-empty-mixed`
+ Fix false positive warning case for PhanParamSignaturePHPDocMismatchParamType when a phpdoc parameter has a default value (#4357)
+ Properly warn about accessing a private class constant as `self::CONST_NAME` from inside of a subclass of the constant's declaring class (#4360)
+ Properly infer `allow_method_param_type_widening` from `minimum_target_php_version` to avoid false positive `PhanParamSignatureRealMismatchHasNoParamType`.
Jan 09 2021, Phan 4.0.2
-----------------------
New Features:
+ Improve suggestions for `PhanUndeclaredThis` inside of static methods/closures (#4336)
Language Server/Daemon mode:
+ Properly generate code completions for `::` and `->` at the end of a line on files using Windows line endings(`\r\n`) instead of Unix newlines(`\n`) on any OS (#4345)
Previously, those were not completed.
Bug fixes:
+ Fix false positive `PhanParamSignatureMismatch` for variadic overriding a function using `func_get_args()` (#4340)
+ Don't emit PhanTypeNoPropertiesForeach for the Countable interface on its own. (#4342)
+ Fix false positive type mismatch warning for casts from callable-object/callable-array/callable-string
to `function(paramtypes):returntype` (#4343)
Dec 31 2020, Phan 4.0.1
-----------------------
New Features:
+ Emit `PhanCompatibleAssertDeclaration` when declaring a function called `assert`. (#4333)
Bug fixes:
+ Fix false positive `PhanInvalidConstantExpression` for named arguments in attributes (#4334)
Merge changes from Phan 3.2.10
Dec 23 2020, Phan 4.0.0
-----------------------
+ Merge changes from Phan 3.2.9.
+ Relax minimum php-ast restrictions when polyfill is used for Phan 4.
+ Fix conflicting class constant seen in polyfill when php-ast 1.0.6 was installed.
The Phan v4 release line has the following changes from Phan 3:
- Bump the minimum required AST version from 70 to 80 (Required to analyze php 8.0 attributes - the rest of the php 8.0 syntax changes are supported in both Phan 3 and Phan 4).
A few third party plugins may be affected by the increase of the AST version.
- Supports analyzing whether `#[...]` attributes are used properly when run with PHP 8.0+
Dec 23 2020, Phan 4.0.0-RC2
---------------------------
Merge changes from Phan 3.2.8.
Dec 13 2020, Phan 4.0.0-RC1
---------------------------
Merge changes from Phan 3.2.7.
Nov 27 2020, Phan 4.0.0-alpha5
------------------------------
Merge changes from Phan 3.2.6.
Nov 26 2020, Phan 4.0.0-alpha4
------------------------------
Merge changes from Phan 3.2.5.
Nov 12 2020, Phan 4.0.0-alpha3
------------------------------
Merge changes from Phan 3.2.4.
Oct 12 2020, Phan 4.0.0-alpha2
------------------------------
Merge changes from Phan 3.2.3.
Sep 19 2020, Phan 4.0.0-alpha1
------------------------------
New features (Analysis):
+ Support analyzing PHP 8.0 attributes when Phan is run with php 8.0 or newer.
Warn if the attribute syntax is likely to be incompatible in php 7.
Warn if using attributes incorrectly or with incorrect argument lists.
New issue types: `PhanCompatibleAttributeGroupOnSameLine`, `PhanCompatibleAttributeGroupOnMultipleLines`,
`PhanAttributeNonAttribute`, `PhanAttributeNonClass`, `PhanAttributeNonRepeatable`,
`PhanUndeclaredClassAttribute`, `PhanAttributeWrongTarget`, `PhanAccessNonPublicAttribute`.
Backwards incompatible changes:
+ Switch from AST version 70 to AST version 80.
`php-ast` should be upgraded to version 1.0.10-dev or newer.
+ Drop the no-op `--polyfill-parse-all-doc-comments` flag.
Miscellaneous:
+ Make various classes from Phan implement `Stringable`.
Dec 31 2020, Phan 3.2.10 (dev)
-----------------------
Bug fixes:
+ Fix false positive PhanPossiblyFalseTypeReturn with strict type checking for substr when target php version is 8.0+ (#4335)
Dec 26 2020, Phan 3.2.9
-----------------------
Bug fixes:
+ Fix a few parameter names for issue messages (#4316)
+ Fix bug that could cause Phan not to warn about `SomeClassWithoutConstruct::__construct`
in some edge cases. (#4323)
+ Properly infer `self` is referring to the current object context even when the object context is unknown in namespaces. (#4070)
Deprecations:
+ Emit a deprecation notice when running this in PHP 7 and php-ast < 1.0.7. (#4189)
This can be suppressed by setting the environment variable `PHAN_SUPPRESS_AST_DEPRECATION=1`.
Dec 23 2020, Phan 3.2.8
-----------------------
Bug fixes:
+ Fix false positive PhanUnusedVariable for variable redefined in loop (#4301)
+ Fix handling of `-z`/`--signature-compatibility` - that option now enables `analyze_signature_compatibility` instead of disabling it. (#4303)
+ Fix possible `PhanCoalescingNeverUndefined` for variable defined in catch block (#4305)
+ Don't emit `PhanCompatibleConstructorPropertyPromotion` when `minimum_target_php_version` is 8.0 or newer. (#4307)
+ Infer that PHP 8.0 constructor property promotion's properties have write references. (#4308)
They are written to by the constructor.
+ Inherit phpdoc parameter types for the property declaration in php 8.0 constructor property promotion (#4311)
Dec 13 2020, Phan 3.2.7
-----------------------
New features (Analysis):
+ Update real parameter names to match php 8.0's parameter names for php's own internal methods (including variadics and those with multiple signatures). (#4263)
Update real parameter names, types, and return types for some PECL extensions.
+ Raise the severity of some php 8.0 incompatibility issues to critical.
+ Fix handling of references after renaming variadic reference parameters of `fscanf`/`scanf`/`mb_convert_variables`
+ Mention if PhanUndeclaredFunction is potentially caused by the target php version being too old. (#4230)
+ Improve real type inference for conditionals on literal types (#4288)
+ Change the way the real type set of array access is inferred for mixes of array shapes and arrays (#4296)
+ Emit `PhanSuspiciousNamedArgumentVariadicInternal` when using named arguments with variadic parameters of internal functions that are
not among the few reflection functions known to support named arguments. (#4284)
+ Don't suggest instance properties as alternatives to undefined variables inside of static methods.
Bug fixes:
+ Support a `non-null-mixed` type and change the way analysis involving nullability is checked for `mixed` (phpdoc and real). (#4278, #4276)
Nov 27 2020, Phan 3.2.6
-----------------------
New features (Analysis):
+ Update many more real parameter names to match php 8.0's parameter names for php's own internal methods. (#4263)
+ Infer that an instance property exists for PHP 8.0 constructor property promotion. (#3938)
+ Infer types of properties from arguments passed into constructor for PHP 8.0 constructor property promotion. (#3938)
+ Emit `PhanInvalidNode` and `PhanRedefineProperty` when misusing syntax for constructor property promotion. (#3938)
+ Emit `PhanCompatibleConstructorPropertyPromotion` when constructor property promotion is used. (#3938)
+ Emit `PhanSuspiciousMagicConstant` when using `__FUNCTION__` inside of a closure. (#4222)
Nov 26 2020, Phan 3.2.5
-----------------------
New features (Analysis):
+ Convert more internal function signature types from resource to the new object types with `target_php_version` of `8.0`+ (#4245, #4246)
+ Make internal function signature types and counts consistent with PHP 8.0's `.php.stub` files used to generate some reflection information.
Bug fixes
+ Fix logic error inferring the real key type of lists and arrays
and infer that the real union type of arrays is `array<int,something>`
when all keys have real type int. (#4251)
+ Fix rendering of processed item count in `--long-progress-bar`.
Miscellaneous:
+ Rename OCI-Collection and OCI-Lob to OCICollection and OCILob internally to prepare for php 8 support.
(Previously `OCI_Collection` and `OCI_Lob` were used to be valid fqsens internally)
Nov 12 2020, Phan 3.2.4
-----------------------
New features (Analysis):
+ Partially support `self<A>` and `static<B>` in phpdoc types. (#4226)
This support is incomplete and may run into issues with inheritance.
Bug fixes:
+ Properly infer the literal string value of `__FUNCTION__` for global functions in namespaces (#4231)
+ Fix false positive `PhanPossiblyInfiniteLoop` for `do {} while (false);` that is unchangeably false (#4236)
+ Infer that array_shift and array_pop return null when the passed in array could be empty, not false. (#4239)
+ Handle `PhpToken::getAll()` getting renamed to `PhpToken::tokenize()` in PHP 8.0.0RC4. (#4189)
Oct 12 2020, Phan 3.2.3
-----------------------
New features (CLI, Config):
+ Add `light_high_contrast` support for `--color-scheme`. (#4203)
This may be useful in terminals or CI web pages that use white backgrounds.
New features (Analysis):
+ Infer that `parent::someMethodReturningStaticType()` is a subtype of the current class, not just the parent class. (#4202)
+ Support phpdoc `@abstract` or `@phan-abstract` on non-abstract class constants, properties, and methods
to indicate that the intent is for non-abstract subclasses to override the definition. (#2278, #2285)
New issue types: `PhanCommentAbstractOnInheritedConstant`, `PhanCommentAbstractOnInheritedProperty`, `PhanCommentOverrideOnNonOverrideProperty`
For example, code using `static::SOME_CONST` or `static::$SOME_PROPERTY` or `$this->someMethod()`
may declare a placeholder `@abstract` constant/property/method,
and use this annotation to ensure that all non-abstract subclasses override the constant/property/method
(if using real abstract methods is not practical for a use case)
+ Warn about `@override` on properties that do not override an ancestor's property definition.
New issue type: `PhanCommentOverrideOnNonOverrideProperty`.
(Phan already warns for constants and methods)
Plugins:
+ Emit `PhanPluginUseReturnValueGenerator` for calling a function returning a generator without using the returned Generator. (#4013)
Bug fixes:
+ Properly analyze the right hand side for `$cond || throw ...;` (e.g. emit `PhanCompatibleThrowException`) (#4199)
+ Don't infer implications of `left || right` on the right hand expression when the right hand side has no side effects. (#4199)
+ Emit `PhanTypeInvalidThrowStatementNonThrowable` for thrown expressions that definitely aren't `\Throwable`
even when `warn_about_undocumented_throw_statements` is disabled or the throw expression is in the top level scope. (#4200)
+ Increase the minimum requirements in composer.json to what Phan actually requires. (#4217)
Sep 19 2020, Phan 3.2.2
-----------------------
New features (Analysis):
+ Improve handling of missing argument info when analyzing calls to functions/methods.
This will result in better issue detection for inherited methods or methods which Phan does not have type info for.
Bug fixes:
+ Fix false positive `PhanUnusedVariable` in `for (; $loop; ...) {}` (#4191)
+ Don't infer defaults of ancestor class properties when analyzing the implementation of `__construct`. (#4195)
This is only affects projects where the config setting `infer_default_properties_in_construct` is overridden to be enabled.
+ Check `minimum_target_php_version` for more compatibility warnings about parameter types.
Sep 13 2020, Phan 3.2.1
-----------------------
New features (Analysis):
+ Don't compare parameter types against alternate method signatures which have too many required parameters.
(e.g. warn about `max([])` but not `max([], [1])`)
+ Support `/** @unused-param $param_name */` in doc comments as an additional way to support suppressing warnings about individual parameters being unused.
+ Warn about loop conditions that potentially don't change due to the body of the loop.
This check uses heuristics and is prone to false positives.
New issue types: `PhanPossiblyInfiniteLoop`
+ Treat `unset($x);` as shadowing variable definitions during dead code detection.
+ Change the way `$i++`, `--$i`, etc. are analyzed during dead code detection
+ Properly enable `allow_method_param_type_widening` by default when the inferred `minimum_target_php_version` is `'7.2'` or newer. (#4168)
+ Start preparing for switching to AST version 80 in an upcoming Phan 4 release. (#4167)`
Bug fixes:
+ Fix various crashes in edge cases.
+ Fix crash with adjacent named labels for gotos.
+ Fix false positive unused parameter warning with php 8.0 constructor property promotion.
Plugins:
+ Warn about `#` comments in `PHPDocInWrongCommentPlugin` if they're not used for the expected `#[` syntax of php 8.0 attributes.
Maintenance:
+ Update polyfill/fallback parser to properly skip attributes in php 8.0.
The upcoming Phan 4 release will support analyzing attributes, which requires AST version 80.
Aug 25 2020, Phan 3.2.0
-----------------------
New features (CLI, Config):
+ **Add the `minimum_target_php_version` config setting and `--minimum-target-php-version` CLI flag.** (#3939)
Phan will use this instead of `target_php_version` for some backwards compatibility checks
(i.e. to check that the feature in question is supported by the oldest php version the project supports).
If this is not configured, Phan will attempt to use the composer.json version ranges if they are available.
Otherwise, `target_php_version` will be used.
Phan will use `target_php_version` instead if `minimum_target_php_version` is greater than `target_php_version`.
Update various checks to use `minimum_target_php_version` instead of `target_php_version`.
+ Add `--always-exit-successfully-after-analysis` flag.
By default, phan exits with a non-zero exit code if 1 or more unsuppressed issues were reported.
When this CLI flag is set, phan will instead exit with exit code 0 as long as the analysis completed.
+ Include the installed php-ast version and the php version used to run Phan in the output of `phan --version`. (#4147)
New features (Analysis):
+ Emit `PhanCompatibleArrowFunction` if using arrow functions with a minimum target php version older than php 7.4.
+ Emit `PhanCompatibleMatchExpression` if using match expressions with a minimum target php version older than php 8.0.
+ Emit `PhanNoopRepeatedSilenceOperator` for `@@expr` or `@(@expr)`.
This is less efficient and only makes a difference in extremely rare edge cases.
+ Avoid false positives for bitwise operations on floats such as unsigned 64-bit numbers (#4106)
+ Incomplete support for analyzing calls with php 8.0's named arguments. (#4037)
New issue types: `PhanUndeclaredNamedArgument*`, `PhanDuplicateNamedArgument*`,
`PhanMissingNamedArgument*`,
`PhanDefinitelyDuplicateNamedArgument`, `PhanPositionalArgumentAfterNamedArgument`, and
`PhanArgumentUnpackingUsedWithNamedArgument`, `PhanSuspiciousNamedArgumentForVariadic`
+ Incomplete support for analyzing uses of PHP 8.0's nullsafe operator(`?->`) for property reads and method calls. (#4067)
+ Warn about using `@var` where `@param` should be used (#1366)
+ Treat undefined variables as definitely null/undefined in various places
when they are used outside of loops and the global scope. (#4148)
+ Don't warn about undeclared global constants after `defined()` conditions. (#3337)
Phan will infer a broad range of types for these constants that can't be narrowed.
+ Parse `lowercase-string` and `non-empty-lowercase-string` in phpdoc for compatibility, but treat them like ordinary strings.
+ Emit `PhanCompatibleTrailingCommaParameterList` and `PhanCompatibleTrailingCommaArgumentList` **when the polyfill is used**. (#2269)
Trailing commas in argument lists require a minimum target version of php 7.3+,
and trailing commas in parameters or closure use lists require php 8.0+.
This is only available in the polyfill because the native `php-ast` parser
exposes the information that php itself tracks internally,
and php deliberately does not track whether any of these node types have trailing commas.
There are already other ways to detect these backwards compatibility issues,
such as `--native-syntax-check path/to/php7.x`.
+ Warn about variable definitions that are unused due to fallthroughs in switch statements. (#4162)
Plugins:
+ Add more aliases to `DeprecateAliasPlugin`
Miscellaneous:
+ Raise the severity of `PhanUndeclaredConstant` and `PhanStaticCallToNonStatic` from normal to critical.
Undeclared constants will become a thrown `Error` at runtime in PHP 8.0+.
Bug fixes:
+ Suppress `PhanParamNameIndicatingUnused` in files loaded from `autoload_internal_extension_signatures`
+ Improve compatibility of polyfill/fallback parser with php 8.0
+ Also try to check against the realpath() of the current working directory when converting absolute paths
to relative paths.
+ Generate baseline files with `/` instead of `\` on Windows in `--save-baseline` (#4149)
Jul 31 2020, Phan 3.1.1
-----------------------
New features (CLI, Config):
+ Add `--baseline-summary-type={ordered_by_count,ordered_by_type,none}` to control the generation
of the summary comment generated by `--save-baseline=path/to/baseline.php` (#4044)
(overrides the new `baseline_summary_type` config).
The default comment summary (`ordered_by_count`) is prone to merge conflicts in large projects.
This does not affect analysis.
+ Add `tool/phan_repl_helpers.php`, a prototype tool that adds some functionality to `php -a`.
It can be required by running `require_once 'path/to/phan/tool/phan_repl_helpers.php'` during an interactive session.
- This replaces the readline code completion and adds autocomplete for `->` on global variables.
This is currently buggy and very limited, and is missing some of the code completion functionality that is available in `php -a`.
(And it's missing a lot of the code completion functionality from the language server)
- This adds a global function `help($element_name_or_object)`. Run `help('help')` for usage and examples.
- Future releases may advantage of Phan's parsing/analysis capabilities in more ways.
- Several alternatives to the php shell already exist, such as [psysh](https://github.com/bobthecow/psysh).
`tool/phan_repl_helpers.php` is an experiment in augmenting the interactive php shell, not an alternative shell.
+ Update progress bar during class analysis phase. (#4099)
New features (Analysis):
+ Support casting `iterable<SubClass>` to `iterable<BaseClass>` (#4089)
+ Change phrasing for `analyze` phase in `--long-progress-bar` with `--analyze-twice`
+ Add `PhanParamNameIndicatingUnused` and `PhanParamNameIndicatingUnusedInClosure`
to indicate that using parameter names(`$unused*`, `$_`) to indicate to Phan that a parameter is unused is no longer recommended. (#4097)
Suppressions or the `@param [Type] $param_name @unused-param` syntax can be used instead.
PHP 8.0 will introduce named argument support.
+ Add a message to `PhanParamSignatureMismatch` indicating the cause of the issue being emitted. (#4103)
Note that `PhanParamSignaturePHPDocMismatch*` and `PhanParamSignatureReal*` have fewer false positives.
+ Warn about invalid types in class constants. (#4104)
Emit `PhanUndeclaredTypeClassConstant` if undeclared types are seen in phpdoc for class constants.
Emit `PhanCommentObjectInClassConstantType` if object types are seen in phpdoc for class constants.
+ Warn about `iterable<UndeclaredClass>` containing undeclared classes. (#4104)
Language Server/Daemon mode:
+ Include PHP keywords such as `__FILE__`, `switch`, `function`, etc. in suggestions for code completions.
Plugins:
+ Make `DuplicateExpressionPlugin` warn if adjacent statements are identical. (#4074)
New issue types: `PhanPluginDuplicateAdjacentStatement`.
+ Consistently make `PhanPluginPrintfNonexistentArgument` have critical severity. (#4080)
Passing too few format string arguments (e.g. `printf("%s %s", "Hello,")`) will be an `ArgumentCountError` in PHP 8.
Bug fixes:
+ Fix false positive `PhanParamSignatureMismatch` issues (#4103)
+ Fix false positive `PhanParamSignaturePHPDocMismatchHasParamType` seen for magic method override of a real method with no real signature types. (#4103)
Jul 16 2020, Phan 3.1.0
-----------------------
New features (CLI, Config):
+ Add `--output-mode=verbose` to print the line of code which caused the issue to be emitted after the textual issue output.
This is only emitted if the line is not whitespace, could be read, and does not exceed the config setting `max_verbose_snippet_length`.
+ Add `included_extension_subset` to limit Phan to using the reflection information to a subset of available extensions. (#4015)
This can be used to make Phan warn about using constants/functions/classes that are not in the target environment or dependency list
of a given PHP project/library.
Note that this may cause issues if a class from an extension in this list depends on classes from another extension that is outside of this list.
New features (Analysis):
+ Don't emit `PhanTypeInvalidLeftOperandOfBitwiseOp` and other binary operation warnings for `mixed`
+ Emit `PhanIncompatibleRealPropertyType` when real property types are incompatible (#4016)
+ Change the way `PhanIncompatibleCompositionProp` is checked for. (#4024)
Only emit it when the property was redeclared in an inherited trait.
+ Emit `PhanProvidingUnusedParameter` when passing an argument to a function with an optional parameter named `$unused*` or `$_`. (#4026)
This can also be suppressed on the functionlike's declaration, and should be suppressed if this does not match the project's parameter naming.
This is limited to functions with no overrides.
+ Emit `PhanParamTooFewInPHPDoc` when a parameter that is marked with `@phan-mandatory-param` is not passed in. (#4026)
This is useful when needing to preserve method signature compatibility in a method override, or when a parameter will become mandatory in a future backwards incompatible release of a project.
+ Emit `PhanTypeMismatchArgumentProbablyReal` instead of `PhanTypeMismatchArgument` when the inferred real type of an argument has nothing in common with the phpdoc type of a user-defined function/method.
This is usually a stronger indicator that the phpdoc parameter type is inaccurate/incomplete or the argument is incorrect.
(Overall, fixing phpdoc errors may help ensure compatibility long-term if the library/framework being used moves to real types (e.g. php 8.0 union types) in the future.)
**Note that Phan provides many ways to suppress issues (including the `--save-baseline=.phan/baseline.php` and `--load-baseline=.phan/baseline.php` functionality) in case
the switch to `ProbablyReal` introduces too many new issues in your codebase.**
(The new `ProbablyReal` issues are more severe than the original issue types.
When they're suppressed, the original less severe issue types will also be suppressed)
+ Emit `PhanTypeMismatchReturnProbablyReal` instead of `PhanTypeMismatchReturn` when the inferred real return type has nothing in common with the declared phpdoc return type of a user-defined function/method. (#4028)
+ Emit `PhanTypeMismatchPropertyProbablyReal` instead of `PhanTypeMismatchProperty` when the inferred assigned property type has nothing in common with a property's declared phpdoc type. (#4029)
+ Emit `PhanTypeMismatchArgumentInternalProbablyReal` instead of `PhanTypeMismatchArgumentInternal` in a few more cases.
+ Be stricter about checking if callables/closures have anything in common with other types.
+ Preserve more specific phpdoc types when the php 8.0 `mixed` type is part of the real type set.
+ Also emit `PhanPluginUseReturnValueNoopVoid` when a function/method's return type is implicitly void (#4049)
+ Support `@param MyType $name one line description @unused-param` to suppress warnings about individual unused method parameters.
This is a new alias of `@phan-unused-param`.
+ Support analyzing [PHP 8.0's match expression](https://wiki.php.net/rfc/match_expression_v2). (#3970)
Plugins:
+ Warn and skip checks instead of crashing when running `InlineHTMLPlugin` without the `tokenizer` extension installed. (#3998)
+ Support throwing `\Phan\PluginV3\UnloadablePluginException` instead of returning a plugin object in plugin files.
+ When a plugin registers for a method definition with `AnalyzeFunctionCallCapability`, automatically register the same closure for all classlikes using the same inherited definition of that method. (#4021)
+ Add `UnsafeCodePlugin` to warn about uses of `eval` or the backtick string shorthand for `shell_exec()`.
+ Add `DeprecateAliasPlugin` to mark known aliases such as `sizeof()` or `join()` as deprecated.
Implement support for `--automatic-fix`.
+ Add `PHPDocInWrongCommentPlugin` to warn about using `/*` instead of `/**` with phpdoc annotations supported by Phan.
Miscellaneous
+ Update more unit tests for php 8.0.
+ Emit a warning and load an extremely limited polyfill for `filter_var` to parse integers/floats if the `filter` extension is not loaded.
Bug Fixes:
+ Make suppressions on trait methods/properties consistently apply to the inherited definitions from classes/traits using those traits.
+ Fix false positive where Phan would think that union types with real types containing `int` and other types had an impossible condition.
Fix another false positive checking if `?A|?B` can cast to another union type.
Jul 03 2020, Phan 3.0.5
-----------------------
New features(CLI, Configs):
+ Add `-X` as an alias of `--dead-code-detection-prefer-false-positive`.
New features(Analysis):
+ Emit `PhanTypeInvalidLeftOperandOfBitwiseOp` and `PhanTypeInvalidRightOperandOfBitwiseOp` for argument types to bitwise operations other than `int|string`.
(affects `^`, `|`, `&`, `^=`, `|=`, `&=`)
Bug fixes:
+ Fix false positives in php 8.0+ type checking against the real `mixed` type. (#3994)
+ Fix unintentionally enabling GC when the `pcntl` extension is not enabled. (#4000)
It should only be enabled when running in daemon mode or as a language server.
Jul 01 2020, Phan 3.0.4
-----------------------
New features(Analysis):
+ Emit `PhanTypeVoidExpression` when using an expression returning `void` in places such as array keys/values.
+ More accurately infer unspecified types when closures are used with `array_map` (#3973)
+ Don't flatten array shapes and literal values passed to closures when analyzing closures. (Continue flattening for methods and global functions)
+ Link to documentation for internal stubs as a suggestion for undeclared class issues when Phan has type information related to the class in its signature files.
See https://github.com/phan/phan/wiki/Frequently-Asked-Questions#undeclared_element
+ Properly render the default values if available(`ReflectionParameter->isDefaultValueAvailable()`) in php 8.0+.
+ Properly set the real union types based on reflection information for functions/methods in more edge cases.
+ Properly infer that union types containing the empty array shape are possibly empty after sorting (#3980)
+ Infer a more accurate real type set from unary ops `~`, `+`, and `-` (#3991)
+ Improve ability to infer assignments within true branch of complex expressions in conditions such as `if (A && complex_expression) { } else { }` (#3992)
Plugins:
+ Add `ShortArrayPlugin`, to suggest using `[]` instead of `array()` or `list()`
+ In `DuplicateExpressionPlugin`, emit `PhanPluginDuplicateExpressionAssignmentOperation` if `X = X op Y` is seen and it can be converted to `X op= Y` (#3985)
(excluding `??=` for now)
+ Add `SimplifyExpressionPlugin`, to suggest shortening expressions such as `$realBool ? true : false` or `$realBool === false`
+ Add `RemoveDebugStatementPlugin`, to suggest removing debugging output statements such as `echo`, `print`, `printf`, `fwrite(STDERR, ...)`, `var_export(...)`, inline html, etc.
This is only useful in applications or libraries that print output in only a few places, as a sanity check that debugging statements are not accidentally left in code.
Bug fixes:
+ Treat `@method static foo()` as an instance method returning the union type `static` (#3981)
Previously, Phan treated it like a static method with type `void` based on an earlier phpdoc spec.
+ Fix the way that Phan inferred the `finally` block's exit status affected the `try` block. (#3987)
Jun 21 2020, Phan 3.0.3
-----------------------
New features(Analysis):
+ Include the most generic types when conditions such as `is_string()` to union types containing `mixed` (#3947)
+ More aggressively infer that `while` and `for` loop bodies are executed at least once in functions outside of other loops (#3948)
+ Infer the union type of `!$expr` from the type of `$expr` (#3948)
+ Re-enable `simplify_ast` by default in `.phan/config.php` (#3944, #3945)
+ Avoid false positives in `--constant-variable-detection` for `++`/`--`
+ Make `if (!$nullableValue) { }` remove truthy literal scalar values such as `'value'` and `1` and `1.0` when they're nullable
+ Emit `PhanTypeVoidArgument` when passing a void return value as a function argument (#3961)
+ Correctly merge the possible union types of pass-by-reference variables (#3959)
+ Improve php 8.0-dev shim support. Fix checking for array references and closure use references in php 8.0+.
+ More aggressively check if expression results should be used for conditionals and binary operators.
Plugins:
+ Add `ConstantVariablePlugin` to point out places where variables are read when they have only one possible scalar value. (#3953)
This may help detect logic errors such as `$x === null ? json_encode($x) : 'default'` or code that could be simplified,
but most issues it emits wouldn't be worth fixing due to hurting readability or being false positives.
+ Add `MergeVariableInfoCapability` for plugins to hook into ContextMergeVisitor and update data for a variable
when merging the outcome of different scopes. (#3956)
+ Make `UseReturnValuePlugin` check if a method is declared as pure before using the dynamic checks based on percentage of
calls where the return value is used, if that option is enabled.
+ In `DuplicateArrayKeyPlugin`, properly check for duplicate non-scalar cases.
Language Server/Daemon mode:
+ Fix bug where the Phan daemon would crash on the next request after analyzing a file outside of the project being analyzed,
when pcntl was disabled or unavailable (#3954)
Bug fixes:
+ Fix `PhanDebugAnnotation` output for variables after the first one in `@phan-debug-var $a, $b` (#3943)
+ Use the correct constant to check if closure use variables are references in php 8.0+
Miscellaneous:
+ Update function signature stubs for the `memcache` PECL (#3841)
Jun 07 2020, Phan 3.0.2
-----------------------
New features(CLI, Configs):
+ Add `--dead-code-detection-prefer-false-positive` to run dead code detection,
erring on the side of reporting potentially dead code even when it is possibly not dead.
(e.g. when methods of unknown objects are invoked, don't mark all methods with the same name as potentially used)
New features(Analysis):
+ Fix false positive `PhanAbstractStaticMethodCall` (#3935)
Also, properly emit `PhanAbstractStaticMethodCall` for a variable containing a string class name.
Plugins:
+ Fix incorrect check and suggestion for `PregRegexCheckerPlugin`'s warning if
`$` allows an optional newline before the end of the string when the configuration includes
`['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`) (#3938)
+ Add `BeforeLoopBodyAnalysisCapability` for plugins to analyze loop conditions before the body (#3936)
+ Warn about suspicious param order for `str_contains`, `str_ends_with`, and `str_starts_with` in `SuspiciousParamOrderPlugin` (#3934)
Bug fixes:
+ Don't report unreferenced class properties of internal stub files during dead code detection
(i.e. files in `autoload_internal_extension_signatures`).
+ Don't remove the leading directory separator when attempting to convert a file outside the project to a relative path.
(in cases where the directory is different but has the project's name as a prefix)
Jun 04 2020, Phan 3.0.1
-----------------------
New features(Analysis):
+ Support analysis of php 8.0's `mixed` type (#3899)
New issue types: `PhanCompatibleMixedType`, `PhanCompatibleUseMixed`.
+ Treat `static` and `false` like real types and emit more severe issues in all php versions.
+ Improve type inferences from negated type assertions (#3923)
(analyze more expression kinds, infer real types in more places)
+ Warn about unnecessary use of `expr ?? null`. (#3925)
New issue types: `PhanCoalescingNeverUndefined`.
+ Support PHP 8.0 non-capturing catches (#3907)
New issue types: `PhanCompatibleNonCapturingCatch`.
+ Infer type of `$x->magicProp` from the signature of `__get`
+ Treat functions/methods that are only called by themselves as unreferenced during dead code detection.
+ Warn about `each()` being deprecated when the `target_php_version` is php 7.2+. (#2746)
This is special cased because PHP does not flag the function itself as deprecated in `ReflectionFunction`.
(PHP only emits the deprecation notice for `each()` once at runtime)
Miscellaneous:
+ Check for keys that are too long when computing levenshtein distances (when Phan suggests alternatives).
Plugins:
+ Add `AnalyzeLiteralStatementCapability` for plugins to analyze no-op string literals (#3911)
+ In `PregRegexCheckerPlugin`, warn if `$` allows an optional newline before the end of the string
when configuration includes `['plugin_config' => ['regex_warn_if_newline_allowed_at_end' => true]]`) (#3915)
+ In `SuspiciousParamOrderPlugin`, warn if an argument has a near-exact name match for a parameter at a different position (#3929)
E.g. warn about calling `foo($b)` or `foo(true, $this->A)` for `function foo($a = false, $b = false)`.
New issue types: `PhanPluginSuspiciousParamPosition`, `PhanPluginSuspiciousParamPositionInternal`
Bug fixes:
+ Fix false positive `PhanTypeMismatchPropertyDefault` involving php 7.4 typed properties with no default
and generic comments (#3917)
+ Don't remove leading directory separator when attempting to convert a file outside the project to a relative path.
May 09 2020, Phan 3.0.0
-----------------------
New features(CLI, Config):
+ Support `PHAN_COLOR_PROGRESS_BAR` as an environment variable to set the color of the progress bar.
Ansi color names (e.g. `light_blue`) or color codes (e.g. `94`) can be used. (See src/Phan/Output/Colorizing.php)
New features(Analysis):
+ Infer that `foreach` keys and values of possibly empty iterables are possibly undefined after the end of a loop. (#3898)
+ Allow using the polyfill parser to parse internal stubs. (#3902)
(To support newer syntax such as union types, trailing commas in parameter lists, etc.)
May 02 2020, Phan 3.0.0-RC2
-----------------------
Fix published GitHub release tag (used `master` instead of `v3`).
May 02 2020, Phan 3.0.0-RC1
-----------------------
Backwards incompatible changes:
+ Drop PHP 7.1 support. PHP 7.1 reached its end of life for security support in December 2019.
Many of Phan's dependencies no longer publish releases supporting php 7.1,
which will likely become a problem running Phan with future 8.x versions
(e.g. in the published phar releases).
+ Drop PluginV2 support (which was deprecated in Phan 2) in favor of PluginV3.
+ Remove deprecated classes and helper methods.
??? ?? 2020, Phan 2.7.3 (dev)
-----------------------
Bug fixes:
+ Fix handling of windows path separators in `phan_client`
+ Fix a crash when emitting `PhanCompatibleAnyReturnTypePHP56` or `PhanCompatibleScalarTypePHP56` for methods with no parameters.
May 02 2020, Phan 2.7.2
-----------------------
New features(CLI, Config):
+ Add a `--native-syntax-check=/path/to/php` option to enable `InvokePHPNativeSyntaxCheckPlugin`
and add that php binary to the `php_native_syntax_check_binaries` array of `plugin_config`
(treated here as initially being the empty array if not configured).
This CLI flag can be repeated to run PHP's native syntax checks with multiple php binaries.
New features(Analysis):
+ Emit `PhanTypeInvalidThrowStatementNonThrowable` when throwing expressions that can't cast to `\Throwable`. (#3853)
+ Include the relevant expression in more issue messages for type errors. (#3844)
+ Emit `PhanNoopSwitchCases` when a switch statement contains only the default case.
+ Warn about unreferenced private methods of the same name as methods in ancestor classes, in dead code detection.
+ Warn about useless loops. Phan considers loops useless when the following conditions hold:
1. Variables defined within the loop aren't used outside of the loop
(requires `unused_variable_detection` to be enabled whether or not there are actually variables)
2. It's likely that the statements within the loop have no side effects
(this is only inferred for a subset of expressions in code)
(Enabling the plugin `UseReturnValuePlugin` (and optionally `'plugin_config' => ['infer_pure_methods' = true]`) helps detect if function calls are useless)
3. The code is in a functionlike scope.
New issue types: `PhanSideEffectFreeForeachBody`, `PhanSideEffectFreeForBody`, `PhanSideEffectFreeWhileBody`, `PhanSideEffectFreeDoWhileBody`
+ Infer that previous conditions are negated when analyzing the cases of a switch statement (#3866)
+ Support using `throw` as an expression, for PHP 8.0 (#3849)
(e.g. `is_string($arg) || throw new InvalidArgumentException()`)
Emit `PhanCompatibleThrowException` when `throw` is used as an expression instead of a statement.
Plugins:
+ Emit `PhanPluginDuplicateCatchStatementBody` in `DuplicateExpressionPlugin` when a catch statement has the same body and variable name as an adjacent catch statement.
(This should be suppressed in projects that support php 7.0 or older)
+ Add `PHP53CompatibilityPlugin` as a demo plugin to catch common incompatibilities with PHP 5.3. (#915)
New issue types: `PhanPluginCompatibilityArgumentUnpacking`, `PhanPluginCompatibilityArgumentUnpacking`, `PhanPluginCompatibilityArgumentUnpacking`
+ Add `DuplicateConstantPlugin` to warn about duplicate constant names (`define('X', value)` or `const X = value`) in the same statement list.
This is only recommended in projects with files with too many global constants to track manually.
Bug Fixes:
+ Fix a bug causing FQSEN names or namespaces to be converted to lowercase even if they were never lowercase in the codebase being analyzed (#3583)
Miscellaneous:
+ Replace `PhanTypeInvalidPropertyDefaultReal` with `TypeMismatchPropertyDefault` (emitted instead of `TypeMismatchProperty`)
and `TypeMismatchPropertyDefaultReal` (#3068)
+ Speed up `ASTHasher` for floats and integers (affects code such as `DuplicateExpressionPlugin`)
+ Call `uopz_allow_exit(true)` if uopz is enabled when initializing Phan. (#3880)
Running Phan with `uopz` is recommended against (unless debugging Phan itself), because `uopz` causes unpredictable behavior.
Use stubs or internal stubs instead.
Apr 11 2020, Phan 2.7.1
-----------------------
@@ -98,7 +945,7 @@ New features(CLI, Configs):
New features(Analysis):
+ Support parsing php 8.0 union types (and the static return type) in the polyfill. (#3419, #3634)
+ Emit `PhanCompatibleUnionType` and `PhanCompatibleStaticType` when the target php version is less than 8.0 and union types or static return types are seen. (#3419, #3634)
+ Be more consistent about warning about issues in values of class constants, global constants, and property defaults.
+ Be more consistent when warning about issues in values of class constants, global constants, and property defaults.
+ Infer key and element types from `iterator_to_array()`
+ Infer that modification of or reading from static properties all use the same property declaration. (#3760)
Previously, Phan would track the static property's type separately for each subclass.
@@ -467,7 +1314,7 @@ New features(Analysis):
+ Properly emit PhanPossiblyInfiniteRecursionSameParams for functions with varargs.
+ Emit `PhanNoopNew` or `PhanNoopNewNoSideEffects` when an object is created with `new expr(...)` but the result is not used (#3410)
This can be suppressed for all instances of a class-like by adding the `@phan-constructor-used-for-side-effects` annotation to the class's doc comment.
+ Emit `PhanPluginUseReturnValueInternalKnown` for about unused results of function calls on the right hand side of control flow operators (`??`/`?:`/`&&`/`||`) (#3408)
+ Emit `PhanPluginUseReturnValueInternalKnown` for unused results of function calls on the right-hand side of control flow operators (`??`/`?:`/`&&`/`||`) (#3408)
Oct 20 2019, Phan 2.3.1
-----------------------
@@ -1258,7 +2105,7 @@ Language Server/Daemon mode:
+ Analyze new but unsaved files, if they would be analyzed by Phan once they actually were saved to disk.
Plugins:
+ Warn about assignments where the left and right hand side are the same expression in `DuplicateExpressionPlugin` (#2641)
+ Warn about assignments where the left-hand and right-hand side are the same expression in `DuplicateExpressionPlugin` (#2641)
New issue type: `PhanPluginDuplicateExpressionAssignment`
Deprecations:
@@ -3163,7 +4010,7 @@ New Features (CLI, Configs)
This config is enabled by default, and requires `check_docblock_signature_return_type_match` to be enabled.
Bug Fixes
+ Work around notice about COMPILER_HALT_OFFSET on windows.
+ Work around notice about COMPILER_HALT_OFFSET on Windows.
+ Fixes #462 : Fix type inferences for instanceof for checks with dynamic class names are provided.
Valid class names are either a string or an instance of the class to check against.
Warn if the class name is definitely invalid.
+27 -23
View File
@@ -4,11 +4,13 @@ Phan looks for common issues and will verify type compatibility on various opera
information is available or can be deduced. Phan has a good (but not comprehensive) understanding of flow control
and can track values in a few use cases (e.g. arrays, integers, and strings).
[![Build Status](https://travis-ci.org/phan/phan.svg?branch=master)](https://travis-ci.org/phan/phan)
[![Build Status (Windows)](https://ci.appveyor.com/api/projects/status/github/phan/phan?branch=master&svg=true)](https://ci.appveyor.com/project/TysonAndre/phan/branch/master)
[![Build Status](https://dev.azure.com/tysonandre775/phan/_apis/build/status/phan.phan?branchName=v5)](https://dev.azure.com/tysonandre775/phan/_build/latest?definitionId=3&branchName=v5)
[![Build Status (Windows)](https://ci.appveyor.com/api/projects/status/github/phan/phan?branch=v5&svg=true)](https://ci.appveyor.com/project/TysonAndre/phan/branch/v5)
[![Gitter](https://badges.gitter.im/phan/phan.svg)](https://gitter.im/phan/phan?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Latest Stable Version](https://img.shields.io/packagist/v/phan/phan.svg)](https://packagist.org/packages/phan/phan)
[![License](https://img.shields.io/packagist/l/phan/phan.svg)](https://github.com/phan/phan/blob/master/LICENSE)
[![License](https://img.shields.io/packagist/l/phan/phan.svg)](https://github.com/phan/phan/blob/v5/LICENSE)
This is the unstable branch for an upcoming Phan 5 release. The branch for the older stable Phan 4 release line is [here](https://github.com/phan/phan/tree/v4).
# Getting Started
@@ -21,7 +23,7 @@ composer require phan/phan
With Phan installed, you'll want to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file) in
your project to tell Phan how to analyze your source code. Once configured, you can run it via `./vendor/bin/phan`.
Phan depends on PHP 7.1+ with the [php-ast](https://github.com/nikic/php-ast) extension (1.0.1+) and supports analyzing PHP version 7.0-7.4 syntax.
Phan depends on PHP 7.2+ with the [php-ast](https://github.com/nikic/php-ast) extension (1.0.14+ is preferred) and supports analyzing PHP version 7.0-8.1 syntax.
Installation instructions for php-ast can be found [here](https://github.com/nikic/php-ast#installation).
(Phan can be used without php-ast by using the CLI option `--allow-polyfill-parser`, but there are slight differences in the parsing of doc comments)
@@ -41,13 +43,13 @@ Phan is able to perform the following kinds of analysis:
* Check that all methods, functions, classes, traits, interfaces, constants, properties and variables are defined and accessible.
* Check for type safety and arity issues on method/function/closure calls.
* Check for PHP7/PHP5 backward compatibility.
* Check for PHP8/PHP7/PHP5 backward compatibility.
* Check for features that weren't supported in older PHP 7.x minor releases (E.g. `object`, `void`, `iterable`, `?T`, `[$x] = ...;`, negative string offsets, multiple exception catches, etc.)
* Check for sanity with array accesses.
* Check for type safety on binary operations.
* Check for valid and type safe return values on methods, functions, and closures.
* Check for No-Ops on arrays, closures, constants, properties, variables, unary operators, and binary operators.
* Check for unused/dead/[unreachable](https://github.com/phan/phan/tree/master/.phan/plugins#unreachablecodepluginphp) code. (Pass in `--dead-code-detection`)
* Check for unused/dead/[unreachable](https://github.com/phan/phan/tree/v5/.phan/plugins#unreachablecodepluginphp) code. (Pass in `--dead-code-detection`)
* Check for unused variables and parameters. (Pass in `--unused-variable-detection`)
* Check for redundant or impossible conditions and pointless casts. (Pass in `--redundant-condition-detection`)
* Check for unused `use` statements.
@@ -78,11 +80,11 @@ Phan is able to perform the following kinds of analysis:
* Can be run on many cores. (requires `pcntl`)
* Output is emitted in text, checkstyle, json, pylint, csv, or codeclimate formats.
* Can run [user plugins on source for checks specific to your code](https://github.com/phan/phan/wiki/Writing-Plugins-for-Phan).
[Phan includes various plugins you may wish to enable for your project](https://github.com/phan/phan/tree/master/.phan/plugins#2-general-use-plugins).
[Phan includes various plugins you may wish to enable for your project](https://github.com/phan/phan/tree/v5/.phan/plugins#2-general-use-plugins).
See [Phan Issue Types](https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan) for descriptions
and examples of all issues that can be detected by Phan. Take a look at the
[\Phan\Issue](https://github.com/phan/phan/blob/master/src/Phan/Issue.php) to see the
[\Phan\Issue](https://github.com/phan/phan/blob/v5/src/Phan/Issue.php) to see the
definition of each error type.
Take a look at the [Tutorial for Analyzing a Large Sloppy Code Base](https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base) to get a sense of what the process of doing ongoing analysis might look like for you.
@@ -90,27 +92,27 @@ Take a look at the [Tutorial for Analyzing a Large Sloppy Code Base](https://git
Phan can be used from [various editors and IDEs](https://github.com/phan/phan/wiki/Editor-Support) for its error checking, "go to definition" support, etc. via the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
Editors and tools can also request analysis of individual files in a project using the simpler [Daemon Mode](https://github.com/phan/phan/wiki/Using-Phan-Daemon-Mode).
See the [tests](https://github.com/phan/phan/blob/master/tests/files) directory for some examples of the various checks.
See the [tests](https://github.com/phan/phan/blob/v5/tests/files) directory for some examples of the various checks.
Phan is imperfect and shouldn't be used to prove that your PHP-based rocket guidance system is free of defects.
## Features provided by plugins
Additional analysis features have been provided by [plugins](https://github.com/phan/phan/tree/master/.phan/plugins#plugins).
Additional analysis features have been provided by [plugins](https://github.com/phan/phan/tree/v5/.phan/plugins#plugins).
- [Checking for syntactically unreachable statements](https://github.com/phan/phan/tree/master/.phan/plugins#unreachablecodepluginphp) (E.g. `{ throw new Exception("Message"); return $value; }`)
- [Checking `*printf()` format strings against the provided arguments](https://github.com/phan/phan/tree/master/.phan/plugins#printfcheckerplugin) (as well as checking for common errors)
- [Checking that PCRE regexes passed to `preg_*()` are valid](https://github.com/phan/phan/tree/master/.phan/plugins#pregregexcheckerplugin)
- [Checking for `@suppress` annotations that are no longer needed.](https://github.com/phan/phan/tree/master/.phan/plugins#unusedsuppressionpluginphp)
- [Checking for duplicate or missing array keys.](https://github.com/phan/phan/tree/master/.phan/plugins#duplicatearraykeypluginphp)
- [Checking coding style conventions](https://github.com/phan/phan/tree/master/.phan/plugins#3-plugins-specific-to-code-styles)
- [Others](https://github.com/phan/phan/tree/master/.phan/plugins#plugins)
- [Checking for syntactically unreachable statements](https://github.com/phan/phan/tree/v5/.phan/plugins#unreachablecodepluginphp) (E.g. `{ throw new Exception("Message"); return $value; }`)
- [Checking `*printf()` format strings against the provided arguments](https://github.com/phan/phan/tree/v5/.phan/plugins#printfcheckerplugin) (as well as checking for common errors)
- [Checking that PCRE regexes passed to `preg_*()` are valid](https://github.com/phan/phan/tree/v5/.phan/plugins#pregregexcheckerplugin)
- [Checking for `@suppress` annotations that are no longer needed.](https://github.com/phan/phan/tree/v5/.phan/plugins#unusedsuppressionpluginphp)
- [Checking for duplicate or missing array keys.](https://github.com/phan/phan/tree/v5/.phan/plugins#duplicatearraykeypluginphp)
- [Checking coding style conventions](https://github.com/phan/phan/tree/v5/.phan/plugins#3-plugins-specific-to-code-styles)
- [Others](https://github.com/phan/phan/tree/v5/.phan/plugins#plugins)
Example: [Phan's plugins for self-analysis.](https://github.com/phan/phan/blob/2.4.1/.phan/config.php#L494-L537)
Example: [Phan's plugins for self-analysis.](https://github.com/phan/phan/blob/3.2.8/.phan/config.php#L601-L674)
# Usage
Phan needs to be configured with details on where to find code to analyze and how to analyze it. The
After [installing Phan](#getting-started), Phan needs to be configured with details on where to find code to analyze and how to analyze it. The
easiest way to tell Phan where to find source code is to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file).
A simple `.phan/config.php` file might look something like the following.
@@ -124,7 +126,8 @@ A simple `.phan/config.php` file might look something like the following.
*/
return [
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`.
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`,
// `'8.0'`, `'8.1'`, `null`.
// If this is set to `null`,
// then Phan assumes the PHP version which is closest to the minor version
// of the php executable used to execute Phan.
@@ -162,10 +165,11 @@ return [
// (e.g. 'AlwaysReturnPlugin')
//
// Documentation about available bundled plugins can be found
// at https://github.com/phan/phan/tree/master/.phan/plugins
// at https://github.com/phan/phan/tree/v5/.phan/plugins
//
// Alternately, you can pass in the full path to a PHP file
// with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php')
// with the plugin's implementation.
// (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php')
'plugins' => [
// checks if a function, closure or method unconditionally returns.
// can also be written as 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'
@@ -287,7 +291,7 @@ contributor is required to adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md)
# Online Demo
**This is experimental, and requires an up to date version of Firefox/Chrome and at least 4GB of free RAM.** (this is a 10MB download)
**This requires an up to date version of Firefox/Chrome and at least 4 GB of free RAM.** (this is a 15 MB download)
[Run Phan entirely in your browser](https://phan.github.io/demo/).
+28
View File
@@ -0,0 +1,28 @@
# https://aka.ms/yaml
trigger:
- master
- v4
- v5
jobs:
- template: .azure/job.yml
parameters:
configurationName: PHP_72_NTS
phpVersion: 7.2
vmImage: 'ubuntu-16.04'
- template: .azure/job.yml
parameters:
configurationName: PHP_73_NTS
phpVersion: 7.3
vmImage: 'ubuntu-18.04'
- template: .azure/job.yml
parameters:
configurationName: PHP_74_NTS
phpVersion: 7.4
vmImage: 'ubuntu-20.04'
- template: .azure/job.yml
parameters:
configurationName: PHP_80_NTS
phpVersion: 8.0
vmImage: 'ubuntu-20.04'
+14 -13
View File
@@ -18,34 +18,35 @@
"config": {
"sort-packages": true,
"platform": {
"php": "7.1.22"
"php": "7.2.24"
}
},
"require": {
"php": "^7.1.0",
"php": "^7.2.0|^8.0.0",
"ext-filter": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"composer/semver": "^1.4",
"composer/xdebug-handler": "^1.3.2",
"composer/semver": "^1.4|^2.0|^3.0",
"composer/xdebug-handler": "^1.3.2|^2.0.0",
"felixfbecker/advanced-json-rpc": "^3.0.4",
"microsoft/tolerant-php-parser": "0.0.20",
"netresearch/jsonmapper": "^1.6.0|^2.0",
"sabre/event": "^5.0",
"symfony/console": "^2.3|^3.0|^4.0|^5.0",
"microsoft/tolerant-php-parser": "^0.1.0",
"netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0",
"sabre/event": "^5.0.3",
"symfony/console": "^3.2|^4.0|^5.0",
"symfony/polyfill-mbstring": "^1.11.0",
"symfony/polyfill-php72": "^1.15"
"symfony/polyfill-php80": "^1.20.0",
"tysonandre/var_representation_polyfill": "^0.0.2"
},
"suggest": {
"ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.6+ is recommended.",
"ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.14+ is recommended.",
"ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8",
"ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable",
"ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8",
"ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions."
"ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions.",
"ext-var_representation": "Suggested for converting values to strings in issue messages"
},
"require-dev": {
"brianium/paratest": "^4.0.0",
"phpunit/phpunit": "^7.5.0"
"phpunit/phpunit": "^8.5.0"
},
"autoload": {
"psr-4": {"Phan\\": "src/Phan"}
+1208 -599
View File
@@ -4,32 +4,33 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "478ca9fe9e8101132fc182c4613e9ac5",
"content-hash": "cc0af707fc392b3959172556c6e575f4",
"packages": [
{
"name": "composer/semver",
"version": "1.5.1",
"version": "3.2.5",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
"reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
"reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
"url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9",
"reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0"
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^4.5 || ^5.0.5"
"phpstan/phpstan": "^0.12.54",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-main": "3.x-dev"
}
},
"autoload": {
@@ -65,28 +66,48 @@
"validation",
"versioning"
],
"time": "2020-01-13T12:06:48+00:00"
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.2.5"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2021-05-24T12:41:47+00:00"
},
{
"name": "composer/xdebug-handler",
"version": "1.4.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7"
"reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7",
"reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339",
"reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0",
"psr/log": "^1.0"
"psr/log": "^1 || ^2 || ^3"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
"phpstan/phpstan": "^0.12.55",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"autoload": {
@@ -109,29 +130,48 @@
"Xdebug",
"performance"
],
"time": "2020-03-01T12:26:26+00:00"
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
"source": "https://github.com/composer/xdebug-handler/tree/2.0.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2021-07-31T17:03:58+00:00"
},
{
"name": "felixfbecker/advanced-json-rpc",
"version": "v3.1.1",
"version": "v3.2.1",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-advanced-json-rpc.git",
"reference": "0ed363f8de17d284d479ec813c9ad3f6834b5c40"
"reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/0ed363f8de17d284d479ec813c9ad3f6834b5c40",
"reference": "0ed363f8de17d284d479ec813c9ad3f6834b5c40",
"url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447",
"reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447",
"shasum": ""
},
"require": {
"netresearch/jsonmapper": "^1.0 || ^2.0",
"php": ">=7.0",
"phpdocumentor/reflection-docblock": "^4.0.0 || ^5.0.0"
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"php": "^7.1 || ^8.0",
"phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0.0"
"phpunit/phpunit": "^7.0 || ^8.0"
},
"type": "library",
"autoload": {
@@ -150,27 +190,31 @@
}
],
"description": "A more advanced JSONRPC implementation",
"time": "2020-03-11T15:21:41+00:00"
"support": {
"issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues",
"source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1"
},
"time": "2021-06-11T22:34:44+00:00"
},
{
"name": "microsoft/tolerant-php-parser",
"version": "v0.0.20",
"version": "v0.1.1",
"source": {
"type": "git",
"url": "https://github.com/microsoft/tolerant-php-parser.git",
"reference": "c5e2bf5d8c9f4f27eef1370bd39ea2d1f374eeb4"
"reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/c5e2bf5d8c9f4f27eef1370bd39ea2d1f374eeb4",
"reference": "c5e2bf5d8c9f4f27eef1370bd39ea2d1f374eeb4",
"url": "https://api.github.com/repos/microsoft/tolerant-php-parser/zipball/6a965617cf484355048ac6d2d3de7b6ec93abb16",
"reference": "6a965617cf484355048ac6d2d3de7b6ec93abb16",
"shasum": ""
},
"require": {
"php": ">=7.0"
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^6.4"
"phpunit/phpunit": "^8.5.15"
},
"type": "library",
"autoload": {
@@ -191,20 +235,24 @@
}
],
"description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios",
"time": "2020-02-18T02:57:19+00:00"
"support": {
"issues": "https://github.com/microsoft/tolerant-php-parser/issues",
"source": "https://github.com/microsoft/tolerant-php-parser/tree/v0.1.1"
},
"time": "2021-07-16T21:28:12+00:00"
},
{
"name": "netresearch/jsonmapper",
"version": "v2.0.0",
"version": "v4.0.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54"
"reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e245890383c3ed38b6d202ee373c23ccfebc0f54",
"reference": "e245890383c3ed38b6d202ee373c23ccfebc0f54",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
"reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d",
"shasum": ""
},
"require": {
@@ -212,10 +260,10 @@
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.6"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0",
"phpunit/phpunit": "~7.5 || ~8.0 || ~9.0",
"squizlabs/php_codesniffer": "~3.5"
},
"type": "library",
@@ -237,32 +285,34 @@
}
],
"description": "Map nested JSON structures onto PHP classes",
"time": "2020-03-04T17:23:33+00:00"
"support": {
"email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues",
"source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0"
},
"time": "2020-12-01T19:48:11+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.0.0",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
"reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "~6"
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
"dev-2.x": "2.x-dev"
}
},
"autoload": {
@@ -289,45 +339,45 @@
"reflection",
"static analysis"
],
"time": "2018-08-07T13:53:10+00:00"
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "4.3.4",
"version": "5.2.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c"
"reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c",
"reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
"reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
"shasum": ""
},
"require": {
"php": "^7.0",
"phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
"phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
"webmozart/assert": "^1.0"
"ext-filter": "*",
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.3",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
"doctrine/instantiator": "^1.0.5",
"mockery/mockery": "^1.0",
"phpdocumentor/type-resolver": "0.4.*",
"phpunit/phpunit": "^6.4"
"mockery/mockery": "~1.3.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": [
"src/"
]
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -338,38 +388,44 @@
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
},
{
"name": "Jaap van Otterdijk",
"email": "account@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2019-12-28T18:55:12+00:00"
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
},
"time": "2020-09-03T19:13:55+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.0.1",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
"reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
"reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
"shasum": ""
},
"require": {
"php": "^7.1",
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
"ext-tokenizer": "^7.1",
"mockery/mockery": "~1",
"phpunit/phpunit": "^7.0"
"ext-tokenizer": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-1.x": "1.x-dev"
}
},
"autoload": {
@@ -388,31 +444,30 @@
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"time": "2019-08-22T18:11:29+00:00"
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
},
"time": "2020-09-17T18:55:26+00:00"
},
{
"name": "psr/container",
"version": "1.0.0",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
@@ -425,7 +480,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
@@ -437,20 +492,24 @@
"container-interop",
"psr"
],
"time": "2017-02-14T16:28:37+00:00"
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2021-03-05T17:36:06+00:00"
},
{
"name": "psr/log",
"version": "1.1.3",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
@@ -474,7 +533,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@@ -484,28 +543,32 @@
"psr",
"psr-3"
],
"time": "2020-03-23T09:12:05+00:00"
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "sabre/event",
"version": "5.1.0",
"version": "5.1.2",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/event.git",
"reference": "d00a17507af0e7544cfe17096372f5d733e3b276"
"reference": "c120bec57c17b6251a496efc82b732418b49d50a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/event/zipball/d00a17507af0e7544cfe17096372f5d733e3b276",
"reference": "d00a17507af0e7544cfe17096372f5d733e3b276",
"url": "https://api.github.com/repos/sabre-io/event/zipball/c120bec57c17b6251a496efc82b732418b49d50a",
"reference": "c120bec57c17b6251a496efc82b732418b49d50a",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.16.1",
"phpunit/phpunit": "^7 || ^8"
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.0"
},
"type": "library",
"autoload": {
@@ -544,45 +607,55 @@
"reactor",
"signal"
],
"time": "2020-01-31T18:52:29+00:00"
"support": {
"forum": "https://groups.google.com/group/sabredav-discuss",
"issues": "https://github.com/sabre-io/event/issues",
"source": "https://github.com/fruux/sabre-event"
},
"time": "2020-10-03T11:02:22+00:00"
},
{
"name": "symfony/console",
"version": "v4.4.7",
"version": "v5.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7"
"reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
"reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7",
"url": "https://api.github.com/repos/symfony/console/zipball/51b71afd6d2dc8f5063199357b9880cea8d8bfe2",
"reference": "51b71afd6d2dc8f5063199357b9880cea8d8bfe2",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/service-contracts": "^1.1|^2"
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2",
"symfony/string": "^5.1"
},
"conflict": {
"symfony/dependency-injection": "<3.4",
"symfony/event-dispatcher": "<4.3|>=5",
"psr/log": ">=3",
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<3.3"
"symfony/process": "<4.4"
},
"provide": {
"psr/log-implementation": "1.0"
"psr/log-implementation": "1.0|2.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "^3.4|^4.0|^5.0",
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
"symfony/event-dispatcher": "^4.3",
"psr/log": "^1|^2",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0",
"symfony/lock": "^4.4|^5.0",
"symfony/process": "^3.4|^4.0|^5.0",
"symfony/var-dumper": "^4.3|^5.0"
"symfony/process": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
},
"suggest": {
"psr/log": "For using the console logger",
@@ -591,11 +664,6 @@
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
@@ -618,8 +686,17 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.3.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -634,24 +711,91 @@
"type": "tidelift"
}
],
"time": "2020-03-30T11:41:10+00:00"
"time": "2021-07-27T19:10:22+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.15.0",
"name": "symfony/deprecation-contracts",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14"
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
"reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
"reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-03-23T23:28:01+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
@@ -659,7 +803,11 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -692,6 +840,9 @@
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -706,24 +857,189 @@
"type": "tidelift"
}
],
"time": "2020-02-27T09:26:54+00:00"
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.15.0",
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "16880ba9c5ebe3642d1995ab866db29270b36535"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
"reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535",
"reference": "16880ba9c5ebe3642d1995ab866db29270b36535",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T12:26:48+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
"reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
@@ -731,7 +1047,11 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -765,6 +1085,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -779,98 +1102,33 @@
"type": "tidelift"
}
],
"time": "2020-03-09T19:04:49+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "37b0976c78b94856543260ce09b460a7bc852747"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747",
"reference": "37b0976c78b94856543260ce09b460a7bc852747",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-02-27T09:26:54+00:00"
"time": "2021-05-27T12:26:48+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.15.0",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7"
"reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
"reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010",
"reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -906,6 +1164,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@@ -920,25 +1181,108 @@
"type": "tidelift"
}
],
"time": "2020-02-27T09:26:54+00:00"
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v1.1.8",
"name": "symfony/polyfill-php80",
"version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf"
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf",
"reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
"reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"psr/container": "^1.0"
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-07-28T13:41:28+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.1"
},
"suggest": {
"symfony/service-implementation": ""
@@ -946,7 +1290,11 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
@@ -978,33 +1326,190 @@
"interoperability",
"standards"
],
"time": "2019-10-14T12:27:06+00:00"
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-04-01T10:43:52+00:00"
},
{
"name": "webmozart/assert",
"version": "1.7.0",
"name": "symfony/string",
"version": "v5.3.3",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
"url": "https://github.com/symfony/string.git",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
"reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
"url": "https://api.github.com/repos/symfony/string/zipball/bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"reference": "bd53358e3eccec6a670b5f33ab680d8dbe1d4ae1",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0",
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "~1.15"
},
"require-dev": {
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"files": [
"Resources/functions.php"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.3.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-06-27T11:44:38+00:00"
},
{
"name": "tysonandre/var_representation_polyfill",
"version": "0.0.2",
"source": {
"type": "git",
"url": "https://github.com/TysonAndre/var_representation_polyfill.git",
"reference": "3f17999ee1f257319ddc6721dd26ebbc5d175f33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TysonAndre/var_representation_polyfill/zipball/3f17999ee1f257319ddc6721dd26ebbc5d175f33",
"reference": "3f17999ee1f257319ddc6721dd26ebbc5d175f33",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": "^7.2.0|^8.0.0"
},
"require-dev": {
"phan/phan": "^4.0",
"phpunit/phpunit": "^8.5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"VarRepresentation\\": "src/VarRepresentation"
},
"files": [
"src/var_representation.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Tyson Andre"
}
],
"description": "Polyfill for var_representation",
"keywords": [
"var_export",
"var_representation"
],
"support": {
"issues": "https://github.com/TysonAndre/var_representation_polyfill/issues",
"source": "https://github.com/TysonAndre/var_representation_polyfill/tree/0.0.2"
},
"time": "2021-06-26T18:55:02+00:00"
},
{
"name": "webmozart/assert",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"vimeo/psalm": "<3.6.0"
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
@@ -1026,147 +1531,41 @@
"check",
"validate"
],
"time": "2020-02-14T12:15:55+00:00"
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.10.0"
},
"time": "2021-03-09T10:59:23+00:00"
}
],
"packages-dev": [
{
"name": "brianium/habitat",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/brianium/habitat.git",
"reference": "d0979e3bb379cbc78ecb42b3ac171bc2b7e06d96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brianium/habitat/zipball/d0979e3bb379cbc78ecb42b3ac171bc2b7e06d96",
"reference": "d0979e3bb379cbc78ecb42b3ac171bc2b7e06d96",
"shasum": ""
},
"require-dev": {
"monolog/monolog": ">=1.5.0",
"phpunit/phpunit": ">=3.7.21"
},
"type": "library",
"autoload": {
"psr-0": {
"Habitat": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian",
"email": "scaturrob@gmail.com",
"homepage": "http://brianscaturro.com",
"role": "Lead"
}
],
"description": "A dependable php environment",
"time": "2013-06-08T04:42:29+00:00"
},
{
"name": "brianium/paratest",
"version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "2a06a82742fa303b59179fb8d6037dfb9897b9b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/2a06a82742fa303b59179fb8d6037dfb9897b9b2",
"reference": "2a06a82742fa303b59179fb8d6037dfb9897b9b2",
"shasum": ""
},
"require": {
"brianium/habitat": "1.0.0",
"composer/semver": "~1.2",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-simplexml": "*",
"php": "^7.1",
"phpunit/php-code-coverage": "^6.1.4|^7.0.2|^8.0",
"phpunit/php-timer": "^2.0|^3.0",
"phpunit/phpunit": "^7.5.8|^8.0|^9.0",
"symfony/console": "^3.4||^4.0||^5.0",
"symfony/process": "^3.4||^4.0||^5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"squizlabs/php_codesniffer": "^3.5"
},
"bin": [
"bin/paratest"
],
"type": "library",
"autoload": {
"psr-4": {
"ParaTest\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Scaturro",
"email": "scaturrob@gmail.com",
"homepage": "http://brianscaturro.com",
"role": "Lead"
}
],
"description": "Parallel testing for PHP",
"homepage": "https://github.com/paratestphp/paratest",
"keywords": [
"concurrent",
"parallel",
"phpunit",
"testing"
],
"time": "2020-02-07T22:07:07+00:00"
},
{
"name": "doctrine/instantiator",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "ae466f726242e637cebdd526a7d991b9433bacf1"
"reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1",
"reference": "ae466f726242e637cebdd526a7d991b9433bacf1",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
"reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"doctrine/coding-standard": "^8.0",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.13",
"phpstan/phpstan-phpunit": "^0.11",
"phpstan/phpstan-shim": "^0.11",
"phpunit/phpunit": "^7.0"
"phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
@@ -1180,7 +1579,7 @@
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "http://ocramius.github.com/"
"homepage": "https://ocramius.github.io/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
@@ -1189,24 +1588,42 @@
"constructor",
"instantiate"
],
"time": "2019-10-21T16:45:58+00:00"
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/1.4.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
"type": "tidelift"
}
],
"time": "2020-11-10T18:47:58+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.9.5",
"version": "1.10.2",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
"reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
"reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
"reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.1 || ^8.0"
},
"replace": {
"myclabs/deep-copy": "self.version"
@@ -1237,32 +1654,43 @@
"object",
"object graph"
],
"time": "2020-01-17T21:11:47+00:00"
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
},
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2020-11-13T09:40:50+00:00"
},
{
"name": "phar-io/manifest",
"version": "1.0.3",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
"reference": "97803eca37d319dfa7826cc2437fc020857acb53"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
"reference": "97803eca37d319dfa7826cc2437fc020857acb53",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
"phar-io/version": "^2.0",
"php": "^5.6 || ^7.0"
"ext-xmlwriter": "*",
"phar-io/version": "^3.0.1",
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "2.0.x-dev"
}
},
"autoload": {
@@ -1292,24 +1720,28 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"time": "2018-07-08T19:23:20+00:00"
"support": {
"issues": "https://github.com/phar-io/manifest/issues",
"source": "https://github.com/phar-io/manifest/tree/2.0.3"
},
"time": "2021-07-20T11:28:43+00:00"
},
{
"name": "phar-io/version",
"version": "2.0.1",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
"reference": "bae7c545bef187884426f042434e561ab1ddb182"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
"reference": "bae7c545bef187884426f042434e561ab1ddb182",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
"php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@@ -1339,37 +1771,41 @@
}
],
"description": "Library for handling version information and constraints",
"time": "2018-07-08T19:19:57+00:00"
"support": {
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/3.1.0"
},
"time": "2021-02-23T14:00:09+00:00"
},
{
"name": "phpspec/prophecy",
"version": "v1.10.3",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "451c3cd1418cf640de218914901e51b064abb093"
"reference": "be1996ed8adc35c3fd795488a653f4b518be70ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
"reference": "451c3cd1418cf640de218914901e51b064abb093",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea",
"reference": "be1996ed8adc35c3fd795488a653f4b518be70ea",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
"sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
"doctrine/instantiator": "^1.2",
"php": "^7.2 || ~8.0, <8.1",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5 || ^3.2",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
"phpspec/phpspec": "^6.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10.x-dev"
"dev-master": "1.11.x-dev"
}
},
"autoload": {
@@ -1402,44 +1838,48 @@
"spy",
"stub"
],
"time": "2020-03-05T15:02:03+00:00"
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/1.13.0"
},
"time": "2021-03-17T13:42:18+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "6.1.4",
"version": "7.0.15",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
"reference": "819f92bba8b001d4363065928088de22f25a3a48"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
"reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48",
"reference": "819f92bba8b001d4363065928088de22f25a3a48",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
"php": "^7.1",
"phpunit/php-file-iterator": "^2.0",
"php": ">=7.2",
"phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-token-stream": "^3.0",
"phpunit/php-token-stream": "^3.1.3 || ^4.0",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
"sebastian/environment": "^3.1 || ^4.0",
"sebastian/environment": "^4.2.2",
"sebastian/version": "^2.0.1",
"theseer/tokenizer": "^1.1"
"theseer/tokenizer": "^1.1.3"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^8.2.2"
},
"suggest": {
"ext-xdebug": "^2.6.0"
"ext-xdebug": "^2.7.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.1-dev"
"dev-master": "7.0-dev"
}
},
"autoload": {
@@ -1465,27 +1905,37 @@
"testing",
"xunit"
],
"time": "2018-10-31T16:06:48+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2021-07-26T12:20:09+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "2.0.2",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "050bedf145a257b1ff02746c31894800e5122946"
"reference": "28af674ff175d0768a5a978e6de83f697d4a7f05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
"reference": "050bedf145a257b1ff02746c31894800e5122946",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/28af674ff175d0768a5a978e6de83f697d4a7f05",
"reference": "28af674ff175d0768a5a978e6de83f697d4a7f05",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
@@ -1515,7 +1965,17 @@
"filesystem",
"iterator"
],
"time": "2018-09-13T20:33:42+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2021-07-19T06:46:01+00:00"
},
{
"name": "phpunit/php-text-template",
@@ -1556,27 +2016,31 @@
"keywords": [
"template"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
"source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
},
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
"version": "2.1.2",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
"reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
"reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
"reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
@@ -1605,25 +2069,35 @@
"keywords": [
"timer"
],
"time": "2019-06-07T04:22:29+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
"source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T08:20:02+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "3.1.1",
"version": "3.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
"reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
"reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
"reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": "^7.1"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
@@ -1654,57 +2128,67 @@
"keywords": [
"tokenizer"
],
"time": "2019-09-17T06:23:10+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
"source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"abandoned": true,
"time": "2021-07-26T12:15:06+00:00"
},
{
"name": "phpunit/phpunit",
"version": "7.5.20",
"version": "8.5.19",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
"reference": "496281b64ec781856ed0a583483b5923b4033722"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
"reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/496281b64ec781856ed0a583483b5923b4033722",
"reference": "496281b64ec781856ed0a583483b5923b4033722",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.1",
"doctrine/instantiator": "^1.3.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"myclabs/deep-copy": "^1.7",
"phar-io/manifest": "^1.0.2",
"phar-io/version": "^2.0",
"php": "^7.1",
"phpspec/prophecy": "^1.7",
"phpunit/php-code-coverage": "^6.0.7",
"phpunit/php-file-iterator": "^2.0.1",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.10.0",
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.2",
"phpspec/prophecy": "^1.10.3",
"phpunit/php-code-coverage": "^7.0.12",
"phpunit/php-file-iterator": "^2.0.4",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-timer": "^2.1",
"sebastian/comparator": "^3.0",
"sebastian/diff": "^3.0",
"sebastian/environment": "^4.0",
"sebastian/exporter": "^3.1",
"sebastian/global-state": "^2.0",
"phpunit/php-timer": "^2.1.2",
"sebastian/comparator": "^3.0.2",
"sebastian/diff": "^3.0.2",
"sebastian/environment": "^4.2.3",
"sebastian/exporter": "^3.1.2",
"sebastian/global-state": "^3.0.0",
"sebastian/object-enumerator": "^3.0.3",
"sebastian/resource-operations": "^2.0",
"sebastian/resource-operations": "^2.0.1",
"sebastian/type": "^1.1.3",
"sebastian/version": "^2.0.1"
},
"conflict": {
"phpunit/phpunit-mock-objects": "*"
},
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*",
"phpunit/php-invoker": "^2.0"
"phpunit/php-invoker": "^2.0.0"
},
"bin": [
"phpunit"
@@ -1712,7 +2196,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.5-dev"
"dev-master": "8.5-dev"
}
},
"autoload": {
@@ -1738,27 +2222,41 @@
"testing",
"xunit"
],
"time": "2020-01-08T08:45:45+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.19"
},
"funding": [
{
"url": "https://phpunit.de/donate.html",
"type": "custom"
},
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2021-07-31T15:15:06+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.1",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
"reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
"reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
"reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.0"
"phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
@@ -1783,29 +2281,39 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"time": "2017-03-04T06:30:41+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
"source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T08:15:22+00:00"
},
{
"name": "sebastian/comparator",
"version": "3.0.2",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
"reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
"reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
"shasum": ""
},
"require": {
"php": "^7.1",
"php": ">=7.1",
"sebastian/diff": "^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
@@ -1823,6 +2331,10 @@
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@@ -1834,10 +2346,6 @@
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
@@ -1847,24 +2355,34 @@
"compare",
"equality"
],
"time": "2018-07-12T15:12:46+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T08:04:30+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.2",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.0",
@@ -1886,13 +2404,13 @@
"BSD-3-Clause"
],
"authors": [
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
@@ -1903,24 +2421,34 @@
"unidiff",
"unified diff"
],
"time": "2019-02-04T06:01:07+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:59:04+00:00"
},
{
"name": "sebastian/environment",
"version": "4.2.3",
"version": "4.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
"reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
"reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5"
@@ -1956,24 +2484,34 @@
"environment",
"hhvm"
],
"time": "2019-11-20T08:46:58+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:53:42+00:00"
},
{
"name": "sebastian/exporter",
"version": "3.1.2",
"version": "3.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
"reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
"reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
"reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
"shasum": ""
},
"require": {
"php": "^7.0",
"php": ">=7.0",
"sebastian/recursion-context": "^3.0"
},
"require-dev": {
@@ -2023,27 +2561,40 @@
"export",
"exporter"
],
"time": "2019-09-14T09:02:43+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:47:53+00:00"
},
{
"name": "sebastian/global-state",
"version": "2.0.0",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
"reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
"reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b",
"reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b",
"shasum": ""
},
"require": {
"php": "^7.0"
"php": ">=7.2",
"sebastian/object-reflector": "^1.1.1",
"sebastian/recursion-context": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
"ext-dom": "*",
"phpunit/phpunit": "^8.0"
},
"suggest": {
"ext-uopz": "*"
@@ -2051,7 +2602,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "3.0-dev"
}
},
"autoload": {
@@ -2074,24 +2625,34 @@
"keywords": [
"global state"
],
"time": "2017-04-27T15:39:26+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:43:24+00:00"
},
{
"name": "sebastian/object-enumerator",
"version": "3.0.3",
"version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
"reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
"reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
"reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"shasum": ""
},
"require": {
"php": "^7.0",
"php": ">=7.0",
"sebastian/object-reflector": "^1.1.1",
"sebastian/recursion-context": "^3.0"
},
@@ -2121,24 +2682,34 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"time": "2017-08-03T12:35:26+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:40:27+00:00"
},
{
"name": "sebastian/object-reflector",
"version": "1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
"reference": "773f97c67f28de00d397be301821b06708fca0be"
"reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
"reference": "773f97c67f28de00d397be301821b06708fca0be",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"shasum": ""
},
"require": {
"php": "^7.0"
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
@@ -2166,24 +2737,34 @@
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"time": "2017-03-29T09:07:27+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
"source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:37:18+00:00"
},
{
"name": "sebastian/recursion-context",
"version": "3.0.0",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
"reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
"reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
"reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
"shasum": ""
},
"require": {
"php": "^7.0"
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
@@ -2204,14 +2785,14 @@
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
@@ -2219,24 +2800,34 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2017-03-03T06:23:57+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:34:24+00:00"
},
{
"name": "sebastian/resource-operations",
"version": "2.0.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
"reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
"reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=7.1"
},
"type": "library",
"extra": {
@@ -2261,7 +2852,74 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"time": "2018-10-04T04:07:39+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/resource-operations/issues",
"source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"abandoned": true,
"time": "2020-11-30T07:30:19+00:00"
},
{
"name": "sebastian/type",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
"reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Collection of value objects that represent the types of the PHP type system",
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:25:11+00:00"
},
{
"name": "sebastian/version",
@@ -2304,90 +2962,31 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
"source": "https://github.com/sebastianbergmann/version/tree/master"
},
"time": "2016-10-03T07:35:21+00:00"
},
{
"name": "symfony/process",
"version": "v4.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "3e40e87a20eaf83a1db825e1fa5097ae89042db3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/3e40e87a20eaf83a1db825e1fa5097ae89042db3",
"reference": "3e40e87a20eaf83a1db825e1fa5097ae89042db3",
"shasum": ""
},
"require": {
"php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-27T16:54:36+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.1.3",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": "^7.0"
"php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@@ -2407,7 +3006,17 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"time": "2019-06-13T22:48:21+00:00"
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.1"
},
"funding": [
{
"url": "https://github.com/theseer",
"type": "github"
}
],
"time": "2021-07-28T10:34:58+00:00"
}
],
"aliases": [],
@@ -2416,14 +3025,14 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.1.0",
"php": "^7.2.0|^8.0.0",
"ext-filter": "*",
"ext-json": "*",
"ext-tokenizer": "*"
},
"platform-dev": [],
"platform-overrides": {
"php": "7.1.22"
"php": "7.2.24"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.1.0"
}
Vendored Regular → Executable
+2 -1
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env php
<?php
// @phan-file-suppress PhanPluginRemoveDebugAny
if (PHP_VERSION_ID < 70000) {
fwrite(STDERR, "ERROR: Phan 2.x requires PHP 7.1+, but it is being run with PHP " . PHP_VERSION . PHP_EOL);
fwrite(STDERR, "ERROR: Phan 5.x requires PHP 7.2+, but it is being run with PHP " . PHP_VERSION . PHP_EOL);
fwrite(STDERR, "PHP 5.6 reached its end of life in December 2018." . PHP_EOL);
fwrite(STDERR, "Exiting without analyzing code." . PHP_EOL);
exit(1);
Vendored Regular → Executable
+17 -4
View File
@@ -2,7 +2,7 @@
<?php
/**
* Usage: phan_client -l path/to/file.php
* Compatible with php 5.6 and php 7.x
* Compatible with php 5.6-8.1
* (The server itself requires a newer php version)
*
* See plugins/vim/snippet.vim for an example of a use of this program.
@@ -29,6 +29,7 @@
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
* @phan-file-suppress PhanPluginDuplicateConditionalNullCoalescing this can't use the `??` operator because it's compatible with php 5.6
* @phan-file-suppress PhanPluginCanUseParamType, PhanPluginCanUsePHP71Void, PhanPluginCanUseReturnType
* @phan-file-suppress PhanPluginRemoveDebugEcho
*/
class PhanPHPLinter
{
@@ -130,6 +131,11 @@ class PhanPHPLinter
exit($failure_code);
}
if (!isset($path)) {
self::debugError("Unexpectedly parsed no files\n");
exit($failure_code);
}
// TODO: Check that everything in $this->file_list is in the same path.
// $path = reset($opts->file_list);
$real = realpath($path);
@@ -166,8 +172,11 @@ class PhanPHPLinter
continue;
}
// Convert this to a relative path
if (strncmp($dirname . '/', $real, strlen($dirname . '/')) === 0) {
$real = substr($real, strlen($dirname . '/'));
if (in_array(substr($real, 0, strlen($dirname) + 1),
[$dirname . DIRECTORY_SEPARATOR, $dirname . '/'],
true
)) {
$real = substr($real, strlen($dirname) + 1);
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable not able to analyze. Not using coalescing because this supports php 5.
$mapped_path = isset($opts->temporary_file_map[$path]) ? $opts->temporary_file_map[$path] : $path;
// If we are analyzing a temporary file, but it's within a project, then output the path to a temporary file for consistency.
@@ -211,7 +220,8 @@ class PhanPHPLinter
// TODO: check if the folder is within a folder with subdirectory .phan/config.php
// TODO: Check if there is a lock before attempting to connect?
$client = @stream_socket_client($opts->url, $errno, $errstr, 20.0);
if (!\is_resource($client)) {
// NOTE: Some future release of php may change stream_socket_client to return an object on success.
if (!$client) {
// TODO: This should attempt to start up the phan daemon for the given folder?
self::debugError("Phan daemon not running on " . ($opts->url));
exit(0);
@@ -744,6 +754,9 @@ EOB;
return '';
} elseif ($key === '') {
return '';
} elseif (strlen($key) > 255) {
// levenshtein refuses to run for longer keys
return '';
}
// include short options in case a typo is made like -aa instead of -a
$known_flags = \array_merge(self::GETOPT_LONG_OPTIONS, $short_options);
+30 -15
View File
@@ -6,7 +6,10 @@ namespace Phan\AST;
use ast\Node;
use function is_float;
use function is_int;
use function is_null;
use function is_object;
use function is_string;
use function md5;
@@ -17,36 +20,48 @@ use function md5;
class ASTHasher
{
/**
* @param string|int|float|null $node
* @param string|int|null $node
* @return string a 16-byte binary key for the array key
* @internal
*/
public static function hashKey($node): string
{
if (is_string($node)) {
return md5('s' . $node, true);
return md5($node, true);
} elseif (is_int($node)) {
if (\PHP_INT_SIZE >= 8) {
return "\0\0\0\0\0\0\0\0" . \pack('J', $node);
} else {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node);
}
}
// Both 2.0 and 2 cast to the string '2'
if (is_int($node)) {
return md5((string) $node, true);
}
return md5('f' . $node, true);
// This is not a valid array key, give up
return md5((string) $node, true);
}
/**
* @param Node|string|int|float|null $node
* @return string a 16-byte binary key for the Node
* @return string a 16-byte binary key for the Node which is unlikely to overlap for ordinary code
*/
public static function hash($node): string
{
if (!($node instanceof Node)) {
if (!is_object($node)) {
// hashKey
if (is_string($node)) {
return md5('s' . $node, true);
return md5($node, true);
} elseif (is_int($node)) {
if (\PHP_INT_SIZE >= 8) {
return "\0\0\0\0\0\0\0\0" . \pack('J', $node);
} else {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node);
}
} elseif (is_float($node)) {
return "\0\0\0\0\0\0\0\1" . \pack('e', $node);
} elseif (is_null($node)) {
return "\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0\0";
}
if (is_int($node)) {
return md5((string) $node, true);
}
return md5('f' . $node, true);
// This is not a valid AST, give up
return md5((string) $node, true);
}
// @phan-suppress-next-line PhanUndeclaredProperty
return $node->hash ?? ($node->hash = self::computeHash($node));
@@ -61,7 +76,7 @@ class ASTHasher
$str = 'N' . $node->kind . ':' . ($node->flags & 0xfffff);
foreach ($node->children as $key => $child) {
// added in PhanAnnotationAdder
if ($key === 'phan_nf') {
if (\is_string($key) && \strncmp($key, 'phan', 4) === 0) {
continue;
}
$str .= self::hashKey($key);
+204 -56
View File
@@ -11,8 +11,13 @@ use Closure;
use Phan\Analysis\PostOrderAnalysisVisitor;
use Phan\AST\TolerantASTConverter\Shim;
use function array_map;
use function implode;
use function is_string;
use function sprintf;
use function var_representation;
use const VAR_REPRESENTATION_SINGLE_LINE;
Shim::load();
@@ -61,37 +66,14 @@ class ASTReverter
public static function toShortString($node): string
{
if (!($node instanceof Node)) {
if ($node === null) {
// use lowercase 'null' instead of 'NULL'
return 'null';
}
if (\is_string($node)) {
return self::escapeString($node);
}
if (\is_resource($node)) {
return 'resource(' . \get_resource_type($node) . ')';
}
// TODO: minimal representations for floats, arrays, etc.
return \var_export($node, true);
return var_representation($node, VAR_REPRESENTATION_SINGLE_LINE);
}
return (self::$closure_map[$node->kind] ?? self::$noop)($node);
}
/**
* Escapes the inner contents to be suitable for a single-line single or double quoted string
*
* @see https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/PrettyPrinter/Standard.php
*/
public static function escapeString(string $string): string
{
if (\preg_match('/([\0-\15\16-\37])/', $string)) {
// Use double quoted strings if this contains newlines, tabs, control characters, etc.
return '"' . self::escapeInnerString($string, '"') . '"';
}
// Otherwise, use single quotes
return \var_export($string, true);
}
/**
* Escapes the inner contents to be suitable for a single-line double quoted string
*
@@ -132,11 +114,17 @@ class ASTReverter
ast\AST_TYPE => static function (Node $node): string {
return PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags];
},
/**
* @suppress PhanPartialTypeMismatchArgument
*/
ast\AST_TYPE_INTERSECTION => static function (Node $node): string {
return implode('&', array_map('self::toShortTypeString', $node->children));
},
/**
* @suppress PhanPartialTypeMismatchArgument
*/
ast\AST_TYPE_UNION => static function (Node $node): string {
return implode('|', \array_map('self::toShortTypeString', $node->children));
return implode('|', array_map('self::toShortTypeString', $node->children));
},
/**
* @suppress PhanTypeMismatchArgumentNullable
@@ -157,10 +145,49 @@ class ASTReverter
return self::formatIncDec('--%s', $node->children['var']);
},
ast\AST_ARG_LIST => static function (Node $node): string {
return '(' . implode(', ', \array_map('self::toShortString', $node->children)) . ')';
return '(' . implode(', ', array_map('self::toShortString', $node->children)) . ')';
},
ast\AST_CALLABLE_CONVERT => /** @unused-param $node */ static function (Node $node): string {
return '(...)';
},
ast\AST_ATTRIBUTE_LIST => static function (Node $node): string {
return implode(' ', array_map('self::toShortString', $node->children));
},
ast\AST_ATTRIBUTE_GROUP => static function (Node $node): string {
return implode(', ', array_map('self::toShortString', $node->children));
},
ast\AST_ATTRIBUTE => static function (Node $node): string {
$result = self::toShortString($node->children['class']);
$args = $node->children['args'];
if ($args) {
$result .= self::toShortString($args);
}
return $result;
},
ast\AST_NAMED_ARG => static function (Node $node): string {
return $node->children['name'] . ': ' . self::toShortString($node->children['expr']);
},
ast\AST_PARAM_LIST => static function (Node $node): string {
return '(' . implode(', ', array_map('self::toShortString', $node->children)) . ')';
},
ast\AST_PARAM => static function (Node $node): string {
$str = '$' . $node->children['name'];
if ($node->flags & ast\flags\PARAM_VARIADIC) {
$str = "...$str";
}
if ($node->flags & ast\flags\PARAM_REF) {
$str = "&$str";
}
if (isset($node->children['type'])) {
$str = ASTReverter::toShortString($node->children['type']) . ' ' . $str;
}
if (isset($node->children['default'])) {
$str .= ' = ' . ASTReverter::toShortString($node->children['default']);
}
return $str;
},
ast\AST_EXPR_LIST => static function (Node $node): string {
return implode(', ', \array_map('self::toShortString', $node->children));
return implode(', ', array_map('self::toShortString', $node->children));
},
ast\AST_CLASS_CONST => static function (Node $node): string {
return self::toShortString($node->children['class']) . '::' . $node->children['const'];
@@ -206,6 +233,9 @@ class ASTReverter
return (string)$result;
}
},
ast\AST_NAME_LIST => static function (Node $node): string {
return implode('|', array_map('self::toShortString', $node->children));
},
ast\AST_ARRAY => static function (Node $node): string {
$parts = [];
foreach ($node->children as $elem) {
@@ -214,12 +244,8 @@ class ASTReverter
$parts[] = '';
continue;
}
$part = self::toShortString($elem->children['value']);
$key_node = $elem->children['key'];
if ($key_node !== null) {
$part = self::toShortString($key_node) . '=>' . $part;
}
$parts[] = $part;
// AST_ARRAY_ELEM or AST_UNPACK
$parts[] = self::toShortString($elem);
}
$string = implode(',', $parts);
switch ($node->flags) {
@@ -233,7 +259,7 @@ class ASTReverter
},
/** @suppress PhanAccessClassConstantInternal */
ast\AST_BINARY_OP => static function (Node $node): string {
return \sprintf(
return sprintf(
"(%s %s %s)",
self::toShortString($node->children['left']),
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown',
@@ -241,14 +267,14 @@ class ASTReverter
);
},
ast\AST_ASSIGN => static function (Node $node): string {
return \sprintf(
return sprintf(
"(%s = %s)",
self::toShortString($node->children['var']),
self::toShortString($node->children['expr'])
);
},
ast\AST_ASSIGN_REF => static function (Node $node): string {
return \sprintf(
return sprintf(
"(%s =& %s)",
self::toShortString($node->children['var']),
self::toShortString($node->children['expr'])
@@ -256,7 +282,7 @@ class ASTReverter
},
/** @suppress PhanAccessClassConstantInternal */
ast\AST_ASSIGN_OP => static function (Node $node): string {
return \sprintf(
return sprintf(
"(%s %s= %s)",
self::toShortString($node->children['var']),
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown',
@@ -273,19 +299,27 @@ class ASTReverter
if (($expr->kind ?? null) !== ast\AST_UNARY_OP) {
return $operation_name . $expr_text;
}
return \sprintf("%s(%s)", $operation_name, $expr_text);
return sprintf("%s(%s)", $operation_name, $expr_text);
},
ast\AST_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return \sprintf(
return sprintf(
'%s->%s',
self::toShortString($node->children['expr']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_NULLSAFE_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return sprintf(
'%s?->%s',
self::toShortString($node->children['expr']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_STATIC_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return \sprintf(
return sprintf(
'%s::%s%s',
self::toShortString($node->children['class']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
@@ -294,30 +328,39 @@ class ASTReverter
},
ast\AST_METHOD_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return \sprintf(
return sprintf(
'%s->%s%s',
self::toShortString($node->children['expr']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
self::toShortString($node->children['args'])
);
},
ast\AST_NULLSAFE_METHOD_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return sprintf(
'%s?->%s%s',
self::toShortString($node->children['expr']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
self::toShortString($node->children['args'])
);
},
ast\AST_STATIC_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return \sprintf(
return sprintf(
'%s::$%s',
self::toShortString($node->children['class']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_INSTANCEOF => static function (Node $node): string {
return \sprintf(
return sprintf(
'(%s instanceof %s)',
self::toShortString($node->children['expr']),
self::toShortString($node->children['class'])
);
},
ast\AST_CAST => static function (Node $node): string {
return \sprintf(
return sprintf(
'(%s)(%s)',
// @phan-suppress-next-line PhanAccessClassConstantInternal
PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags] ?? 'unknown',
@@ -325,14 +368,15 @@ class ASTReverter
);
},
ast\AST_CALL => static function (Node $node): string {
return \sprintf(
return sprintf(
'%s%s',
self::toShortString($node->children['expr']),
self::toShortString($node->children['args'])
);
},
ast\AST_NEW => static function (Node $node): string {
return \sprintf(
// TODO: add parenthesis in case this is used as (new X())->method(), or properties, but only when necessary
return sprintf(
'new %s%s',
self::toShortString($node->children['class']),
self::toShortString($node->children['args'])
@@ -341,7 +385,7 @@ class ASTReverter
ast\AST_CLONE => static function (Node $node): string {
// clone($x)->someMethod() has surprising precedence,
// so surround `clone $x` with parenthesis.
return \sprintf(
return sprintf(
'(clone(%s))',
self::toShortString($node->children['expr'])
);
@@ -349,36 +393,59 @@ class ASTReverter
ast\AST_CONDITIONAL => static function (Node $node): string {
['cond' => $cond, 'true' => $true, 'false' => $false] = $node->children;
if ($true !== null) {
return \sprintf('(%s ? %s : %s)', self::toShortString($cond), self::toShortString($true), self::toShortString($false));
return sprintf('(%s ? %s : %s)', self::toShortString($cond), self::toShortString($true), self::toShortString($false));
}
return \sprintf('(%s ?: %s)', self::toShortString($cond), self::toShortString($false));
return sprintf('(%s ?: %s)', self::toShortString($cond), self::toShortString($false));
},
/** @suppress PhanPossiblyUndeclaredProperty */
ast\AST_MATCH => static function (Node $node): string {
['cond' => $cond, 'stmts' => $stmts] = $node->children;
return sprintf('match (%s) {%s}', ASTReverter::toShortString($cond), $stmts->children ? ' ' . ASTReverter::toShortString($stmts) . ' ' : '');
},
ast\AST_MATCH_ARM_LIST => static function (Node $node): string {
return implode(', ', array_map(self::class . '::toShortString', $node->children));
},
ast\AST_MATCH_ARM => static function (Node $node): string {
['cond' => $cond, 'expr' => $expr] = $node->children;
return sprintf('%s => %s', $cond !== null ? ASTReverter::toShortString($cond) : 'default', ASTReverter::toShortString($expr));
},
ast\AST_ISSET => static function (Node $node): string {
return \sprintf(
return sprintf(
'isset(%s)',
self::toShortString($node->children['var'])
);
},
ast\AST_EMPTY => static function (Node $node): string {
return \sprintf(
return sprintf(
'empty(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_PRINT => static function (Node $node): string {
return \sprintf(
return sprintf(
'print(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_ECHO => static function (Node $node): string {
return 'echo ' . ASTReverter::toShortString($node->children['expr']) . ';';
},
ast\AST_ARRAY_ELEM => static function (Node $node): string {
$value_representation = self::toShortString($node->children['value']);
$key_node = $node->children['key'];
if ($key_node !== null) {
return self::toShortString($key_node) . '=>' . $value_representation;
}
return $value_representation;
},
ast\AST_UNPACK => static function (Node $node): string {
return \sprintf(
return sprintf(
'...(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_INCLUDE_OR_EVAL => static function (Node $node): string {
return \sprintf(
return sprintf(
'%s(%s)',
self::EXEC_NODE_FLAG_NAMES[$node->flags],
self::toShortString($node->children['expr'])
@@ -390,11 +457,92 @@ class ASTReverter
if ($c instanceof Node) {
$parts[] = '{' . self::toShortString($c) . '}';
} else {
$parts[] = self::escapeInnerString((string)$c);
$parts[] = self::escapeInnerString((string)$c, '"');
}
}
return '"' . implode('', $parts) . '"';
},
ast\AST_SHELL_EXEC => static function (Node $node): string {
$parts = [];
$expr = $node->children['expr'];
if ($expr instanceof Node) {
foreach ($expr->children as $c) {
if ($c instanceof Node) {
$parts[] = '{' . self::toShortString($c) . '}';
} else {
$parts[] = self::escapeInnerString((string)$c, '`');
}
}
} else {
$parts[] = self::escapeInnerString((string)$expr, '`');
}
return '`' . implode('', $parts) . '`';
},
// Slightly better short placeholders than (unknown)
ast\AST_CLOSURE => static function (Node $_): string {
return '(function)';
},
ast\AST_ARROW_FUNC => static function (Node $_): string {
return '(fn)';
},
ast\AST_RETURN => static function (Node $node): string {
return sprintf(
'return %s;',
self::toShortString($node->children['expr'])
);
},
ast\AST_THROW => static function (Node $node): string {
return sprintf(
'(throw %s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_FOR => static function (Node $_): string {
return '(for loop)';
},
ast\AST_WHILE => static function (Node $_): string {
return '(while loop)';
},
ast\AST_DO_WHILE => static function (Node $_): string {
return '(do-while loop)';
},
ast\AST_FOREACH => static function (Node $_): string {
return '(foreach loop)';
},
ast\AST_IF => static function (Node $_): string {
return '(if statement)';
},
ast\AST_IF_ELEM => static function (Node $_): string {
return '(if statement element)';
},
ast\AST_TRY => static function (Node $_): string {
return '(try statement)';
},
ast\AST_SWITCH => static function (Node $_): string {
return '(switch statement)';
},
ast\AST_SWITCH_LIST => static function (Node $_): string {
return '(switch case list)';
},
ast\AST_SWITCH_CASE => static function (Node $_): string {
return '(switch case statement)';
},
ast\AST_EXIT => static function (Node $node): string {
return 'exit(' . self::toShortString($node->children['expr']) . ')';
},
ast\AST_YIELD => static function (Node $node): string {
['value' => $value, 'key' => $key] = $node->children;
if ($value !== null) {
return '(yield)';
}
if ($key !== null) {
return sprintf('(yield %s => %s)', self::toShortString($key), self::toShortString($value));
}
return sprintf('(yield %s)', self::toShortString($value));
},
ast\AST_YIELD_FROM => static function (Node $node): string {
return '(yield from ' . self::toShortString($node->children['expr']) . ')';
},
// TODO: AST_SHELL_EXEC, AST_ENCAPS_LIST(in shell_exec or double quotes)
];
}
@@ -426,7 +574,7 @@ class ASTReverter
$str = '(' . $str . ')';
}
// @phan-suppress-next-line PhanPluginPrintfVariableFormatString
return \sprintf($format, $str);
return sprintf($format, $str);
}
}
ASTReverter::init();
+59 -59
View File
@@ -18,7 +18,7 @@ use function in_array;
/**
* This simplifies a PHP AST into a form which is easier to analyze,
* and returns the new Node.
* The original \ast\Node objects are not modified.
* The original ast\Node objects are not modified.
*
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
* @phan-file-suppress PhanPossiblyUndeclaredProperty
@@ -36,30 +36,30 @@ class ASTSimplifier
private static function apply(Node $node): array
{
switch ($node->kind) {
case \ast\AST_FUNC_DECL:
case \ast\AST_METHOD:
case \ast\AST_CLOSURE:
case \ast\AST_CLASS:
case \ast\AST_DO_WHILE:
case \ast\AST_FOREACH:
case ast\AST_FUNC_DECL:
case ast\AST_METHOD:
case ast\AST_CLOSURE:
case ast\AST_CLASS:
case ast\AST_DO_WHILE:
case ast\AST_FOREACH:
return [self::applyToStmts($node)];
case \ast\AST_FOR:
case ast\AST_FOR:
return self::normalizeForStatement($node);
case \ast\AST_WHILE:
case ast\AST_WHILE:
return self::normalizeWhileStatement($node);
//case \ast\AST_BREAK:
//case \ast\AST_CONTINUE:
//case \ast\AST_RETURN:
//case \ast\AST_THROW:
//case \ast\AST_EXIT:
//case ast\AST_BREAK:
//case ast\AST_CONTINUE:
//case ast\AST_RETURN:
//case ast\AST_THROW:
//case ast\AST_EXIT:
default:
return [$node];
case \ast\AST_STMT_LIST:
case ast\AST_STMT_LIST:
return [self::applyToStatementList($node)];
// Conditional blocks:
case \ast\AST_IF:
case ast\AST_IF:
return self::normalizeIfStatement($node);
case \ast\AST_TRY:
case ast\AST_TRY:
return [self::normalizeTryStatement($node)];
}
}
@@ -90,7 +90,7 @@ class ASTSimplifier
*/
private static function applyToStatementList(Node $statement_list): Node
{
if ($statement_list->kind !== \ast\AST_STMT_LIST) {
if ($statement_list->kind !== ast\AST_STMT_LIST) {
$statement_list = self::buildStatementList($statement_list->lineno, $statement_list);
}
$new_children = [];
@@ -113,12 +113,12 @@ class ASTSimplifier
}
/**
* Creates a new node with kind \ast\AST_STMT_LIST from a list of 0 or more child nodes.
* Creates a new node with kind ast\AST_STMT_LIST from a list of 0 or more child nodes.
*/
private static function buildStatementList(int $lineno, Node ...$child_nodes): Node
{
return new Node(
\ast\AST_STMT_LIST,
ast\AST_STMT_LIST,
0,
$child_nodes,
$lineno
@@ -138,7 +138,7 @@ class ASTSimplifier
if (!($stmt instanceof Node)) {
continue;
}
if ($stmt->kind !== \ast\AST_IF) {
if ($stmt->kind !== ast\AST_IF) {
continue;
}
// Run normalizeIfStatement again.
@@ -192,17 +192,17 @@ class ASTSimplifier
return true;
}
switch ($node->kind) {
case \ast\AST_CONST:
case \ast\AST_MAGIC_CONST:
case \ast\AST_NAME:
case ast\AST_CONST:
case ast\AST_MAGIC_CONST:
case ast\AST_NAME:
return true;
case \ast\AST_UNARY_OP:
case ast\AST_UNARY_OP:
return self::isExpressionWithoutSideEffects($node->children['expr']);
case \ast\AST_BINARY_OP:
case ast\AST_BINARY_OP:
return self::isExpressionWithoutSideEffects($node->children['left']) &&
self::isExpressionWithoutSideEffects($node->children['right']);
case \ast\AST_CLASS_CONST:
case \ast\AST_CLASS_NAME:
case ast\AST_CLASS_CONST:
case ast\AST_CLASS_NAME:
return self::isExpressionWithoutSideEffects($node->children['class']);
default:
return false;
@@ -230,11 +230,11 @@ class ASTSimplifier
break; // No transformation rules apply here.
}
if ($if_cond->kind === \ast\AST_UNARY_OP &&
if ($if_cond->kind === ast\AST_UNARY_OP &&
$if_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $if_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === \ast\AST_UNARY_OP &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfDoubleNegateReduction($node));
continue;
@@ -244,17 +244,17 @@ class ASTSimplifier
continue;
}
}
if ($if_cond->kind === \ast\AST_BINARY_OP && in_array($if_cond->flags, self::NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS, true)) {
if ($if_cond->kind === ast\AST_BINARY_OP && in_array($if_cond->flags, self::NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS, true)) {
// if (($var = A) === B) {X} -> $var = A; if ($var === B) { X}
$if_cond_children = $if_cond->children;
if (in_array($if_cond_children['left']->kind ?? 0, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true) &&
($if_cond_children['left']->children['var']->kind ?? 0) === \ast\AST_VAR &&
if (in_array($if_cond_children['left']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) &&
($if_cond_children['left']->children['var']->kind ?? 0) === ast\AST_VAR &&
self::isExpressionWithoutSideEffects($if_cond_children['right'])) {
self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInLeftSideOfBinaryOpReduction($node));
continue;
}
if (in_array($if_cond_children['right']->kind ?? 0, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true) &&
($if_cond_children['right']->children['var']->kind ?? 0) === \ast\AST_VAR &&
if (in_array($if_cond_children['right']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) &&
($if_cond_children['right']->children['var']->kind ?? 0) === ast\AST_VAR &&
self::isExpressionWithoutSideEffects($if_cond_children['left'])) {
self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInRightSideOfBinaryOpReduction($node));
continue;
@@ -263,7 +263,7 @@ class ASTSimplifier
// (But `foo($y = something()) && $x = $y` is not safe to rearrange)
}
if (count($node->children) === 1) {
if ($if_cond->kind === \ast\AST_BINARY_OP &&
if ($if_cond->kind === ast\AST_BINARY_OP &&
$if_cond->flags === flags\BINARY_BOOL_AND) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfAndReduction($node));
// if (A && B) {X} -> if (A) { if (B) {X}}
@@ -271,7 +271,7 @@ class ASTSimplifier
continue;
}
} elseif (count($node->children) === 2) {
if ($if_cond->kind === \ast\AST_UNARY_OP &&
if ($if_cond->kind === ast\AST_UNARY_OP &&
$if_cond->flags === flags\UNARY_BOOL_NOT &&
$node->children[1]->children['cond'] === null) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfNegateReduction($node));
@@ -281,8 +281,8 @@ class ASTSimplifier
self::replaceLastNodeWithNodeList($nodes, self::applyIfChainReduction($node));
continue;
}
if ($if_cond->kind === \ast\AST_ASSIGN &&
($if_cond->children['var']->kind ?? null) === \ast\AST_VAR) {
if ($if_cond->kind === ast\AST_ASSIGN &&
($if_cond->children['var']->kind ?? null) === ast\AST_VAR) {
// if ($var = A) {X} -> $var = A; if ($var) {X}
// do this whether or not there is an else.
// TODO: Could also reduce `if (($var = A) && B) {X} else if (C) {Y} -> $var = A; ....
@@ -311,18 +311,18 @@ class ASTSimplifier
break; // No transformation rules apply here.
}
if ($while_cond->kind === \ast\AST_UNARY_OP &&
if ($while_cond->kind === ast\AST_UNARY_OP &&
$while_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $while_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === \ast\AST_UNARY_OP &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
$node = self::applyWhileDoubleNegateReduction($node);
continue;
}
break;
}
if ($while_cond->kind === \ast\AST_BINARY_OP &&
if ($while_cond->kind === ast\AST_BINARY_OP &&
$while_cond->flags === flags\BINARY_BOOL_AND) {
// TODO: Also support `and` operator.
$node = self::applyWhileAndReduction($node);
@@ -357,11 +357,11 @@ class ASTSimplifier
if (!($for_cond instanceof Node)) {
break;
}
if ($for_cond->kind === \ast\AST_UNARY_OP &&
if ($for_cond->kind === ast\AST_UNARY_OP &&
$for_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $for_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === \ast\AST_UNARY_OP &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
$node = self::applyForDoubleNegateReduction($node);
continue;
@@ -387,8 +387,8 @@ class ASTSimplifier
}
$inner_assign_var = $inner_assign_statement->children['var'];
if ($inner_assign_var->kind !== \ast\AST_VAR) {
throw new AssertionError('Expected $inner_assign_var->kind === \ast\AST_VAR');
if ($inner_assign_var->kind !== ast\AST_VAR) {
throw new AssertionError('Expected $inner_assign_var->kind === ast\AST_VAR');
}
$new_node_elem = clone($node->children[0]);
@@ -423,12 +423,12 @@ class ASTSimplifier
}
/**
* Creates a new node with kind \ast\AST_IF from two branches
* Creates a new node with kind ast\AST_IF from two branches
*/
private static function buildIfNode(Node $l, Node $r): Node
{
return new Node(
\ast\AST_IF,
ast\AST_IF,
0,
[$l, $r],
$l->lineno
@@ -454,7 +454,7 @@ class ASTSimplifier
$r->children['stmts']->flags = 0;
$inner_if_node = self::buildIfNode($l, $r);
$new_r = new Node(
\ast\AST_IF_ELEM,
ast\AST_IF_ELEM,
0,
[
'cond' => null,
@@ -465,12 +465,12 @@ class ASTSimplifier
$children[] = $new_r;
}
// $children is an array of 2 nodes of type IF_ELEM
return new Node(\ast\AST_IF, 0, $children, $node->lineno);
return new Node(ast\AST_IF, 0, $children, $node->lineno);
}
/**
* Converts if (A && B) {X}` -> `if (A) { if (B){X}}`
* @return Node simplified node logically equivalent to $node, with kind \ast\AST_IF.
* @return Node simplified node logically equivalent to $node, with kind ast\AST_IF.
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyIfAndReduction(Node $node): Node
@@ -485,19 +485,19 @@ class ASTSimplifier
// Normalize code such as `if (A && (B && C)) {...}` recursively.
$inner_node_stmts = self::normalizeIfStatement(new Node(
\ast\AST_IF,
ast\AST_IF,
0,
[$inner_node_elem],
$inner_node_lineno
));
$inner_node_stmt_list = new Node(\ast\AST_STMT_LIST, 0, $inner_node_stmts, $inner_node_lineno);
$inner_node_stmt_list = new Node(ast\AST_STMT_LIST, 0, $inner_node_stmts, $inner_node_lineno);
$outer_node_elem = clone($node->children[0]); // AST_IF_ELEM
$outer_node_elem->children['cond'] = $node->children[0]->children['cond']->children['left'];
$outer_node_elem->children['stmts'] = $inner_node_stmt_list;
$outer_node_elem->flags = 0;
return new Node(
\ast\AST_IF,
ast\AST_IF,
0,
[$outer_node_elem],
$node->lineno
@@ -506,7 +506,7 @@ class ASTSimplifier
/**
* Converts `while (A && B) {X}` -> `while (A) { if (!B) { break;} X}`
* @return Node simplified node logically equivalent to $node, with kind \ast\AST_IF.
* @return Node simplified node logically equivalent to $node, with kind ast\AST_IF.
*/
private static function applyWhileAndReduction(Node $node): Node
{
@@ -701,7 +701,7 @@ class ASTSimplifier
}
$lineno = $if_elem->lineno;
$new_else_elem = new Node(
\ast\AST_IF_ELEM,
ast\AST_IF_ELEM,
0,
[
'cond' => null,
@@ -710,16 +710,16 @@ class ASTSimplifier
$lineno
);
$new_if_elem = new Node(
\ast\AST_IF_ELEM,
ast\AST_IF_ELEM,
0,
[
'cond' => $if_elem->children['cond']->children['expr'],
'stmts' => new Node(\ast\AST_STMT_LIST, 0, [], $if_elem->lineno),
'stmts' => new Node(ast\AST_STMT_LIST, 0, [], $if_elem->lineno),
],
$lineno
);
return new Node(
\ast\AST_IF,
ast\AST_IF,
0,
[$new_if_elem, $new_else_elem],
$node->lineno
+3 -1
View File
@@ -63,7 +63,9 @@ class ArrowFunc
*/
private function recordUse($name, Node $n): void
{
$this->uses[$name] = $this->uses[$name] ?? $n;
if ($name !== 'this') {
$this->uses[$name] = $this->uses[$name] ?? $n;
}
}
private function buildUses(Node $n): void
+210 -122
View File
@@ -9,7 +9,7 @@ use ast;
use ast\Node;
use Error;
use Exception;
use Phan\Analysis\ConditionVisitorUtil;
use Phan\Analysis\ConditionVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Exception\CodeBaseException;
@@ -19,6 +19,7 @@ use Phan\Exception\IssueException;
use Phan\Exception\NodeException;
use Phan\Exception\RecursionDepthException;
use Phan\Exception\UnanalyzableException;
use Phan\Exception\UnanalyzableMagicPropertyException;
use Phan\Issue;
use Phan\IssueFixSuggester;
use Phan\Language\Context;
@@ -57,10 +58,6 @@ use function strcasecmp;
use function strpos;
use function strtolower;
if (!\function_exists('spl_object_id')) {
require_once __DIR__ . '/../../spl_object_id.php';
}
/**
* Methods for an AST node in context
* @phan-file-suppress PhanPartialTypeMismatchArgument, PhanTypeMismatchArgumentNullable
@@ -466,7 +463,7 @@ class ContextNode
if ($int_or_string_type === null) {
$int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string|null');
}
if (!$name_node_type->canCastToUnionType($int_or_string_type)) {
if (!$name_node_type->canCastToUnionType($int_or_string_type, $this->code_base)) {
$this->emitIssue(Issue::TypeSuspiciousIndirectVariable, $name_node->lineno ?? 0, (string)$name_node_type);
}
@@ -477,21 +474,6 @@ class ContextNode
return (string)$name_node;
}
/**
* @return UnionType the union type of the class for this class node. (Typically has just one Type, but only for kind \ast\AST_NAME)
* @throws FQSENException if class union type is invalid
* @deprecated call UnionTypeVisitor::unionTypeFromClassNode
* @suppress PhanUnreferencedPublicMethod
*/
public function getClassUnionType(): UnionType
{
return UnionTypeVisitor::unionTypeFromClassNode(
$this->code_base,
$this->context,
$this->node
);
}
// Constants for getClassList() API
public const CLASS_LIST_ACCEPT_ANY = 0;
public const CLASS_LIST_ACCEPT_OBJECT = 1;
@@ -566,9 +548,8 @@ class ContextNode
* exceptions will be inhibited
*
* @param int $expected_type_categories
* Does not affect the returned classes, but will cause phan to emit issues. Does not emit by default.
* If set to CLASS_LIST_ACCEPT_ANY, this will not warn.
* If set to CLASS_LIST_ACCEPT_OBJECT, this will warn if the inferred type is exclusively non-object types.
* If set to CLASS_LIST_ACCEPT_OBJECT, this will warn if the inferred type is exclusively non-object types. This will not add classes based on LiteralStringType
* If set to CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, this will warn if the inferred type is exclusively non-object and non-string types.
*
* @param ?string $custom_issue_type
@@ -587,30 +568,45 @@ class ContextNode
* An exception is thrown if fetching the requested class name
* would trigger an issue (e.g. Issue::ContextNotObject)
*/
public function getClassList(bool $ignore_missing_classes = false, int $expected_type_categories = self::CLASS_LIST_ACCEPT_ANY, string $custom_issue_type = null): array
{
public function getClassList(
bool $ignore_missing_classes = false,
int $expected_type_categories = self::CLASS_LIST_ACCEPT_ANY,
string $custom_issue_type = null,
bool $warn_if_wrong_type = true
): array {
[$union_type, $class_list] = $this->getClassListInner($ignore_missing_classes);
if ($union_type->isEmpty()) {
return [];
}
// TODO: Should this check that count($class_list) > 0 instead? Or just always check?
if (\count($class_list) === 0 && $expected_type_categories !== self::CLASS_LIST_ACCEPT_ANY) {
if (!$union_type->hasTypeMatchingCallback(static function (Type $type) use ($expected_type_categories): bool {
return $type->isObject() || ($type instanceof MixedType) || ($expected_type_categories === self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME && $type instanceof StringType);
})) {
if ($custom_issue_type === Issue::TypeExpectedObjectPropAccess) {
if ($union_type->isType(NullType::instance(false))) {
$custom_issue_type = Issue::TypeExpectedObjectPropAccessButGotNull;
// TODO: Improve for intersection types
if (\count($class_list) === 0) {
if (!$union_type->hasTypeMatchingCallback(function (Type $type) use ($expected_type_categories): bool {
if ($this->node instanceof Node) {
if ($this->node->kind === ast\AST_NAME) {
return $type->isObjectWithKnownFQSEN();
}
if ($this->node->kind === ast\AST_TYPE) {
return $this->node->flags !== ast\flags\TYPE_STATIC;
}
}
$this->emitIssue(
$custom_issue_type ?? ($expected_type_categories === self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME ? Issue::TypeExpectedObjectOrClassName : Issue::TypeExpectedObject),
$this->node->lineno ?? $this->context->getLineNumberStart(),
ASTReverter::toShortString($this->node),
(string)$union_type->asNonLiteralType()
);
} elseif ($expected_type_categories === self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME) {
return $type->isObject() || ($type instanceof MixedType) || ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT && $type instanceof StringType);
})) {
if ($warn_if_wrong_type) {
if ($custom_issue_type === Issue::TypeExpectedObjectPropAccess) {
if ($union_type->isType(NullType::instance(false))) {
$custom_issue_type = Issue::TypeExpectedObjectPropAccessButGotNull;
}
}
$this->emitIssue(
$custom_issue_type ?? ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT ? Issue::TypeExpectedObjectOrClassName : Issue::TypeExpectedObject),
$this->node->lineno ?? $this->context->getLineNumberStart(),
ASTReverter::toShortString($this->node),
(string)$union_type->asNonLiteralType()
);
}
} elseif ($expected_type_categories !== self::CLASS_LIST_ACCEPT_OBJECT) {
foreach ($union_type->getTypeSet() as $type) {
if ($type instanceof LiteralStringType) {
$type_value = $type->getValue();
@@ -618,7 +614,7 @@ class ContextNode
$fqsen = FullyQualifiedClassName::fromFullyQualifiedString($type_value);
if ($this->code_base->hasClassWithFQSEN($fqsen)) {
$class_list[] = $this->code_base->getClassByFQSEN($fqsen);
} else {
} elseif ($warn_if_wrong_type) {
$this->emitIssue(
Issue::UndeclaredClass,
$this->node->lineno ?? $this->context->getLineNumberStart(),
@@ -626,11 +622,13 @@ class ContextNode
);
}
} catch (FQSENException $e) {
$this->emitIssue(
$e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike,
$this->node->lineno ?? $this->context->getLineNumberStart(),
$e->getFQSEN()
);
if ($warn_if_wrong_type) {
$this->emitIssue(
$e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike,
$this->node->lineno ?? $this->context->getLineNumberStart(),
$e->getFQSEN()
);
}
}
}
}
@@ -648,7 +646,7 @@ class ContextNode
* @param bool $is_static
* Set to true if this is a static method call
*
* @param bool $is_direct
* @param bool $is_direct @phan-mandatory-param
* Set to true if this is directly invoking the method (guaranteed not to be special syntax)
*
* @param bool $is_new_expression
@@ -712,7 +710,12 @@ class ContextNode
$this->context,
$node->children['expr']
?? $node->children['class']
))->getClassList(false, $is_new_expression ? self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : self::CLASS_LIST_ACCEPT_ANY);
))->getClassList(
false,
$is_static || $is_new_expression ? self::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : self::CLASS_LIST_ACCEPT_OBJECT,
null,
$is_new_expression // emit warnings about the class if this is for `new $className`
);
} catch (CodeBaseException $exception) {
$exception_fqsen = $exception->getFQSEN();
throw new IssueException(
@@ -748,17 +751,18 @@ class ContextNode
);
}
if (!$union_type->isEmpty()
&& $union_type->isNativeType()
&& !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullable() && ($type instanceof MixedType || $type instanceof ObjectType);
})
// reject `$stringVar->method()` but not `$stringVar::method()` and not (`new $stringVar()`
&& !(($is_static || $is_new_expression) && $union_type->hasNonNullStringType())
&& !(
Config::get_null_casts_as_any_type()
&& $union_type->hasType(NullType::instance(false))
)
if ($union_type->isDefinitelyUndefined()
|| (!$union_type->isEmpty()
&& $union_type->isNativeType()
&& !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullableLabeled() && ($type instanceof MixedType || $type instanceof ObjectType);
})
// reject `$stringVar->method()` but not `$stringVar::method()` and not (`new $stringVar()`
&& !(($is_static || $is_new_expression) && $union_type->hasNonNullStringType())
&& !(
Config::get_null_casts_as_any_type()
&& $union_type->hasType(NullType::instance(false))
))
) {
throw new IssueException(
Issue::fromType(Issue::NonClassMethodCall)(
@@ -786,13 +790,10 @@ class ContextNode
// TODO: Could favor the most generic subclass in a union type
continue;
}
$method = $class->getMethodByName(
$this->code_base,
$method_name
);
$method = $class->getMethodByName($this->code_base, $method_name);
if ($method->hasTemplateType()) {
try {
return $method->resolveTemplateType(
$method = $method->resolveTemplateType(
$this->code_base,
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class'])
);
@@ -807,9 +808,11 @@ class ContextNode
$class_without_method = $class->getFQSEN();
}
}
$method = $method ?? $call_method;
if (!$method || ($is_direct && $method->isFakeConstructor())) {
$method = $call_method;
}
if ($method) {
if ($class_without_method && Config::get_strict_method_checking()) {
if ($class_without_method && Config::get_strict_method_checking() && !$this->isDefinitelyPossiblyUndeclaredMethod($node, $method_name, $is_direct)) {
$this->emitIssue(
Issue::PossiblyUndeclaredMethod,
$node->lineno,
@@ -852,6 +855,43 @@ class ContextNode
);
}
/**
* @throws IssueException
*/
private function isDefinitelyPossiblyUndeclaredMethod(Node $node, string $method_name, bool $is_direct): bool
{
try {
$union_type = UnionTypeVisitor::unionTypeFromClassNode(
$this->code_base,
$this->context,
$node->children['expr']
?? $node->children['class']
);
} catch (FQSENException $e) {
throw new IssueException(
Issue::fromType($e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)(
$this->context->getFile(),
$node->lineno,
[$e->getFQSEN()]
)
);
}
// Typically, this should only return false for intersection types that include a mix of types that have and don't have the method.
foreach ($union_type->getTypeSet() as $type) {
if (!$type->hasObjectWithKnownFQSEN()) {
continue;
}
foreach ($type->asPHPDocUnionType()->asClassList($this->code_base, $this->context) as $class) {
if ($class->hasMethodWithName($this->code_base, $method_name, $is_direct)) {
continue 2;
}
}
// Part of the union type includes a type or intersection type that does not have that method.
return false;
}
return true;
}
/**
* Yields a list of FunctionInterface objects for the 'expr' of an AST_CALL.
* @return iterable<mixed, FunctionInterface>
@@ -924,7 +964,7 @@ class ContextNode
}
}
if (!$has_type) {
if (!$union_type->hasPossiblyCallableType()) {
if (!$union_type->hasPossiblyCallableType($code_base)) {
Issue::maybeEmit(
$code_base,
$context,
@@ -935,7 +975,7 @@ class ContextNode
return;
}
}
if (Config::get_strict_method_checking() && $union_type->containsDefiniteNonCallableType()) {
if (Config::get_strict_method_checking() && $union_type->containsDefiniteNonCallableType($code_base)) {
Issue::maybeEmit(
$code_base,
$context,
@@ -949,8 +989,12 @@ class ContextNode
/**
* @throws IssueException for PhanUndeclaredFunction to be caught and reported by the caller
*/
private function returnStubOrThrowUndeclaredFunctionIssueException(FullyQualifiedFunctionName $function_fqsen, bool $suggest_in_global_namespace, FullyQualifiedFunctionName $namespaced_function_fqsen = null, bool $return_placeholder_for_undefined = false): Func
{
private function returnStubOrThrowUndeclaredFunctionIssueException(
FullyQualifiedFunctionName $function_fqsen,
bool $suggest_in_global_namespace,
FullyQualifiedFunctionName $namespaced_function_fqsen = null,
bool $return_placeholder_for_undefined = false
): Func {
if ($return_placeholder_for_undefined) {
$functions = $this->code_base->getPlaceholdersForUndeclaredFunction($function_fqsen);
Issue::maybeEmitWithParameters(
@@ -1333,7 +1377,7 @@ class ContextNode
$property_name = (string)$property_name;
}
if (!\is_string($property_name)) {
throw $this->createExceptionForInvalidPropertyName($node, $is_static);
$this->throwExceptionForInvalidPropertyName($node, $is_static);
}
}
@@ -1355,7 +1399,8 @@ class ContextNode
Issue::fromType($is_static ? Issue::UndeclaredClassStaticProperty : Issue::UndeclaredClassProperty)(
$this->context->getFile(),
$node->lineno,
[ $property_name, $exception_fqsen ]
[ $property_name, $exception_fqsen ],
IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen)
)
);
}
@@ -1395,8 +1440,10 @@ class ContextNode
// bets are off. However, @phan-forbid-undeclared-magic-properties
// will make this method analyze the code as if all properties were declared or had @property annotations.
if (!$is_static && $class->hasGetMethod($this->code_base) && !$class->getForbidUndeclaredMagicProperties($this->code_base)) {
throw new UnanalyzableException(
throw new UnanalyzableMagicPropertyException(
$node,
$class,
$property_name,
"Can't determine if property {$property_name} exists in class {$class->getFQSEN()} with __get defined"
);
}
@@ -1447,42 +1494,13 @@ class ContextNode
}
if (!$is_static && Config::get_strict_object_checking() &&
!($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) {
$union_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
$invalid = UnionType::empty();
foreach ($union_type->getTypeSet() as $type) {
if (!$type->isPossiblyObject()) {
$invalid = $invalid->withType($type);
} elseif ($type->isNullable()) {
$invalid = $invalid->withType(NullType::instance(false));
}
}
if (!$invalid->isEmpty()) {
if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_NULLABLE) {
$invalid = $invalid->nonNullableClone();
}
if (!$invalid->isEmpty()) {
$this->emitIssue(
Issue::PossiblyUndeclaredProperty,
$node->lineno,
$property_name,
$union_type,
$invalid
);
if ($property) {
return $property;
}
}
}
self::checkPossiblyUndeclaredInstanceProperty($this->code_base, $this->context, $node, $property_name);
}
if ($property) {
if ($class_without_property && Config::get_strict_object_checking() &&
!($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) {
$this->emitIssue(
Issue::PossiblyUndeclaredProperty,
Issue::PossiblyUndeclaredPropertyOfClass,
$node->lineno,
$property_name,
UnionTypeVisitor::unionTypeFromNode(
@@ -1541,7 +1559,7 @@ class ContextNode
// If the class isn't found, we'll get the message elsewhere
if ($class_fqsen) {
$suggestion = null;
if ($class) {
if (isset($class)) {
$suggestion = IssueFixSuggester::suggestSimilarProperty($this->code_base, $this->context, $class, $property_name, $is_static);
}
@@ -1573,19 +1591,59 @@ class ContextNode
}
/**
* @return NodeException|IssueException
* Warn if the expression of an AST_PROP is possibly invalid for an instance property
* (both for reading and for writing)
*/
private function createExceptionForInvalidPropertyName(Node $node, bool $is_static): Exception
public static function checkPossiblyUndeclaredInstanceProperty(CodeBase $code_base, Context $context, Node $node, string $property_name): void
{
$union_type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$node->children['expr']
);
$invalid = UnionType::empty();
foreach ($union_type->getTypeSet() as $type) {
if (!$type->isPossiblyObject()) {
$invalid = $invalid->withType($type);
} elseif ($type->isNullableLabeled()) {
$invalid = $invalid->withType(NullType::instance(false));
}
}
if (!$invalid->isEmpty()) {
if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_NULLABLE) {
$invalid = $invalid->nonNullableClone();
}
if (!$invalid->isEmpty()) {
// XXX: Previously, this would only warn about null/nullable, not about scalars and arrays.
// Probably to reduce false positives.
Issue::maybeEmit(
$code_base,
$context,
Issue::PossiblyUndeclaredProperty,
$node->lineno,
$property_name,
$union_type,
$invalid
);
}
}
}
/**
* @throws NodeException|IssueException
* @return no-return
*/
private function throwExceptionForInvalidPropertyName(Node $node, bool $is_static): void
{
$property_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['prop']);
if ($property_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) {
if ($property_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType(), $this->code_base)) {
// If we know it can be a string, throw a NodeException instead of a specific issue
return new NodeException(
throw new NodeException(
$node,
"Cannot figure out property name"
);
}
return new IssueException(
throw new IssueException(
Issue::fromType($is_static ? Issue::TypeInvalidStaticPropertyName : Issue::TypeInvalidPropertyName)(
$this->context->getFile(),
$node->lineno,
@@ -1628,6 +1686,7 @@ class ContextNode
// For instance properties, ignore it,
// because we'll create our own property
// @phan-suppress-next-line PhanPluginDuplicateCatchStatementBody
} catch (UnanalyzableException $exception) {
if ($is_static) {
throw $exception;
@@ -1754,6 +1813,13 @@ class ContextNode
if (\strpos($constant_name, '\\') !== false) {
$this->throwUndeclaredGlobalConstantIssueException($code_base, $context, $fqsen);
}
// @phan-suppress-next-line PhanAccessClassConstantInternal
$constant_exists_variable = $context->getScope()->getVariableByNameOrNull(ConditionVisitor::CONSTANT_EXISTS_PREFIX . \ltrim($fqsen->__toString(), '\\'));
if ($constant_exists_variable &&
!$constant_exists_variable->getUnionType()->isPossiblyUndefined() &&
$constant_exists_variable->getFileRef()->getFile() === $context->getFile()) {
return $this->createPlaceholderGlobalConstant($fqsen);
}
$fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString(
$constant_name
);
@@ -1766,12 +1832,17 @@ class ContextNode
);
}
} catch (FQSENException $e) {
throw new AssertionError("Impossible FQSENException: " . $e->getMessage(), $e);
throw new AssertionError("Impossible FQSENException: " . $e->getMessage());
}
// This is either a fully qualified constant,
// or a relative constant for which nothing was found in the namespace
if (!$code_base->hasGlobalConstantWithFQSEN($fqsen)) {
// @phan-suppress-next-line PhanAccessClassConstantInternal
$constant_exists_variable = $context->getScope()->getVariableByNameOrNull(ConditionVisitor::CONSTANT_EXISTS_PREFIX . \ltrim($fqsen->__toString(), '\\'));
if ($constant_exists_variable && !$constant_exists_variable->getUnionType()->isPossiblyUndefined() && $constant_exists_variable->getFileRef()->getFile() === $context->getFile()) {
return $this->createPlaceholderGlobalConstant($fqsen);
}
$this->throwUndeclaredGlobalConstantIssueException($code_base, $context, $fqsen);
}
@@ -1798,8 +1869,22 @@ class ContextNode
return $constant;
}
private function createPlaceholderGlobalConstant(
FullyQualifiedGlobalConstantName $fqsen
): GlobalConstant {
return new GlobalConstant(
$this->context,
$fqsen->getName(),
// This can't be an object.
UnionType::fromFullyQualifiedRealString('?array|?bool|?float|?int|?resource|?string'),
0,
$fqsen
);
}
/**
* @throws IssueException
* @return no-return
*/
private function throwUndeclaredGlobalConstantIssueException(CodeBase $code_base, Context $context, FullyQualifiedGlobalConstantName $fqsen): void
{
@@ -1841,6 +1926,9 @@ class ContextNode
}
$constant_name = $node->children['const'];
if (!is_string($constant_name)) {
throw new AssertionError('$constant_name must be a string');
}
if (!\strcasecmp($constant_name, 'class')) {
$constant_name = 'class';
}
@@ -2001,33 +2089,32 @@ class ContextNode
return;
}
if ($this->node->kind === ast\AST_STATIC_CALL ||
$this->node->kind === ast\AST_METHOD_CALL) {
$kind = $this->node->kind;
if (\in_array($kind, [ast\AST_STATIC_CALL, ast\AST_METHOD_CALL, ast\AST_NULLSAFE_METHOD_CALL], true)) {
return;
}
$llnode = $this->node;
if ($this->node->kind !== ast\AST_DIM) {
if (!($this->node->children['expr'] instanceof Node)) {
if ($kind !== ast\AST_DIM) {
$expr = $this->node->children['expr'];
if (!($expr instanceof Node)) {
return;
}
if ($this->node->children['expr']->kind !== ast\AST_DIM) {
if ($expr->kind !== ast\AST_DIM) {
(new ContextNode(
$this->code_base,
$this->context,
$this->node->children['expr']
$expr
))->analyzeBackwardCompatibility();
return;
}
$temp = $this->node->children['expr']->children['expr'];
$llnode = $this->node->children['expr'];
$lnode = $temp;
$temp = $expr->children['expr'];
$llnode = $expr;
} else {
$temp = $this->node->children['expr'];
$lnode = $temp;
}
// Strings can have DIMs, it turns out.
@@ -2041,6 +2128,7 @@ class ContextNode
return;
}
$lnode = $temp;
while ($temp instanceof Node
&& ($temp->kind === ast\AST_PROP
|| $temp->kind === ast\AST_STATIC_PROP)
@@ -2060,7 +2148,7 @@ class ContextNode
// Foo::$bar['baz'](); is a problem
// Foo::$bar['baz'] is not
if ($lnode->kind === ast\AST_STATIC_PROP
&& $this->node->kind !== ast\AST_CALL
&& $kind !== ast\AST_CALL
) {
return;
}
@@ -2544,7 +2632,7 @@ class ContextNode
if (\count($arg_list) !== 1) {
return $node;
}
$raw_function_name = ConditionVisitorUtil::getFunctionName($node);
$raw_function_name = ConditionVisitor::getFunctionName($node);
if (!is_string($raw_function_name)) {
return $node;
}
+50 -15
View File
@@ -12,10 +12,12 @@ use Phan\AST\Visitor\KindVisitorImplementation;
use Phan\CodeBase;
use Phan\Exception\NodeException;
use Phan\Language\Context;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\Type;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\BoolType;
use Phan\Language\Type\ClosureDeclarationType;
use Phan\Language\Type\ClosureType;
use Phan\Language\Type\FloatType;
use Phan\Language\Type\IntType;
@@ -37,8 +39,7 @@ use Phan\Language\UnionType;
* @see UnionTypeVisitor for what should be used for the vast majority of use cases
* @see FallbackMethodTypesVisitor for the code using this.
*
* @phan-file-suppress PhanPartialTypeMismatchArgument, PhanTypeMismatchArgumentNullable node is complicated
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal node is complicated
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal, PhanPartialTypeMismatchArgument node is complicated
*/
class FallbackUnionTypeVisitor extends KindVisitorImplementation
{
@@ -128,7 +129,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
/**
* Visit a node with kind `\ast\AST_CLONE`
*
* @param Node $_
* @param Node $node @unused-param
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
@@ -136,7 +137,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
* The set of types that are possibly produced by the
* given node
*/
public function visitClone(Node $_): UnionType
public function visitClone(Node $node): UnionType
{
return ObjectType::instance(false)->asRealUnionType();
}
@@ -229,7 +230,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
// Rarely, a conditional will always be true or always be false.
if ($cond_truthiness !== null) {
// TODO: Add no-op checks in another PR, if they don't already exist for conditional.
if ($cond_truthiness === true) {
if ($cond_truthiness) {
// The condition is unconditionally true
return self::unionTypeFromNode(
$this->code_base,
@@ -283,7 +284,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
/**
* Visit a node with kind `\ast\AST_ARRAY`
*
* @param Node $_
* @param Node $node @unused-param
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
@@ -291,7 +292,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
* The set of types that are possibly produced by the
* given node
*/
public function visitArray(Node $_): UnionType
public function visitArray(Node $node): UnionType
{
// TODO: More precise
return ArrayType::instance(false)->asRealUnionType();
@@ -454,7 +455,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
/**
* Visit a node with kind `\ast\AST_INSTANCEOF`
*
* @param Node $_
* @param Node $node @unused-param
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
@@ -462,7 +463,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
* The set of types that are possibly produced by the
* given node
*/
public function visitInstanceOf(Node $_): UnionType
public function visitInstanceOf(Node $node): UnionType
{
return BoolType::instance(false)->asRealUnionType();
}
@@ -641,7 +642,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
// NOTE: Deliberately do not use the closure for $function->hasDependentReturnType().
// Most plugins expect the context to have variables, which this won't provide.
$function_types = $function->getUnionType();
$function_types = self::getDependentFallbackReturnTypeOfCall($function, $node);
if ($possible_types instanceof UnionType) {
$possible_types = $possible_types->withUnionType($function_types);
} else {
@@ -655,6 +656,21 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
}
}
/**
* @return UnionType - the union type of the result of the call, or of the closure generated by first-class callable conversion
*/
private static function getDependentFallbackReturnTypeOfCall(FunctionInterface $function, Node $node): UnionType
{
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
if ($function instanceof ClosureDeclarationType) {
return $function->asRealUnionType();
} else {
return ClosureType::instanceWithClosureFQSEN($function->getFQSEN(), $function)->asRealUnionType();
}
}
return $function->getUnionType();
}
/**
* Visit a node with kind `\ast\AST_STATIC_CALL`
*
@@ -676,11 +692,11 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
try {
$possible_types = null;
foreach (UnionTypeVisitor::classListFromNodeAndContext($this->code_base, $this->context, $class_node) as $class) {
if (!$class->hasMethodWithName($this->code_base, $method_name)) {
if (!$class->hasMethodWithName($this->code_base, $method_name, true)) {
return UnionType::empty();
}
$method = $class->getMethodByName($this->code_base, $method_name);
$method_types = $method->getUnionType();
$method_types = self::getDependentFallbackReturnTypeOfCall($method, $node);
if ($possible_types instanceof UnionType) {
$possible_types = $possible_types->withUnionType($method_types);
} else {
@@ -724,7 +740,7 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
}
try {
$class = $this->context->getClassInScope($this->code_base);
if (!$class->hasMethodWithName($this->code_base, $method_name)) {
if (!$class->hasMethodWithName($this->code_base, $method_name, true)) {
return UnionType::empty();
}
$method = $class->getMethodByName($this->code_base, $method_name);
@@ -734,6 +750,25 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
}
}
/**
* Visit a node with kind `\ast\AST_NULLSAFE_METHOD_CALL`.
*
* Conservatively try to infer the returned union type of calls such
* as $this?->someMethod(...)
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
*/
public function visitNullsafeMethodCall(Node $node): UnionType
{
return $this->visitMethodCall($node)->nullableClone();
}
/**
* Visit a node with kind `\ast\AST_ASSIGN`
*
@@ -793,11 +828,11 @@ class FallbackUnionTypeVisitor extends KindVisitorImplementation
return LiteralIntType::instanceForValue(1, false)->asRealUnionType();
}
/*
/**
* @param Node $node
* A node holding a class name
*
* @return UnionType
* @return ?UnionType
* The set of types that are possibly produced by the
* given node
*/
@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use ast;
use ast\Node;
use Phan\CodeBase;
use Phan\Exception\NodeException;
use Phan\Language\Context;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Plugin\Internal\UseReturnValuePlugin;
use Phan\Plugin\Internal\UseReturnValuePlugin\UseReturnValueVisitor;
use function is_string;
/**
* Used to check if a snippet in a method is pure.
* Throws NodeException if it sees a node that isn't likely to be in a method that is free of side effects.
* (or if the snippet can jump to a location outside of the snippet)
*
* This ignores many edge cases, including:
* - Magic properties
* - The possibility of emitting notices or throwing
* - Whether or not referenced elements exist (Phan checks that elsewhere)
*
* @phan-file-suppress PhanThrowTypeAbsent
*/
class InferPureSnippetVisitor extends InferPureVisitor
{
public function __construct(CodeBase $code_base, Context $context)
{
parent::__construct($code_base, $context, '{unknown}');
}
/**
* Returns true if the snippet $node is likely free of side effects and is not going to jump to outside of the snippet
*
* TODO: Use the types of local variables as a heuristic in this subclass, e.g. $knownClass->sideEffectFreeMethod()
*
* @param Node|int|string|float|null $node
*/
public static function isSideEffectFreeSnippet(CodeBase $code_base, Context $context, $node): bool
{
if (!$node instanceof Node) {
return true;
}
try {
(new self($code_base, $context))->__invoke($node);
return true;
} catch (NodeException $_) {
return false;
}
}
public function visitReturn(Node $node): void
{
throw new NodeException($node);
}
// visitThrow throws already
// TODO(optional): Bother tracking actual loop/switch depth
public function visitBreak(Node $node): void
{
if ($node->children['depth'] > 1) {
throw new NodeException($node);
}
}
public function visitContinue(Node $node): void
{
if ($node->children['depth'] > 1) {
throw new NodeException($node);
}
}
public function visitYield(Node $node): void
{
throw new NodeException($node);
}
public function visitYieldFrom(Node $node): void
{
throw new NodeException($node);
}
// TODO(optional) track actual goto labels
public function visitGoto(Node $node): void
{
throw new NodeException($node);
}
// NOTE: Checks of assignment, increment or decrement are deferred to --unused-variable-detection
public function visitUnset(Node $node): void
{
throw new NodeException($node);
}
// TODO: Return all classes in union and intersection types instead
protected function getClassForVariable(Node $expr): Clazz
{
if ($expr->kind !== ast\AST_VAR) {
// TODO: Support static properties, (new X()), other expressions with inferable types
throw new NodeException($expr, 'expected simple variable');
}
$var_name = $expr->children['name'];
if (!is_string($var_name)) {
throw new NodeException($expr, 'variable name is not a string');
}
if ($var_name !== 'this') {
$variable = $this->context->getScope()->getVariableByNameOrNull($var_name);
if (!$variable) {
throw new NodeException($expr, 'unknown variable');
}
$union_type = $variable->getUnionType()->asNormalizedTypes();
$known_fqsen = null;
foreach ($union_type->getUniqueFlattenedTypeSet() as $type) {
if (!$type->isObjectWithKnownFQSEN()) {
continue;
}
$fqsen = $type->asFQSEN();
if ($known_fqsen && $known_fqsen !== $fqsen) {
throw new NodeException($expr, 'unknown class');
}
$known_fqsen = $fqsen;
}
if (!$known_fqsen instanceof FullyQualifiedClassName) {
throw new NodeException($expr, 'unknown class');
}
if (!$this->code_base->hasClassWithFQSEN($known_fqsen)) {
throw new NodeException($expr, 'unknown class');
}
return $this->code_base->getClassByFQSEN($known_fqsen);
}
if (!$this->context->isInClassScope()) {
throw new NodeException($expr, 'Not in class scope');
}
return $this->context->getClassInScope($this->code_base);
}
/**
* @param Node $node the node of the call, with 'args'
* @override
*/
protected function checkCalledFunction(Node $node, FunctionInterface $method): void
{
if ($method->isPure()) {
// avoid false positives - throw when calling void methods that were marked as free of side effects.
if ($method->isPHPInternal() || (($method instanceof Method && $method->isAbstract()) || $method->hasReturn() || $method->hasYield())) {
return;
}
}
$label = self::getLabelForFunction($method);
$value = (UseReturnValuePlugin::HARDCODED_FQSENS[$label] ?? false);
if ($value === true) {
return;
} elseif ($value === UseReturnValuePlugin::SPECIAL_CASE) {
if (UseReturnValueVisitor::doesSpecialCaseHaveSideEffects($label, $node)) {
// infer that var_export($x, true) is pure but not var_export($x)
throw new NodeException($node, $label);
}
return;
}
throw new NodeException($node, $label);
}
}
+133 -38
View File
@@ -11,6 +11,7 @@ use Phan\CodeBase;
use Phan\Exception\CodeBaseException;
use Phan\Exception\NodeException;
use Phan\Language\Context;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
@@ -18,6 +19,8 @@ use Phan\Plugin\Internal\UseReturnValuePlugin;
use Phan\Plugin\Internal\UseReturnValuePlugin\PureMethodGraph;
use Phan\Plugin\Internal\UseReturnValuePlugin\UseReturnValueVisitor;
use function is_string;
/**
* Used to check if a method is pure.
* Throws NodeException if it sees a node that isn't likely to be in a method that is free of side effects.
@@ -114,22 +117,34 @@ class InferPureVisitor extends AnalysisVisitor
}
}
/** @override */
public function visitClassName(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitClassName(Node $node): void
{
}
/** @override */
public function visitMagicConst(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitMagicConst(Node $node): void
{
}
/** @override */
public function visitConst(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitConst(Node $node): void
{
}
/** @override */
/**
* @unused-param $node
* @override
*/
public function visitEmpty(Node $node): void
{
$this->maybeInvoke($node->children['expr']);
@@ -141,13 +156,19 @@ class InferPureVisitor extends AnalysisVisitor
$this->maybeInvoke($node->children['var']);
}
/** @override */
public function visitContinue(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitContinue(Node $node): void
{
}
/** @override */
public function visitBreak(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitBreak(Node $node): void
{
}
@@ -202,7 +223,7 @@ class InferPureVisitor extends AnalysisVisitor
$this->checkPureIncDec($node);
}
private function checkPureIncDec(Node $node): void
protected function checkPureIncDec(Node $node): void
{
$var = $node->children['var'];
if (!$var instanceof Node) {
@@ -241,6 +262,11 @@ class InferPureVisitor extends AnalysisVisitor
$this->maybeInvoke($node->children['dim']);
}
public function visitNullsafeProp(Node $node): void
{
$this->visitProp($node);
}
public function visitProp(Node $node): void
{
['expr' => $expr, 'prop' => $prop] = $node->children;
@@ -352,12 +378,42 @@ class InferPureVisitor extends AnalysisVisitor
}
/** @override */
public function visitGoto(Node $_): void
public function visitMatch(Node $node): void
{
$this->maybeInvokeAllChildNodes($node);
}
/** @override */
public function visitLabel(Node $_): void
public function visitMatchArmList(Node $node): void
{
$this->maybeInvokeAllChildNodes($node);
}
/** @override */
public function visitMatchArm(Node $node): void
{
$this->maybeInvokeAllChildNodes($node);
}
/** @override */
public function visitExprList(Node $node): void
{
$this->maybeInvokeAllChildNodes($node);
}
/**
* @unused-param $node
* @override
*/
public function visitGoto(Node $node): void
{
}
/**
* @unused-param $node
* @override
*/
public function visitLabel(Node $node): void
{
}
@@ -388,7 +444,7 @@ class InferPureVisitor extends AnalysisVisitor
} elseif ($var->kind === ast\AST_PROP) {
// Functions that assign to properties aren't pure,
// unless assigning to $this->prop in a constructor.
if (\preg_match('/::__construct$/i', $this->function_fqsen_label)) {
if (\preg_match('/::__construct$/iD', $this->function_fqsen_label)) {
$name = $var->children['expr'];
if ($name instanceof Node && $name->kind === ast\AST_VAR && $name->children['name'] === 'this') {
return;
@@ -404,7 +460,8 @@ class InferPureVisitor extends AnalysisVisitor
if (!($name_node instanceof Node && $name_node->kind === ast\AST_NAME)) {
throw new NodeException($node);
}
$this->visitArgList($node->children['args']);
// "Fatal error: Cannot create Closure for new expression" (for AST_CALLABLE_CONVERT) is caught elsewhere
$this->__invoke($node->children['args']);
try {
$class_list = (new ContextNode($this->code_base, $this->context, $name_node))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME);
} catch (Exception $_) {
@@ -418,7 +475,7 @@ class InferPureVisitor extends AnalysisVisitor
// TODO build a list of internal classes where result of new() is often unused.
continue;
}
if (!$class->hasMethodWithName($this->code_base, '__construct')) {
if (!$class->hasMethodWithName($this->code_base, '__construct', true)) {
throw new NodeException($name_node, 'no __construct found');
}
$this->checkCalledFunction($node, $class->getMethodByName($this->code_base, '__construct'));
@@ -447,8 +504,11 @@ class InferPureVisitor extends AnalysisVisitor
$this->maybeInvoke($node->children['expr']);
}
/** @override */
public function visitName(Node $_): void
/**
* @unused-param $node
* @override
*/
public function visitName(Node $node): void
{
// do nothing
}
@@ -483,7 +543,7 @@ class InferPureVisitor extends AnalysisVisitor
if (!$found_function) {
throw new NodeException($expr, 'not a function');
}
$this->visitArgList($node->children['args']);
$this->__invoke($node->children['args']);
}
public function visitStaticCall(Node $node): void
@@ -508,6 +568,7 @@ class InferPureVisitor extends AnalysisVisitor
} catch (Exception $_) {
throw new NodeException($class, 'could not get type');
}
// TODO: Check all classes in union and intersection types instead up to a limit?
if ($union_type->typeCount() !== 1) {
throw new NodeException($class);
}
@@ -527,48 +588,66 @@ class InferPureVisitor extends AnalysisVisitor
} catch (Exception $_) {
throw new NodeException($node);
}
if (!$class->hasMethodWithName($this->code_base, $method)) {
if (!$class->hasMethodWithName($this->code_base, $method, true)) {
throw new NodeException($node, 'no method');
}
$this->checkCalledFunction($node, $class->getMethodByName($this->code_base, $method));
$this->visitArgList($node->children['args']);
$this->__invoke($node->children['args']);
}
public function visitNullsafeMethodCall(Node $node): void
{
$this->visitMethodCall($node);
}
public function visitMethodCall(Node $node): void
{
if (!$this->context->isInClassScope()) {
// We don't track variables in UseReturnValuePlugin
throw new NodeException($node, 'method call seen outside class scope');
}
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
throw new NodeException($node);
}
$expr = $node->children['expr'];
if (!($expr instanceof Node)) {
if (!$expr instanceof Node) {
throw new NodeException($node);
}
if ($expr->kind !== ast\AST_VAR) {
throw new NodeException($expr, 'not a var');
}
if ($expr->children['name'] !== 'this') {
throw new NodeException($expr, 'not $this');
}
$class = $this->context->getClassInScope($this->code_base);
if (!$class->hasMethodWithName($this->code_base, $method_name)) {
$class = $this->getClassForVariable($expr);
if (!$class->hasMethodWithName($this->code_base, $method_name, true)) {
throw new NodeException($expr, 'does not have method');
}
$this->checkCalledFunction($node, $class->getMethodByName($this->code_base, $method_name));
$this->visitArgList($node->children['args']);
$this->__invoke($node->children['args']);
}
protected function getClassForVariable(Node $expr): Clazz
{
if (!$this->context->isInClassScope()) {
// We don't track variables in UseReturnValuePlugin
throw new NodeException($expr, 'method call seen outside class scope');
}
if ($expr->kind !== ast\AST_VAR) {
throw new NodeException($expr, 'expected simple variable');
}
$var_name = $expr->children['name'];
if (!is_string($var_name)) {
// TODO: Support static properties, (new X()), other expressions with inferable types
throw new NodeException($expr, 'variable name is not a string');
}
if ($var_name !== 'this') {
throw new NodeException($expr, 'not $this');
}
if (!$this->context->isInClassScope()) {
throw new NodeException($expr, 'Not in class scope');
}
return $this->context->getClassInScope($this->code_base);
}
/**
* @param Node $node the node of the call, with 'args'
*/
private function checkCalledFunction(Node $node, FunctionInterface $method): void
protected function checkCalledFunction(Node $node, FunctionInterface $method): void
{
if ($method->isPure()) {
return;
@@ -624,4 +703,20 @@ class InferPureVisitor extends AnalysisVisitor
}
}
}
/**
* @unused-param $node
* @override
*/
public function visitCallableConvert(Node $node): void
{
}
public function visitNamedArg(Node $node): void
{
$expr = $node->children['expr'];
if ($expr instanceof Node) {
$this->__invoke($expr);
}
}
}
+38 -22
View File
@@ -42,7 +42,7 @@ use function error_reporting;
*/
class Parser
{
/** @var ?Cache<ParseResult> */
/** @var ?DiskCache<ParseResult> */
private static $cache = null;
/**
@@ -62,10 +62,9 @@ class Parser
}
/**
* @return Cache<ParseResult>
* @suppress PhanPartialTypeMismatchReturn
* @return DiskCache<ParseResult>
*/
private static function getCache(): Cache
private static function getCache(): DiskCache
{
return self::$cache ?? self::$cache = self::makeNewCache();
}
@@ -118,9 +117,7 @@ class Parser
return self::parseCodePolyfill($code_base, $context, $file_path, $file_contents, $suppress_parse_errors, $request);
}
return self::parseCodeHandlingDeprecation($code_base, $context, $file_contents, $file_path);
} catch (ParseError $native_parse_error) {
return self::handleParseError($code_base, $context, $file_path, $file_contents, $suppress_parse_errors, $native_parse_error, $request);
} catch (CompileError $native_parse_error) {
} catch (CompileError | ParseError $native_parse_error) {
return self::handleParseError($code_base, $context, $file_path, $file_contents, $suppress_parse_errors, $native_parse_error, $request);
}
}
@@ -196,12 +193,14 @@ class Parser
Error $native_parse_error,
?Request $request = null
): Node {
if (!$suppress_parse_errors) {
self::emitSyntaxErrorForNativeParseError($code_base, $context, $file_path, new FileCacheEntry($file_contents), $native_parse_error, $request);
}
if (!Config::getValue('use_fallback_parser')) {
// By default, don't try to re-parse files with syntax errors.
throw $native_parse_error;
if ($file_path !== 'internal') {
if (!$suppress_parse_errors) {
self::emitSyntaxErrorForNativeParseError($code_base, $context, $file_path, new FileCacheEntry($file_contents), $native_parse_error, $request);
}
if (!Config::getValue('use_fallback_parser')) {
// By default, don't try to re-parse files with syntax errors.
throw $native_parse_error;
}
}
// If there's a parse error in a file that's excluded from analysis, give up on parsing it.
@@ -278,9 +277,10 @@ class Parser
return 0;
}
$message = $native_parse_error->getMessage();
if (!\preg_match("/ unexpected '(.+)' \((T_\w+)\)/", $message, $matches)) {
if (!\preg_match("/ unexpected '(.+)', expecting/", $message, $matches)) {
if (!\preg_match("/ unexpected '(.+)'$/", $message, $matches)) {
$prefix = "unexpected (?:token )?('(?:.+)'|\"(?:.+)\")";
if (!\preg_match("/$prefix \((T_\w+)\)/", $message, $matches)) {
if (!\preg_match("/$prefix, expecting/", $message, $matches)) {
if (!\preg_match("/$prefix$/D", $message, $matches)) {
return 0;
}
}
@@ -294,7 +294,7 @@ class Parser
} else {
$token_kind = null;
}
$token_str = $matches[1];
$token_str = \substr($matches[1], 1, -1);
$tokens = \token_get_all($file_cache_entry->getContents());
$candidates = [];
$desired_line = $native_parse_error->getLine();
@@ -371,8 +371,9 @@ class Parser
static $errors = [];
if ($last_file_contents !== $file_contents) {
unset($errors);
$errors = [];
// Create a brand new reference group
$new_errors = [];
$errors = & $new_errors;
try {
self::parseCodePolyfill($code_base, $context, $file_path, $file_contents, true, $request, $errors);
} catch (Throwable $_) {
@@ -419,9 +420,14 @@ class Parser
* @param ?Request $request - May affect the parser used for $file_path
* @param list<Diagnostic> &$errors @phan-output-reference
* @throws ParseException
* @suppress PhanThrowTypeMismatch
*/
public static function parseCodePolyfill(CodeBase $code_base, Context $context, string $file_path, string $file_contents, bool $suppress_parse_errors, ?Request $request, array &$errors = []): Node
{
// @phan-suppress-next-line PhanRedundantCondition
if (!\in_array(Config::AST_VERSION, TolerantASTConverter::SUPPORTED_AST_VERSIONS, true)) {
throw new \Error(\sprintf("Unexpected polyfill version: want %s, got %d", \implode(', ', TolerantASTConverter::SUPPORTED_AST_VERSIONS), Config::AST_VERSION));
}
$converter = self::createConverter($file_path, $file_contents, $request);
$converter->setPHPVersionId(Config::get_closest_target_php_version_id());
$errors = [];
@@ -489,11 +495,17 @@ class Parser
{
if (\in_array($error['type'], [\E_DEPRECATED, \E_COMPILE_WARNING], true) &&
\basename($error['file']) === 'PhpTokenizer.php') {
$line = $error['line'];
if (\preg_match('/line ([0-9]+)$/D', $error['message'], $matches)) {
$line = (int)$matches[1];
}
Issue::maybeEmit(
$code_base,
$context,
$error['type'] === \E_COMPILE_WARNING ? Issue::SyntaxCompileWarning : Issue::CompatibleSyntaxNotice,
$error['line'],
$line,
$error['message']
);
}
@@ -587,10 +599,14 @@ class Parser
// TODO: Refactor and make more code use this check
private static function shouldUseNativeAST(): bool
{
if (\PHP_VERSION_ID >= 70400) {
if (\PHP_VERSION_ID >= 80100) {
$min_version = '1.0.14';
} elseif (\PHP_VERSION_ID >= 80000) {
$min_version = '1.0.10';
} elseif (\PHP_VERSION_ID >= 70400) {
$min_version = '1.0.2';
} else {
$min_version = '1.0.1';
$min_version = Config::MINIMUM_AST_EXTENSION_VERSION;
}
return \version_compare(\phpversion('ast') ?: '0.0.0', $min_version) >= 0;
}
+13 -3
View File
@@ -120,6 +120,16 @@ class PhanAnnotationAdder
}
}
};
/**
* @param Node $node
* @return void
*/
$initializes_handler = static function (Node $node): void {
$inner_node = $node->children['var'];
if ($inner_node instanceof Node) {
self::markNode($inner_node, self::FLAG_IGNORE_NULLABLE_AND_UNDEF | self::FLAG_INITIALIZES);
}
};
/**
* @param Node $node
* @return void
@@ -179,8 +189,8 @@ class PhanAnnotationAdder
ast\AST_EMPTY => $ignore_nullable_and_undef_expr_handler,
ast\AST_ISSET => $ignore_nullable_and_undef_handler,
ast\AST_UNSET => $ignore_nullable_and_undef_handler,
ast\AST_ASSIGN => $ignore_nullable_and_undef_handler,
ast\AST_ASSIGN_REF => $ignore_nullable_and_undef_handler,
ast\AST_ASSIGN => $initializes_handler,
ast\AST_ASSIGN_REF => $initializes_handler,
// Skip over AST_ARRAY
ast\AST_ARRAY_ELEM => $ast_array_elem_handler,
];
@@ -193,7 +203,7 @@ class PhanAnnotationAdder
{
if ($node instanceof Node) {
$closure = self::$closures_for_kind[$node->kind] ?? null;
if ($closure !== null) {
if (\is_object($closure)) {
$closure($node);
}
foreach ($node->children as $inner) {
@@ -98,7 +98,7 @@ class ScopeImpactCheckingVisitor extends InferPureVisitor
throw new NodeException($node);
}
private function checkPureIncDec(Node $node): void
protected function checkPureIncDec(Node $node): void
{
$var = $node->children['var'];
if (!$var instanceof Node) {
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Phan\AST\TolerantASTConverter;
use Microsoft\PhpParser\Parser;
use Microsoft\PhpParser\TokenStreamProviderInterface;
use const PHP_VERSION_ID;
/**
* Tokenizes content using PHP's built-in `token_get_all`, and converts to "lightweight" Token representation.
*
* Initially we tried hand-spinning the lexer (see `experiments/Lexer.php`), but we had difficulties optimizing
* performance (especially when working with Unicode characters.)
*
* Class PhpTokenizer
* @package Microsoft\PhpParser
* @suppress PhanUndeclaredConstant TODO: Make it only necessary on the class constant declaration
*/
class CompatibleParser extends Parser
{
/**
* Create a parser to accommodate edge cases in the current php minor version and tolerant-php-parser version
*/
public static function create(): Parser
{
if (PHP_VERSION_ID >= 80100) {
return new self();
}
return new Parser();
}
/**
* @override
*/
protected function makeLexer(string $fileContents): TokenStreamProviderInterface
{
return new CompatiblePhpTokenizer($fileContents);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Phan\AST\TolerantASTConverter;
use Microsoft\PhpParser\PhpTokenizer;
use const PHP_VERSION_ID;
/**
* Like PhpTokenizer but supports the following:
*
* 1. Converting older tokens to new token types
* 2. Supporting new tokens in new php versions not yet released in microsoft/tolerant-php-parser
*
* @suppress PhanUndeclaredConstant TODO:
*/
class CompatiblePhpTokenizer extends PhpTokenizer
{
/** @suppress PhanUndeclaredConstant */
protected const T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG = PHP_VERSION_ID >= 80100 ? \T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG : -1;
/** @suppress PhanUndeclaredConstant */
protected const T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG = PHP_VERSION_ID >= 80100 ? \T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG : -1;
/**
* @return list<string|array{0:int,1:string,2:int}>
* @override
*/
protected static function tokenGetAll(string $content, $parseContext): array
{
$tokens = parent::tokenGetAll($content, $parseContext);
if (PHP_VERSION_ID < 80100) {
return $tokens;
}
foreach ($tokens as $i => $token) {
if (\is_array($token)) {
switch ($token[0]) {
case self::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG:
case self::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG:
$tokens[$i] = '&';
break;
}
}
}
return $tokens;
}
}
@@ -125,7 +125,7 @@ class NodeDumper
$padding,
$key !== '' ? $key . ': ' : '',
self::dumpClassName($ast_node),
$this->include_offset ? ' (@' . $ast_node->getStart() . ')' : ''
$this->include_offset ? ' (@' . $ast_node->getStartPosition() . ')' : ''
);
$result = $first_part;
@@ -145,7 +145,7 @@ class NodeDumper
\Phan\Library\StringUtil::jsonEncode(\substr($this->file_contents, $ast_node->fullStart, $ast_node->length))
);
} elseif (\is_scalar($ast_node)) {
return \var_export($ast_node, true);
return \var_representation($ast_node);
} elseif ($ast_node === null) {
return 'null';
} else {
@@ -159,6 +159,7 @@ class NodeDumper
* @param string $padding (to be echoed before the current node
* @throws Exception for invalid $ast_node values
* @suppress PhanUnreferencedPublicMethod
* @suppress PhanPluginRemoveDebugEcho
*/
public function dumpTree($ast_node, string $key = '', string $padding = ''): void
{
@@ -16,7 +16,7 @@ use Microsoft\PhpParser\Diagnostic;
*/
class PhpParserNodeEntry
{
/** @var PhpParser\Node the node generated for the given file contents */
/** @var PhpParser\Node\SourceFileNode the node generated for the given file contents */
public $node;
/** @var Diagnostic[] the list of diagnostics generated for the given file contents */
public $errors;
@@ -24,7 +24,7 @@ class PhpParserNodeEntry
/**
* @param Diagnostic[] $errors
*/
public function __construct(PhpParser\Node $node, array $errors)
public function __construct(PhpParser\Node\SourceFileNode $node, array $errors)
{
$this->node = $node;
$this->errors = $errors;
+86 -17
View File
@@ -4,6 +4,12 @@ declare(strict_types=1);
namespace Phan\AST\TolerantASTConverter;
use ast;
use function class_exists;
use function define;
use function defined;
/**
* Loads missing declarations
*/
@@ -14,33 +20,96 @@ class Shim
*/
public static function load(): void
{
if (!\class_exists('\ast\Node')) {
if (!class_exists('\ast\Node')) {
// Fix for https://github.com/phan/phan/issues/2287
require_once __DIR__ . '/ast_shim.php';
}
if (!\defined('ast\AST_PROP_GROUP')) {
\define('ast\AST_PROP_GROUP', 545);
// Define node kinds that may be absent
if (!defined('ast\AST_PROP_GROUP')) {
define('ast\AST_PROP_GROUP', 0x2ef);
}
if (!\defined('ast\AST_CLASS_NAME')) {
\define('ast\AST_CLASS_NAME', 287);
if (!defined('ast\AST_CLASS_CONST_GROUP')) {
define('ast\AST_CLASS_CONST_GROUP', 0x2ee);
}
if (!\defined('ast\AST_ARROW_FUNC')) {
\define('ast\AST_ARROW_FUNC', 71);
if (!defined('ast\AST_CLASS_NAME')) {
define('ast\AST_CLASS_NAME', 0x23d);
}
if (!\defined('ast\AST_TYPE_UNION')) {
\define('ast\AST_TYPE_UNION', 254);
if (!defined('ast\AST_ARROW_FUNC')) {
define('ast\AST_ARROW_FUNC', 71);
}
if (!\defined('ast\flags\DIM_ALTERNATIVE_SYNTAX')) {
\define('ast\flags\DIM_ALTERNATIVE_SYNTAX', 1 << 1);
if (!defined('ast\AST_TYPE_UNION')) {
define('ast\AST_TYPE_UNION', 254);
}
if (!\defined('ast\flags\PARENTHESIZED_CONDITIONAL')) {
\define('ast\flags\PARENTHESIZED_CONDITIONAL', 1);
if (!defined('ast\AST_ATTRIBUTE_LIST')) {
define('ast\AST_ATTRIBUTE_LIST', 253);
}
if (!\defined('ast\flags\TYPE_FALSE')) {
\define('ast\flags\TYPE_FALSE', 2);
if (!defined('ast\AST_MATCH_ARM_LIST')) {
define('ast\AST_MATCH_ARM_LIST', 252);
}
if (!\defined('ast\flags\TYPE_STATIC')) {
\define('ast\flags\TYPE_STATIC', \PHP_MAJOR_VERSION >= 80000 ? 15 : 20);
if (!defined('ast\AST_ATTRIBUTE_GROUP')) {
define('ast\AST_ATTRIBUTE_GROUP', 251);
}
if (!defined('ast\AST_TYPE_INTERSECTION')) {
define('ast\AST_TYPE_INTERSECTION', 250);
}
if (!defined('ast\AST_CALLABLE_CONVERT')) {
define('ast\AST_CALLABLE_CONVERT', 249);
}
if (!defined('ast\AST_MATCH')) {
define('ast\AST_MATCH', 0x2fc);
}
if (!defined('ast\AST_MATCH_ARM')) {
define('ast\AST_MATCH_ARM', 0x2fb);
}
if (!defined('ast\AST_ATTRIBUTE')) {
define('ast\AST_ATTRIBUTE', 0x2fa);
}
if (!defined('ast\AST_NAMED_ARG')) {
define('ast\AST_NAMED_ARG', 0x2f9);
}
if (!defined('ast\AST_NULLSAFE_PROP')) {
define('ast\AST_NULLSAFE_PROP', 0x2f8);
}
if (!defined('ast\AST_NULLSAFE_METHOD_CALL')) {
define('ast\AST_NULLSAFE_METHOD_CALL', 0x3ff);
}
if (!defined('ast\AST_ENUM_CASE')) {
define('ast\AST_ENUM_CASE', 0x4ff);
}
// Define flags
if (!defined('ast\flags\DIM_ALTERNATIVE_SYNTAX')) {
define('ast\flags\DIM_ALTERNATIVE_SYNTAX', 1 << 1);
}
if (!defined('ast\flags\PARENTHESIZED_CONDITIONAL')) {
define('ast\flags\PARENTHESIZED_CONDITIONAL', 1);
}
$max_param_flag = \max(ast\flags\PARAM_REF, ast\flags\PARAM_VARIADIC);
if (!defined('ast\flags\PARAM_MODIFIER_PUBLIC')) {
define('ast\flags\PARAM_MODIFIER_PUBLIC', $max_param_flag << 1);
}
if (!defined('ast\flags\PARAM_MODIFIER_PROTECTED')) {
define('ast\flags\PARAM_MODIFIER_PROTECTED', $max_param_flag << 2);
}
if (!defined('ast\flags\PARAM_MODIFIER_PRIVATE')) {
define('ast\flags\PARAM_MODIFIER_PRIVATE', $max_param_flag << 3);
}
if (!defined('ast\flags\MODIFIER_READONLY')) {
define('ast\flags\MODIFIER_READONLY', $max_param_flag << 4);
}
if (!defined('ast\flags\TYPE_FALSE')) {
define('ast\flags\TYPE_FALSE', 2);
}
if (!defined('ast\flags\TYPE_STATIC')) {
define('ast\flags\TYPE_STATIC', \PHP_MAJOR_VERSION >= 80000 ? 15 : 20);
}
if (!defined('ast\flags\TYPE_MIXED')) {
define('ast\flags\TYPE_MIXED', \PHP_MAJOR_VERSION >= 80000 ? 16 : 21);
}
if (!defined('ast\flags\TYPE_NEVER')) {
define('ast\flags\TYPE_NEVER', \PHP_MAJOR_VERSION >= 80000 ? 17 : 22);
}
if (!defined('ast\flags\CLASS_ENUM')) {
define('ast\flags\CLASS_ENUM', 0x10000000);
}
}
}
@@ -14,6 +14,7 @@ class ShimFunctions
{
private const KIND_LOOKUP = [
ast\AST_ARG_LIST => 'AST_ARG_LIST',
ast\AST_LIST => 'AST_LIST',
ast\AST_ARRAY => 'AST_ARRAY',
ast\AST_ENCAPS_LIST => 'AST_ENCAPS_LIST',
ast\AST_EXPR_LIST => 'AST_EXPR_LIST',
@@ -29,14 +30,18 @@ class ShimFunctions
ast\AST_NAME_LIST => 'AST_NAME_LIST',
ast\AST_TRAIT_ADAPTATIONS => 'AST_TRAIT_ADAPTATIONS',
ast\AST_USE => 'AST_USE',
ast\AST_TYPE_INTERSECTION => 'AST_TYPE_INTERSECTION',
ast\AST_TYPE_UNION => 'AST_TYPE_UNION',
ast\AST_ATTRIBUTE_LIST => 'AST_ATTRIBUTE_LIST',
ast\AST_MATCH_ARM_LIST => 'AST_MATCH_ARM_LIST',
ast\AST_NAME => 'AST_NAME',
ast\AST_CLOSURE_VAR => 'AST_CLOSURE_VAR',
ast\AST_NULLABLE_TYPE => 'AST_NULLABLE_TYPE',
ast\AST_FUNC_DECL => 'AST_FUNC_DECL',
ast\AST_CLOSURE => 'AST_CLOSURE',
ast\AST_METHOD => 'AST_METHOD',
ast\AST_CLASS => 'AST_CLASS',
ast\AST_ARROW_FUNC => 'AST_ARROW_FUNC',
ast\AST_CLASS => 'AST_CLASS',
ast\AST_MAGIC_CONST => 'AST_MAGIC_CONST',
ast\AST_TYPE => 'AST_TYPE',
ast\AST_VAR => 'AST_VAR',
@@ -68,8 +73,10 @@ class ShimFunctions
ast\AST_BREAK => 'AST_BREAK',
ast\AST_CONTINUE => 'AST_CONTINUE',
ast\AST_CLASS_NAME => 'AST_CLASS_NAME',
ast\AST_CLASS_CONST_GROUP => 'AST_CLASS_CONST_GROUP',
ast\AST_DIM => 'AST_DIM',
ast\AST_PROP => 'AST_PROP',
ast\AST_NULLSAFE_PROP => 'AST_NULLSAFE_PROP',
ast\AST_STATIC_PROP => 'AST_STATIC_PROP',
ast\AST_CALL => 'AST_CALL',
ast\AST_CLASS_CONST => 'AST_CLASS_CONST',
@@ -89,6 +96,7 @@ class ShimFunctions
ast\AST_SWITCH_CASE => 'AST_SWITCH_CASE',
ast\AST_DECLARE => 'AST_DECLARE',
ast\AST_PROP_ELEM => 'AST_PROP_ELEM',
ast\AST_PROP_GROUP => 'AST_PROP_GROUP',
ast\AST_CONST_ELEM => 'AST_CONST_ELEM',
ast\AST_USE_TRAIT => 'AST_USE_TRAIT',
ast\AST_TRAIT_PRECEDENCE => 'AST_TRAIT_PRECEDENCE',
@@ -97,13 +105,16 @@ class ShimFunctions
ast\AST_USE_ELEM => 'AST_USE_ELEM',
ast\AST_TRAIT_ALIAS => 'AST_TRAIT_ALIAS',
ast\AST_GROUP_USE => 'AST_GROUP_USE',
ast\AST_PROP_GROUP => 'AST_PROP_GROUP',
ast\AST_ATTRIBUTE => 'AST_ATTRIBUTE',
ast\AST_MATCH => 'AST_MATCH',
ast\AST_MATCH_ARM => 'AST_MATCH_ARM',
ast\AST_NAMED_ARG => 'AST_NAMED_ARG',
ast\AST_METHOD_CALL => 'AST_METHOD_CALL',
ast\AST_NULLSAFE_METHOD_CALL => 'AST_NULLSAFE_METHOD_CALL',
ast\AST_STATIC_CALL => 'AST_STATIC_CALL',
ast\AST_CONDITIONAL => 'AST_CONDITIONAL',
ast\AST_TRY => 'AST_TRY',
ast\AST_CATCH => 'AST_CATCH',
ast\AST_PARAM => 'AST_PARAM',
ast\AST_FOR => 'AST_FOR',
ast\AST_FOREACH => 'AST_FOREACH',
];
@@ -77,6 +77,9 @@ final class StringUtil
*/
public static function parse(string $str): string
{
if ($str === '') {
return '';
}
$c = $str[0];
if ($c === '<') {
return self::parseHeredoc($str);
@@ -19,14 +19,12 @@ use Microsoft\PhpParser\MissingToken;
use Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression;
use Microsoft\PhpParser\Node\Expression\TernaryExpression;
use Microsoft\PhpParser\Node\SourceFileNode;
use Microsoft\PhpParser\Parser;
use Microsoft\PhpParser\Token;
use Microsoft\PhpParser\TokenKind;
use Phan\CLI;
use Phan\Library\Cache;
use RuntimeException;
use function array_merge;
use function class_exists;
use function count;
use function get_class;
@@ -36,6 +34,7 @@ use function is_string;
use function sprintf;
use function substr;
use function var_export;
use function var_representation;
use const FILTER_FLAG_ALLOW_HEX;
use const FILTER_FLAG_ALLOW_OCTAL;
@@ -75,7 +74,7 @@ Shim::load();
*
* The MIT License (MIT)
*
* Copyright (c) 2017-2018 Tyson Andre
* Copyright (c) 2017-2020 Tyson Andre
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -108,9 +107,11 @@ Shim::load();
*/
class TolerantASTConverter
{
use TolerantASTConverterTrait;
// The latest stable version of php-ast.
// For something != 70, update the library's release.
public const AST_VERSION = 70;
// For something != 85, update the library's release.
public const AST_VERSION = 85;
// The versions that this supports
public const SUPPORTED_AST_VERSIONS = [self::AST_VERSION];
@@ -130,6 +131,93 @@ class TolerantASTConverter
public const INCOMPLETE_PROPERTY = '__INCOMPLETE_PROPERTY__';
public const INCOMPLETE_VARIABLE = '__INCOMPLETE_VARIABLE__';
private const CAST_EXPRESSION_TYPE_LOOKUP = [
// From Parser->parseCastExpression()
TokenKind::ArrayCastToken => flags\TYPE_ARRAY,
TokenKind::BoolCastToken => flags\TYPE_BOOL,
TokenKind::DoubleCastToken => flags\TYPE_DOUBLE,
TokenKind::IntCastToken => flags\TYPE_LONG,
TokenKind::ObjectCastToken => flags\TYPE_OBJECT,
TokenKind::StringCastToken => flags\TYPE_STRING,
TokenKind::UnsetCastToken => flags\TYPE_NULL,
// From Parser->parseCastExpressionGranular()
// This is a syntax error, but try to match what the intent was
TokenKind::ArrayKeyword => flags\TYPE_ARRAY,
TokenKind::BinaryReservedWord => flags\TYPE_STRING,
TokenKind::BoolReservedWord => flags\TYPE_BOOL,
TokenKind::BooleanReservedWord => flags\TYPE_BOOL,
TokenKind::DoubleReservedWord => flags\TYPE_DOUBLE,
TokenKind::IntReservedWord => flags\TYPE_LONG,
TokenKind::IntegerReservedWord => flags\TYPE_LONG,
TokenKind::FloatReservedWord => flags\TYPE_DOUBLE,
TokenKind::ObjectReservedWord => flags\TYPE_OBJECT,
TokenKind::RealReservedWord => flags\TYPE_DOUBLE,
TokenKind::StringReservedWord => flags\TYPE_STRING,
TokenKind::UnsetKeyword => flags\TYPE_NULL,
TokenKind::StaticKeyword => flags\TYPE_STATIC,
];
private const UNARY_OP_EXPRESSION_LOOKUP = [
TokenKind::TildeToken => flags\UNARY_BITWISE_NOT,
TokenKind::MinusToken => flags\UNARY_MINUS,
TokenKind::PlusToken => flags\UNARY_PLUS,
TokenKind::ExclamationToken => flags\UNARY_BOOL_NOT,
// ErrorControlExpression is separate from UnaryOpExpression
];
private const BINARY_EXPRESSION_LOOKUP = [
TokenKind::AmpersandAmpersandToken => flags\BINARY_BOOL_AND,
TokenKind::AmpersandToken => flags\BINARY_BITWISE_AND,
TokenKind::AndKeyword => flags\BINARY_BOOL_AND,
TokenKind::AsteriskAsteriskToken => flags\BINARY_POW,
TokenKind::AsteriskToken => flags\BINARY_MUL,
TokenKind::BarBarToken => flags\BINARY_BOOL_OR,
TokenKind::BarToken => flags\BINARY_BITWISE_OR,
TokenKind::CaretToken => flags\BINARY_BITWISE_XOR,
TokenKind::DotToken => flags\BINARY_CONCAT,
TokenKind::EqualsEqualsEqualsToken => flags\BINARY_IS_IDENTICAL,
TokenKind::EqualsEqualsToken => flags\BINARY_IS_EQUAL,
TokenKind::ExclamationEqualsEqualsToken => flags\BINARY_IS_NOT_IDENTICAL,
TokenKind::ExclamationEqualsToken => flags\BINARY_IS_NOT_EQUAL,
TokenKind::GreaterThanEqualsToken => flags\BINARY_IS_GREATER_OR_EQUAL,
TokenKind::GreaterThanGreaterThanToken => flags\BINARY_SHIFT_RIGHT,
TokenKind::GreaterThanToken => flags\BINARY_IS_GREATER,
TokenKind::LessThanEqualsGreaterThanToken => flags\BINARY_SPACESHIP,
TokenKind::LessThanEqualsToken => flags\BINARY_IS_SMALLER_OR_EQUAL,
TokenKind::LessThanLessThanToken => flags\BINARY_SHIFT_LEFT,
TokenKind::LessThanToken => flags\BINARY_IS_SMALLER,
TokenKind::MinusToken => flags\BINARY_SUB,
TokenKind::OrKeyword => flags\BINARY_BOOL_OR,
TokenKind::PercentToken => flags\BINARY_MOD,
TokenKind::PlusToken => flags\BINARY_ADD,
TokenKind::QuestionQuestionToken => flags\BINARY_COALESCE,
TokenKind::SlashToken => flags\BINARY_DIV,
TokenKind::XorKeyword => flags\BINARY_BOOL_XOR,
];
private const BINARY_ASSIGN_EXPRESSION_LOOKUP = [
TokenKind::AmpersandEqualsToken => flags\BINARY_BITWISE_AND,
TokenKind::AsteriskAsteriskEqualsToken => flags\BINARY_POW,
TokenKind::AsteriskEqualsToken => flags\BINARY_MUL,
TokenKind::BarEqualsToken => flags\BINARY_BITWISE_OR,
TokenKind::CaretEqualsToken => flags\BINARY_BITWISE_XOR,
TokenKind::DotEqualsToken => flags\BINARY_CONCAT,
TokenKind::MinusEqualsToken => flags\BINARY_SUB,
TokenKind::PercentEqualsToken => flags\BINARY_MOD,
TokenKind::PlusEqualsToken => flags\BINARY_ADD,
TokenKind::SlashEqualsToken => flags\BINARY_DIV,
TokenKind::GreaterThanGreaterThanEqualsToken => flags\BINARY_SHIFT_RIGHT,
TokenKind::LessThanLessThanEqualsToken => flags\BINARY_SHIFT_LEFT,
TokenKind::QuestionQuestionEqualsToken => flags\BINARY_COALESCE,
];
/**
* @var FilePositionMap maps byte offsets of the currently parsed file to line numbers.
* @internal
*/
public static $file_position_map;
/**
* @var int - A version in SUPPORTED_AST_VERSIONS
*/
@@ -146,9 +234,6 @@ class TolerantASTConverter
/** @var string the contents of the file currently being parsed */
protected static $file_contents = '';
/** @var FilePositionMap maps byte offsets of the currently parsed file to line numbers */
protected static $file_position_map;
/** @var bool Sets equivalent static option in self::_start_parsing() */
protected $instance_should_add_placeholders = false;
@@ -250,7 +335,9 @@ class TolerantASTConverter
*/
public static function phpParserParse(string $file_contents, array &$errors = []): PhpParser\Node\SourceFileNode
{
$parser = new Parser(); // TODO: In php 7.3, we might need to provide a version, due to small changes in lexing?
// TODO: In php 7.3, we might need to provide a version, due to small changes in lexing?
// This may stop being an issue when php 7.2 support is dropped.
$parser = CompatibleParser::create();
$result = $parser->parseSourceFile($file_contents);
$errors = DiagnosticsProvider::getDiagnostics($result);
return $result;
@@ -289,11 +376,12 @@ class TolerantASTConverter
/**
* @param null|bool|int|string|PhpParser\Node|Token|(PhpParser\Node|Token)[] $n
* @throws Exception if node is invalid
* @internal
*/
protected static function debugDumpNodeOrToken($n): string
public static function debugDumpNodeOrToken($n): string
{
if (\is_scalar($n)) {
return var_export($n, true);
return var_representation($n);
}
if (!\is_array($n)) {
$n = [$n];
@@ -371,6 +459,76 @@ class TolerantASTConverter
return new ast\Node(ast\AST_STMT_LIST, 0, $children, $lineno ?? 0);
}
/**
* @param ?PhpParser\Node\AttributeGroup[] $attribute_groups
* This is represented as a single node for `if` with a colon (macro style)
* @return ?ast\Node a node of kind ast\AST_ATTRIBUTE_LIST, or null.
*/
private static function phpParserAttributeGroupsToAstAttributeList(?array $attribute_groups): ?\ast\Node
{
if (!$attribute_groups) {
return null;
}
$children = [];
foreach ($attribute_groups as $attribute_group) {
if (!$attribute_group instanceof PhpParser\Node\AttributeGroup) {
continue;
}
$ast_group = self::phpParserAttributeGroupToAstAttributeGroup($attribute_group);
if ($ast_group) {
$children[] = $ast_group;
}
}
if (!$children) {
return null;
}
return new ast\Node(
ast\AST_ATTRIBUTE_LIST,
0,
$children,
$children[0]->lineno
);
}
private static function phpParserAttributeGroupToAstAttributeGroup(PhpParser\Node\AttributeGroup $group): ?ast\Node
{
$children = [];
foreach ($group->attributes->children ?? [] as $parser_attribute) {
if (!$parser_attribute instanceof PhpParser\Node\Attribute) {
continue;
}
$children[] = self::phpParserAttributeToAstAttribute($parser_attribute);
}
if (!$children) {
return null;
}
$result = new ast\Node(
ast\AST_ATTRIBUTE_GROUP,
0,
$children,
self::getStartLine($group)
);
// Not part of php-ast, but useful as an indicator that the attribute group syntax is probably incompatible with php 7 and older
// if it spans multiple lines.
$result->endLineno = static::getEndLine($group);
return $result;
}
private static function phpParserAttributeToAstAttribute(PhpParser\Node\Attribute $attribute): ast\Node
{
$args = $attribute->argumentExpressionList;
$start_line = self::getStartLine($attribute);
return new ast\Node(
ast\AST_ATTRIBUTE,
0,
[
'class' => static::phpParserNonValueNodeToAstNode($attribute->name),
'args' => $args || $attribute->openParen || $attribute->closeParen ? static::phpParserArgListToAstArgList($args, $start_line) : null
],
$start_line
);
}
private static function phpParserExprListToExprList(PhpParser\Node\DelimitedList\ExpressionList $expressions_list, int $lineno): ast\Node
{
$children = [];
@@ -404,37 +562,6 @@ class TolerantASTConverter
);
}
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
* This does not convert names to ast\AST_CONST.
* @throws InvalidArgumentException if Phan doesn't know what $n is
*/
protected static function phpParserNonValueNodeToAstNode($n)
{
static $callback_map;
static $fallback_closure;
if (\is_null($callback_map)) {
$callback_map = static::initHandleMap();
/**
* @param PhpParser\Node|Token $n
* @return ast\Node - Not a real node, but a node indicating the TODO
* @throws InvalidArgumentException for invalid node classes
* @throws Error if the environment variable AST_THROW_INVALID is set (for debugging)
*/
$fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
// @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw
throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n));
}
return static::astStub($n);
};
}
$callback = $callback_map[\get_class($n)] ?? $fallback_closure;
// @phan-suppress-next-line PhanThrowTypeMismatch
return $callback($n, self::getStartLine($n));
}
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
@@ -453,39 +580,6 @@ class TolerantASTConverter
}
}
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|null - whatever ast\parse_code would return as the equivalent.
*/
protected static function phpParserNodeToAstNode($n)
{
static $callback_map;
static $fallback_closure;
if (\is_null($callback_map)) {
$callback_map = static::initHandleMap();
/**
* @param PhpParser\Node|Token $n
* @return ast\Node - Not a real node, but a node indicating the TODO
* @throws InvalidArgumentException|Exception for invalid node classes
* @throws Error if the environment variable AST_THROW_INVALID is set to debug.
*/
$fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . static::debugDumpNodeOrToken($n));
}
return static::astStub($n);
};
}
$callback = $callback_map[\get_class($n)] ?? $fallback_closure;
// @phan-suppress-next-line PhanThrowTypeAbsent
$result = $callback($n, self::$file_position_map->getStartLine($n));
if (($result instanceof ast\Node) && $result->kind === ast\AST_NAME) {
return new ast\Node(ast\AST_CONST, 0, ['name' => $result], $result->lineno);
}
return $result;
}
/**
* @param PhpParser\Node|Token $n
* @throws InvalidNodeException if this was called on an unexpected type
@@ -522,6 +616,9 @@ class TolerantASTConverter
* - There are a lot of local variables to look at.
*
* @return array<string,Closure(object,int):(\ast\Node|int|string|float|null)>
*
* NOTE: Make sure that the only caller of this is TolerantASTConverterTrait
* @suppress PhanTypeMismatchReturn todo: why?
*/
protected static function initHandleMap(): array
{
@@ -530,9 +627,15 @@ class TolerantASTConverter
'Microsoft\PhpParser\Node\SourceFileNode' => static function (PhpParser\Node\SourceFileNode $n, int $start_line): ?\ast\Node {
return static::phpParserStmtlistToAstNode($n->statementList, $start_line, false);
},
/** @return mixed */
/**
* @return mixed
*/
'Microsoft\PhpParser\Node\Expression\ArgumentExpression' => static function (PhpParser\Node\Expression\ArgumentExpression $n, int $start_line) {
$result = static::phpParserNodeToAstNode($n->expression);
$expression = $n->expression;
if ($expression === null) {
throw new InvalidNodeException($n);
}
$result = static::phpParserNodeToAstNode($expression);
if ($n->dotDotDotToken !== null) {
return new ast\Node(ast\AST_UNPACK, 0, ['expr' => $result], $start_line);
}
@@ -583,50 +686,6 @@ class TolerantASTConverter
* @return ast\Node|string|float|int (can return a non-Node if the left or right-hand side could not be parsed
*/
'Microsoft\PhpParser\Node\Expression\BinaryExpression' => static function (PhpParser\Node\Expression\BinaryExpression $n, int $start_line) {
static $lookup = [
TokenKind::AmpersandAmpersandToken => flags\BINARY_BOOL_AND,
TokenKind::AmpersandToken => flags\BINARY_BITWISE_AND,
TokenKind::AndKeyword => flags\BINARY_BOOL_AND,
TokenKind::AsteriskAsteriskToken => flags\BINARY_POW,
TokenKind::AsteriskToken => flags\BINARY_MUL,
TokenKind::BarBarToken => flags\BINARY_BOOL_OR,
TokenKind::BarToken => flags\BINARY_BITWISE_OR,
TokenKind::CaretToken => flags\BINARY_BITWISE_XOR,
TokenKind::DotToken => flags\BINARY_CONCAT,
TokenKind::EqualsEqualsEqualsToken => flags\BINARY_IS_IDENTICAL,
TokenKind::EqualsEqualsToken => flags\BINARY_IS_EQUAL,
TokenKind::ExclamationEqualsEqualsToken => flags\BINARY_IS_NOT_IDENTICAL,
TokenKind::ExclamationEqualsToken => flags\BINARY_IS_NOT_EQUAL,
TokenKind::GreaterThanEqualsToken => flags\BINARY_IS_GREATER_OR_EQUAL,
TokenKind::GreaterThanGreaterThanToken => flags\BINARY_SHIFT_RIGHT,
TokenKind::GreaterThanToken => flags\BINARY_IS_GREATER,
TokenKind::LessThanEqualsGreaterThanToken => flags\BINARY_SPACESHIP,
TokenKind::LessThanEqualsToken => flags\BINARY_IS_SMALLER_OR_EQUAL,
TokenKind::LessThanLessThanToken => flags\BINARY_SHIFT_LEFT,
TokenKind::LessThanToken => flags\BINARY_IS_SMALLER,
TokenKind::MinusToken => flags\BINARY_SUB,
TokenKind::OrKeyword => flags\BINARY_BOOL_OR,
TokenKind::PercentToken => flags\BINARY_MOD,
TokenKind::PlusToken => flags\BINARY_ADD,
TokenKind::QuestionQuestionToken => flags\BINARY_COALESCE,
TokenKind::SlashToken => flags\BINARY_DIV,
TokenKind::XorKeyword => flags\BINARY_BOOL_XOR,
];
static $assign_lookup = [
TokenKind::AmpersandEqualsToken => flags\BINARY_BITWISE_AND,
TokenKind::AsteriskAsteriskEqualsToken => flags\BINARY_POW,
TokenKind::AsteriskEqualsToken => flags\BINARY_MUL,
TokenKind::BarEqualsToken => flags\BINARY_BITWISE_OR,
TokenKind::CaretEqualsToken => flags\BINARY_BITWISE_XOR,
TokenKind::DotEqualsToken => flags\BINARY_CONCAT,
TokenKind::MinusEqualsToken => flags\BINARY_SUB,
TokenKind::PercentEqualsToken => flags\BINARY_MOD,
TokenKind::PlusEqualsToken => flags\BINARY_ADD,
TokenKind::SlashEqualsToken => flags\BINARY_DIV,
TokenKind::GreaterThanGreaterThanEqualsToken => flags\BINARY_SHIFT_RIGHT,
TokenKind::LessThanLessThanEqualsToken => flags\BINARY_SHIFT_LEFT,
TokenKind::QuestionQuestionEqualsToken => flags\BINARY_COALESCE,
];
$kind = $n->operator->kind;
if ($kind === TokenKind::InstanceOfKeyword) {
return new ast\Node(ast\AST_INSTANCEOF, 0, [
@@ -634,9 +693,9 @@ class TolerantASTConverter
'class' => static::phpParserNonValueNodeToAstNode($n->rightOperand),
], $start_line);
}
$ast_kind = $lookup[$kind] ?? null;
$ast_kind = self::BINARY_EXPRESSION_LOOKUP[$kind] ?? null;
if ($ast_kind === null) {
$ast_kind = $assign_lookup[$kind] ?? null;
$ast_kind = self::BINARY_ASSIGN_EXPRESSION_LOOKUP[$kind] ?? null;
if ($ast_kind === null) {
throw new AssertionError("missing $kind (" . Token::getTokenKindNameFromValue($kind) . ")");
}
@@ -645,14 +704,8 @@ class TolerantASTConverter
return static::astNodeBinaryop($ast_kind, $n, $start_line);
},
'Microsoft\PhpParser\Node\Expression\UnaryOpExpression' => static function (PhpParser\Node\Expression\UnaryOpExpression $n, int $start_line): ast\Node {
static $lookup = [
TokenKind::TildeToken => flags\UNARY_BITWISE_NOT,
TokenKind::MinusToken => flags\UNARY_MINUS,
TokenKind::PlusToken => flags\UNARY_PLUS,
TokenKind::ExclamationToken => flags\UNARY_BOOL_NOT,
];
$kind = $n->operator->kind;
$ast_kind = $lookup[$kind] ?? null;
$ast_kind = self::UNARY_OP_EXPRESSION_LOOKUP[$kind] ?? null;
if ($ast_kind === null) {
throw new AssertionError("missing $kind(" . Token::getTokenKindNameFromValue($kind) . ")");
}
@@ -664,34 +717,8 @@ class TolerantASTConverter
);
},
'Microsoft\PhpParser\Node\Expression\CastExpression' => static function (PhpParser\Node\Expression\CastExpression $n, int $start_line): ast\Node {
static $lookup = [
// From Parser->parseCastExpression()
TokenKind::ArrayCastToken => flags\TYPE_ARRAY,
TokenKind::BoolCastToken => flags\TYPE_BOOL,
TokenKind::DoubleCastToken => flags\TYPE_DOUBLE,
TokenKind::IntCastToken => flags\TYPE_LONG,
TokenKind::ObjectCastToken => flags\TYPE_OBJECT,
TokenKind::StringCastToken => flags\TYPE_STRING,
TokenKind::UnsetCastToken => flags\TYPE_NULL,
// From Parser->parseCastExpressionGranular()
// This is a syntax error, but try to match what the intent was
TokenKind::ArrayKeyword => flags\TYPE_ARRAY,
TokenKind::BinaryReservedWord => flags\TYPE_STRING,
TokenKind::BoolReservedWord => flags\TYPE_BOOL,
TokenKind::BooleanReservedWord => flags\TYPE_BOOL,
TokenKind::DoubleReservedWord => flags\TYPE_DOUBLE,
TokenKind::IntReservedWord => flags\TYPE_LONG,
TokenKind::IntegerReservedWord => flags\TYPE_LONG,
TokenKind::FloatReservedWord => flags\TYPE_DOUBLE,
TokenKind::ObjectReservedWord => flags\TYPE_OBJECT,
TokenKind::RealReservedWord => flags\TYPE_DOUBLE,
TokenKind::StringReservedWord => flags\TYPE_STRING,
TokenKind::UnsetKeyword => flags\TYPE_NULL,
TokenKind::StaticKeyword => flags\TYPE_STATIC,
];
$kind = $n->castType->kind;
$ast_kind = $lookup[$kind] ?? null;
$ast_kind = self::CAST_EXPRESSION_TYPE_LOOKUP[$kind] ?? null;
if ($ast_kind === null) {
throw new AssertionError("missing $kind");
}
@@ -706,20 +733,28 @@ class TolerantASTConverter
PhpParser\Node\Expression\AnonymousFunctionCreationExpression $n,
int $start_line
): ast\Node {
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
if ($n->functionKeyword) {
$start_line = self::getStartLine($n->functionKeyword);
}
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnTypeList, static::getEndLine($n->returnTypeList) ?: $start_line);
if (($ast_return_type->children['name'] ?? null) === '') {
$ast_return_type = null;
}
if ($n->questionToken !== null && $ast_return_type !== null) {
$ast_return_type = new ast\Node(ast\AST_NULLABLE_TYPE, 0, ['type' => $ast_return_type], $start_line);
}
$use_variable_name_list = $n->anonymousFunctionUseClause->useVariableNameList ?? null;
if (!$use_variable_name_list instanceof PhpParser\Node\DelimitedList\UseVariableNameList) {
$use_variable_name_list = null;
}
return static::astDeclClosure(
$n->byRefToken !== null,
$n->staticModifier !== null,
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
static::phpParserParamsToAstParams($n->parameters, $start_line),
static::phpParserClosureUsesToAstClosureUses($n->anonymousFunctionUseClause->useVariableNameList ?? null, $start_line),
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable, PhanPossiblyUndeclaredProperty return_null_on_empty is false.
static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon->statements, self::getStartLine($n->compoundStatementOrSemicolon), false),
static::phpParserClosureUsesToAstClosureUses($use_variable_name_list, $start_line),
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable $return_null_on_empty is false
static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon->statements ?? [], self::getStartLine($n->compoundStatementOrSemicolon), false),
$ast_return_type,
$start_line,
static::getEndLine($n),
@@ -730,7 +765,10 @@ class TolerantASTConverter
PhpParser\Node\Expression\ArrowFunctionCreationExpression $n,
int $start_line
): ast\Node {
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
if ($n->functionKeyword) {
$start_line = self::getStartLine($n->functionKeyword);
}
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnTypeList, static::getEndLine($n->returnTypeList) ?: $start_line);
if (($ast_return_type->children['name'] ?? null) === '') {
$ast_return_type = null;
}
@@ -750,6 +788,7 @@ class TolerantASTConverter
$return_line
),
'returnType' => $ast_return_type,
'attributes' => static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
],
$start_line,
static::resolveDocCommentForClosure($n),
@@ -853,6 +892,7 @@ class TolerantASTConverter
$arg_list = static::phpParserArgListToAstArgList($n->argumentExpressionList, $start_line);
if ($callable_expression instanceof PhpParser\Node\Expression\MemberAccessExpression) { // $a->f()
return static::astNodeMethodCall(
$callable_expression->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_METHOD_CALL : ast\AST_METHOD_CALL,
static::phpParserNonValueNodeToAstNode($callable_expression->dereferencableExpression),
static::phpParserNodeToAstNode($callable_expression->memberName),
$arg_list,
@@ -931,12 +971,14 @@ class TolerantASTConverter
$class_node = static::astStmtClass(
flags\CLASS_ANONYMOUS,
null,
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
$base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null,
$n->classInterfaceClause,
static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], $start_line, false),
$start_line,
$end_line,
$n->getDocCommentText()
$n->getDocCommentText(),
null
);
} else {
$class_node = static::phpParserNonValueNodeToAstNode($class_type_designator);
@@ -1065,7 +1107,7 @@ class TolerantASTConverter
'@phan-var Token $part';
$imploded_parts = static::tokenToString($part);
if ($part->kind === TokenKind::Name) {
if (\preg_match('@^__(LINE|FILE|DIR|FUNCTION|CLASS|TRAIT|METHOD|NAMESPACE)__$@i', $imploded_parts) > 0) {
if (\preg_match('@^__(LINE|FILE|DIR|FUNCTION|CLASS|TRAIT|METHOD|NAMESPACE)__$@iD', $imploded_parts) > 0) {
return new ast\Node(
ast\AST_MAGIC_CONST,
self::MAGIC_CONST_LOOKUP[\strtoupper($imploded_parts)],
@@ -1087,14 +1129,23 @@ class TolerantASTConverter
return new ast\Node(ast\AST_NAME, $ast_kind, ['name' => $imploded_parts], $start_line);
},
'Microsoft\PhpParser\Node\Parameter' => static function (PhpParser\Node\Parameter $n, int $start_line): ast\Node {
$type_line = static::getEndLine($n->typeDeclaration) ?: $start_line;
$start_line_token = $n->visibilityToken ?:
$n->questionToken ?:
$n->typeDeclarationList ?:
$n->byRefToken ?:
$n->variableName;
if ($start_line_token) {
$start_line = static::getStartLine($start_line_token);
}
$type_declaration_list = $n->typeDeclarationList;
$type_line = $type_declaration_list ? static::getStartLine($type_declaration_list) : $start_line;
$default = $n->default;
$default_node = $default !== null ? static::phpParserNodeToAstNode($default) : null;
return static::astNodeParam(
return self::astNodeParam(
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
$n->questionToken !== null,
$n->byRefToken !== null,
$n->dotDotDotToken !== null,
static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line),
self::getParamFlags($n),
static::phpParserUnionTypeToAstNode($type_declaration_list, $type_line),
static::variableTokenToString($n->variableName),
$default_node,
$start_line
@@ -1110,7 +1161,7 @@ class TolerantASTConverter
if ($as_int !== false) {
return $as_int;
}
if (\preg_match('/^0[0-7]+$/', $text)) {
if (\preg_match('/^0[0-7]+$/D', $text)) {
// this is octal - FILTER_VALIDATE_FLOAT would treat it like decimal
return \intval($text, 8);
}
@@ -1190,68 +1241,99 @@ class TolerantASTConverter
return new ast\Node($kind, 0, ['depth' => $breakout_level], $start_line);
},
'Microsoft\PhpParser\Node\CatchClause' => static function (PhpParser\Node\CatchClause $n, int $start_line): ast\Node {
$qualified_name = $n->qualifiedName;
$catch_inner_list = [];
// Handle `catch()` syntax error
if ($qualified_name instanceof PhpParser\Node\QualifiedName) {
$catch_inner_list[] = static::phpParserNonValueNodeToAstNode($qualified_name);
}
foreach ($n->otherQualifiedNameList as $other_qualified_name) {
// @phan-suppress-next-line PhanUndeclaredProperty incorrect phpdoc in tolerant-php-parser 0.1.0
foreach ($n->qualifiedNameList->children ?? [] as $other_qualified_name) {
if ($other_qualified_name instanceof PhpParser\Node\QualifiedName) {
$catch_inner_list[] = static::phpParserNonValueNodeToAstNode($other_qualified_name);
}
}
$catch_list_node = new ast\Node(ast\AST_NAME_LIST, 0, $catch_inner_list, $catch_inner_list[0]->lineno ?? $start_line);
// TODO: Change to handle multiple exception types in catch clauses
// after https://github.com/Microsoft/tolerant-php-parser/issues/103 is supported
$variableName = $n->variableName;
return static::astStmtCatch(
$catch_list_node,
static::variableTokenToString($n->variableName),
$variableName !== null ? static::variableTokenToString($variableName) : null,
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable return_null_on_empty is false.
static::phpParserStmtlistToAstNode($n->compoundStatement, $start_line, false),
$start_line
);
},
'Microsoft\PhpParser\Node\Statement\InterfaceDeclaration' => static function (PhpParser\Node\Statement\InterfaceDeclaration $n, int $start_line): ast\Node {
if ($n->interfaceKeyword) {
$start_line = self::getStartLine($n->interfaceKeyword);
}
$end_line = static::getEndLine($n) ?: $start_line;
return static::astStmtClass(
flags\CLASS_INTERFACE,
static::tokenToString($n->name),
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
static::interfaceBaseClauseToNode($n->interfaceBaseClause),
null,
static::phpParserStmtlistToAstNode($n->interfaceMembers->interfaceMemberDeclarations ?? [], $start_line, false),
$start_line,
$end_line,
$n->getDocCommentText()
$n->getDocCommentText(),
null
);
},
/**
* @unused-param $start_line
*/
'Microsoft\PhpParser\Node\Statement\ClassDeclaration' => static function (PhpParser\Node\Statement\ClassDeclaration $n, int $start_line): ast\Node {
$end_line = static::getEndLine($n);
$base_class = $n->classBaseClause->baseClass ?? null;
return static::astStmtClass(
static::phpParserClassModifierToAstClassFlags($n->abstractOrFinalModifier),
static::tokenToString($n->name),
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
$base_class !== null ? static::phpParserNonValueNodeToAstNode($base_class) : null,
$n->classInterfaceClause,
static::phpParserStmtlistToAstNode($n->classMembers->classMemberDeclarations ?? [], self::getStartLine($n->classMembers), false),
$start_line,
static::getStartLine($n->classKeyword),
$end_line,
$n->getDocCommentText()
$n->getDocCommentText(),
null
);
},
'Microsoft\PhpParser\Node\Statement\TraitDeclaration' => static function (PhpParser\Node\Statement\TraitDeclaration $n, int $start_line): ast\Node {
if ($n->traitKeyword) {
$start_line = self::getStartLine($n->traitKeyword);
}
$end_line = static::getEndLine($n) ?: $start_line;
return static::astStmtClass(
flags\CLASS_TRAIT,
static::tokenToString($n->name),
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
null,
null,
static::phpParserStmtlistToAstNode($n->traitMembers->traitMemberDeclarations ?? [], self::getStartLine($n->traitMembers), false),
$start_line,
$end_line,
$n->getDocCommentText()
$n->getDocCommentText(),
null
);
},
/**
* @unused-param $start_line
*/
'Microsoft\PhpParser\Node\Statement\EnumDeclaration' => static function (PhpParser\Node\Statement\EnumDeclaration $n, int $start_line): ast\Node {
$end_line = static::getEndLine($n);
return static::astStmtClass(
flags\CLASS_ENUM | flags\CLASS_FINAL,
static::tokenToString($n->name),
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
null,
null,
static::phpParserStmtlistToAstNode($n->enumMembers->enumMemberDeclarations ?? [], self::getStartLine($n->enumMembers), false),
static::getStartLine($n->enumKeyword),
$end_line,
$n->getDocCommentText(),
self::phpParserTypeToAstNode($n->enumType, $start_line)
);
},
'Microsoft\PhpParser\Node\EnumCaseDeclaration' => static function (PhpParser\Node\EnumCaseDeclaration $n, int $start_line): ast\Node {
return static::phpParserEnumCaseDeclarationToAstNode($n, $start_line);
},
'Microsoft\PhpParser\Node\ClassConstDeclaration' => static function (PhpParser\Node\ClassConstDeclaration $n, int $start_line): ast\Node {
return static::phpParserClassConstToAstNode($n, $start_line);
},
@@ -1260,12 +1342,22 @@ class TolerantASTConverter
// This node type is generated for something that isn't a function/constant/property. e.g. "public example();"
return null;
},
/** @return null - A stub that will be removed by the caller. */
'Microsoft\PhpParser\Node\MissingDeclaration' => static function (PhpParser\Node\MissingDeclaration $unused_n, int $unused_start_line) {
// This node type is generated for something that starts with an attribute but isn't a declaration.
return null;
},
/**
* @throws InvalidNodeException
*/
'Microsoft\PhpParser\Node\MethodDeclaration' => static function (PhpParser\Node\MethodDeclaration $n, int $start_line): ast\Node {
$statements = $n->compoundStatementOrSemicolon;
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
if (isset($n->modifiers[0])) {
$start_line = self::getStartLine($n->modifiers[0]);
} elseif ($n->functionKeyword) {
$start_line = self::getStartLine($n->functionKeyword);
}
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnTypeList, static::getEndLine($n->returnTypeList) ?: $start_line);
if (($ast_return_type->children['name'] ?? null) === '') {
$ast_return_type = null;
}
@@ -1289,6 +1381,7 @@ class TolerantASTConverter
'params' => static::phpParserParamsToAstParams($n->parameters, $start_line),
'stmts' => static::phpParserStmtlistToAstNode($statements, self::getStartLine($statements), true),
'returnType' => $ast_return_type,
'attributes' => static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
],
$start_line,
$n->getDocCommentText(),
@@ -1302,12 +1395,8 @@ class TolerantASTConverter
},
'Microsoft\PhpParser\Node\Statement\DeclareStatement' => static function (PhpParser\Node\Statement\DeclareStatement $n, int $start_line): ast\Node {
$doc_comment = $n->getDocCommentText();
$directive = $n->declareDirective;
if (!($directive instanceof PhpParser\Node\DeclareDirective)) {
throw new AssertionError("Unexpected type for directive");
}
return static::astStmtDeclare(
static::phpParserDeclareListToAstDeclares($directive, $start_line, $doc_comment),
static::phpParserDeclareListToAstDeclares($n, $start_line, $doc_comment),
$n->statements !== null ? static::phpParserStmtlistToAstNode($n->statements, $start_line, true) : null,
$start_line
);
@@ -1326,7 +1415,7 @@ class TolerantASTConverter
/**
* @return ast\Node|ast\Node[]
*/
'Microsoft\PhpParser\Node\Expression\EchoExpression' => static function (PhpParser\Node\Expression\EchoExpression $n, int $start_line) {
'Microsoft\PhpParser\Node\Statement\EchoStatement' => static function (PhpParser\Node\Statement\EchoStatement $n, int $start_line) {
$ast_echos = [];
foreach ($n->expressions->children ?? [] as $expr) {
if ($expr instanceof Token && $expr->kind === TokenKind::CommaToken) {
@@ -1384,8 +1473,11 @@ class TolerantASTConverter
* @throws InvalidNodeException
*/
'Microsoft\PhpParser\Node\Statement\FunctionDeclaration' => static function (PhpParser\Node\Statement\FunctionDeclaration $n, int $start_line): ast\Node {
if ($n->functionKeyword) {
$start_line = self::getStartLine($n->functionKeyword);
}
$end_line = static::getEndLine($n) ?: $start_line;
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnType, $n->otherReturnTypes, static::getEndLine($n->returnType) ?: $start_line);
$ast_return_type = static::phpParserUnionTypeToAstNode($n->returnTypeList, static::getEndLine($n->returnTypeList) ?: $start_line);
if (($ast_return_type->children['name'] ?? null) === '') {
$ast_return_type = null;
}
@@ -1400,6 +1492,7 @@ class TolerantASTConverter
return static::astDeclFunction(
$n->byRefToken !== null,
static::tokenToString($name),
static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
static::phpParserParamsToAstParams($n->parameters, $start_line),
$ast_return_type,
static::phpParserStmtlistToAstNode($n->compoundStatementOrSemicolon, self::getStartLine($n->compoundStatementOrSemicolon), false),
@@ -1518,13 +1611,11 @@ class TolerantASTConverter
'Microsoft\PhpParser\Node\Statement\SwitchStatementNode' => static function (PhpParser\Node\Statement\SwitchStatementNode $n, int $_): ast\Node {
return static::phpParserSwitchListToAstSwitch($n);
},
'Microsoft\PhpParser\Node\Statement\ThrowStatement' => static function (PhpParser\Node\Statement\ThrowStatement $n, int $start_line): ast\Node {
return new ast\Node(
ast\AST_THROW,
0,
['expr' => static::phpParserNodeToAstNode($n->expression)],
$start_line
);
'Microsoft\PhpParser\Node\Expression\ThrowExpression' => static function (PhpParser\Node\Expression\ThrowExpression $n, int $start_line): ast\Node {
return static::phpParserThrowToASTThrow($n, $start_line);
},
'Microsoft\PhpParser\Node\Expression\MatchExpression' => static function (PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node {
return self::phpParserMatchToAstMatch($n, $start_line);
},
'Microsoft\PhpParser\Node\TraitUseClause' => static function (PhpParser\Node\TraitUseClause $n, int $start_line): ast\Node {
@@ -1573,7 +1664,7 @@ class TolerantASTConverter
return null;
}
$target_name_list = array_merge([$n->targetName], $n->remainingTargetNames ?? []);
$target_name_list = $n->targetNameList->children ?? $n->targetNameList;
if (\is_object($member_name_list)) {
$member_name_list = [$member_name_list];
}
@@ -1603,7 +1694,7 @@ class TolerantASTConverter
$method_node = static::phpParserNameToString($name);
}
$flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false);
$target_name = $n->targetName;
$target_name = $n->targetNameList;
$target_name = $target_name instanceof PhpParser\Node\QualifiedName ? static::phpParserNameToString($target_name) : null;
$children = [
'method' => new ast\Node(ast\AST_METHOD_REFERENCE, 0, [
@@ -1627,7 +1718,7 @@ class TolerantASTConverter
);
},
/** @return ast\Node|ast\Node[] */
'Microsoft\PhpParser\Node\Expression\UnsetIntrinsicExpression' => static function (PhpParser\Node\Expression\UnsetIntrinsicExpression $n, int $start_line) {
'Microsoft\PhpParser\Node\Statement\UnsetStatement' => static function (PhpParser\Node\Statement\UnsetStatement $n, int $start_line) {
$stmts = [];
foreach ($n->expressions->children ?? [] as $var) {
if ($var instanceof Token) {
@@ -1649,11 +1740,8 @@ class TolerantASTConverter
'Microsoft\PhpParser\Node\Statement\GotoStatement' => static function (PhpParser\Node\Statement\GotoStatement $n, int $start_line): ast\Node {
return new ast\Node(ast\AST_GOTO, 0, ['label' => static::tokenToString($n->name)], $start_line);
},
/** @return ast\Node[] */
'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => static function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line): array {
$label = new ast\Node(ast\AST_LABEL, 0, ['name' => static::tokenToString($n->name)], $start_line);
$statement = static::phpParserNodeToAstNode($n->statement);
return [$label, $statement];
'Microsoft\PhpParser\Node\Statement\NamedLabelStatement' => static function (PhpParser\Node\Statement\NamedLabelStatement $n, int $start_line): ast\Node {
return new ast\Node(ast\AST_LABEL, 0, ['name' => static::tokenToString($n->name)], $start_line);
},
];
@@ -1712,14 +1800,15 @@ class TolerantASTConverter
return new ast\Node(ast\AST_TRY, 0, $children, $start_line);
}
private static function astStmtCatch(ast\Node $types, string $var, \ast\Node $stmts, int $lineno): ast\Node
private static function astStmtCatch(ast\Node $types, ?string $var, \ast\Node $stmts, int $lineno): ast\Node
{
return new ast\Node(
ast\AST_CATCH,
0,
[
'class' => $types,
'var' => new ast\Node(ast\AST_VAR, 0, ['name' => $var], $lineno),
// php 8.0 allows catch statements without variables
'var' => is_string($var) ? new ast\Node(ast\AST_VAR, 0, ['name' => $var], $lineno) : null,
'stmts' => $stmts,
],
$lineno
@@ -1806,21 +1895,22 @@ class TolerantASTConverter
}
/**
* @param PhpParser\Node\QualifiedName|Token|null $type
* @param ?(PhpParser\Node\DelimitedList\QualifiedNameList|MissingToken) $types_node
*/
protected static function phpParserUnionTypeToAstNode($type, ?PhpParser\Node\DelimitedList\QualifiedNameList $other_types, int $line): ?\ast\Node
protected static function phpParserUnionTypeToAstNode(?object $types_node, int $line): ?\ast\Node
{
$types = [];
if (!\is_null($type)) {
$result = static::phpParserTypeToAstNode($type, $line);
if ($result) {
$types[] = $result;
}
}
if ($other_types instanceof PhpParser\Node\DelimitedList\QualifiedNameList) {
foreach ($other_types->children as $child) {
if ($child instanceof Token && $child->kind === TokenKind::BarToken) {
continue;
$is_intersection = false;
if ($types_node instanceof PhpParser\Node\DelimitedList\QualifiedNameList) {
foreach ($types_node->children as $child) {
if ($child instanceof Token) {
if ($child->kind === TokenKind::BarToken) {
continue;
}
if ($child->kind === TokenKind::AmpersandToken) {
$is_intersection = true;
continue;
}
}
$result = static::phpParserTypeToAstNode($child, static::getEndLine($child) ?: $line);
if ($result) {
@@ -1834,7 +1924,7 @@ class TolerantASTConverter
} elseif ($n === 1) {
return $types[0];
}
return new ast\Node(ast\AST_TYPE_UNION, 0, $types, $types[0]->lineno);
return new ast\Node($is_intersection ? ast\AST_TYPE_INTERSECTION : ast\AST_TYPE_UNION, 0, $types, $types[0]->lineno);
}
/**
@@ -1889,6 +1979,9 @@ class TolerantASTConverter
case 'static':
$flags = flags\TYPE_STATIC;
break;
case 'never':
$flags = flags\TYPE_NEVER;
break;
default:
// TODO: Refactor this into a function accepting a QualifiedName
if ($original_type instanceof PhpParser\Node\QualifiedName) {
@@ -1915,12 +2008,11 @@ class TolerantASTConverter
}
/**
* @param bool $by_ref
* @param ?ast\Node $type
* @param string $name
* @param ?ast\Node|?int|?string|?float $default
*/
private static function astNodeParam(bool $is_nullable, bool $by_ref, bool $variadic, ?\ast\Node $type, string $name, $default, int $line): ast\Node
private static function astNodeParam(?ast\Node $attributes, bool $is_nullable, int $flags, ?\ast\Node $type, string $name, $default, int $line): ast\Node
{
if ($is_nullable) {
$type = new ast\Node(
@@ -1932,17 +2024,40 @@ class TolerantASTConverter
}
return new ast\Node(
ast\AST_PARAM,
($by_ref ? flags\PARAM_REF : 0) | ($variadic ? flags\PARAM_VARIADIC : 0),
$flags,
[
'type' => $type,
'name' => $name,
'default' => $default,
'attributes' => $attributes,
'docComment' => null,
],
$line
);
}
private static function phpParserParamsToAstParams(?\Microsoft\PhpParser\node\delimitedlist\parameterdeclarationlist $parser_params, int $line): ast\Node
private const VISIBILITY_FLAG_MAP = [
TokenKind::PublicKeyword => ast\flags\MODIFIER_PUBLIC,
TokenKind::ProtectedKeyword => ast\flags\MODIFIER_PROTECTED,
TokenKind::PrivateKeyword => ast\flags\MODIFIER_PRIVATE,
TokenKind::ReadonlyKeyword => ast\flags\MODIFIER_READONLY,
];
private static function getParamFlags(PhpParser\Node\Parameter $n): int
{
$flags = ($n->byRefToken ? flags\PARAM_REF : 0) | ($n->dotDotDotToken ? flags\PARAM_VARIADIC : 0);
if ($visibilityToken = $n->visibilityToken) {
$flags |= (self::VISIBILITY_FLAG_MAP[$visibilityToken->kind] ?? 0);
}
foreach ($n->modifiers ?? [] as $visibilityToken) {
if ($visibilityToken instanceof Token) {
$flags |= (self::VISIBILITY_FLAG_MAP[$visibilityToken->kind] ?? 0);
}
}
return $flags;
}
private static function phpParserParamsToAstParams(?\Microsoft\PhpParser\Node\DelimitedList\ParameterDeclarationList $parser_params, int $line): ast\Node
{
$new_params = [];
foreach ($parser_params->children ?? [] as $parser_node) {
@@ -1951,19 +2066,25 @@ class TolerantASTConverter
}
$new_params[] = static::phpParserNodeToAstNode($parser_node);
}
return new ast\Node(
$result = new ast\Node(
ast\AST_PARAM_LIST,
0,
$new_params,
$new_params[0]->lineno ?? $line
);
if (($parser_node->kind ?? null) === TokenKind::CommaToken) {
// @phan-suppress-next-line PhanUndeclaredProperty
$result->polyfill_has_trailing_comma = true;
}
return $result;
}
/**
* @param PhpParser\Node|PhpParser\Token $parser_node
* @suppress UnusedSuppression, TypeMismatchProperty
* @internal
*/
protected static function astStub($parser_node): ast\Node
final public static function astStub(object $parser_node): ast\Node
{
// Debugging code.
if (\getenv(self::ENV_AST_THROW_INVALID)) {
@@ -1995,9 +2116,14 @@ class TolerantASTConverter
if (!($use instanceof PhpParser\Node\UseVariableName)) {
throw new AssertionError("Expected UseVariableName");
}
$ast_uses[] = new ast\Node(ast\AST_CLOSURE_VAR, $use->byRef ? 1 : 0, ['name' => static::tokenToString($use->variableName)], self::getStartLine($use));
$ast_uses[] = new ast\Node(ast\AST_CLOSURE_VAR, $use->byRef ? ast\flags\CLOSURE_USE_REF : 0, ['name' => static::tokenToString($use->variableName)], self::getStartLine($use));
}
return new ast\Node(ast\AST_CLOSURE_USES, 0, $ast_uses, $ast_uses[0]->lineno ?? $line);
$result = new ast\Node(ast\AST_CLOSURE_USES, 0, $ast_uses, $ast_uses[0]->lineno ?? $line);
if (($use->kind ?? null) === TokenKind::CommaToken) {
// @phan-suppress-next-line PhanUndeclaredProperty
$result->polyfill_has_trailing_comma = true;
}
return $result;
}
private static function resolveDocCommentForClosure(PhpParser\Node\Expression $node): ?string
@@ -2081,6 +2207,7 @@ class TolerantASTConverter
private static function astDeclClosure(
bool $by_ref,
bool $static,
?ast\Node $attributes,
ast\Node $params,
?\ast\Node $uses,
ast\Node $stmts,
@@ -2097,6 +2224,7 @@ class TolerantASTConverter
'uses' => $uses,
'stmts' => $stmts,
'returnType' => $return_type,
'attributes' => $attributes, // TODO implement
],
$start_line,
$doc_comment,
@@ -2114,6 +2242,7 @@ class TolerantASTConverter
private static function astDeclFunction(
bool $by_ref,
string $name,
?\ast\Node $attributes,
ast\Node $params,
?\ast\Node $return_type,
?\ast\Node $stmts,
@@ -2128,6 +2257,7 @@ class TolerantASTConverter
'params' => $params,
'stmts' => $stmts,
'returnType' => $return_type,
'attributes' => $attributes,
],
$line,
$doc_comment,
@@ -2183,12 +2313,14 @@ class TolerantASTConverter
private static function astStmtClass(
int $flags,
?string $name,
?\ast\Node $extends,
?\Microsoft\PhpParser\node\classinterfaceclause $implements,
?\ast\Node $stmts,
?ast\Node $attributes,
?ast\Node $extends,
?PhpParser\Node\ClassInterfaceClause $implements,
?ast\Node $stmts,
int $line,
int $end_line,
?string $doc_comment
?string $doc_comment,
?ast\Node $type
): ast\Node {
// NOTE: `null` would be an anonymous class.
@@ -2203,6 +2335,8 @@ class TolerantASTConverter
'extends' => null,
'implements' => $extends,
'stmts' => $stmts,
'attributes' => $attributes,
'type' => null,
];
} else {
if ($implements !== null) {
@@ -2230,6 +2364,8 @@ class TolerantASTConverter
'extends' => $extends,
'implements' => $ast_implements,
'stmts' => $stmts,
'attributes' => $attributes,
'type' => $type,
];
}
@@ -2245,7 +2381,7 @@ class TolerantASTConverter
);
}
private static function phpParserArgListToAstArgList(?\Microsoft\PhpParser\node\delimitedlist\argumentexpressionlist $args, int $line): ast\Node
private static function phpParserArgListToAstArgList(?\Microsoft\PhpParser\Node\DelimitedList\ArgumentExpressionList $args, int $line): ast\Node
{
$ast_args = [];
foreach ($args->children ?? [] as $arg) {
@@ -2254,7 +2390,96 @@ class TolerantASTConverter
}
$ast_args[] = static::phpParserNodeToAstNode($arg);
}
return new ast\Node(ast\AST_ARG_LIST, 0, $ast_args, $args ? self::getStartLine($args) : $line);
$result = new ast\Node(ast\AST_ARG_LIST, 0, $ast_args, $args ? self::getStartLine($args) : $line);
if (($arg->kind ?? null) === TokenKind::CommaToken) {
// NOTE: This is deliberately using a dynamic property instead of a flag because other applications may use flags
// @phan-suppress-next-line PhanUndeclaredProperty
$result->polyfill_has_trailing_comma = true;
}
return $result;
}
private static function phpParserThrowToASTThrow(PhpParser\Node\Expression\ThrowExpression $n, int $start_line): ast\Node
{
$expression = $n->expression;
if (!$expression) {
throw new InvalidNodeException();
}
return new ast\Node(
ast\AST_THROW,
0,
['expr' => static::phpParserNodeToAstNode($expression)],
$start_line
);
}
protected static function phpParserMatchToAstMatch(PhpParser\Node\Expression\MatchExpression $n, int $start_line): ast\Node
{
$expression = $n->expression;
if (!$expression) {
throw new InvalidNodeException();
}
return new ast\Node(
ast\AST_MATCH,
0,
[
'cond' => static::phpParserNodeToAstNode($expression),
'stmts' => static::phpParserMatchArmListToAstMatchArmList($n->arms, $start_line),
],
$start_line
);
}
protected static function phpParserMatchArmListToAstMatchArmList(?\Microsoft\PhpParser\Node\DelimitedList\MatchExpressionArmList $arms, int $start_line): ast\Node
{
$ast_arms = [];
foreach ($arms->children ?? [] as $arm) {
if (!$arm instanceof PhpParser\Node\MatchArm) {
continue;
}
try {
$ast_arms[] = static::phpParserMatchArmToAstMatchArm($arm);
} catch (InvalidNodeException $_) {
continue;
}
}
return new ast\Node(ast\AST_MATCH_ARM_LIST, 0, $ast_arms, $arms ? self::getStartLine($arms) : $start_line);
}
private static function phpParserMatchConditionListToAstNode(?PhpParser\Node\DelimitedList\MatchArmConditionList $condition_list): ?ast\Node
{
if (!$condition_list) {
throw new InvalidNodeException();
}
$conditions = [];
foreach ($condition_list->children ?? [] as $phpparser_condition) {
if ($phpparser_condition instanceof Token) {
switch ($phpparser_condition->kind) {
case TokenKind::DefaultKeyword:
return null;
case TokenKind::CommaToken:
continue 2;
}
}
$conditions[] = static::phpParserNodeToAstNode($phpparser_condition);
}
if (!$conditions) {
throw new InvalidNodeException();
}
return new ast\Node(ast\AST_EXPR_LIST, 0, $conditions, self::getStartLine($condition_list));
}
private static function phpParserMatchArmToAstMatchArm(PhpParser\Node\MatchArm $arm): ast\Node
{
return new ast\Node(
ast\AST_MATCH_ARM,
0,
[
'cond' => static::phpParserMatchConditionListToAstNode($arm->conditionList),
'expr' => static::phpParserNodeToAstNode($arm->body),
],
self::getStartLine($arm)
);
}
/**
@@ -2375,7 +2600,7 @@ class TolerantASTConverter
if (!($case instanceof PhpParser\Node\CaseStatementNode)) {
continue;
}
$case_line = static::getEndLine($case);
$case_line = static::getStartLine($case);
$stmts[] = new ast\Node(
ast\AST_SWITCH_CASE,
0,
@@ -2588,6 +2813,9 @@ class TolerantASTConverter
case TokenKind::FinalKeyword:
$ast_visibility |= flags\MODIFIER_FINAL;
break;
case TokenKind::ReadonlyKeyword:
$ast_visibility |= flags\MODIFIER_READONLY;
break;
default:
throw new \RuntimeException("Unexpected visibility modifier '" . Token::getTokenKindNameFromValue($token->kind) . "'");
}
@@ -2607,17 +2835,18 @@ class TolerantASTConverter
if ($prop instanceof Token) {
continue;
}
// @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node
// @phan-suppress-next-line PhanTypeMismatchArgumentSuperType casting to a more specific node
$prop_elems[] = static::phpParserPropelemToAstPropelem($prop, $i === 0 ? $doc_comment : null);
}
$flags = static::phpParserVisibilityToAstVisibility($n->modifiers, false);
$line = $prop_elems[0]->lineno ?? (self::getStartLine($n) ?: $start_line);
$prop_decl = new ast\Node(ast\AST_PROP_DECL, 0, $prop_elems, $line);
$type_line = static::getEndLine($n->typeDeclaration) ?: $start_line;
$type_line = static::getEndLine($n->typeDeclarationList) ?: $start_line;
return new ast\Node(ast\AST_PROP_GROUP, $flags, [
'type' => static::phpParserUnionTypeToAstNode($n->typeDeclaration, $n->otherTypeDeclarations, $type_line),
'type' => static::phpParserUnionTypeToAstNode($n->typeDeclarationList, $type_line),
'props' => $prop_decl,
'attributes' => static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
], $line);
}
@@ -2629,12 +2858,37 @@ class TolerantASTConverter
if ($const_elem instanceof Token) {
continue;
}
// @phan-suppress-next-line PhanTypeMismatchArgument casting to a more specific node
// @phan-suppress-next-line PhanTypeMismatchArgumentSuperType casting to a more specific node
$const_elems[] = static::phpParserConstelemToAstConstelem($const_elem, $i === 0 ? $doc_comment : null);
}
$flags = static::phpParserVisibilityToAstVisibility($n->modifiers);
$const_start_line = $const_elems[0]->lineno ?? $start_line;
$const_list_node = new ast\Node(ast\AST_CLASS_CONST_DECL, 0, $const_elems, $const_start_line);
return new ast\Node(
ast\AST_CLASS_CONST_GROUP,
$flags,
[
'const' => $const_list_node,
'attributes' => static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
],
$const_start_line
);
}
return new ast\Node(ast\AST_CLASS_CONST_DECL, $flags, $const_elems, $const_elems[0]->lineno ?? $start_line);
/**
* @suppress PhanTypeMismatchArgument
*/
private static function phpParserEnumCaseDeclarationToAstNode(PhpParser\Node\EnumCaseDeclaration $n, int $start_line): ast\Node
{
$assignment = $n->assignment;
$children = [
'name' => static::variableTokenToString($n->name),
'expr' => $assignment !== null ? static::phpParserNodeToAstNode($assignment) : null,
'docComment' => static::extractPhpdocComment($n),
'attributes' => static::phpParserAttributeGroupsToAstAttributeList($n->attributes),
];
return new ast\Node(ast\AST_ENUM_CASE, 0, $children, $start_line);
}
/**
@@ -2657,24 +2911,32 @@ class TolerantASTConverter
return new ast\Node(ast\AST_CONST_DECL, 0, $const_elems, $const_elems[0]->lineno ?? $start_line);
}
private static function phpParserDeclareListToAstDeclares(PhpParser\Node\DeclareDirective $declare, int $start_line, ?string $first_doc_comment): ast\Node
private static function phpParserDeclareListToAstDeclares(PhpParser\Node\Statement\DeclareStatement $declareStatement, int $start_line, ?string $first_doc_comment): ast\Node
{
$ast_declare_elements = [];
if ($declare->name->length > 0 && $declare->literal->length > 0) {
// Skip SkippedToken or MissingToken
$children = [
'name' => static::tokenToString($declare->name),
'value' => static::tokenToScalar($declare->literal),
];
$doc_comment = static::extractPhpdocComment($declare) ?? $first_doc_comment;
// $first_doc_comment = null;
$children['docComment'] = $doc_comment;
$node = new ast\Node(ast\AST_CONST_ELEM, 0, $children, self::getStartLine($declare));
$ast_declare_elements[] = $node;
foreach ($declareStatement->declareDirectiveList->children ?? [] as $other_declare) {
if ($other_declare instanceof PhpParser\Node\DeclareDirective) {
$ast_declare_elements[] = self::phpParserDeclareDirectiveToAstNode($other_declare, $first_doc_comment);
}
}
if (!$ast_declare_elements) {
throw new InvalidNodeException();
}
return new ast\Node(ast\AST_CONST_DECL, 0, $ast_declare_elements, $start_line);
}
private static function phpParserDeclareDirectiveToAstNode(PhpParser\Node\DeclareDirective $declare, ?string $first_doc_comment): ast\Node
{
$children = [
'name' => static::tokenToString($declare->name),
'value' => static::tokenToScalar($declare->literal),
];
$doc_comment = static::extractPhpdocComment($declare) ?? $first_doc_comment;
// $first_doc_comment = null;
$children['docComment'] = $doc_comment;
return new ast\Node(ast\AST_CONST_ELEM, 0, $children, self::getStartLine($declare));
}
private static function astStmtDeclare(ast\Node $declares, ?\ast\Node $stmts, int $start_line): ast\Node
{
$children = [
@@ -2703,9 +2965,9 @@ class TolerantASTConverter
* @param ast\Node|string $expr (can parse non-nodes, but they'd cause runtime errors)
* @param ast\Node|string $method
*/
private static function astNodeMethodCall($expr, $method, ast\Node $args, int $start_line): ast\Node
private static function astNodeMethodCall(int $kind, $expr, $method, ast\Node $args, int $start_line): ast\Node
{
return new ast\Node(ast\AST_METHOD_CALL, 0, ['expr' => $expr, 'method' => $method, 'args' => $args], $start_line);
return new ast\Node($kind, 0, ['expr' => $expr, 'method' => $method, 'args' => $args], $start_line);
}
/**
@@ -2807,7 +3069,7 @@ class TolerantASTConverter
], self::getStartLine($item));
continue;
}
$flags = $item->byRef ? flags\PARAM_REF : 0;
$flags = $item->byRef ? flags\ARRAY_ELEM_REF : 0;
$element_key = $item->elementKey;
$ast_items[] = new ast\Node(ast\AST_ARRAY_ELEM, $flags, [
'value' => static::phpParserNodeToAstNode($item->elementValue),
@@ -2846,10 +3108,15 @@ class TolerantASTConverter
throw $e;
}
}
return new ast\Node(ast\AST_PROP, 0, [
'expr' => static::phpParserNodeToAstNode($n->dereferencableExpression),
'prop' => $name, // ast\Node|string
], $start_line);
return new ast\Node(
$n->arrowToken->kind === TokenKind::QuestionArrowToken ? ast\AST_NULLSAFE_PROP : ast\AST_PROP,
0,
[
'expr' => static::phpParserNodeToAstNode($n->dereferencableExpression),
'prop' => $name, // ast\Node|string
],
$start_line
);
}
/**
@@ -2875,7 +3142,7 @@ class TolerantASTConverter
*/
private static function parseQuotedString(PhpParser\Node\StringLiteral $n): string
{
$start = $n->getStart();
$start = $n->getStartPosition();
$text = (string)substr(self::$file_contents, $start, $n->getEndPosition() - $start);
return StringUtil::parse($text);
}
@@ -3152,4 +3419,3 @@ class TolerantASTConverter
return $outer;
}
}
class_exists(TolerantASTConverterWithNodeMapping::class);
@@ -18,6 +18,8 @@ use Microsoft\PhpParser\Token;
*/
class TolerantASTConverterPreservingOriginal extends TolerantASTConverter
{
use TolerantASTConverterTrait;
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Phan\AST\TolerantASTConverter;
use ast;
use Closure;
use Error;
use Exception;
use InvalidArgumentException;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Token;
/**
* This is a trait to be used multiple times to account for https://wiki.php.net/rfc/static_variable_inheritance changing behavior in php 8.1
*/
trait TolerantASTConverterTrait
{
/**
* @return array<string,Closure(object,int):(\ast\Node|int|string|float|null)>
*/
abstract protected static function initHandleMap(): array;
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|bool|null - whatever ast\parse_code would return as the equivalent.
* This does not convert names to ast\AST_CONST.
* @throws InvalidArgumentException if Phan doesn't know what $n is
*
* FIXME static will behave differently in php 8.1
* @suppress PhanAbstractStaticMethodCallInTrait
*/
protected static function phpParserNonValueNodeToAstNode($n)
{
static $callback_map;
static $fallback_closure;
if (\is_null($callback_map)) {
$callback_map = static::initHandleMap();
/**
* @param PhpParser\Node|Token $n
* @return ast\Node - Not a real node, but a node indicating the TODO
* @throws InvalidArgumentException for invalid node classes
* @throws Error if the environment variable AST_THROW_INVALID is set (for debugging)
*/
$fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
// @phan-suppress-next-line PhanThrowTypeMismatchForCall debugDumpNodeOrToken can throw
throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . TolerantASTConverter::debugDumpNodeOrToken($n));
}
return TolerantASTConverter::astStub($n);
};
}
$callback = $callback_map[\get_class($n)] ?? $fallback_closure;
// @phan-suppress-next-line PhanThrowTypeMismatch
return $callback($n, TolerantASTConverter::getStartLine($n));
}
/**
* @param PhpParser\Node|Token $n - The node from PHP-Parser
* @return ast\Node|ast\Node[]|string|int|float|null - whatever ast\parse_code would return as the equivalent.
* @suppress PhanAbstractStaticMethodCallInTrait
*/
protected static function phpParserNodeToAstNode($n)
{
static $callback_map;
static $fallback_closure;
if (\is_null($callback_map)) {
$callback_map = static::initHandleMap();
/**
* @param PhpParser\Node|Token $n
* @return ast\Node - Not a real node, but a node indicating the TODO
* @throws InvalidArgumentException|Exception for invalid node classes
* @throws Error if the environment variable AST_THROW_INVALID is set to debug.
*/
$fallback_closure = static function ($n, int $unused_start_line): \ast\Node {
if (!($n instanceof PhpParser\Node) && !($n instanceof Token)) {
throw new \InvalidArgumentException("Invalid type for node: " . (\is_object($n) ? \get_class($n) : \gettype($n)) . ": " . TolerantASTConverter::debugDumpNodeOrToken($n));
}
return TolerantASTConverter::astStub($n);
};
}
$callback = $callback_map[\get_class($n)] ?? $fallback_closure;
// @phan-suppress-next-line PhanThrowTypeAbsent
$result = $callback($n, TolerantASTConverter::$file_position_map->getStartLine($n));
if (($result instanceof ast\Node) && $result->kind === ast\AST_NAME) {
return new ast\Node(ast\AST_CONST, 0, ['name' => $result], $result->lineno);
}
return $result;
}
}
@@ -43,8 +43,10 @@ use function preg_match;
* The logging to STDERR can be uncommented if you have issues debugging why
* Phan can't locate a given node's definition.
*/
class TolerantASTConverterWithNodeMapping extends TolerantASTConverter
final class TolerantASTConverterWithNodeMapping extends TolerantASTConverter
{
use TolerantASTConverterTrait;
/**
* @var PhpParser\Node|Token|null
* This is the closest node or token from tolerant-php-parser
@@ -99,10 +101,11 @@ class TolerantASTConverterWithNodeMapping extends TolerantASTConverter
/**
* @param Diagnostic[] &$errors @phan-output-reference
* @unused-param $cache
* @throws InvalidArgumentException for invalid $version
* @throws Throwable (after logging) if anything is thrown by the parser
*/
public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $unused_cache = null): \ast\Node
public function parseCodeAsPHPAST(string $file_contents, int $version, array &$errors = [], Cache $cache = null): \ast\Node
{
// Force the byte offset to be within the
$byte_offset = \max(0, \min(\strlen($file_contents), $this->instance_desired_byte_offset));
@@ -130,9 +133,11 @@ class TolerantASTConverterWithNodeMapping extends TolerantASTConverter
}
/**
* @unused-param $file_contents
* @unused-param $version
* @return ?string - null if this should not be cached
*/
public function generateCacheKey(string $unused_file_contents, int $unused_version): ?string
public function generateCacheKey(string $file_contents, int $version): ?string
{
return null;
}
@@ -12,13 +12,12 @@ declare(strict_types=1);
* With modifications to be a functional replacement for the data
* structures and global constants of ext-ast. (for class ast\Node)
*
* This supports AST version 70
* This supports AST version 85
*
* However, this file does not define any global functions such as
* ast\parse_code() and ast\parse_file(). (to avoid confusion)
*
* TODO: Make it so that constant values will be identical to php-ast
* for PHP 7.0-7.3
* TODO: Add remaining constants
*
* @phan-file-suppress PhanUnreferencedConstant, UnusedPluginFileSuppression - Plugins may reference some of these constants
* @phan-file-suppress PhanPluginUnknownArrayPropertyType, PhanPluginUnknownArrayMethodParamType this is a stub
@@ -30,6 +29,7 @@ declare(strict_types=1);
namespace ast;
const AST_ARG_LIST = 128;
const AST_LIST = 255;
const AST_ARRAY = 129;
const AST_ENCAPS_LIST = 130;
const AST_EXPR_LIST = 131;
@@ -46,14 +46,19 @@ const AST_NAME_LIST = 141;
const AST_TRAIT_ADAPTATIONS = 142;
const AST_USE = 143;
const AST_TYPE_UNION = 144;
const AST_ATTRIBUTE_LIST = 145;
const AST_ATTRIBUTE_GROUP = 146;
const AST_MATCH_ARM_LIST = 147;
const AST_TYPE_INTERSECTION = 148;
const AST_CALLABLE_CONVERT = 149;
const AST_NAME = 2048;
const AST_CLOSURE_VAR = 2049;
const AST_NULLABLE_TYPE = 2050;
const AST_FUNC_DECL = 66;
const AST_CLOSURE = 67;
const AST_METHOD = 68;
const AST_CLASS = 69;
const AST_FUNC_DECL = 67;
const AST_CLOSURE = 68;
const AST_METHOD = 69;
const AST_ARROW_FUNC = 71;
const AST_CLASS = 70;
const AST_MAGIC_CONST = 0;
const AST_TYPE = 1;
const AST_VAR = 256;
@@ -73,56 +78,64 @@ const AST_PRE_DEC = 272;
const AST_POST_INC = 273;
const AST_POST_DEC = 274;
const AST_YIELD_FROM = 275;
const AST_GLOBAL = 276;
const AST_UNSET = 277;
const AST_RETURN = 278;
const AST_LABEL = 279;
const AST_REF = 280;
const AST_HALT_COMPILER = 281;
const AST_ECHO = 282;
const AST_THROW = 283;
const AST_GOTO = 284;
const AST_BREAK = 285;
const AST_CONTINUE = 286;
const AST_CLASS_NAME = 287;
const AST_GLOBAL = 277;
const AST_UNSET = 278;
const AST_RETURN = 279;
const AST_LABEL = 280;
const AST_REF = 281;
const AST_HALT_COMPILER = 282;
const AST_ECHO = 283;
const AST_THROW = 284;
const AST_GOTO = 285;
const AST_BREAK = 286;
const AST_CONTINUE = 287;
const AST_CLASS_NAME = 276;
const AST_CLASS_CONST_GROUP = 546;
const AST_DIM = 512;
const AST_PROP = 513;
const AST_STATIC_PROP = 514;
const AST_CALL = 515;
const AST_CLASS_CONST = 516;
const AST_ASSIGN = 517;
const AST_ASSIGN_REF = 518;
const AST_ASSIGN_OP = 519;
const AST_BINARY_OP = 520;
const AST_ARRAY_ELEM = 525;
const AST_NEW = 526;
const AST_INSTANCEOF = 527;
const AST_YIELD = 528;
const AST_STATIC = 530;
const AST_WHILE = 531;
const AST_DO_WHILE = 532;
const AST_IF_ELEM = 533;
const AST_SWITCH = 534;
const AST_SWITCH_CASE = 535;
const AST_DECLARE = 536;
const AST_PROP_ELEM = 774;
const AST_CONST_ELEM = 775;
const AST_USE_TRAIT = 537;
const AST_TRAIT_PRECEDENCE = 538;
const AST_METHOD_REFERENCE = 539;
const AST_NAMESPACE = 540;
const AST_USE_ELEM = 541;
const AST_TRAIT_ALIAS = 542;
const AST_GROUP_USE = 543;
const AST_PROP_GROUP = 545;
const AST_NULLSAFE_PROP = 514;
const AST_STATIC_PROP = 515;
const AST_CALL = 516;
const AST_CLASS_CONST = 517;
const AST_ASSIGN = 518;
const AST_ASSIGN_REF = 519;
const AST_ASSIGN_OP = 520;
const AST_BINARY_OP = 521;
const AST_ARRAY_ELEM = 526;
const AST_NEW = 527;
const AST_INSTANCEOF = 528;
const AST_YIELD = 529;
const AST_STATIC = 532;
const AST_WHILE = 533;
const AST_DO_WHILE = 534;
const AST_IF_ELEM = 535;
const AST_SWITCH = 536;
const AST_SWITCH_CASE = 537;
const AST_DECLARE = 538;
const AST_PROP_ELEM = 775;
const AST_PROP_GROUP = 774;
const AST_CONST_ELEM = 776;
const AST_USE_TRAIT = 539;
const AST_TRAIT_PRECEDENCE = 540;
const AST_METHOD_REFERENCE = 541;
const AST_NAMESPACE = 542;
const AST_USE_ELEM = 543;
const AST_TRAIT_ALIAS = 544;
const AST_GROUP_USE = 545;
const AST_ATTRIBUTE = 547;
const AST_MATCH = 548;
const AST_MATCH_ARM = 549;
const AST_NAMED_ARG = 550;
const AST_METHOD_CALL = 768;
const AST_STATIC_CALL = 769;
const AST_CONDITIONAL = 770;
const AST_TRY = 771;
const AST_CATCH = 772;
const AST_PARAM = 773;
const AST_NULLSAFE_METHOD_CALL = 769;
const AST_STATIC_CALL = 770;
const AST_CONDITIONAL = 771;
const AST_TRY = 772;
const AST_CATCH = 773;
const AST_FOR = 1024;
const AST_FOREACH = 1025;
const AST_PARAM = 1280;
const AST_ENUM_CASE = 1279;
// END AST KIND CONSTANTS
// AST FLAG CONSTANTS
@@ -131,44 +144,50 @@ namespace ast\flags;
const NAME_FQ = 0;
const NAME_NOT_FQ = 1;
const NAME_RELATIVE = 2;
const MODIFIER_PUBLIC = 256;
const MODIFIER_PROTECTED = 512;
const MODIFIER_PRIVATE = 1024;
const MODIFIER_STATIC = 1;
const MODIFIER_ABSTRACT = 2;
const MODIFIER_FINAL = 4;
const RETURNS_REF = 67108864;
const FUNC_RETURNS_REF = 67108864;
const FUNC_GENERATOR = 4194304; // NOTE: Not set in all PHP versions.
const MODIFIER_PUBLIC = 1;
const MODIFIER_PROTECTED = 2;
const MODIFIER_PRIVATE = 4;
const MODIFIER_STATIC = 16;
const MODIFIER_ABSTRACT = 64;
const MODIFIER_FINAL = 32;
const PARAM_MODIFIER_PUBLIC = 1;
const PARAM_MODIFIER_PROTECTED = 2;
const PARAM_MODIFIER_PRIVATE = 4;
const RETURNS_REF = 4096;
const FUNC_RETURNS_REF = 4096;
const FUNC_GENERATOR = 16777216;
const ARRAY_ELEM_REF = 1;
const CLOSURE_USE_REF = 1;
const CLASS_ABSTRACT = 32;
const CLASS_FINAL = 4;
const CLASS_TRAIT = 128;
const CLASS_INTERFACE = 64;
const CLASS_ANONYMOUS = 256;
const PARAM_REF = 1;
const PARAM_VARIADIC = 2;
const CLASS_ABSTRACT = 64;
const CLASS_FINAL = 32;
const CLASS_TRAIT = 2;
const CLASS_INTERFACE = 1;
const CLASS_ANONYMOUS = 4;
const CLASS_ENUM = 268435456;
const PARAM_REF = 8;
const PARAM_VARIADIC = 16;
const TYPE_NULL = 1;
const TYPE_FALSE = 2;
const TYPE_BOOL = 13;
const TYPE_BOOL = 17;
const TYPE_LONG = 4;
const TYPE_DOUBLE = 5;
const TYPE_STRING = 6;
const TYPE_ARRAY = 7;
const TYPE_OBJECT = 8;
const TYPE_CALLABLE = 14;
const TYPE_VOID = 18;
const TYPE_ITERABLE = 19;
const TYPE_STATIC = 20;
const UNARY_BOOL_NOT = 13;
const UNARY_BITWISE_NOT = 12;
const TYPE_CALLABLE = 12;
const TYPE_VOID = 14;
const TYPE_ITERABLE = 13;
const TYPE_STATIC = 15;
const TYPE_MIXED = 16;
const TYPE_NEVER = 17;
const UNARY_BOOL_NOT = 14;
const UNARY_BITWISE_NOT = 13;
const UNARY_SILENCE = 260;
const UNARY_PLUS = 261;
const UNARY_MINUS = 262;
const BINARY_BOOL_AND = 259;
const BINARY_BOOL_OR = 258;
const BINARY_BOOL_XOR = 14;
const BINARY_BOOL_XOR = 15;
const BINARY_BITWISE_OR = 9;
const BINARY_BITWISE_AND = 10;
const BINARY_BITWISE_XOR = 11;
@@ -178,15 +197,15 @@ const BINARY_SUB = 2;
const BINARY_MUL = 3;
const BINARY_DIV = 4;
const BINARY_MOD = 5;
const BINARY_POW = 166;
const BINARY_POW = 12;
const BINARY_SHIFT_LEFT = 6;
const BINARY_SHIFT_RIGHT = 7;
const BINARY_IS_IDENTICAL = 15;
const BINARY_IS_NOT_IDENTICAL = 16;
const BINARY_IS_EQUAL = 17;
const BINARY_IS_NOT_EQUAL = 18;
const BINARY_IS_SMALLER = 19;
const BINARY_IS_SMALLER_OR_EQUAL = 20;
const BINARY_IS_IDENTICAL = 16;
const BINARY_IS_NOT_IDENTICAL = 17;
const BINARY_IS_EQUAL = 18;
const BINARY_IS_NOT_EQUAL = 19;
const BINARY_IS_SMALLER = 20;
const BINARY_IS_SMALLER_OR_EQUAL = 21;
const BINARY_IS_GREATER = 256;
const BINARY_IS_GREATER_OR_EQUAL = 257;
const BINARY_SPACESHIP = 170;
@@ -196,17 +215,17 @@ const EXEC_INCLUDE = 2;
const EXEC_INCLUDE_ONCE = 4;
const EXEC_REQUIRE = 8;
const EXEC_REQUIRE_ONCE = 16;
const USE_NORMAL = 361;
const USE_FUNCTION = 346;
const USE_CONST = 347;
const MAGIC_LINE = 370;
const MAGIC_FILE = 371;
const MAGIC_DIR = 372;
const MAGIC_NAMESPACE = 389;
const MAGIC_FUNCTION = 376;
const MAGIC_METHOD = 375;
const MAGIC_CLASS = 373;
const MAGIC_TRAIT = 374;
const USE_NORMAL = 1;
const USE_FUNCTION = 2;
const USE_CONST = 4;
const MAGIC_LINE = 375;
const MAGIC_FILE = 376;
const MAGIC_DIR = 377;
const MAGIC_NAMESPACE = 382;
const MAGIC_FUNCTION = 381;
const MAGIC_METHOD = 380;
const MAGIC_CLASS = 378;
const MAGIC_TRAIT = 379;
const ARRAY_SYNTAX_LIST = 1;
const ARRAY_SYNTAX_LONG = 2;
const ARRAY_SYNTAX_SHORT = 3;
+639 -204
View File
@@ -10,6 +10,7 @@ use ast\Node;
use Closure;
use Phan\Analysis\AssignOperatorFlagVisitor;
use Phan\Analysis\BinaryOperatorFlagVisitor;
use Phan\Analysis\BlockExitStatusChecker;
use Phan\Analysis\ConditionVisitor;
use Phan\Analysis\NegatedConditionVisitor;
use Phan\AST\Visitor\Element;
@@ -24,6 +25,7 @@ use Phan\Exception\IssueException;
use Phan\Exception\NodeException;
use Phan\Exception\RecursionDepthException;
use Phan\Exception\UnanalyzableException;
use Phan\Exception\UnanalyzableMagicPropertyException;
use Phan\Issue;
use Phan\IssueFixSuggester;
use Phan\Language\Context;
@@ -44,16 +46,19 @@ use Phan\Language\Type\AssociativeArrayType;
use Phan\Language\Type\BoolType;
use Phan\Language\Type\CallableType;
use Phan\Language\Type\ClassStringType;
use Phan\Language\Type\ClosureDeclarationType;
use Phan\Language\Type\ClosureType;
use Phan\Language\Type\FalseType;
use Phan\Language\Type\FloatType;
use Phan\Language\Type\GenericArrayType;
use Phan\Language\Type\IntersectionType;
use Phan\Language\Type\IntType;
use Phan\Language\Type\IterableType;
use Phan\Language\Type\ListType;
use Phan\Language\Type\LiteralIntType;
use Phan\Language\Type\LiteralStringType;
use Phan\Language\Type\MixedType;
use Phan\Language\Type\NeverType;
use Phan\Language\Type\NonEmptyMixedType;
use Phan\Language\Type\NullType;
use Phan\Language\Type\ObjectType;
@@ -169,7 +174,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$union_type = (new self(
$code_base,
$context,
$should_catch_issue_exception
true
))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'visit'}($node);
$context->setCachedUnionTypeOfNode($node_id, $union_type, true);
return $union_type;
@@ -186,7 +191,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$union_type = (new self(
$code_base,
$context,
$should_catch_issue_exception
false
))->{Element::VISIT_LOOKUP_TABLE[$node->kind] ?? 'visit'}($node);
$context->setCachedUnionTypeOfNode($node_id, $union_type, false);
@@ -233,7 +238,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
$node->children['var'],
$this->should_catch_issue_exception
)->asNonLiteralType();
}
@@ -255,7 +261,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
$node->children['var'],
$this->should_catch_issue_exception
)->asNonLiteralType();
}
@@ -277,7 +284,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
$node->children['var'],
$this->should_catch_issue_exception
)->asNonLiteralType()->getTypeAfterIncOrDec();
}
@@ -301,7 +309,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
$node->children['var'],
$this->should_catch_issue_exception
)->asNonLiteralType()->getTypeAfterIncOrDec();
}
@@ -322,7 +331,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$type = self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
$node->children['expr'],
$this->should_catch_issue_exception
)->objectTypes();
if ($type->isEmpty()) {
return ObjectType::instance(false)->asRealUnionType();
@@ -440,7 +450,24 @@ class UnionTypeVisitor extends AnalysisVisitor
case ast\flags\MAGIC_FUNCTION:
if ($this->context->isInFunctionLikeScope()) {
$fqsen = $this->context->getFunctionLikeFQSEN();
return self::literalStringUnionType($fqsen->isClosure() ? '{closure}' : $fqsen->getName());
if ($fqsen instanceof FullyQualifiedMethodName) {
// For NS\MyClass::methodName, return 'methodName'
$value = $fqsen->getName();
} else {
if ($fqsen->isClosure()) {
$this->emitIssue(
Issue::SuspiciousMagicConstant,
$node->lineno,
'__FUNCTION__',
"used inside of a closure instead of a function/method - the value is always '{closure}'"
);
$value = '{closure}';
} else {
// For \NS\my_function, return 'NS\my_function'.
$value = \ltrim($fqsen->__toString(), '\\');
}
}
return self::literalStringUnionType($value);
}
$this->warnAboutUndeclaredMagicConstant($node, 'used outside of functionlike');
break;
@@ -649,6 +676,10 @@ class UnionTypeVisitor extends AnalysisVisitor
return FalseType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_STATIC:
return StaticType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_MIXED:
return MixedType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_NEVER:
return NeverType::instance(false)->asRealUnionType();
default:
\Phan\Debug::printNode($node);
throw new AssertionError("All flags must match. Found ($node->flags) "
@@ -656,6 +687,55 @@ class UnionTypeVisitor extends AnalysisVisitor
}
}
/**
* Visit a node with kind `\ast\AST_TYPE_INTERSECTION`
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
*
* @throws AssertionError if the type flags were unknown
*/
public function visitTypeIntersection(Node $node): UnionType
{
// TODO: Validate that there aren't any duplicates
if (\count($node->children) === 1) {
// Might be possible due to the polyfill in the future.
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
return $this->__invoke($node->children[0]);
}
$types = [];
foreach ($node->children as $c) {
if (!$c instanceof Node) {
throw new AssertionError("Saw non-node in union type");
}
$kind = $c->kind;
if ($kind === ast\AST_TYPE) {
$types[] = $this->visitType($c);
} elseif ($kind === ast\AST_NAME) {
if ($this->context->getScope()->isInTraitScope()) {
$name = \strtolower($c->children['name']);
if ($name === 'self') {
$types[] = SelfType::instance(false)->asRealUnionType();
continue;
} elseif ($name === 'static') {
$types[] = StaticType::instance(false)->asRealUnionType();
continue;
}
}
$types[] = $this->visitName($c);
} else {
throw new AssertionError("Expected union type to be composed of types and names");
}
}
$result = [IntersectionType::createFromTypes($types, $this->code_base, $this->context)];
return UnionType::of($result, $result);
}
/**
* Visit a node with kind `\ast\AST_TYPE_UNION`
*
@@ -687,7 +767,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$types[] = $this->visitType($c);
} elseif ($kind === ast\AST_NAME) {
if ($this->context->getScope()->isInTraitScope()) {
$name = \strtolower($node->children['name']);
$name = \strtolower($c->children['name']);
if ($name === 'self') {
$types[] = SelfType::instance(false)->asRealUnionType();
continue;
@@ -794,8 +874,10 @@ class UnionTypeVisitor extends AnalysisVisitor
$result = $this->visitName($node);
} elseif ($kind === ast\AST_TYPE_UNION) {
$result = $this->visitTypeUnion($node);
} elseif ($kind === ast\AST_TYPE_INTERSECTION) {
$result = $this->visitTypeIntersection($node);
} else {
throw new AssertionError("Expected a type, union type, or a name in the signature: node: " . Debug::nodeToString($node));
throw new AssertionError("Expected a type, union type, intersection type, or a name in the signature: node: " . Debug::nodeToString($node));
}
if ($is_nullable) {
return $result->nullableClone();
@@ -854,16 +936,18 @@ class UnionTypeVisitor extends AnalysisVisitor
// For the shorthand $a ?: $b, the cond node will be the truthy value.
// Note: an ast node will never be null(can be unset), it will be a const AST node with the name null.
$true_node = $node->children['true'] ?? $cond_node;
$false_node = $node->children['false'];
// Rarely, a conditional will always be true or always be false.
if ($cond_truthiness !== null) {
// TODO: Add no-op checks in another PR, if they don't already exist for conditional.
if ($cond_truthiness === true) {
if ($cond_truthiness) {
// The condition is unconditionally true
return UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$true_node
$true_node,
$this->should_catch_issue_exception
);
} else {
// The condition is unconditionally false
@@ -872,7 +956,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['false']
$node->children['false'],
$this->should_catch_issue_exception
);
}
}
@@ -881,7 +966,8 @@ class UnionTypeVisitor extends AnalysisVisitor
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$cond_node
$cond_node,
$this->should_catch_issue_exception
);
}
// TODO: emit no-op if $cond_node is a literal, such as `if (2)`
@@ -910,14 +996,20 @@ class UnionTypeVisitor extends AnalysisVisitor
$true_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$true_context,
$true_node
$true_node,
$this->should_catch_issue_exception
);
$false_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$false_context,
$node->children['false']
$false_node,
$this->should_catch_issue_exception
);
if ($false_node instanceof Node && BlockExitStatusChecker::willUnconditionallyThrowOrReturn($false_node)) {
return $true_type->nonFalseyClone();
}
$true_type_is_empty = $true_type->isEmpty();
if (!$false_type->isEmpty()) {
// E.g. `foo() ?: 2` where foo is nullable or possibly false.
@@ -952,14 +1044,22 @@ class UnionTypeVisitor extends AnalysisVisitor
$true_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$true_context,
$true_node
$true_node,
$this->should_catch_issue_exception
);
$false_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$false_context,
$node->children['false']
$node->children['false'],
$this->should_catch_issue_exception
);
if ($false_type->isNeverType()) {
return $true_type;
}
if ($true_type->isNeverType()) {
return $false_type;
}
// Add the type for the 'true' side to the 'false' side
$union_type = $true_type->withUnionType($false_type);
@@ -979,6 +1079,38 @@ class UnionTypeVisitor extends AnalysisVisitor
return $union_type;
}
/**
* Visit a node with kind `\ast\AST_MATCH`
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
* @suppress PhanPossiblyUndeclaredProperty
*/
public function visitMatch(Node $node): UnionType
{
// TODO: Support inferring the type from the conditional
$union_types = [];
foreach ($node->children['stmts']->children as $arm_node) {
if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($arm_node)) {
$union_types[] = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
clone($this->context),
$arm_node->children['expr'],
$this->should_catch_issue_exception
);
}
}
if (!$union_types) {
return VoidType::instance(false)->asRealUnionType();
}
return UnionType::merge($union_types);
}
/**
* Visit a node with kind `\ast\AST_ARRAY`
*
@@ -1019,6 +1151,7 @@ class UnionTypeVisitor extends AnalysisVisitor
// XXX is this slow for extremely large arrays because of in_array check in UnionTypeBuilder?
$is_definitely_non_empty = false;
$has_key = false;
$has_unpack_string_key = false;
foreach ($children as $child) {
if (!($child instanceof Node)) {
// Skip this, we already emitted a syntax error.
@@ -1028,7 +1161,13 @@ class UnionTypeVisitor extends AnalysisVisitor
}
if ($child->kind === ast\AST_UNPACK) {
// Analyze PHP 7.4's array spread operator, e.g. `[$a, ...$array, $b]`
$new_union_type = $this->analyzeUnpack($child, true);
[$new_union_type, $new_union_type_has_string_keys] = $this->analyzeUnpack($child, true);
if ($new_union_type_has_string_keys) {
$has_key = true;
$has_unpack_string_key = true;
}
$has_key = $has_key || $new_union_type_has_string_keys;
$value_types_builder->addUnionType($new_union_type);
$record_real_union_type($new_union_type);
continue;
@@ -1047,6 +1186,13 @@ class UnionTypeVisitor extends AnalysisVisitor
$value_types_builder->addType(MixedType::instance(false));
$real_value_types_builder = null;
} else {
if ($element_value_type->isVoidType()) {
$this->emitIssue(
Issue::TypeVoidExpression,
$node->lineno,
ASTReverter::toShortString($value)
);
}
$value_types_builder->addUnionType($element_value_type);
$record_real_union_type($element_value_type);
}
@@ -1067,7 +1213,7 @@ class UnionTypeVisitor extends AnalysisVisitor
} else {
$result = $result->asNonEmptyListTypes();
}
$result = $result->withRealTypeSet(self::arrayTypeFromRealTypeBuilder($real_value_types_builder, $has_key));
$result = $result->withRealTypeSet($this->arrayTypeFromRealTypeBuilder($real_value_types_builder, $node, $has_key, $has_unpack_string_key));
if ($is_definitely_non_empty) {
return $result->nonFalseyClone();
}
@@ -1082,21 +1228,49 @@ class UnionTypeVisitor extends AnalysisVisitor
/**
* @return list<ArrayType>
*/
private static function arrayTypeFromRealTypeBuilder(?UnionTypeBuilder $builder, bool $has_key): array
private function arrayTypeFromRealTypeBuilder(?UnionTypeBuilder $builder, Node $node, bool $has_key, bool $has_unpack_string_key): array
{
if (!$builder || $builder->isEmpty()) {
static $array_type_set = null;
if ($array_type_set === null) {
$array_type_set = [ArrayType::instance(false)];
// Here, we only check for the real type being an integer.
// Unknown strings such as '0' will cast to integers when used as array keys,
// and if we knew all of the array keys were literals we would have generated an array shape instead.
$has_exclusively_int_keys = !$has_unpack_string_key;
if ($has_key && $has_exclusively_int_keys) {
foreach ($node->children as $child_node) {
$key = $child_node->children['key'] ?? null;
if (!isset($key)) {
continue;
}
$key_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$key,
$this->should_catch_issue_exception
);
if (!$key_type->getRealUnionType()->isIntTypeOrNull()) {
$has_exclusively_int_keys = false;
break;
}
}
return $array_type_set;
}
if (!$builder || $builder->isEmpty()) {
if (!$has_key) {
// @phan-suppress-next-line PhanTypeMismatchReturn
return UnionType::typeSetFromString('list');
}
// @phan-suppress-next-line PhanTypeMismatchReturn
return UnionType::typeSetFromString($has_exclusively_int_keys ? 'array<int,mixed>' : 'array');
}
$real_types = [];
foreach ($builder->getTypeSet() as $type) {
if ($has_key) {
$real_types[] = ListType::fromElementType($type, false, GenericArrayType::KEY_MIXED);
// TODO: Could be more precise if all keys are known to be non-numeric strings or integers
$real_types[] = GenericArrayType::fromElementType(
$type,
false,
$has_exclusively_int_keys ? GenericArrayType::KEY_INT : GenericArrayType::KEY_MIXED
);
} else {
$real_types[] = GenericArrayType::fromElementType($type, false, GenericArrayType::KEY_MIXED);
$real_types[] = ListType::fromElementType($type, false, GenericArrayType::KEY_MIXED);
}
}
return $real_types;
@@ -1105,14 +1279,14 @@ class UnionTypeVisitor extends AnalysisVisitor
/**
* Visit a node with kind `\ast\AST_YIELD`
*
* @param Node $unused_node
* @param Node $node @unused-param
* A yield node. Does not affect the union type
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
*/
public function visitYield(Node $unused_node): UnionType
public function visitYield(Node $node): UnionType
{
$context = $this->context;
if (!$context->isInFunctionLikeScope()) {
@@ -1160,19 +1334,32 @@ class UnionTypeVisitor extends AnalysisVisitor
// NOTE: this has some overlap with DuplicateKeyPlugin
if ($key_node === null) {
$elements[] = $child_node;
} elseif (is_scalar($key_node)) {
$elements[$key_node] = $child_node; // Check for float?
} else {
continue;
}
if (\is_object($key_node)) {
if ($context_node === null) {
$context_node = new ContextNode($this->code_base, $this->context, null);
}
$key = $context_node->getEquivalentPHPValueForNode($key_node, ContextNode::RESOLVE_CONSTANTS);
if (is_scalar($key)) {
$elements[$key] = $child_node;
} else {
$key_node = $context_node->getEquivalentPHPValueForNode($key_node, ContextNode::RESOLVE_CONSTANTS);
if (\is_object($key_node)) {
return null;
}
}
// TODO: Add a warning elsewhere about implicit float to int conversion when analyzing arrays
// PHP 8.1 deprecated implicit float to int conversions
if (\is_scalar($key_node)) {
if (!\is_string($key_node)) {
$key_node = (int)$key_node;
}
} elseif (\is_array($key_node)) {
return null;
} elseif (\is_resource($key_node)) {
$key_node = \get_resource_id($key_node);
} else {
// null
$key_node = (string)$key_node;
}
$elements[$key_node] = $child_node; // Check for float?
}
return $elements;
}
@@ -1189,7 +1376,7 @@ class UnionTypeVisitor extends AnalysisVisitor
}
// e.g. `[$x, ...$array]` in PHP 7.4
// TODO: Support array expressions when their value is constant
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr);
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, $this->should_catch_issue_exception);
// TODO: Warn if non-array
if ($union_type->typeCount() === 1 && $union_type->hasTopLevelArrayShapeTypeInstances() && !$union_type->hasTopLevelNonArrayShapeTypeInstances()) {
@@ -1247,6 +1434,8 @@ class UnionTypeVisitor extends AnalysisVisitor
);
if ($element_value_type->isEmpty()) {
$element_value_type = MixedType::instance(false)->asPHPDocUnionType();
} else {
$element_value_type = $element_value_type->convertUndefinedToNullable();
}
} else {
$element_value_type = Type::fromObject($value)->asRealUnionType();
@@ -1318,11 +1507,21 @@ class UnionTypeVisitor extends AnalysisVisitor
// TODO: Check if the cast would throw an error at runtime, based on the type (e.g. casting object to string/int)
// RedundantConditionCallPlugin contains unrelated checks of whether this is redundant.
$expr = $node->children['expr'];
$expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, $this->should_catch_issue_exception);
if ($expr_type->isVoidType()) {
$this->emitIssue(
Issue::TypeVoidExpression,
$expr->lineno ?? $node->lineno,
ASTReverter::toShortString($expr)
);
}
switch ($node->flags) {
case \ast\flags\TYPE_NULL:
return NullType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_BOOL:
return BoolType::instance(false)->asRealUnionType();
return $expr_type->applyBoolCast();
// TODO: Warn about invalid casts (#2806)
case \ast\flags\TYPE_LONG:
return IntType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_DOUBLE:
@@ -1332,7 +1531,7 @@ class UnionTypeVisitor extends AnalysisVisitor
case \ast\flags\TYPE_ARRAY:
return ArrayType::instance(false)->asRealUnionType();
case \ast\flags\TYPE_OBJECT:
return $this->typeAfterCastToObject($node->children['expr']);
return $this->typeAfterCastToObject($expr_type);
default:
throw new NodeException(
$node,
@@ -1342,16 +1541,14 @@ class UnionTypeVisitor extends AnalysisVisitor
}
/**
* @param Node|string|int|float $expr
* @suppress PhanThrowTypeAbsentForCall
*/
private function typeAfterCastToObject($expr): UnionType
private static function typeAfterCastToObject(UnionType $expr_type): UnionType
{
static $stdclass;
if ($stdclass === null) {
$stdclass = Type::fromFullyQualifiedString('\stdClass');
}
$expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr);
$has_array = $expr_type->hasArray();
if ($has_array) {
if ($expr_type->isExclusivelyArray()) {
@@ -1403,6 +1600,15 @@ class UnionTypeVisitor extends AnalysisVisitor
);
return $object_type->asRealUnionType();
}
$args_node = $node->children['args'];
if ($args_node->kind !== ast\AST_ARG_LIST) {
$this->emitIssue(
Issue::InvalidNode,
$node->lineno,
"Cannot create Closure for new expression"
);
return $object_type->asRealUnionType();
}
$union_type = $this->visitClassNameNode($class_node);
if ($union_type->isEmpty()) {
return $object_type->asRealUnionType();
@@ -1413,7 +1619,7 @@ class UnionTypeVisitor extends AnalysisVisitor
// For any types that are templates, map them to concrete
// types based on the parameters passed in.
$type_set = \array_map(function (Type $type) use ($node): Type {
$type_set = \array_map(function (Type $type) use ($args_node): Type {
// Get a fully qualified name for the type
// TODO: Add a test of `new $closure()` warning.
@@ -1443,9 +1649,10 @@ class UnionTypeVisitor extends AnalysisVisitor
return UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$arg_node
$arg_node,
$this->should_catch_issue_exception
);
}, $node->children['args']->children);
}, $args_node->children);
// Get closures to extract template types based on the types of the constructor
// so that we can figure out what template types we're going to be mapping
@@ -1491,7 +1698,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$code_base = $this->code_base;
$context = $this->context;
// Check to make sure the left side is valid
UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr']);
UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr'], $this->should_catch_issue_exception);
// Get the type that we're checking it against, check if it is valid.
$class_node = $node->children['class'];
if (!($class_node instanceof Node)) {
@@ -1500,19 +1707,18 @@ class UnionTypeVisitor extends AnalysisVisitor
$type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$class_node
$class_node,
$this->should_catch_issue_exception
);
// TODO: Unify UnionTypeVisitor, AssignmentVisitor, and PostOrderAnalysisVisitor
if (!$type->isEmpty() && !$type->hasObjectTypes()) {
if ($class_node->kind !== \ast\AST_NAME &&
!$type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())
) {
if (!$type->isEmpty() && $type->objectTypesWithKnownFQSENs()->isEmpty()) {
if ($class_node->kind === \ast\AST_NAME || !$type->hasStringType()) {
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeInvalidInstanceof,
$context->getLineNumberStart(),
ASTReverter::toShortString($node),
ASTReverter::toShortString($class_node),
(string)$type
);
}
@@ -1540,12 +1746,14 @@ class UnionTypeVisitor extends AnalysisVisitor
*/
public function visitDim(Node $node, bool $treat_undef_as_nullable = false): UnionType
{
$code_base = $this->code_base;
$context = $this->context;
$union_type = self::unionTypeFromNode(
$this->code_base,
$this->context,
$code_base,
$context,
$node->children['expr'],
$this->should_catch_issue_exception
)->withStaticResolvedInContext($this->context);
)->withStaticResolvedInContext($context);
if ($union_type->isEmpty()) {
return UnionType::empty();
@@ -1584,6 +1792,7 @@ class UnionTypeVisitor extends AnalysisVisitor
Issue::TypePossiblyInvalidDimOffset,
$node->lineno,
ASTReverter::toShortString($node->children['dim']),
ASTReverter::toShortString($node->children['expr']),
$union_type
);
if ($treat_undef_as_nullable || Config::getValue('convert_possibly_undefined_offset_to_nullable')) {
@@ -1597,30 +1806,31 @@ class UnionTypeVisitor extends AnalysisVisitor
}
$dim_type = self::unionTypeFromNode(
$this->code_base,
$this->context,
$code_base,
$context,
$node->children['dim'],
true
$this->should_catch_issue_exception
);
// Figure out what the types of accessed array
// elements would be.
$generic_types = $union_type->genericArrayElementTypes(true);
$generic_types = $union_type->genericArrayElementTypes(true, $code_base);
// If we have generics, we're all set
if (!$generic_types->isEmpty()) {
$generic_types = $generic_types->asNormalizedTypes();
if (!($node->flags & self::FLAG_IGNORE_NULLABLE) && $union_type->containsNullable()) {
if (!($node->flags & self::FLAG_IGNORE_NULLABLE) && $union_type->containsNonMixedNullable()) {
$this->emitIssue(
Issue::TypeArraySuspiciousNullable,
$node->lineno,
ASTReverter::toShortString($node->children['expr']),
(string)$union_type
);
}
if (!$dim_type->isEmpty()) {
try {
$should_check = !$union_type->hasMixedType() && !$union_type->asExpandedTypes($this->code_base)->hasArrayAccess();
$should_check = !$union_type->hasMixedOrNonEmptyMixedType() && !$union_type->hasArrayAccess($code_base);
} catch (RecursionDepthException $_) {
$should_check = false;
}
@@ -1634,10 +1844,10 @@ class UnionTypeVisitor extends AnalysisVisitor
);
}
if (!$dim_type->canCastToUnionType($expected_key_type)) {
if (!$dim_type->canCastToUnionType($expected_key_type, $code_base)) {
$issue_type = Issue::TypeMismatchDimFetch;
if ($dim_type->containsNullable() && $dim_type->nonNullableClone()->canCastToUnionType($expected_key_type)) {
if ($dim_type->containsNullable() && $dim_type->nonNullableClone()->canCastToUnionType($expected_key_type, $code_base)) {
$issue_type = Issue::TypeMismatchDimFetchNullable;
}
@@ -1654,7 +1864,7 @@ class UnionTypeVisitor extends AnalysisVisitor
throw new IssueException(
Issue::fromType($issue_type)(
$this->context->getFile(),
$context->getFile(),
$node->lineno,
[(string)$union_type, (string)$dim_type, (string)$expected_key_type]
)
@@ -1671,7 +1881,8 @@ class UnionTypeVisitor extends AnalysisVisitor
if (!($node->flags & self::FLAG_IGNORE_NULLABLE)) {
$this->emitIssue(
Issue::TypeArraySuspiciousNull,
$node->lineno
$node->lineno,
ASTReverter::toShortString($node->children['expr'])
);
}
if ($union_type->getRealUnionType()->isNull()) {
@@ -1686,16 +1897,15 @@ class UnionTypeVisitor extends AnalysisVisitor
// so we'll add the string type to the result if we're
// indexing something that could be a string
if ($union_type->isNonNullStringType()
|| ($union_type->canCastToUnionType($string_union_type) && !$union_type->hasMixedType())
|| ($union_type->canCastToUnionType($string_union_type, $code_base) && !$union_type->hasMixedOrNonEmptyMixedType())
) {
if (Config::get_closest_target_php_version_id() < 70100 && $union_type->isNonNullStringType()) {
if (Config::get_closest_minimum_target_php_version_id() < 70100 && $union_type->isNonNullStringType()) {
$this->analyzeNegativeStringOffsetCompatibility($node, $dim_type);
}
$this->checkIsValidStringOffset($union_type, $node, $dim_type);
if (!$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_union_type)) {
// TODO: Efficient implementation of asExpandedTypes()->hasArrayAccess()?
if (!$union_type->isEmpty() && !$union_type->asExpandedTypes($this->code_base)->hasArrayLike()) {
if (!$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_union_type, $code_base)) {
if (!$union_type->isEmpty() && !$union_type->hasArrayLike($code_base)) {
$this->emitIssue(
Issue::TypeMismatchDimFetch,
$node->lineno,
@@ -1708,7 +1918,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$element_types = $element_types->withType($string_type);
if ($union_type->hasRealTypeSet()) {
// @phan-suppress-next-line PhanAccessMethodInternal
$element_types = $element_types->withRealTypeSet(UnionType::computeRealElementTypesForDimAccess($union_type->getRealTypeSet()));
$element_types = $element_types->withRealTypeSet(UnionType::computeRealElementTypesForDimAccess($union_type->getRealTypeSet(), $code_base));
}
}
@@ -1716,22 +1926,23 @@ class UnionTypeVisitor extends AnalysisVisitor
// Hunt for any types that are viable class names and
// see if they inherit from ArrayAccess
try {
foreach ($union_type->asClassList($this->code_base, $this->context) as $class) {
$expanded_types = $class->getUnionType()->asExpandedTypes($this->code_base);
foreach ($union_type->asClassList($code_base, $context) as $class) {
$expanded_types = $class->getUnionType()->asExpandedTypes($code_base);
if ($expanded_types->hasType($array_access_type) ||
$expanded_types->hasType($simple_xml_element_type)
) {
return $element_types;
}
}
} catch (CodeBaseException $_) {
} catch (RecursionDepthException $_) {
} catch (CodeBaseException | RecursionDepthException $_) {
// ignore
}
if (!$union_type->hasArrayLike() && !$union_type->hasMixedType()) {
if (!$union_type->hasArrayLike($code_base) && !$union_type->hasMixedOrNonEmptyMixedType()) {
$this->emitIssue(
Issue::TypeArraySuspicious,
$node->lineno,
ASTReverter::toShortString($node->children['expr']),
(string)$union_type
);
return $element_types;
@@ -1740,6 +1951,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$this->emitIssue(
Issue::TypeArraySuspiciousNullable,
$node->lineno,
ASTReverter::toShortString($node->children['expr']),
(string)$union_type
);
}
@@ -1791,6 +2003,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$node->children['dim']->lineno ?? $node->lineno,
[
$dim_type,
ASTReverter::toShortString($node->children['expr']),
(string)$union_type
]
)
@@ -1846,6 +2059,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$dim_node->lineno ?? $node->lineno,
[
is_scalar($dim_value) ? StringUtil::jsonEncode($dim_value) : ASTReverter::toShortString($dim_value),
ASTReverter::toShortString($node->children['expr']),
(string)$union_type
]
)
@@ -1862,7 +2076,7 @@ class UnionTypeVisitor extends AnalysisVisitor
return null;
}
$resulting_element_type = self::resolveArrayShapeElementTypesForOffset($union_type, $dim_value);
$resulting_element_type = self::resolveArrayShapeElementTypesForOffset($union_type, $dim_value, false, $this->code_base);
if ($resulting_element_type === null) {
return null;
@@ -1874,7 +2088,7 @@ class UnionTypeVisitor extends AnalysisVisitor
Issue::fromType(Issue::TypeInvalidDimOffset)(
$this->context->getFile(),
$dim_node->lineno ?? $node->lineno,
[StringUtil::jsonEncode($dim_value), (string)$union_type]
[StringUtil::jsonEncode($dim_value), ASTReverter::toShortString($node->children['expr']), (string)$union_type]
)
);
if ($this->should_catch_issue_exception) {
@@ -1885,11 +2099,37 @@ class UnionTypeVisitor extends AnalysisVisitor
}
// $union_type is exclusively array shape types, but those don't contain the field $dim_value.
// It's undefined (which becomes null)
return NullType::instance(false)->asPHPDocUnionType();
if (self::couldRealTypesHaveKey($union_type->getRealTypeSet(), $dim_value)) {
return NullType::instance(false)->asPHPDocUnionType();
}
return NullType::instance(false)->asRealUnionType();
}
return $resulting_element_type;
}
/**
* @param list<Type> $real_type_set
* @param int|string|float $dim_value
*/
private static function couldRealTypesHaveKey(array $real_type_set, $dim_value): bool
{
foreach ($real_type_set as $type) {
if ($type instanceof ArrayShapeType) {
if (\array_key_exists($dim_value, $type->getFieldTypes())) {
return true;
}
} elseif ($type instanceof ListType) {
$filtered = \is_int($dim_value) ? $dim_value : \filter_var($dim_value, \FILTER_VALIDATE_INT);
if (\is_int($filtered) && $filtered >= 0) {
return true;
}
} else {
return true;
}
}
return \count($real_type_set) === 0;
}
/**
* @param UnionType $union_type a union type with at least one top-level array shape type
* @param int|string|float|bool $dim_value a scalar dimension. TODO: Warn about null?
@@ -1897,7 +2137,7 @@ class UnionTypeVisitor extends AnalysisVisitor
* returns false if there the offset was invalid and there are no ways to get that offset
* returns null if the dim_value offset could not be found, but there were other generic array types
*/
public static function resolveArrayShapeElementTypesForOffset(UnionType $union_type, $dim_value, bool $is_computing_real_type_set = false)
public static function resolveArrayShapeElementTypesForOffset(UnionType $union_type, $dim_value, bool $is_computing_real_type_set, CodeBase $code_base)
{
/**
* @var bool $has_non_array_shape_type this will be true if there are types that support array access
@@ -1921,10 +2161,15 @@ class UnionTypeVisitor extends AnalysisVisitor
} else {
// TODO: Warn about string indices of strings?
}
} elseif ($type->isArrayLike() || $type->isObject() || $type instanceof MixedType) {
} elseif ($type->isArrayLike($code_base) || $type->isObject() || $type instanceof MixedType) {
if ($type instanceof ListType && (!\is_numeric($dim_value) || $dim_value < 0)) {
continue;
}
if ($is_computing_real_type_set) {
// Avoid false positives for real type checking.
// TODO: Improve handling for GenericArrayType, strings, etc.
return null;
}
// TODO: Could be more precise about check for ArrayAccess
$has_generic_array = true;
continue;
@@ -1964,10 +2209,11 @@ class UnionTypeVisitor extends AnalysisVisitor
}
}
if (!$resulting_element_type->containsNullableOrUndefined() && $union_type->containsNullableOrUndefined()) {
$resulting_element_type = $resulting_element_type->nullableClone();
// Here, this uses Foo|null instead of ?Foo to only warn when strict types are used.
$resulting_element_type = $resulting_element_type->withType(NullType::instance(false));
}
if (!$is_computing_real_type_set) {
$resulting_real_element_type = self::resolveArrayShapeElementTypesForOffset($union_type->getRealUnionType(), $dim_value, true);
$resulting_real_element_type = self::resolveArrayShapeElementTypesForOffset($union_type->getRealUnionType(), $dim_value, true, $code_base);
return $resulting_element_type->withRealTypeSet(
\is_object($resulting_real_element_type) ? $resulting_real_element_type->getRealTypeSet() : []
);
@@ -1984,7 +2230,7 @@ class UnionTypeVisitor extends AnalysisVisitor
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
* given node (the type of a value OF the unpacked iterable)
*
* @throws IssueException
* if the unpack is on an invalid expression
@@ -1992,7 +2238,7 @@ class UnionTypeVisitor extends AnalysisVisitor
*/
public function visitUnpack(Node $node): UnionType
{
return $this->analyzeUnpack($node, isset($node->is_in_array));
return $this->analyzeUnpack($node, isset($node->is_in_array))[0];
}
/**
@@ -2006,14 +2252,15 @@ class UnionTypeVisitor extends AnalysisVisitor
* If true, this is the array spread operator,
* which tolerates integers that aren't consecutive.
*
* @return UnionType
* @return array{0:UnionType, 1:bool}
* The set of types that are possibly produced by the
* given node
* given node (the type of a value OF the unpacked iterable),
* as well as whether this is likely to contain non-integer keys (imperfect check).
*
* @throws IssueException
* if the unpack is on an invalid expression
*/
private function analyzeUnpack(Node $node, bool $is_array_spread): UnionType
private function analyzeUnpack(Node $node, bool $is_array_spread): array
{
$union_type = self::unionTypeFromNode(
$this->code_base,
@@ -2023,7 +2270,7 @@ class UnionTypeVisitor extends AnalysisVisitor
)->withStaticResolvedInContext($this->context);
if ($union_type->isEmpty()) {
return $union_type;
return [$union_type, false];
}
// Figure out what the types of accessed array
@@ -2035,8 +2282,8 @@ class UnionTypeVisitor extends AnalysisVisitor
// If we have generics, we're all set
try {
if ($generic_types->isEmpty()) {
if (!$union_type->asExpandedTypes($this->code_base)->hasIterable() && !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullable() && $type instanceof MixedType;
if (!$union_type->hasIterable($this->code_base) && !$union_type->hasTypeMatchingCallback(static function (Type $type): bool {
return !$type->isNullableLabeled() && $type instanceof MixedType;
})) {
throw new IssueException(
Issue::fromType(Issue::TypeMismatchUnpackValue)(
@@ -2046,21 +2293,29 @@ class UnionTypeVisitor extends AnalysisVisitor
)
);
}
return $generic_types;
return [$generic_types, false];
}
$this->checkInvalidUnpackKeyType($node, $union_type, $is_array_spread);
foreach ($union_type->iterableKeyUnionType($this->code_base)->getTypeSet() as $key_type) {
if ($key_type instanceof StringType) {
return [$generic_types, true];
}
}
} catch (IssueException $exception) {
Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance());
return [$generic_types, true];
}
return $generic_types;
return [$generic_types, false];
}
private function checkInvalidUnpackKeyType(Node $node, UnionType $union_type, bool $is_array_spread): void
{
$is_invalid_because_associative = false;
if (!$is_array_spread) {
$minimum_target_php_version_id = Config::get_closest_minimum_target_php_version_id();
// Treat foo(...$associativeArgs) as invalid unless the minimum target php version is 8.0 (i.e. array unpacking is supported)
if (!$is_array_spread && $minimum_target_php_version_id < 80000) {
foreach ($union_type->getTypeSet() as $type) {
if ($type->isIterable()) {
if ($type->isIterable($this->code_base)) {
if ($type instanceof AssociativeArrayType) {
$is_invalid_because_associative = true;
} else {
@@ -2074,10 +2329,16 @@ class UnionTypeVisitor extends AnalysisVisitor
// Check that this is possibly valid, e.g. array<int, mixed>, Generator<int, mixed>, or iterable<int, mixed>
// TODO: Warn if key_type contains nullable types (excluding VoidType)
// TODO: Warn about union types that are partially invalid.
if ($is_invalid_because_associative || !$key_type->isEmpty() && !$key_type->hasTypeMatchingCallback(static function (Type $type): bool {
return $type instanceof IntType || $type instanceof MixedType;
})
) {
if ($is_invalid_because_associative || (!$key_type->isEmpty() && !$key_type->hasTypeMatchingCallback(static function (Type $type) use ($minimum_target_php_version_id, $is_array_spread): bool {
if ($type instanceof IntType || $type instanceof MixedType) {
return true;
}
if ($type instanceof StringType) {
// TODO: Forbid invalid parameter identifiers such as 'foo-bar' in the overall array shape?
return ($is_array_spread ? $minimum_target_php_version_id >= 80100 : $minimum_target_php_version_id >= 80000);
}
return false;
}))) {
throw new IssueException(
Issue::fromType($is_array_spread ? Issue::TypeMismatchUnpackKeyArraySpread : Issue::TypeMismatchUnpackKey)(
$this->context->getFile(),
@@ -2162,7 +2423,7 @@ class UnionTypeVisitor extends AnalysisVisitor
if ($int_or_string_type === null) {
$int_or_string_type = UnionType::fromFullyQualifiedPHPDocString('int|string|null');
}
if (!$name_node_type->canCastToUnionType($int_or_string_type)) {
if (!$name_node_type->canCastToUnionType($int_or_string_type, $this->code_base)) {
Issue::maybeEmit($this->code_base, $this->context, Issue::TypeSuspiciousIndirectVariable, $name_node->lineno, (string)$name_node_type);
return MixedType::instance(false)->asPHPDocUnionType();
}
@@ -2177,47 +2438,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$variable_name =
(string)$name_node;
if (!$this->context->getScope()->hasVariableWithName($variable_name)) {
if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) {
// @phan-suppress-next-line PhanTypeMismatchReturnNullable variable existence was checked
return Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name);
}
if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF) {
if (!$this->context->isInGlobalScope()) {
if ($this->should_catch_issue_exception && !(($node->flags & PhanAnnotationAdder::FLAG_INITIALIZES) && $this->context->isInLoop())) {
// Warn about `$var ??= expr;`, except when it's done in a loop.
$this->emitIssueWithSuggestion(
Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
);
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
return NullType::instance(false)->asRealUnionType();
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
return NullType::instance(false)->asPHPDocUnionType();
}
if (!($this->context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope'))) {
throw new IssueException(
Issue::fromType(Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name))(
$this->context->getFile(),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
)
);
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
} else {
if ($this->context->getScope()->hasVariableWithName($variable_name)) {
$variable = $this->context->getScope()->getVariableByName(
$variable_name
);
@@ -2253,6 +2474,63 @@ class UnionTypeVisitor extends AnalysisVisitor
return $union_type;
}
if (Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())) {
// @phan-suppress-next-line PhanTypeMismatchReturnNullable variable existence was checked
return Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name);
}
if ($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF) {
if (!$this->context->isInGlobalScope()) {
if ($this->should_catch_issue_exception && !(($node->flags & PhanAnnotationAdder::FLAG_INITIALIZES) && $this->context->isInLoop())) {
// Warn about `$var ??= expr;`, except when it's done in a loop.
$this->emitIssueWithSuggestion(
Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
);
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
// Be more certain that unknown variables are not set inside of function scopes than the global scope.
return NullType::instance(false)->asRealUnionType();
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
return NullType::instance(false)->asPHPDocUnionType();
}
if (!($this->context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope'))) {
if (!$this->should_catch_issue_exception) {
throw new IssueException(
Issue::fromType(Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name))(
$this->context->getFile(),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
)
);
}
Issue::maybeEmitWithParameters(
$this->code_base,
$this->context,
Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
);
}
if ($variable_name === 'this') {
return ObjectType::instance(false)->asRealUnionType();
}
if (!$this->context->isInGlobalScope()) {
if (!$this->context->isInLoop()) {
return NullType::instance(false)->asRealUnionType()->withIsDefinitelyUndefined();
}
return NullType::instance(false)->asRealUnionType();
}
return UnionType::empty();
}
@@ -2275,7 +2553,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$part_string = $part instanceof Node ? UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$part
$part,
$this->should_catch_issue_exception
)->asSingleScalarValueOrNullOrSelf() : $part;
if (\is_object($part_string)) {
return StringType::instance(false)->asRealUnionType();
@@ -2377,6 +2656,9 @@ class UnionTypeVisitor extends AnalysisVisitor
$node
))->getClassConst();
$union_type = $constant->getUnionType();
if ($constant->isFinal()) {
return $union_type;
}
$class_node = $node->children['class'];
if (!$class_node instanceof Node || $class_node->kind !== ast\AST_NAME) {
// ignore nonsense like (0)::class, and dynamic accesses such as $var::CLASS
@@ -2459,12 +2741,45 @@ class UnionTypeVisitor extends AnalysisVisitor
false
);
}
if (\is_string($name) && \strcasecmp($name, 'static') === 0 && (!$class || !$class->isFinal())) {
if (\is_string($name) && \strcasecmp($name, 'static') === 0 && (!isset($class) || !$class->isFinal())) {
return UnionType::of($types, [ClassStringType::instance(false)]);
}
return UnionType::of($types, $types);
}
/**
* Visit a node with kind `\ast\AST_NULLSAFE_PROP`
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
* @override
*/
public function visitNullsafeProp(Node $node): UnionType
{
$expr_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr'],
$this->should_catch_issue_exception
)->getRealUnionType();
$result = $this->analyzeProp($node, false);
if ($expr_type->isEmpty()) {
return $result->nullableClone();
}
if ($expr_type->isNull()) {
return NullType::instance(false)->asRealUnionType();
}
if ($expr_type->containsNullableOrUndefined()) {
return $result->nullableClone();
}
return $result;
}
/**
* Visit a node with kind `\ast\AST_PROP`
*
@@ -2536,7 +2851,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$expression_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$expr_node
$expr_node,
$this->should_catch_issue_exception
);
$union_type = $union_type->withTemplateParameterTypeMap(
@@ -2591,9 +2907,9 @@ class UnionTypeVisitor extends AnalysisVisitor
["{$exception_fqsen}->{$property_name}"],
$suggestion
);
} catch (UnanalyzableException $_) {
// Swallow it. There are some constructs that we
// just can't figure out.
} catch (UnanalyzableMagicPropertyException $exception) {
$class = $exception->getClass();
return $class->getMethodByName($this->code_base, '__get')->getUnionType();
} catch (NodeException $_) {
// Swallow it. There are some constructs that we
// just can't figure out.
@@ -2670,11 +2986,7 @@ class UnionTypeVisitor extends AnalysisVisitor
foreach ($function_list_generator as $function) {
$function->analyzeReturnTypes($this->code_base); // For daemon/server mode, call this to consistently ensure accurate return types.
if ($function->hasDependentReturnType()) {
$function_types = $function->getDependentReturnType($this->code_base, $this->context, $node->children['args']->children);
} else {
$function_types = $function->getUnionType();
}
$function_types = $this->getDependentReturnTypeOfCall($function, $node);
if ($possible_types) {
'@phan-var UnionType $possible_types';
$possible_types = $possible_types->withUnionType($function_types);
@@ -2686,6 +2998,23 @@ class UnionTypeVisitor extends AnalysisVisitor
return $possible_types ?? UnionType::empty();
}
/**
* @return UnionType - the union type of the result of the call, or of the closure generated by first-class callable conversion
*/
private function getDependentReturnTypeOfCall(FunctionInterface $function, Node $node): UnionType
{
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
if ($function instanceof ClosureDeclarationType) {
return $function->asRealUnionType();
} else {
return ClosureType::instanceWithClosureFQSEN($function->getFQSEN(), $function)->asRealUnionType();
}
} elseif ($function->hasDependentReturnType()) {
return $function->getDependentReturnType($this->code_base, $this->context, $node->children['args']->children);
}
return $function->getUnionType();
}
/**
* Visit a node with kind `\ast\AST_STATIC_CALL`
*
@@ -2702,6 +3031,38 @@ class UnionTypeVisitor extends AnalysisVisitor
return $this->visitMethodCall($node);
}
/**
* Visit a node with kind `\ast\AST_NULLSAFE_METHOD_CALL`
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @return UnionType
* The set of types that are possibly produced by the
* given node
*/
public function visitNullsafeMethodCall(Node $node): UnionType
{
$expr_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr'],
$this->should_catch_issue_exception
)->getRealUnionType();
$result = $this->visitMethodCall($node);
if ($result->isEmpty()) {
return $result->nullableClone();
}
if ($expr_type->isNull()) {
return NullType::instance(false)->asRealUnionType();
}
if ($expr_type->containsNullableOrUndefined()) {
return $result->nullableClone();
}
return $result;
}
/**
* Visit a node with kind `\ast\AST_METHOD_CALL`
*
@@ -2734,7 +3095,8 @@ class UnionTypeVisitor extends AnalysisVisitor
}
try {
$class_node = $node->children['class'] ?? $node->children['expr'];
$static_class_node = $node->children['class'] ?? null;
$class_node = $static_class_node ?? $node->children['expr'];
if (!($class_node instanceof Node)) {
// E.g. `'string_literal'->method()`
// Other places will also emit NonClassMethodCall for the same node
@@ -2742,17 +3104,18 @@ class UnionTypeVisitor extends AnalysisVisitor
Issue::NonClassMethodCall,
$node->lineno,
$method_name,
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node)
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$class_node,
$this->should_catch_issue_exception
)
);
return UnionType::empty();
}
$combined_union_type = null;
foreach ($this->classListFromNode($class_node) as $class) {
if (!$class->hasMethodWithName(
$this->code_base,
$method_name
)
) {
if (!$class->hasMethodWithName($this->code_base, $method_name, true)) {
continue;
}
@@ -2767,17 +3130,18 @@ class UnionTypeVisitor extends AnalysisVisitor
try {
$method = $method->resolveTemplateType(
$this->code_base,
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node)
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$class_node,
$this->should_catch_issue_exception
)
);
} catch (RecursionDepthException $_) {
}
}
if ($method->hasDependentReturnType()) {
$union_type = $method->getDependentReturnType($this->code_base, $this->context, $node->children['args']->children);
} else {
$union_type = $method->getUnionType();
}
$union_type = $this->getDependentReturnTypeOfCall($method, $node);
// Map template types to concrete types
// TODO: When the template types are part of the method doc comment, don't look it up in the class union type
@@ -2786,7 +3150,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$expression_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
$node->children['expr'],
$this->should_catch_issue_exception
);
// Map template types to concrete types
@@ -2795,8 +3160,16 @@ class UnionTypeVisitor extends AnalysisVisitor
);
}
// Resolve any references to \static or \static[]
$union_type = $union_type->withStaticResolvedInContext($class->getInternalContext());
// Resolve any references to `static` or `static[]`
if ($this->context->isInClassScope() &&
$static_class_node instanceof Node &&
$static_class_node->kind === ast\AST_NAME &&
\strcasecmp($static_class_node->children['name'], 'parent') === 0) {
// If parent::foo() returns `static`, then use the current class instead of the parent class
$union_type = $union_type->withStaticResolvedInContext($this->context);
} else {
$union_type = $union_type->withStaticResolvedInContext($class->getInternalContext());
}
if ($combined_union_type) {
'@phan-var UnionType $combined_union_type';
@@ -2842,7 +3215,8 @@ class UnionTypeVisitor extends AnalysisVisitor
return self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
$node->children['expr'],
$this->should_catch_issue_exception
);
}
@@ -2859,17 +3233,19 @@ class UnionTypeVisitor extends AnalysisVisitor
*/
public function visitUnaryOp(Node $node): UnionType
{
// Shortcut some easy operators
$flags = $node->flags;
if ($flags === \ast\flags\UNARY_BOOL_NOT) {
return BoolType::instance(false)->asRealUnionType();
}
$result = self::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
$node->children['expr'],
$this->should_catch_issue_exception
);
// Shortcut some easy operators
$flags = $node->flags;
if ($flags === \ast\flags\UNARY_BOOL_NOT) {
return $result->applyUnaryNotOperator();
}
if ($flags === \ast\flags\UNARY_MINUS) {
$this->warnAboutInvalidUnaryOp(
$node,
@@ -2899,7 +3275,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$node,
static function (Type $type): bool {
// Adding $type instanceof StringType in case it becomes necessary later
return $type->isValidNumericOperand() || $type instanceof StringType;
// @phan-suppress-next-line PhanAccessMethodInternal
return ($type->isValidNumericOperand() && $type->isValidBitwiseOperand()) || $type instanceof StringType;
},
$result,
'~',
@@ -2945,7 +3322,7 @@ class UnionTypeVisitor extends AnalysisVisitor
return LiteralIntType::instanceForValue(1, false)->asRealUnionType();
}
/*
/**
* @param Node $node
* A node holding a class name
*
@@ -2973,7 +3350,7 @@ class UnionTypeVisitor extends AnalysisVisitor
))->getUnqualifiedNameForAnonymousClass();
// Turn that into a fully qualified name, and that into a union type
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
// @phan-suppress-next-line PhanThrowTypeMismatchForCall
$fqsen = FullyQualifiedClassName::fromStringInContext(
$anonymous_class_name,
$this->context
@@ -2997,7 +3374,7 @@ class UnionTypeVisitor extends AnalysisVisitor
return StaticType::instance(false)->asRealUnionType();
}
if (!Type::isSelfTypeString($class_name)) {
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
// @phan-suppress-next-line PhanThrowTypeMismatchForCall assuming FQSENException won't be thrown optimistically for valid ASTs
return self::unionTypeFromClassNode(
$this->code_base,
$this->context,
@@ -3017,7 +3394,7 @@ class UnionTypeVisitor extends AnalysisVisitor
}
// Reference to a parent class
if ($class_name === 'parent') {
if (\strcasecmp($class_name, 'parent') === 0) {
$class = $this->context->getClassInScope(
$this->code_base
);
@@ -3039,12 +3416,25 @@ class UnionTypeVisitor extends AnalysisVisitor
return $this->context->getClassFQSEN()->asType()->asRealUnionType();
}
/**
* @param Node $node @phan-unused-param
* A node containing a throw expression.
*
* @return UnionType
* `void` is as close as possible to `no-return` or `never` for types currently available in Phan.
*/
public function visitThrow(Node $node): UnionType
{
return NeverType::instance(false)->asRealUnionType();
}
private function classTypesForNonName(Node $node): UnionType
{
$node_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node
$node,
$this->should_catch_issue_exception
);
if ($node_type->isEmpty()) {
return UnionType::empty();
@@ -3216,7 +3606,7 @@ class UnionTypeVisitor extends AnalysisVisitor
// Check to see if the name is fully qualified
if ($node->flags & \ast\flags\NAME_NOT_FQ) {
self::checkValidClassFQSEN($class_name);
self::checkValidClassFQSEN($context, $node, $class_name);
$type = Type::fromStringInContext(
$class_name,
$context,
@@ -3237,7 +3627,7 @@ class UnionTypeVisitor extends AnalysisVisitor
$class_name = '\\' . $class_name;
}
self::checkValidClassFQSEN($class_name);
self::checkValidClassFQSEN($context, $node, $class_name);
$type = Type::fromFullyQualifiedString(
$class_name
);
@@ -3249,17 +3639,19 @@ class UnionTypeVisitor extends AnalysisVisitor
/**
* @throws FQSENException if invalid
*/
private static function checkValidClassFQSEN(string $class_name): void
private static function checkValidClassFQSEN(Context $context, Node $node, string $class_name): void
{
// @phan-suppress-next-line PhanAccessClassConstantInternal
if (\preg_match(FullyQualifiedGlobalStructuralElement::VALID_STRUCTURAL_ELEMENT_REGEX, $class_name)) {
return;
}
if ($class_name === '\\') {
throw new EmptyFQSENException("empty fqsen", $class_name);
} else {
throw new InvalidFQSENException("invalid fqsen", $class_name);
}
throw new IssueException(
Issue::fromType($class_name === '\\' ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)(
$context->getFile(),
$node->lineno,
[ $class_name ]
)
);
}
/**
@@ -3285,14 +3677,28 @@ class UnionTypeVisitor extends AnalysisVisitor
$union_type = self::unionTypeFromNode(
$this->code_base,
$this->context,
$node
$node,
$this->should_catch_issue_exception
)->withStaticResolvedInContext($this->context);
// Iterate over each viable class type to see if any
// have the constant we're looking for
foreach ($union_type->nonNativeTypes()->getTypeSet() as $class_type) {
foreach ($union_type->nonNativeTypes()->getUniqueFlattenedTypeSet() as $class_type) {
if (!$class_type->isObjectWithKnownFQSEN()) {
continue;
}
// Get the class FQSEN
$class_fqsen = FullyQualifiedClassName::fromType($class_type);
try {
$class_fqsen = FullyQualifiedClassName::fromType($class_type);
} catch (InvalidFQSENException $e) {
throw new IssueException(
Issue::fromType($e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike)(
$this->context->getFile(),
$node->lineno,
[ (string)$class_type ]
)
);
}
// See if the class exists
if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) {
@@ -3434,13 +3840,13 @@ class UnionTypeVisitor extends AnalysisVisitor
$code_base = $this->code_base;
$context = $this->context;
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $class_or_expr);
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $class_or_expr, $this->should_catch_issue_exception);
if ($union_type->isEmpty()) {
return [];
}
$object_types = $union_type->objectTypes();
if ($object_types->isEmpty()) {
if (!$union_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) {
if (!$union_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType(), $code_base)) {
$this->emitIssue(
Issue::TypeInvalidCallableObjectOfMethod,
$context->getLineNumberStart(),
@@ -3480,7 +3886,7 @@ class UnionTypeVisitor extends AnalysisVisitor
continue;
}
$class = $code_base->getClassByFQSEN($class_fqsen);
if (!$class->hasMethodWithName($code_base, $method_name)) {
if (!$class->hasMethodWithName($code_base, $method_name, true)) {
// emit error below
continue;
}
@@ -3583,13 +3989,13 @@ class UnionTypeVisitor extends AnalysisVisitor
}
$method_name = (new ContextNode($code_base, $context, $method_name))->getEquivalentPHPScalarValue();
if (!is_string($method_name)) {
$method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $method_name);
if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) {
$method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $method_name, $this->should_catch_issue_exception);
if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType(), $code_base)) {
Issue::maybeEmit(
$this->code_base,
$this->context,
$code_base,
$context,
Issue::TypeInvalidCallableMethodName,
$method_name->lineno ?? $this->context->getLineNumberStart(),
$method_name->lineno ?? $context->getLineNumberStart(),
$method_name_type
);
}
@@ -3642,7 +4048,7 @@ class UnionTypeVisitor extends AnalysisVisitor
return [];
}
$class = $code_base->getClassByFQSEN($class_fqsen);
if (!$class->hasMethodWithName($code_base, $method_name)) {
if (!$class->hasMethodWithName($code_base, $method_name, true)) {
$this->emitIssue(
Issue::UndeclaredStaticMethodInCallable,
$context->getLineNumberStart(),
@@ -3746,7 +4152,8 @@ class UnionTypeVisitor extends AnalysisVisitor
$union_type = self::unionTypeFromNode(
$this->code_base,
$this->context,
$node
$node,
$this->should_catch_issue_exception
);
$closure_types = [];
@@ -3844,6 +4251,7 @@ class UnionTypeVisitor extends AnalysisVisitor
return null;
}
// Precondition: minimum_target_php_version_id < 70100
private function analyzeNegativeStringOffsetCompatibility(Node $node, UnionType $dim_type): void
{
$dim_value = $dim_type->asSingleScalarValueOrNull();
@@ -3855,4 +4263,31 @@ class UnionTypeVisitor extends AnalysisVisitor
$node->children['dim']->lineno ?? $node->lineno
);
}
/**
* Returns the union of all union types of expressions in this expression list (ast\AST_EXPR_LIST).
*
* This is useful for match arm conditions.
*
* For other use cases, get the union type of the last node (if one exists) instead.
*
* @override
*/
public function visitExprList(Node $node): UnionType
{
$types = [];
foreach ($node->children as $child_node) {
$types[] = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $child_node, $this->should_catch_issue_exception);
}
return UnionType::merge($types);
}
/**
* @unused-param $node
* @override
*/
public function visitExit(Node $node): UnionType
{
return NeverType::instance(false)->asRealUnionType();
}
}
+21 -3
View File
@@ -38,11 +38,11 @@ class Element
}
// TODO: Revert this change back to the switch statement
// when php 7.2 is released and Phan supports php 7.2.
// in commonly used places if performance is better - php 7.2 optimizes this when opcache is enabled.
// TODO: Also look into initializing mappings of ast\Node->kind to ReflectionMethod->getClosure for those methods,
// it may be more efficient.
// See https://github.com/php/php-src/pull/2427/files
// This decreased the duration of running phan by about 4%
// This decreased the duration of running phan by about 4% prior to 7.2
public const VISIT_LOOKUP_TABLE = [
ast\AST_ARG_LIST => 'visitArgList',
ast\AST_ARRAY => 'visitArray',
@@ -51,14 +51,19 @@ class Element
ast\AST_ASSIGN => 'visitAssign',
ast\AST_ASSIGN_OP => 'visitAssignOp',
ast\AST_ASSIGN_REF => 'visitAssignRef',
ast\AST_ATTRIBUTE => 'visitAttribute',
ast\AST_ATTRIBUTE_LIST => 'visitAttributeList',
ast\AST_ATTRIBUTE_GROUP => 'visitAttributeGroup',
ast\AST_BINARY_OP => 'visitBinaryOp',
ast\AST_BREAK => 'visitBreak',
ast\AST_CALL => 'visitCall',
ast\AST_CALLABLE_CONVERT => 'visitCallableConvert',
ast\AST_CAST => 'visitCast',
ast\AST_CATCH => 'visitCatch',
ast\AST_CLASS => 'visitClass',
ast\AST_CLASS_CONST => 'visitClassConst',
ast\AST_CLASS_CONST_DECL => 'visitClassConstDecl',
ast\AST_CLASS_CONST_GROUP => 'visitClassConstGroup',
ast\AST_CLASS_NAME => 'visitClassName',
ast\AST_CLOSURE => 'visitClosure',
ast\AST_CLOSURE_USES => 'visitClosureUses',
@@ -72,6 +77,7 @@ class Element
ast\AST_ECHO => 'visitEcho',
ast\AST_EMPTY => 'visitEmpty',
ast\AST_ENCAPS_LIST => 'visitEncapsList',
ast\AST_ENUM_CASE => 'visitEnumCase',
ast\AST_EXIT => 'visitExit',
ast\AST_EXPR_LIST => 'visitExprList',
ast\AST_FOREACH => 'visitForeach',
@@ -83,11 +89,17 @@ class Element
ast\AST_IF_ELEM => 'visitIfElem',
ast\AST_INSTANCEOF => 'visitInstanceof',
ast\AST_MAGIC_CONST => 'visitMagicConst',
ast\AST_MATCH => 'visitMatch',
ast\AST_MATCH_ARM => 'visitMatchArm',
ast\AST_MATCH_ARM_LIST => 'visitMatchArmList',
ast\AST_METHOD => 'visitMethod',
ast\AST_METHOD_CALL => 'visitMethodCall',
ast\AST_NAME => 'visitName',
ast\AST_NAMED_ARG => 'visitNamedArg',
ast\AST_NAMESPACE => 'visitNamespace',
ast\AST_NEW => 'visitNew',
ast\AST_NULLSAFE_METHOD_CALL => 'visitNullsafeMethodCall',
ast\AST_NULLSAFE_PROP => 'visitNullsafeProp',
ast\AST_PARAM => 'visitParam',
ast\AST_PARAM_LIST => 'visitParamList',
ast\AST_PRE_INC => 'visitPreInc',
@@ -105,6 +117,7 @@ class Element
ast\AST_SWITCH_CASE => 'visitSwitchCase',
ast\AST_SWITCH_LIST => 'visitSwitchList',
ast\AST_TYPE => 'visitType',
ast\AST_TYPE_INTERSECTION => 'visitTypeIntersection',
ast\AST_TYPE_UNION => 'visitTypeUnion',
ast\AST_NULLABLE_TYPE => 'visitNullableType',
ast\AST_UNARY_OP => 'visitUnaryOp',
@@ -215,7 +228,12 @@ class Element
*/
public function acceptClassFlagVisitor(FlagVisitor $visitor)
{
switch ($this->node->flags) {
$flags = $this->node->flags;
if ($flags & flags\CLASS_ENUM) {
// ENUM is combined with abstract
return $visitor->visitClassEnum($this->node);
}
switch ($flags) {
case flags\CLASS_ABSTRACT:
return $visitor->visitClassAbstract($this->node);
case flags\CLASS_FINAL:
+5
View File
@@ -117,6 +117,11 @@ interface FlagVisitor
*/
public function visitClassAbstract(Node $node);
/**
* Visit a node with flag `\ast\flags\CLASS_ENUM`
*/
public function visitClassEnum(Node $node);
/**
* Visit a node with flag `\ast\flags\CLASS_FINAL`
*/
@@ -150,6 +150,11 @@ abstract class FlagVisitorImplementation implements FlagVisitor
return $this->visit($node);
}
public function visitClassEnum(Node $node)
{
return $this->visit($node);
}
public function visitClassFinal(Node $node)
{
return $this->visit($node);
+66
View File
@@ -28,6 +28,22 @@ interface KindVisitor
*/
public function visitArrayElem(Node $node);
// Attributes require AST version 80
/**
* Visit a node with kind `ast\AST_ATTRIBUTE`
*/
public function visitAttribute(Node $node);
/**
* Visit a node with kind `ast\AST_ATTRIBUTE_LIST`
*/
public function visitAttributeList(Node $node);
/**
* Visit a node with kind `ast\AST_ATTRIBUTE_GROUP`
*/
public function visitAttributeGroup(Node $node);
/**
* Visit a node with kind `ast\AST_ARROW_FUNC`
*/
@@ -63,6 +79,11 @@ interface KindVisitor
*/
public function visitCall(Node $node);
/**
* Visit a node with kind `\ast\AST_CALLABLE_CONVERT`
*/
public function visitCallableConvert(Node $node);
/**
* Visit a node with kind `\ast\AST_CAST`
*/
@@ -88,6 +109,11 @@ interface KindVisitor
*/
public function visitClassConstDecl(Node $node);
/**
* Visit a node with kind `\ast\AST_CLASS_CONST_GROUP`
*/
public function visitClassConstGroup(Node $node);
/**
* Visit a node with kind `\ast\AST_CLASS_NAME`
*/
@@ -153,6 +179,11 @@ interface KindVisitor
*/
public function visitEncapsList(Node $node);
/**
* Visit a node with kind `\ast\AST_ENUM_CASE`
*/
public function visitEnumCase(Node $node);
/**
* Visit a node with kind `\ast\AST_EXIT`
*/
@@ -218,11 +249,21 @@ interface KindVisitor
*/
public function visitMethodCall(Node $node);
/**
* Visit a node with kind `\ast\AST_NULLSAFE_METHOD_CALL`
*/
public function visitNullsafeMethodCall(Node $node);
/**
* Visit a node with kind `\ast\AST_NAME`
*/
public function visitName(Node $node);
/**
* Visit a node with kind `\ast\AST_NAMED_ARG`
*/
public function visitNamedArg(Node $node);
/**
* Visit a node with kind `\ast\AST_NAMESPACE`
*/
@@ -258,6 +299,11 @@ interface KindVisitor
*/
public function visitProp(Node $node);
/**
* Visit a node with kind `\ast\AST_NULLSAFE_PROP`
*/
public function visitNullsafeProp(Node $node);
/**
* Visit a node with kind `\ast\AST_PROP_DECL`
*/
@@ -313,11 +359,31 @@ interface KindVisitor
*/
public function visitSwitchList(Node $node);
/**
* Visit a node with kind `\ast\AST_MATCH`
*/
public function visitMatch(Node $node);
/**
* Visit a node with kind `\ast\AST_MATCH_ARM`
*/
public function visitMatchArm(Node $node);
/**
* Visit a node with kind `\ast\AST_MATCH_ARM_LIST`
*/
public function visitMatchArmList(Node $node);
/**
* Visit a node with kind `\ast\AST_TYPE`
*/
public function visitType(Node $node);
/**
* Visit a node with kind `\ast\AST_TYPE_INTERSECTION`
*/
public function visitTypeIntersection(Node $node);
/**
* Visit a node with kind `\ast\AST_TYPE_UNION`
*/
@@ -37,6 +37,7 @@ abstract class KindVisitorImplementation implements KindVisitor
/**
* @suppress PhanUnreferencedPublicMethod
* @suppress PhanPluginRemoveDebugAny deliberate warning for unhandled node kind
*/
public function handleMissingNodeKind(Node $node)
{
@@ -79,6 +80,22 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
// Attributes require AST version 80
public function visitAttribute(Node $node)
{
return $this->visit($node);
}
public function visitAttributeList(Node $node)
{
return $this->visit($node);
}
public function visitAttributeGroup(Node $node)
{
return $this->visit($node);
}
public function visitBinaryOp(Node $node)
{
return $this->visit($node);
@@ -94,6 +111,11 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitCallableConvert(Node $node)
{
return $this->visit($node);
}
public function visitCast(Node $node)
{
return $this->visit($node);
@@ -119,6 +141,11 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitClassConstGroup(Node $node)
{
return $this->visit($node);
}
public function visitClassName(Node $node)
{
return $this->visit($node);
@@ -179,6 +206,11 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitEnumCase(Node $node)
{
return $this->visit($node);
}
public function visitEncapsList(Node $node)
{
return $this->visit($node);
@@ -254,6 +286,11 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitNamedArg(Node $node)
{
return $this->visit($node);
}
public function visitNamespace(Node $node)
{
return $this->visit($node);
@@ -264,6 +301,16 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitNullsafeMethodCall(Node $node)
{
return $this->visit($node);
}
public function visitNullsafeProp(Node $node)
{
return $this->visit($node);
}
public function visitParam(Node $node)
{
return $this->visit($node);
@@ -344,11 +391,31 @@ abstract class KindVisitorImplementation implements KindVisitor
return $this->visit($node);
}
public function visitMatch(Node $node)
{
return $this->visit($node);
}
public function visitMatchArm(Node $node)
{
return $this->visit($node);
}
public function visitMatchArmList(Node $node)
{
return $this->visit($node);
}
public function visitType(Node $node)
{
return $this->visit($node);
}
public function visitTypeIntersection(Node $node)
{
return $this->visit($node);
}
public function visitTypeUnion(Node $node)
{
return $this->visit($node);
+54 -36
View File
@@ -9,6 +9,7 @@ use ast\Node;
use CompileError;
use InvalidArgumentException;
use ParseError;
use Phan\Analysis\AttributeAnalyzer;
use Phan\Analysis\DuplicateFunctionAnalyzer;
use Phan\Analysis\ParameterTypesAnalyzer;
use Phan\Analysis\ReferenceCountsAnalyzer;
@@ -34,6 +35,7 @@ use Phan\Parse\ParseVisitor;
use Phan\Plugin\ConfigPluginSet;
use Throwable;
use function count;
use function strlen;
use const STDERR;
@@ -106,17 +108,15 @@ class Analysis
return $context;
}
// TODO: Figure out why Phan doesn't suggest combining these catches except in language server mode
try {
$node = Parser::parseCode($code_base, $context, $request, $file_path, $file_contents, $suppress_parse_errors);
} catch (ParseError $_) {
return $context;
} catch (CompileError $_) {
return $context;
} catch (ParseException $_) {
} catch (ParseError | CompileError | ParseException $_) {
return $context;
}
if (Config::getValue('dump_ast')) {
// @phan-file-suppress PhanPluginRemoveDebugEcho
echo $file_path . "\n"
. \str_repeat("\u{00AF}", strlen($file_path))
. "\n";
@@ -199,7 +199,7 @@ class Analysis
if ($kind === ast\AST_DECLARE) {
// Check for class declarations, etc. within the statements of a declare directive.
$child_node = $node->children['stmts'];
if ($child_node !== null) {
if (\is_object($child_node)) {
// Step into each child node and get an
// updated context for the node
return self::parseNodeInContext($code_base, $inner_context, $child_node);
@@ -218,25 +218,23 @@ class Analysis
}
}
// For closed context elements (that have an inner scope)
// return the outer context instead of their inner context
// after we finish parsing their children.
if (\in_array($kind, [
ast\AST_CLASS,
ast\AST_METHOD,
ast\AST_FUNC_DECL,
ast\AST_ARROW_FUNC,
ast\AST_CLOSURE,
], true)) {
return $context;
switch ($kind) {
case ast\AST_CLASS:
case ast\AST_METHOD:
case ast\AST_FUNC_DECL:
case ast\AST_ARROW_FUNC:
case ast\AST_CLOSURE:
// For closed context elements (that have an inner scope)
// return the outer context instead of their inner context
// after we finish parsing their children.
return $context;
case ast\AST_STMT_LIST:
// Workaround that ensures that the context from namespace blocks gets passed to the caller.
return $child_context;
default:
// Pass the context back up to our parent
return $inner_context;
}
if ($kind === ast\AST_STMT_LIST) {
// Workaround that ensures that the context from namespace blocks gets passed to the caller.
return $child_context;
}
// Pass the context back up to our parent
return $inner_context;
}
/**
@@ -278,6 +276,11 @@ class Analysis
$function_or_method
);
AttributeAnalyzer::analyzeAttributesOfFunctionInterface(
$code_base,
$function_or_method
);
// Infer more accurate return types
// For daemon mode/the language server, we also call this whenever we use the return type of a function/method.
$function_or_method->analyzeReturnTypes($code_base);
@@ -313,7 +316,7 @@ class Analysis
$function_map = $code_base->getFunctionMap();
foreach ($function_map as $function) { // iterate, ignoring $fqsen
if ($show_progress) {
CLI::progress('function', (++$i) / (\count($function_map)), $function);
CLI::progress('function', (++$i) / (count($function_map)), $function);
}
$analyze_function_or_method($function);
}
@@ -328,7 +331,7 @@ class Analysis
// I suspect that method analysis is hydrating some of the classes,
// adding even more inherited methods to the end of the set.
// This recalculation is needed so that the progress bar is accurate.
CLI::progress('method', (++$i) / (\count($method_set)), $method);
CLI::progress('method', (++$i) / (count($method_set)), $method);
}
$analyze_function_or_method($method);
}
@@ -348,6 +351,7 @@ class Analysis
try {
$fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string);
} catch (FQSENException | InvalidArgumentException $e) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
\fprintf(STDERR, "getReturnTypeOverrides returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage());
continue;
}
@@ -403,6 +407,7 @@ class Analysis
}
}
} catch (FQSENException | InvalidArgumentException $e) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
\fprintf(STDERR, "getReturnTypeOverrides returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage());
}
}
@@ -420,7 +425,17 @@ class Analysis
// Note: This is used because it will create methods such as __construct if they do not exist.
if ($class->hasMethodWithName($code_base, $method_name, false)) {
$method = $class->getMethodByName($code_base, $method_name);
$method->setFunctionCallAnalyzer($closure);
$method->addFunctionCallAnalyzer($closure);
$methods_by_defining_fqsen = $methods_by_defining_fqsen ?? $code_base->getMethodsMapGroupedByDefiningFQSEN();
$fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($fqsen_string);
if (!$methods_by_defining_fqsen->offsetExists($fqsen)) {
continue;
}
foreach ($methods_by_defining_fqsen->offsetGet($fqsen) as $child_method) {
$child_method->addFunctionCallAnalyzer($closure);
}
}
} else {
// This is an override of a function.
@@ -430,8 +445,9 @@ class Analysis
$function->setFunctionCallAnalyzer($closure);
}
}
} catch (FQSENException $e) {
\fprintf(STDERR, "getAnalyzeFunctionCallClosures returned an invalid FQSEN %s\n", $e->getFQSEN());
} catch (FQSENException | InvalidArgumentException $e) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
\fprintf(STDERR, "getAnalyzeFunctionCallClosures returned an invalid FQSEN %s: %s\n", $fqsen_string, $e->getMessage());
}
}
}
@@ -444,6 +460,7 @@ class Analysis
*/
public static function analyzeClasses(CodeBase $code_base, array $path_filter = null): void
{
CLI::progress('classes', 0.0, null);
$classes = $code_base->getUserDefinedClassMap();
if (\is_array($path_filter)) {
// If a list of files is provided, then limit analysis to classes defined in those files.
@@ -455,13 +472,20 @@ class Analysis
}
}
}
$i = 0;
foreach ($classes as $class) {
CLI::progress('classes', $i++ / count($classes), null);
try {
$class->analyze($code_base);
} catch (RecursionDepthException $_) {
continue;
}
AttributeAnalyzer::analyzeAttributesOfClass(
$code_base,
$class
);
}
CLI::progress('classes', 1.0, null);
}
/**
@@ -533,13 +557,7 @@ class Analysis
return $context;
}
$node = Parser::parseCode($code_base, $context, $request, $file_path, $file_contents, false);
} catch (ParseException $_) {
// Issue::SyntaxError was already emitted.
return $context;
} catch (ParseError $_) {
// Issue::SyntaxError was already emitted.
return $context;
} catch (CompileError $_) {
} catch (ParseException | ParseError | CompileError $_) {
// Issue::SyntaxError was already emitted.
return $context;
}
+2 -2
View File
@@ -72,7 +72,7 @@ trait Analyzable
/**
* Clears the node so that it won't be used for analysis.
* @suppress PhanTypeMismatchProperty
* @suppress PhanTypeMismatchPropertyProbablyReal
*/
protected function clearNode(): void
{
@@ -113,7 +113,7 @@ trait Analyzable
if (!$definition_node) {
return $context;
}
self::ensureDidAnnotate($definition_node);
static::ensureDidAnnotate($definition_node);
// Closures depend on the context surrounding them such
// as for getting `use(...)` variables. Since we don't
+535 -66
View File
@@ -8,6 +8,7 @@ use AssertionError;
use ast;
use ast\Node;
use Closure;
use Exception;
use Phan\AST\ASTReverter;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
@@ -17,7 +18,9 @@ use Phan\Exception\CodeBaseException;
use Phan\Exception\IssueException;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\IssueFixSuggester;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Element\Parameter;
@@ -27,6 +30,7 @@ use Phan\Language\Type\FalseType;
use Phan\Language\Type\NullType;
use Phan\Language\UnionType;
use Phan\PluginV3\StopParamAnalysisException;
use Phan\Suggestion;
use function is_string;
@@ -67,7 +71,7 @@ final class ArgumentType
self::checkIsDeprecatedOrInternal($code_base, $context, $method);
if ($method->hasFunctionCallAnalyzer()) {
try {
$method->analyzeFunctionCall($code_base, $context->withLineNumberStart($node->lineno), $node->children['args']->children, $node);
$method->analyzeFunctionCall($code_base, $context->withLineNumberStart($node->lineno), $node->children['args']->children ?? [], $node);
} catch (StopParamAnalysisException $_) {
return;
}
@@ -75,10 +79,11 @@ final class ArgumentType
// Emit an issue if this is an externally accessed internal method
$arglist = $node->children['args'];
$argcount = \count($arglist->children);
$arglist_children = $arglist->children ?? [];
$argcount = \count($arglist_children);
// Make sure we have enough arguments
if ($argcount < $method->getNumberOfRequiredParameters() && !self::isUnpack($arglist->children)) {
if ($argcount < $method->getNumberOfRequiredParameters() && !self::isUnpack($arglist_children)) {
$alternate_found = false;
foreach ($method->alternateGenerator($code_base) as $alternate_method) {
$alternate_found = $alternate_found || (
@@ -130,12 +135,16 @@ final class ArgumentType
}
// Check the parameter types
self::analyzeParameterList(
$code_base,
$method,
$arglist,
$context
);
// NOTE: Attributes have an optional arg list, which is the same as 0 args.
// Because there are 0 args, no argument types need to be checked.
if ($arglist instanceof Node) {
self::analyzeParameterList(
$code_base,
$method,
$arglist,
$context
);
}
}
/**
@@ -183,13 +192,27 @@ final class ArgumentType
}
}
}
} else {
try {
$class_type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$class_node
);
} catch (Exception $_) {
return;
}
if ($class_type->isEmpty() || $class_type->hasPossiblyObjectTypes()) {
return;
}
}
Issue::maybeEmit(
$code_base,
$context,
$issue_type,
$node->lineno,
$method->getRepresentationForIssue()
$method->getRepresentationForIssue(),
ASTReverter::toShortString($node)
);
}
@@ -239,7 +262,8 @@ final class ArgumentType
$context,
Issue::DeprecatedFunctionInternal,
$context->getLineNumberStart(),
$method->getRepresentationForIssue()
$method->getRepresentationForIssue(),
$method->getDeprecationReason()
);
}
} else {
@@ -341,10 +365,10 @@ final class ArgumentType
if ($argcount < $method->getNumberOfRequiredParameters() && !self::isUnpack($arg_nodes)) {
$alternate_found = false;
foreach ($method->alternateGenerator($code_base) as $alternate_method) {
$alternate_found = $alternate_found || (
$argcount >=
$alternate_method->getNumberOfRequiredParameters()
);
if ($argcount >= $alternate_method->getNumberOfRequiredParameters()) {
$alternate_found = true;
break;
}
}
if (!$alternate_found) {
@@ -426,10 +450,82 @@ final class ArgumentType
return;
}
}
$positions_used = null;
foreach ($arg_nodes as $i => $argument) {
// Get the parameter associated with this argument
$parameter = $method->getParameterForCaller($i);
foreach ($arg_nodes as $original_i => $argument) {
if (!\is_int($original_i)) {
throw new AssertionError("Expected argument index to be an integer");
}
$i = $original_i;
if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) {
['name' => $argument_name, 'expr' => $argument_expression] = $argument->children;
if ($argument_expression === null) {
throw new AssertionError("Expected argument to have an expression");
}
$found = false;
// TODO: Could optimize for long lists by precomputing a map, probably not worth it
foreach ($method->getRealParameterList() as $j => $parameter) {
if ($parameter->getName() === $argument_name) {
if ($parameter->isVariadic()) {
self::emitSuspiciousNamedArgumentForVariadic($code_base, $context, $method, $argument);
}
$found = true;
$i = $j;
break;
}
}
if (!isset($parameter)) {
self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument);
continue;
}
if (!$found) {
if (!$parameter->isVariadic()) {
self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument);
} elseif ($method->isPHPInternal()) {
self::emitSuspiciousNamedArgumentVariadicInternal($code_base, $context, $method, $argument);
}
continue;
}
if (!\is_array($positions_used)) {
$positions_used = \array_slice($arg_nodes, 0, $original_i);
}
} else {
// Get the parameter associated with this argument
// FIXME: Use the real parameter name all the time for named arguments if it exists
$parameter = $method->getParameterForCaller($i);
$argument_expression = $argument;
}
if (\is_array($positions_used)) {
$reused_argument = $positions_used[$i] ?? null;
if ($reused_argument !== null && $parameter && !$parameter->isVariadic()) {
if ($method->isPHPInternal()) {
Issue::maybeEmit(
$code_base,
$context,
Issue::DuplicateNamedArgumentInternal,
$argument->lineno ?? $context->getLineNumberStart(),
ASTReverter::toShortString($argument),
ASTReverter::toShortString($reused_argument),
$method->getRepresentationForIssue(true)
);
} else {
Issue::maybeEmit(
$code_base,
$context,
Issue::DuplicateNamedArgument,
$argument->lineno ?? $context->getLineNumberStart(),
ASTReverter::toShortString($argument),
ASTReverter::toShortString($reused_argument),
$method->getRepresentationForIssue(true),
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart()
);
}
} else {
$positions_used[$i] = $argument;
}
}
// This issue should be caught elsewhere
if (!$parameter) {
@@ -446,7 +542,17 @@ final class ArgumentType
Issue::maybeEmitInstance($code_base, $context, $e->getIssueInstance());
continue;
}
self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno ?? $context->getLineNumberStart(), $i, $argument);
$lineno = $argument->lineno ?? $context->getLineNumberStart();
self::analyzeParameter(
$code_base,
$context,
$method,
$argument_type,
$lineno,
$i,
$argument,
new ast\Node(ast\AST_ARG_LIST, 0, $arg_nodes, $lineno)
);
if ($parameter->isPassByReference()) {
if ($argument instanceof Node) {
// @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers
@@ -454,6 +560,9 @@ final class ArgumentType
}
}
}
if (\is_array($positions_used)) {
self::checkAllNamedArgumentsPassed($code_base, $context, $context->getLineNumberStart(), $method, $positions_used);
}
}
/**
@@ -492,23 +601,95 @@ final class ArgumentType
return;
}
}
$positions_used = null;
foreach ($node->children as $i => $argument) {
if (!\is_int($i)) {
foreach ($node->children as $original_i => $argument) {
if (!\is_int($original_i)) {
throw new AssertionError("Expected argument index to be an integer");
}
$i = $original_i;
if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) {
['name' => $argument_name, 'expr' => $argument_expression] = $argument->children;
if ($argument_expression === null) {
throw new AssertionError("Expected argument to have an expression");
}
$found = false;
// TODO: Could optimize for long lists by precomputing a map, probably not worth it
foreach ($method->getRealParameterList() as $j => $parameter) {
if ($parameter->getName() === $argument_name) {
if ($parameter->isVariadic()) {
self::emitSuspiciousNamedArgumentForVariadic($code_base, $context, $method, $argument);
}
$found = true;
$i = $j;
break;
}
}
if (!isset($parameter)) {
self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument);
continue;
}
if (!$found) {
if (!$parameter->isVariadic()) {
self::emitUndeclaredNamedArgument($code_base, $context, $method, $argument);
} elseif ($method->isPHPInternal()) {
self::emitSuspiciousNamedArgumentVariadicInternal($code_base, $context, $method, $argument);
}
continue;
}
if (!\is_array($positions_used)) {
$positions_used = \array_slice($node->children, 0, $original_i);
}
} else {
// Get the parameter associated with this argument
// FIXME: Use the real parameter name all the time for named arguments if it exists
$parameter = $method->getParameterForCaller($i);
$argument_expression = $argument;
}
if (\is_array($positions_used)) {
$reused_argument = $positions_used[$i] ?? null;
if ($reused_argument !== null && $parameter && !$parameter->isVariadic()) {
if ($method->isPHPInternal()) {
Issue::maybeEmit(
$code_base,
$context,
Issue::DuplicateNamedArgumentInternal,
$argument->lineno ?? $node->lineno,
ASTReverter::toShortString($argument),
ASTReverter::toShortString($reused_argument),
$method->getRepresentationForIssue(true)
);
} else {
Issue::maybeEmit(
$code_base,
$context,
Issue::DuplicateNamedArgument,
$argument->lineno ?? $node->lineno,
ASTReverter::toShortString($argument),
ASTReverter::toShortString($reused_argument),
$method->getRepresentationForIssue(true),
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart()
);
}
} else {
$positions_used[$i] = $argument;
}
}
// Get the parameter associated with this argument
$parameter = $method->getParameterForCaller($i);
// This issue should be caught elsewhere
if (!$parameter) {
$argument_type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$argument,
$argument_expression,
true
);
if ($argument_type->isVoidType()) {
self::warnVoidTypeArgument($code_base, $context, $argument_expression, $node);
}
continue;
}
@@ -517,8 +698,8 @@ final class ArgumentType
// If this is a pass-by-reference parameter, make sure
// we're passing an allowable argument
if ($parameter->isPassByReference()) {
if ((!$argument instanceof Node) || !\in_array($argument_kind, self::REFERENCE_NODE_KINDS, true)) {
$is_possible_reference = self::isExpressionReturningReference($code_base, $context, $argument);
if ((!$argument_expression instanceof Node) || !\in_array($argument_kind, self::REFERENCE_NODE_KINDS, true)) {
$is_possible_reference = self::isExpressionReturningReference($code_base, $context, $argument_expression);
if (!$is_possible_reference) {
Issue::maybeEmit(
@@ -527,14 +708,14 @@ final class ArgumentType
Issue::TypeNonVarPassByRef,
$argument->lineno ?? $node->lineno ?? 0,
($i + 1),
$method->getRepresentationForIssue()
$method->getRepresentationForIssue(true)
);
}
} else {
$variable_name = (new ContextNode(
$code_base,
$context,
$argument
$argument_expression
))->getVariableName();
if (Type::isSelfTypeString($variable_name)
@@ -545,7 +726,7 @@ final class ArgumentType
$code_base,
$context,
Issue::ContextNotObject,
$argument->lineno ?? $node->lineno ?? 0,
$argument->lineno ?? $node->lineno,
"$variable_name"
);
}
@@ -557,21 +738,184 @@ final class ArgumentType
$argument_type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$argument,
$argument_expression,
true
);
if ($argument_type->isVoidType()) {
self::warnVoidTypeArgument($code_base, $context, $argument_expression, $node);
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno ?? $node->lineno, $i, $argument);
self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno ?? $node->lineno, $i, $argument_expression, $node);
if ($parameter->isPassByReference()) {
if ($argument instanceof Node) {
if ($argument_expression instanceof Node) {
// @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers
$argument->is_reference = true;
$argument_expression->is_reference = true;
}
}
if ($argument_kind === ast\AST_UNPACK && $argument instanceof Node) {
self::analyzeRemainingParametersForVariadic($code_base, $context, $method, $i + 1, $node, $argument, $argument_type);
if ($argument_kind === ast\AST_UNPACK && $argument_expression instanceof Node) {
self::analyzeRemainingParametersForVariadic($code_base, $context, $method, $i + 1, $node, $argument_expression, $argument_type);
}
}
if (\is_array($positions_used)) {
self::checkAllNamedArgumentsPassed($code_base, $context, $node->lineno, $method, $positions_used);
}
}
private static function emitSuspiciousNamedArgumentForVariadic(
CodeBase $code_base,
Context $context,
FunctionInterface $method,
Node $argument
): void {
$argument_name = $argument->children['name'];
Issue::maybeEmit(
$code_base,
$context,
Issue::SuspiciousNamedArgumentForVariadic,
$argument->lineno,
$argument_name,
$method->getRepresentationForIssue(true),
$argument_name
);
}
private static function emitUndeclaredNamedArgument(
CodeBase $code_base,
Context $context,
FunctionInterface $method,
Node $argument
): void {
$parameter_suggestions = [];
foreach ($method->getRealParameterList() as $parameter) {
if (!$parameter->isVariadic()) {
$name = $parameter->getName();
$parameter_suggestions[$name] = $name;
}
}
$argument_name = $argument->children['name'];
$suggested_arguments = IssueFixSuggester::getSuggestionsForStringSet($argument_name, $parameter_suggestions);
$suggestion = $suggested_arguments ? Suggestion::fromString('Did you mean ' . \implode(' ', $suggested_arguments)) : null;
if ($method->isPHPInternal()) {
Issue::maybeEmitWithParameters(
$code_base,
$context,
Issue::UndeclaredNamedArgumentInternal,
$argument->lineno,
[ASTReverter::toShortString($argument), $method->getRepresentationForIssue(true)],
$suggestion
);
} else {
Issue::maybeEmitWithParameters(
$code_base,
$context,
Issue::UndeclaredNamedArgument,
$argument->lineno,
[
ASTReverter::toShortString($argument),
$method->getRepresentationForIssue(true),
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart(),
],
$suggestion
);
}
}
/**
* Warn about using named arguments with internal functions,
* ignoring known exceptions such as call_user_func, ReflectionMethod->invoke, etc.
* @param FunctionInterface $method an internal function
* @param Node $argument a node of kind ast\AST_NAMED_ARG
*/
private static function emitSuspiciousNamedArgumentVariadicInternal(
CodeBase $code_base,
Context $context,
FunctionInterface $method,
Node $argument
): void {
$fqsen = $method instanceof Method ? $method->getRealDefiningFQSEN() : $method->getFQSEN();
if (!\in_array($fqsen->__toString(), [
'\call_user_func',
'\ReflectionMethod::invoke',
'\ReflectionMethod::newInstance',
'\ReflectionFunction::invoke',
'\ReflectionFunction::newInstance',
'\ReflectionFunctionAbstract::invoke',
'\ReflectionFunctionAbstract::newInstance',
'\Closure::call',
'\Closure::__invoke',
], true)) {
Issue::maybeEmitWithParameters(
$code_base,
$context,
Issue::SuspiciousNamedArgumentVariadicInternal,
$argument->lineno,
[
ASTReverter::toShortString($argument),
$method->getRepresentationForIssue(true),
]
);
}
}
/**
* @param array<int,mixed> $positions_used
*/
private static function checkAllNamedArgumentsPassed(
CodeBase $code_base,
Context $context,
int $lineno,
FunctionInterface $method,
array $positions_used
): void {
foreach ($method->getRealParameterList() as $i => $parameter) {
if ($parameter->isOptional() || $parameter->isVariadic()) {
continue;
}
if (isset($positions_used[$i])) {
continue;
}
if ($method->isPHPInternal()) {
Issue::maybeEmit(
$code_base,
$context,
Issue::MissingNamedArgumentInternal,
$lineno,
$parameter,
$method->getRepresentationForIssue(true)
);
} else {
Issue::maybeEmit(
$code_base,
$context,
Issue::MissingNamedArgument,
$lineno,
$parameter,
$method->getRepresentationForIssue(true),
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart()
);
}
}
}
/**
* @param Node|string|int|float|null $argument
*/
private static function warnVoidTypeArgument(
CodeBase $code_base,
Context $context,
$argument,
Node $node
): void {
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeVoidArgument,
$argument->lineno ?? $node->lineno,
ASTReverter::toShortString($argument)
);
}
private static function analyzeRemainingParametersForVariadic(
@@ -613,14 +957,14 @@ final class ArgumentType
Issue::TypeNonVarPassByRef,
$argument->lineno ?? $node->lineno ?? 0,
($i + 1),
$method->getRepresentationForIssue()
$method->getRepresentationForIssue(true)
);
}
}
// Omit ContextNotObject check, this was checked for the first matching parameter
}
self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno, $i, $argument);
self::analyzeParameter($code_base, $context, $method, $argument_type, $argument->lineno, $i, $argument, $node);
if ($parameter->isPassByReference()) {
// @phan-suppress-next-line PhanUndeclaredProperty this is added for analyzers
$argument->is_reference = true;
@@ -631,14 +975,17 @@ final class ArgumentType
/**
* Analyze passing the an argument of type $argument_type to the ith parameter of the (possibly variadic) method $method,
* for a call made from the line $lineno.
*
* @param int $i the index of the parameter.
* @param Node|string|int|float $argument_node
* @param ?Node $node the node of the call TODO: Default
*/
public static function analyzeParameter(CodeBase $code_base, Context $context, FunctionInterface $method, UnionType $argument_type, int $lineno, int $i, $argument_node): void
public static function analyzeParameter(CodeBase $code_base, Context $context, FunctionInterface $method, UnionType $argument_type, int $lineno, int $i, $argument_node, ?Node $node): void
{
// Expand it to include all parent types up the chain
try {
$argument_type_expanded_resolved =
$argument_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base);
$argument_type_resolved = $argument_type->withStaticResolvedInContext($context);
$argument_type_expanded_resolved = $argument_type_resolved->asExpandedTypes($code_base);
} catch (RecursionDepthException $_) {
return;
}
@@ -656,21 +1003,36 @@ final class ArgumentType
if (\is_null($candidate_alternate_parameter)) {
continue;
}
if ($alternate_parameter && $node) {
// If another function was already checked which had the right number of alternate parameters, don't bother allowing checks with param
$arglist = $node->kind === ast\AST_ARG_LIST ? $node : ($node->children['args'] ?? null);
if ($arglist) {
$argcount = \count($arglist->children);
// Make sure we have enough arguments
if ($argcount < $alternate_method->getNumberOfRequiredParameters() && !self::isUnpack($arglist->children)) {
continue;
}
}
}
$alternate_parameter = $candidate_alternate_parameter;
$alternate_parameter_type = $alternate_parameter->getNonVariadicUnionType()->withStaticResolvedInFunctionLike($alternate_method);
// See if the argument can be cast to the
// parameter
if ($argument_type_expanded_resolved->canCastToUnionType($alternate_parameter_type)) {
// See if the argument can be cast to the parameter.
// TODO: In order for intersection types to work (e.g. casting ArrayObject to ArrayAccess&Countable),
// the codebase must be passed into canCastToUnionType (instead of expanding types), because ArrayAccess|Countable is not a type that casts to ArrayAccess&Countable
//
// TODO: Stop expanding the argument type
if ($argument_type_resolved->canCastToUnionType($alternate_parameter_type, $code_base)) {
if ($alternate_parameter_type->hasRealTypeSet() && $argument_type->hasRealTypeSet()) {
$real_parameter_type = $alternate_parameter_type->getRealUnionType();
$real_argument_type = $argument_type->getRealUnionType();
$real_argument_type_expanded_resolved = $real_argument_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base);
if (!$real_argument_type_expanded_resolved->canCastToDeclaredType($code_base, $context, $real_parameter_type)) {
$real_argument_type_expanded_resolved_nonnull = $real_argument_type_expanded_resolved->nonNullableClone();
if ($real_argument_type_expanded_resolved_nonnull->isEmpty() ||
!$real_argument_type_expanded_resolved_nonnull->canCastToDeclaredType($code_base, $context, $real_parameter_type)) {
$real_argument_type_resolved = $real_argument_type->withStaticResolvedInContext($context);
if (!$real_argument_type_resolved->canCastToDeclaredType($code_base, $context, $real_parameter_type)) {
$real_argument_type_resolved_nonnull = $real_argument_type_resolved->nonNullableClone();
if ($real_argument_type_resolved_nonnull->isEmpty() ||
!$real_argument_type_resolved_nonnull->canCastToDeclaredType($code_base, $context, $real_parameter_type)) {
// We know that the inferred real types don't match with the strict_types setting of the caller
// (e.g. null -> any non-null type)
// Try checking any other alternates, and emit PhanTypeMismatchArgumentReal if that fails.
@@ -684,6 +1046,9 @@ final class ArgumentType
if (Config::get_strict_param_checking() && $argument_type->typeCount() > 1) {
self::analyzeParameterStrict($code_base, $context, $method, $argument_node, $argument_type, $alternate_parameter, $alternate_parameter_type, $lineno, $i);
}
if ($alternate_parameter->shouldWarnIfProvided()) {
self::maybeWarnProvidingUnusedParameter($code_base, $context, $lineno, $method, $alternate_parameter, $i);
}
return;
}
}
@@ -694,6 +1059,9 @@ final class ArgumentType
if (!isset($alternate_parameter_type)) {
throw new AssertionError('Impossible - should be set if $alternate_parameter is set');
}
if ($alternate_parameter->shouldWarnIfProvided()) {
self::maybeWarnProvidingUnusedParameter($code_base, $context, $lineno, $method, $alternate_parameter, $i);
}
if ($alternate_parameter->isPassByReference() && $alternate_parameter->getReferenceType() === Parameter::REFERENCE_WRITE_ONLY) {
return;
@@ -706,10 +1074,11 @@ final class ArgumentType
// TODO: Warn about the type without the templates?
return;
}
// FIXME: This may be obsolete after changing canCastToDeclaredType
if ($alternate_parameter_type->hasTemplateParameterTypes()) {
// TODO: Make the check for templates recursive
$argument_type_expanded_templates = $argument_type->asExpandedTypesPreservingTemplate($code_base);
if ($argument_type_expanded_templates->canCastToUnionTypeHandlingTemplates($alternate_parameter_type, $code_base)) {
if ($argument_type_expanded_templates->canCastToUnionType($alternate_parameter_type, $code_base)) {
// - can cast MyClass<\stdClass> to MyClass<mixed>
// - can cast Some<\stdClass> to Option<\stdClass>
// - cannot cast Some<\SomeOtherClass> to Option<\stdClass>
@@ -723,8 +1092,8 @@ final class ArgumentType
// and the argument we are passing has a __toString method then it is ok
if (!$context->isStrictTypes() && $alternate_parameter_type->hasNonNullStringType()) {
try {
foreach ($argument_type_expanded_resolved->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, "__toString")) {
foreach ($argument_type_resolved->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, "__toString", true)) {
return;
}
}
@@ -737,6 +1106,48 @@ final class ArgumentType
self::warnInvalidArgumentType($code_base, $context, $method, $alternate_parameter, $alternate_parameter_type, $argument_node, $argument_type, $argument_type->asExpandedTypes($code_base), $argument_type_expanded_resolved, $lineno, $i);
}
private static function maybeWarnProvidingUnusedParameter(
CodeBase $code_base,
Context $context,
int $lineno,
FunctionInterface $method,
Parameter $parameter,
int $i
): void {
if ($method->getNumberOfRequiredParameters() > $i) {
// handle required parameter after optional
return;
}
if ($method->isPHPInternal()) {
// not supported for stubs
return;
}
$fqsen = $method->getFQSEN();
if ($fqsen->getAlternateId() > 0) {
return;
}
if ($method instanceof Method) {
if ($method->isOverriddenByAnother() || $code_base->hasMethodWithFQSEN($fqsen->withAlternateId(1))) {
return;
}
}
$issue_type = $method instanceof Func && $method->isClosure() ? Issue::ProvidingUnusedParameterOfClosure : Issue::ProvidingUnusedParameter;
if ($method->hasSuppressIssue($issue_type)) {
// For convenience, allow suppressing it on the method definition as well.
return;
}
Issue::maybeEmit(
$code_base,
$context,
$issue_type,
$lineno,
$parameter->getName(),
$method->getRepresentationForIssue(true),
$method->getFileRef()->getFile(),
$method->getFileRef()->getLineNumberStart()
);
}
/**
* @param Node|string|int|float $argument_node
*/
@@ -753,6 +1164,7 @@ final class ArgumentType
int $lineno,
int $i
): void {
$context = (clone $context)->withLineNumberStart($lineno);
/**
* @return ?string
*/
@@ -762,8 +1174,9 @@ final class ArgumentType
// Record that the most severe issue type suppression was used and don't emit any issue.
return null;
}
// @phan-suppress-next-line PhanAccessMethodInternal
if (!$argument_type_expanded_resolved->canCastToUnionTypeIfNonNull($alternate_parameter_type)) {
// @phan-suppress-next-next-line PhanAccessMethodInternal
if ($argument_type_expanded_resolved->isNull() ||
!($argument_type_expanded_resolved->containsNullable() && $argument_type_expanded_resolved->canCastToUnionTypeIfNonNull($alternate_parameter_type, $code_base))) {
if ($argument_type->hasRealTypeSet() && $alternate_parameter_type->hasRealTypeSet()) {
$real_arg_type = $argument_type->getRealUnionType();
$real_parameter_type = $alternate_parameter_type->getRealUnionType();
@@ -773,6 +1186,7 @@ final class ArgumentType
}
return $issue_type;
}
// Intended postcondition: The expanded argument type contains null but isn't exclusively null, or the nullability doesn't affect the issue.
if (Issue::shouldSuppressIssue($code_base, $context, $issue_type, $lineno, [])) {
return null;
}
@@ -786,7 +1200,6 @@ final class ArgumentType
}
if ($issue_type === Issue::TypeMismatchArgumentInternal) {
if ($argument_type->hasRealTypeSet() &&
!$alternate_parameter_type->hasRealTypeSet() &&
!$argument_type->getRealUnionType()->canCastToDeclaredType($code_base, $context, $alternate_parameter_type)) {
// PHP 7.x doesn't have reflection types for many methods and global functions and won't throw,
// but will emit a warning and fail the call.
@@ -857,6 +1270,60 @@ final class ArgumentType
);
return;
}
if ($context->hasSuppressIssue($code_base, Issue::TypeMismatchArgumentProbablyReal)) {
// Suppressing ProbablyReal also suppresses the less severe version.
return;
}
if ($issue_type === Issue::TypeMismatchArgument) {
if ($argument_type->hasRealTypeSet() &&
!$argument_type->getRealUnionType()->canCastToDeclaredType($code_base, $context, $alternate_parameter_type)) {
// The argument's real type is completely incompatible with the documented phpdoc type.
//
// Either the phpdoc type is wrong or the argument is likely wrong.
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeMismatchArgumentProbablyReal,
$lineno,
($i + 1),
$alternate_parameter->getName(),
ASTReverter::toShortString($argument_node),
$argument_type_expanded,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($argument_type),
$method->getRepresentationForIssue(),
$alternate_parameter_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($alternate_parameter_type),
$method->getFileRef()->getFile(),
$method->getFileRef()->getLineNumberStart()
);
return;
}
if ($context->hasSuppressIssue($code_base, Issue::TypeMismatchArgument)) {
// Suppressing ProbablyReal also suppresses the less severe version.
return;
}
if (PostOrderAnalysisVisitor::doesExpressionHaveSuperClassOfTargetType(
$code_base,
$argument_type,
$alternate_parameter_type
)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeMismatchArgumentSuperType,
$lineno,
($i + 1),
$alternate_parameter->getName(),
ASTReverter::toShortString($argument_node),
$argument_type_expanded->withUnionType($argument_type_expanded_resolved),
$method->getRepresentationForIssue(),
(string)$alternate_parameter_type,
$method->getFileRef()->getFile(),
$method->getFileRef()->getLineNumberStart()
);
return;
}
}
Issue::maybeEmit(
$code_base,
$context,
@@ -887,37 +1354,38 @@ final class ArgumentType
}
$mismatch_type_set = UnionType::empty();
$mismatch_expanded_types = null;
$mismatch_resolved_types = null;
// For the strict
foreach ($type_set as $type) {
// Expand it to include all parent types up the chain
$individual_type_expanded = $type->withStaticResolvedInContext($context)->asExpandedTypes($code_base);
$type_resolved = $type->withStaticResolvedInContext($context)->asPHPDocUnionType();
// See if the argument can be cast to the
// parameter
if (!$individual_type_expanded->canCastToUnionType(
$parameter_type
if (!$type_resolved->canCastToUnionType(
$parameter_type,
$code_base
)) {
if ($method->isPHPInternal()) {
// If we are not in strict mode and we accept a string parameter
// and the argument we are passing has a __toString method then it is ok
if (!$context->isStrictTypes() && $parameter_type->hasNonNullStringType()) {
if ($individual_type_expanded->hasClassWithToStringMethod($code_base, $context)) {
if ($type_resolved->hasClassWithToStringMethod($code_base, $context)) {
continue; // don't warn about $type
}
}
}
$mismatch_type_set = $mismatch_type_set->withType($type);
if ($mismatch_expanded_types === null) {
if ($mismatch_resolved_types === null) {
// Warn about the first type
$mismatch_expanded_types = $individual_type_expanded;
$mismatch_resolved_types = $type_resolved;
}
}
}
if ($mismatch_expanded_types === null) {
if ($mismatch_resolved_types === null) {
// No mismatches
return;
}
@@ -934,7 +1402,7 @@ final class ArgumentType
$argument_type,
$method->getRepresentationForIssue(),
(string)$parameter_type,
$mismatch_expanded_types
$mismatch_resolved_types->asExpandedTypes($code_base)
);
return;
}
@@ -949,7 +1417,7 @@ final class ArgumentType
$argument_type,
$method->getRepresentationForIssue(),
(string)$parameter_type,
$mismatch_expanded_types,
$mismatch_resolved_types->asExpandedTypes($code_base),
$method->getFileRef()->getFile(),
$method->getFileRef()->getLineNumberStart()
);
@@ -999,7 +1467,7 @@ final class ArgumentType
return true;
}
}
} elseif ($node_kind === ast\AST_STATIC_CALL || $node_kind === ast\AST_METHOD_CALL) {
} elseif (\in_array($node_kind, [ast\AST_STATIC_CALL, ast\AST_METHOD_CALL, ast\AST_NULLSAFE_METHOD_CALL], true)) {
$method_name = $node->children['method'] ?? null;
if (is_string($method_name)) {
$class_node = $node->children['class'] ?? $node->children['expr'];
@@ -1014,7 +1482,8 @@ final class ArgumentType
) as $class) {
if (!$class->hasMethodWithName(
$code_base,
$method_name
$method_name,
true
)) {
continue;
}
@@ -157,6 +157,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$this->emitIssue(
Issue::InvalidWriteToTemporaryExpression,
$assign_op_node->lineno,
ASTReverter::toShortString($node),
Type::fromObject($node)
);
return $this->context;
@@ -166,6 +167,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$this->emitIssue(
Issue::InvalidWriteToTemporaryExpression,
$node->lineno,
ASTReverter::toShortString($node),
Type::fromObject($expr_node)
);
return $this->context;
@@ -244,6 +246,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$this->emitIssue(
Issue::InvalidWriteToTemporaryExpression,
$assign_op_node->lineno,
ASTReverter::toShortString($node),
Type::fromObject($node)
);
return $this->context;
@@ -253,6 +256,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$this->emitIssue(
Issue::InvalidWriteToTemporaryExpression,
$node->lineno,
ASTReverter::toShortString($node),
Type::fromObject($expr_node)
);
return $this->context;
@@ -327,7 +331,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
// If both left and right are arrays, then this is array
// concatenation.
if ($left->isGenericArray() && $right->isGenericArray()) {
BinaryOperatorFlagVisitor::checkInvalidArrayShapeCombination($this->code_base, $this->context, $node, $left, $right);
BinaryOperatorFlagVisitor::checkInvalidArrayShapeCombination($code_base, $context, $node, $left, $right);
if ($left->isEqualTo($right)) {
return $left;
}
@@ -367,12 +371,12 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
}
$left_is_array = (
!$left->genericArrayElementTypes()->isEmpty()
!$left->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $left->nonArrayTypes()->isEmpty()
) || $left->isType($array_type);
$right_is_array = (
!$right->genericArrayElementTypes()->isEmpty()
!$right->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $right->nonArrayTypes()->isEmpty()
) || $right->isType($array_type);
@@ -384,7 +388,8 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
if ($left_is_array
&& !$right->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
ArrayType::instance(false)->asPHPDocUnionType(),
$code_base
)
) {
$this->emitIssue(
@@ -392,7 +397,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$node->lineno ?? 0
);
return UnionType::empty();
} elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType())) {
} elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType(), $code_base)) {
$this->emitIssue(
Issue::TypeInvalidLeftOperand,
$node->lineno ?? 0
@@ -566,6 +571,17 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
// Expect int|string
$right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
$this->warnAboutInvalidUnionType(
$node,
static function (Type $type): bool {
return ($type instanceof IntType || $type instanceof StringType || $type instanceof MixedType) && !$type->isNullableLabeled();
},
$left_type,
$right_type,
Issue::TypeInvalidLeftOperandOfBitwiseOp,
Issue::TypeInvalidRightOperandOfBitwiseOp
);
if (!$this->context->isInLoop()) {
if ($left_type->isNonNullNumberType() && $right_type->isNonNullNumberType()) {
return BinaryOperatorFlagVisitor::computeIntOrFloatOperationResult($node, $left_type, $right_type);
@@ -705,7 +721,7 @@ class AssignOperatorAnalysisVisitor extends FlagVisitorImplementation
$this->warnAboutInvalidUnionType(
$node,
static function (Type $type): bool {
return $type instanceof IntType && !$type->isNullable();
return ($type instanceof IntType || $type instanceof MixedType) && !$type->isNullableLabeled();
},
$left,
$right,
@@ -269,17 +269,10 @@ class AssignOperatorFlagVisitor extends FlagVisitorImplementation
$probably_array_type = UnionType::of([ArrayType::instance(false)], $int_or_float_or_array);
$unknown_type = UnionType::of([], $int_or_float_or_array);
}
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
$code_base = $this->code_base;
$context = $this->context;
$left = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['var']);
$right = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr']);
// fast-track common cases
if ($left->isNonNullIntType()
@@ -308,33 +301,37 @@ class AssignOperatorFlagVisitor extends FlagVisitorImplementation
}
$left_is_array = (
!$left->genericArrayElementTypes()->isEmpty()
!$left->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $left->nonArrayTypes()->isEmpty()
) || $left->isType(ArrayType::instance(false));
$right_is_array = (
!$right->genericArrayElementTypes()->isEmpty()
!$right->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $right->nonArrayTypes()->isEmpty()
) || $right->isType(ArrayType::instance(false));
if ($left_is_array
&& !$right->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
ArrayType::instance(false)->asPHPDocUnionType(),
$code_base
)
) {
Issue::maybeEmit(
$this->code_base,
$this->context,
$code_base,
$context,
Issue::TypeInvalidRightOperand,
$node->lineno ?? 0
);
return $unknown_type;
} elseif ($right_is_array
&& !$left->canCastToUnionType(ArrayType::instance(false)->asPHPDocUnionType())
&& !$left->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType(),
$code_base
)
) {
Issue::maybeEmit(
$this->code_base,
$this->context,
$code_base,
$context,
Issue::TypeInvalidLeftOperand,
$node->lineno ?? 0
);
@@ -348,8 +345,11 @@ class AssignOperatorFlagVisitor extends FlagVisitorImplementation
return $probably_int_or_float_type;
}
/** @override */
public function visitBinaryDiv(Node $_): UnionType
/**
* @unused-param $node
* @override
*/
public function visitBinaryDiv(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return FloatType::instance(false)->asRealUnionType();
@@ -391,28 +391,40 @@ class AssignOperatorFlagVisitor extends FlagVisitorImplementation
return FloatType::instance(false)->asRealUnionType();
}
/** @override */
public function visitBinaryMod(Node $_): UnionType
/**
* @unused-param $node
* @override
*/
public function visitBinaryMod(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return IntType::instance(false)->asRealUnionType();
}
/** @override */
public function visitBinaryPow(Node $_): UnionType
/**
* @unused-param $node
* @override
*/
public function visitBinaryPow(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return FloatType::instance(false)->asRealUnionType();
}
/** @override */
public function visitBinaryShiftLeft(Node $_): UnionType
/**
* @unused-param $node
* @override
*/
public function visitBinaryShiftLeft(Node $node): UnionType
{
return IntType::instance(false)->asRealUnionType();
}
/** @override */
public function visitBinaryShiftRight(Node $_): UnionType
/**
* @unused-param $node
* @override
*/
public function visitBinaryShiftRight(Node $node): UnionType
{
return IntType::instance(false)->asRealUnionType();
}
+242 -101
View File
@@ -26,7 +26,6 @@ use Phan\Language\Context;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Element\Parameter;
use Phan\Language\Element\PassByReferenceVariable;
use Phan\Language\Element\Property;
use Phan\Language\Element\TypedElementInterface;
@@ -151,6 +150,8 @@ class AssignmentVisitor extends AnalysisVisitor
return $this->context;
}
// TODO visitNullsafeMethodCall should not be possible on the left hand side?
/**
* The following is an example of how this would happen.
* (TODO: Check if the right-hand side is an object with offsetSet() or a reference?
@@ -170,9 +171,15 @@ class AssignmentVisitor extends AnalysisVisitor
* @return Context
* A new or an unchanged context resulting from
* analyzing the node
*
* @throws UnanalyzableException for first-class callable conversion
*/
public function visitMethodCall(Node $node): Context
{
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
// Warn about this being unanalyzable
return $this->visit($node);
}
if ($this->dim_depth >= 2) {
return $this->context;
}
@@ -192,7 +199,7 @@ class AssignmentVisitor extends AnalysisVisitor
$this->code_base,
$this->context,
$node
))->getMethod($method_name, false);
))->getMethod($method_name, false, true);
$this->checkAssignmentToFunctionResult($node, [$method]);
} catch (Exception $_) {
// ignore it
@@ -220,9 +227,16 @@ class AssignmentVisitor extends AnalysisVisitor
* @return Context
* A new or an unchanged context resulting from
* analyzing the node
*
* @throws UnanalyzableException for first-class callable conversion
*/
public function visitCall(Node $node): Context
{
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
// Warn about this being unanalyzable
return $this->visit($node);
}
// TODO: Warn about first-class callable conversion
$expression = $node->children['expr'];
if ($this->dim_depth < 2) {
// Get the function.
@@ -288,6 +302,8 @@ class AssignmentVisitor extends AnalysisVisitor
* @return Context
* A new or an unchanged context resulting from
* analyzing the node
*
* @throws UnanalyzableException for first-class callable conversion
*/
public function visitStaticCall(Node $node): Context
{
@@ -335,7 +351,8 @@ class AssignmentVisitor extends AnalysisVisitor
if ($bitmask === 3) {
$this->emitIssue(
Issue::SyntaxMixedKeyNoKeyArrayDestructuring,
$c->lineno ?? $node->lineno
$c->lineno ?? $node->lineno,
ASTReverter::toShortString($node)
);
return;
}
@@ -353,8 +370,8 @@ class AssignmentVisitor extends AnalysisVisitor
/** @suppress PhanAccessMethodInternal */
$get_fallback_element_type = function () use (&$fallback_element_type): UnionType {
return $fallback_element_type ?? ($fallback_element_type = (
$this->right_type->genericArrayElementTypes()
->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($this->right_type->getRealTypeSet()))));
$this->right_type->genericArrayElementTypes(false, $this->code_base)
->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($this->right_type->getRealTypeSet(), $this->code_base))));
};
$expect_string_keys_lineno = false;
@@ -417,7 +434,7 @@ class AssignmentVisitor extends AnalysisVisitor
}
if (\is_scalar($key_value)) {
$element_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($this->right_type, $key_value);
$element_type = UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($this->right_type, $key_value, false, $this->code_base);
if ($element_type === null) {
$element_type = $get_fallback_element_type();
} elseif ($element_type === false) {
@@ -425,13 +442,14 @@ class AssignmentVisitor extends AnalysisVisitor
Issue::TypeInvalidDimOffsetArrayDestructuring,
$child_node->lineno,
StringUtil::jsonEncode($key_value),
ASTReverter::toShortString($child_node),
(string)$this->right_type
);
$element_type = $get_fallback_element_type();
} else {
if ($element_type->hasRealTypeSet()) {
$element_type = self::withComputedRealUnionType($element_type, $this->right_type, static function (UnionType $new_right_type) use ($key_value): UnionType {
return UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($new_right_type, $key_value) ?: UnionType::empty();
$element_type = self::withComputedRealUnionType($element_type, $this->right_type, function (UnionType $new_right_type) use ($key_value): UnionType {
return UnionTypeVisitor::resolveArrayShapeElementTypesForOffset($new_right_type, $key_value, false, $this->code_base) ?: UnionType::empty();
});
}
}
@@ -509,9 +527,7 @@ class AssignmentVisitor extends AnalysisVisitor
// Set the element type on each element of
// the list
$this->analyzeSetUnionType($property, $element_type, $value_node);
} catch (UnanalyzableException $_) {
// Ignore it. There's nothing we can do.
} catch (NodeException $_) {
} catch (UnanalyzableException | NodeException $_) {
// Ignore it. There's nothing we can do.
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
@@ -537,7 +553,7 @@ class AssignmentVisitor extends AnalysisVisitor
* This should be used for warning about assignments such as `$leftHandSide = $str`, but not `is_string($var)`,
* when typed properties could be used.
*
* @param Node|string|int|float $node
* @param Node|string|int|float|null $node
*/
private function analyzeSetUnionType(
TypedElementInterface $element,
@@ -549,7 +565,8 @@ class AssignmentVisitor extends AnalysisVisitor
$element_type = $element_type->withIsPossiblyUndefined(false);
$element->setUnionType($element_type);
if ($element instanceof PassByReferenceVariable) {
self::analyzeSetUnionTypePassByRef($this->code_base, $this->context, $element, $element_type, $node);
$assign_node = new Node(ast\AST_ASSIGN, 0, ['expr' => $node], $node->lineno ?? $this->assignment_node->lineno);
self::analyzeSetUnionTypePassByRef($this->code_base, $this->context, $element, $element_type, $assign_node);
}
}
@@ -571,7 +588,13 @@ class AssignmentVisitor extends AnalysisVisitor
): void {
$element->setUnionType($element_type);
if ($element instanceof PassByReferenceVariable) {
self::analyzeSetUnionTypePassByRef($code_base, $context, $element, $element_type, $node);
self::analyzeSetUnionTypePassByRef(
$code_base,
$context,
$element,
$element_type,
new Node(ast\AST_ASSIGN, 0, ['expr' => $node], $node->lineno ?? $context->getLineNumberStart())
);
}
}
@@ -580,7 +603,7 @@ class AssignmentVisitor extends AnalysisVisitor
* This should be used for warning about assignments such as `$leftHandSideRef = $str`, but not `is_string($varRef)`,
* when typed properties could be used.
*
* @param Node|string|int|float $node
* @param Node|string|int|float $node the assignment expression
*/
private static function analyzeSetUnionTypePassByRef(
CodeBase $code_base,
@@ -606,6 +629,7 @@ class AssignmentVisitor extends AnalysisVisitor
$reference_context,
Issue::TypeMismatchPropertyRealByRef,
$reference_context->getLineNumberStart(),
isset($node->children['expr']) ? ASTReverter::toShortString($node->children['expr']) : '(unknown)',
$new_type,
$element->getRepresentationForIssue(),
$real_union_type,
@@ -615,7 +639,7 @@ class AssignmentVisitor extends AnalysisVisitor
}
return;
}
if (!$new_type->asExpandedTypes($code_base)->canCastToUnionType($element->getPHPDocUnionType())) {
if (!$new_type->canCastToUnionType($element->getPHPDocUnionType(), $code_base)) {
$reference_context = $reference_element->getContextOfCreatedReference();
if ($reference_context) {
Issue::maybeEmit(
@@ -623,6 +647,7 @@ class AssignmentVisitor extends AnalysisVisitor
$reference_context,
Issue::TypeMismatchPropertyByRef,
$reference_context->getLineNumberStart(),
isset($node->children['expr']) ? ASTReverter::toShortString($node->children['expr']) : '(unknown)',
$new_type,
$element->getRepresentationForIssue(),
$element->getPHPDocUnionType(),
@@ -636,6 +661,7 @@ class AssignmentVisitor extends AnalysisVisitor
/**
* Analyzes code such as list($a) = function_returning_array();
* @param Node $node the ast\AST_ARRAY node on the left hand side of the assignment
* @see self::visitArray()
*/
private function analyzeGenericArrayAssignment(Node $node): void
@@ -650,13 +676,14 @@ class AssignmentVisitor extends AnalysisVisitor
$this->emitIssue(
Issue::TypeInvalidExpressionArrayDestructuring,
$node->lineno,
$this->getAssignedExpressionString(),
$right_type,
'array|ArrayAccess'
);
}
$element_type =
$array_access_types->genericArrayElementTypes()
->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($right_type->getRealTypeSet()));
$array_access_types->genericArrayElementTypes(false, $this->code_base)
->withRealTypeSet(UnionType::computeRealElementTypesForDestructuringAccess($right_type->getRealTypeSet(), $this->code_base));
// @phan-suppress-previous-line PhanAccessMethodInternal
}
@@ -730,9 +757,7 @@ class AssignmentVisitor extends AnalysisVisitor
// Set the element type on each element of
// the list
$this->analyzeSetUnionType($property, $element_type, $value_node);
} catch (UnanalyzableException $_) {
// Ignore it. There's nothing we can do.
} catch (NodeException $_) {
} catch (UnanalyzableException | NodeException $_) {
// Ignore it. There's nothing we can do.
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
@@ -801,6 +826,7 @@ class AssignmentVisitor extends AnalysisVisitor
$this->emitIssue(
Issue::InvalidWriteToTemporaryExpression,
$node->lineno,
ASTReverter::toShortString($node),
Type::fromObject($expr_node)
);
return $this->context;
@@ -864,19 +890,15 @@ class AssignmentVisitor extends AnalysisVisitor
}
$right_inner_type = $this->right_type;
if ($right_inner_type->isEmpty()) {
if ($key_type_enum === GenericArrayType::KEY_MIXED) {
$right_type = ArrayType::instance(false)->asRealUnionType();
} else {
$right_type = GenericArrayType::fromElementType(MixedType::instance(false), false, $key_type_enum)->asRealUnionType();
}
$right_type = GenericArrayType::fromElementType(MixedType::instance(false), false, $key_type_enum)->asRealUnionType();
} else {
$right_type = $right_inner_type->asGenericArrayTypes($key_type_enum);
}
} else {
$right_type = $this->right_type->asNonEmptyListTypes();
$right_type = $this->right_type->asNonEmptyListTypes()->nonFalseyClone();
}
if (!$right_type->hasRealTypeSet()) {
$right_type = $right_type->withRealTypeSet([ArrayType::instance(false)]);
$right_type = $right_type->withRealTypeSet(UnionType::typeSetFromString('non-empty-array'));
}
}
@@ -929,6 +951,8 @@ class AssignmentVisitor extends AnalysisVisitor
return $this->context;
}
// TODO: visitNullsafeProp should not be possible on the left hand side? Emit Issue::InvalidNode
/**
* @param Node $node
* A node to analyze as the target of an assignment.
@@ -946,14 +970,11 @@ class AssignmentVisitor extends AnalysisVisitor
$this->context,
$node->children['expr']
))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT, Issue::TypeExpectedObjectPropAccess);
} catch (CodeBaseException $_) {
// This really shouldn't happen since the code
// parsed cleanly. This should fatal.
// throw $exception;
return $this->context;
} catch (\Exception $_) {
// If we can't figure out what kind of a class
// this is, don't worry about it
// this is, don't worry about it.
//
// Note that CodeBaseException is one possible exception due to invalid code created by the fallback parser, etc.
return $this->context;
}
@@ -973,11 +994,23 @@ class AssignmentVisitor extends AnalysisVisitor
$this->handleThisPropertyAssignmentInLocalScopeByName($node, $property_name);
}
if (Config::get_strict_object_checking()) {
ContextNode::checkPossiblyUndeclaredInstanceProperty($this->code_base, $this->context, $node, $property_name);
}
$property = null;
$class_with_property = null;
$class_without_property = null;
foreach ($class_list as $clazz) {
if ($clazz->isImmutableAtRuntime()) {
$this->emitTypeModifyImmutableObjectPropertyIssue($clazz, $property_name, $node);
return $this->context;
}
// Check to see if this class has the property or
// a setter
if (!$clazz->hasPropertyWithName($this->code_base, $property_name)) {
if (!$clazz->hasMethodWithName($this->code_base, '__set')) {
if (!$clazz->hasMethodWithName($this->code_base, '__set', true)) {
$class_without_property = $clazz;
continue;
}
}
@@ -991,6 +1024,7 @@ class AssignmentVisitor extends AnalysisVisitor
$node,
true
);
$class_with_property = $clazz;
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
@@ -999,8 +1033,24 @@ class AssignmentVisitor extends AnalysisVisitor
);
return $this->context;
}
}
if ($property && $class_with_property) {
if ($class_without_property && Config::get_strict_object_checking()) {
$this->emitIssue(
Issue::PossiblyUndeclaredPropertyOfClass,
$node->lineno,
$property_name,
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr'] ?? $node->children['class']
),
$class_without_property->getFQSEN()
);
}
try {
return $this->analyzePropAssignment($clazz, $property, $node);
return $this->analyzePropAssignment($class_with_property, $property, $node);
} catch (RecursionDepthException $_) {
return $this->context;
}
@@ -1056,6 +1106,7 @@ class AssignmentVisitor extends AnalysisVisitor
*/
private function analyzePropAssignment(Clazz $clazz, Property $property, Node $node): Context
{
$code_base = $this->code_base;
if ($property->isReadOnly()) {
$this->analyzeAssignmentToReadOnlyProperty($property, $node);
}
@@ -1070,13 +1121,13 @@ class AssignmentVisitor extends AnalysisVisitor
if ($this->dim_depth > 0) {
if ($resolved_right_type->canCastToExpandedUnionType(
$property_union_type,
$this->code_base
$code_base
)) {
$this->addTypesToProperty($property, $node);
if (Config::get_strict_property_checking() && $resolved_right_type->typeCount() > 1) {
$this->analyzePropertyAssignmentStrict($property, $resolved_right_type, $node);
}
} elseif ($property_union_type->asExpandedTypes($this->code_base)->hasArrayAccess()) {
} elseif ($property_union_type->hasArrayAccess($code_base)) {
// Add any type if this is a subclass with array access.
$this->addTypesToProperty($property, $node);
} else {
@@ -1091,16 +1142,16 @@ class AssignmentVisitor extends AnalysisVisitor
// TODO: More precise than canCastToExpandedUnionType
if (!$new_types->canCastToExpandedUnionType(
$property_union_type,
$this->code_base
$code_base
)) {
// echo "Emitting warning for $new_types\n";
// TODO: Don't emit if array shape type is compatible with the original value of $property_union_type
$this->emitIssue(
self::isRealMismatch($this->code_base, $property->getRealUnionType(), $resolved_right_type) ? Issue::TypeMismatchPropertyReal : Issue::TypeMismatchProperty,
$node->lineno,
(string)$new_types,
$property->getRepresentationForIssue(),
(string)$property_union_type
$this->emitTypeMismatchPropertyIssue(
$node,
$property,
$resolved_right_type,
$new_types,
$property_union_type
);
} else {
if (Config::get_strict_property_checking() && $resolved_right_type->typeCount() > 1) {
@@ -1118,15 +1169,15 @@ class AssignmentVisitor extends AnalysisVisitor
return $this->context;
} else {
// This is a regular assignment, not an assignment to an offset
if (!$resolved_right_type->canCastToExpandedUnionType(
$property_union_type,
$this->code_base
if (!$resolved_right_type->canCastToUnionType(
$property_union_type->asExpandedTypes($code_base),
$code_base
)
&& !($resolved_right_type->hasTypeInBoolFamily() && $property_union_type->hasTypeInBoolFamily())
&& !$clazz->hasDynamicProperties($this->code_base)
&& !$clazz->hasDynamicProperties($code_base)
&& !$property->isDynamicProperty()
) {
if ($resolved_right_type->nonNullableClone()->canCastToExpandedUnionType($property_union_type, $this->code_base) &&
if ($resolved_right_type->nonNullableClone()->canCastToUnionType($property_union_type->asExpandedTypes($code_base), $code_base) &&
!$resolved_right_type->isType(NullType::instance(false))) {
if ($this->shouldSuppressIssue(Issue::TypeMismatchProperty, $node->lineno)) {
return $this->context;
@@ -1134,20 +1185,15 @@ class AssignmentVisitor extends AnalysisVisitor
$this->emitIssue(
Issue::PossiblyNullTypeMismatchProperty,
$node->lineno,
$this->getAssignedExpressionString(),
(string)$this->right_type->withUnionType($resolved_right_type),
$property->getRepresentationForIssue(),
(string)$property_union_type,
'null'
);
} else {
// echo "Emitting warning for {$resolved_right_type->asExpandedTypes($this->code_base)} to {$property_union_type->asExpandedTypes($this->code_base)}\n";
$this->emitIssue(
self::isRealMismatch($this->code_base, $property->getRealUnionType(), $resolved_right_type) ? Issue::TypeMismatchPropertyReal : Issue::TypeMismatchProperty,
$node->lineno,
(string)$this->right_type->withUnionType($resolved_right_type),
$property->getRepresentationForIssue(),
(string)$property_union_type
);
// echo "Emitting warning for {$resolved_right_type->asExpandedTypes($code_base)} to {$property_union_type->asExpandedTypes($code_base)}\n";
$this->emitTypeMismatchPropertyIssue($node, $property, $resolved_right_type, $this->right_type->withUnionType($resolved_right_type), $property_union_type);
}
return $this->context;
}
@@ -1163,6 +1209,73 @@ class AssignmentVisitor extends AnalysisVisitor
return $this->context;
}
/**
* @param UnionType $resolved_right_type the type of the expression to use when checking for real type mismatches
* @param UnionType $warn_type the type to use in issue messages
*/
private function emitTypeMismatchPropertyIssue(
Node $node,
Property $property,
UnionType $resolved_right_type,
UnionType $warn_type,
UnionType $property_union_type
): void {
if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchPropertyReal)) {
return;
}
if (self::isRealMismatch($this->code_base, $property->getRealUnionType(), $resolved_right_type)) {
$this->emitIssue(
Issue::TypeMismatchPropertyReal,
$node->lineno,
$this->getAssignedExpressionString(),
$warn_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($warn_type),
$property->getRepresentationForIssue(),
$property_union_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($property_union_type)
);
return;
}
if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchPropertyProbablyReal)) {
return;
}
if ($resolved_right_type->hasRealTypeSet() &&
!$resolved_right_type->getRealUnionType()->canCastToDeclaredType($this->code_base, $this->context, $property_union_type)) {
$this->emitIssue(
Issue::TypeMismatchPropertyProbablyReal,
$node->lineno,
$this->getAssignedExpressionString(),
$warn_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($warn_type),
$property->getRepresentationForIssue(),
$property_union_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($property_union_type)
);
return;
}
$this->emitIssue(
Issue::TypeMismatchProperty,
$node->lineno,
$this->getAssignedExpressionString(),
$warn_type,
$property->getRepresentationForIssue(),
$property_union_type
);
}
private function getAssignedExpressionString(): string
{
$expr = $this->assignment_node->children['expr'] ?? null;
if ($expr === null) {
return '(unknown)';
}
$str = ASTReverter::toShortString($expr);
if ($this->dim_depth > 0) {
return "($str as a field)";
}
return $str;
}
/**
* Returns true if Phan should emit a more severe issue type for real type mismatch
*/
@@ -1171,7 +1284,7 @@ class AssignmentVisitor extends AnalysisVisitor
if ($real_property_type->isEmpty()) {
return false;
}
return !$real_actual_type->asExpandedTypes($code_base)->isStrictSubtypeOf($code_base, $real_property_type);
return !$real_actual_type->isStrictSubtypeOf($code_base, $real_property_type);
}
/**
@@ -1206,13 +1319,15 @@ class AssignmentVisitor extends AnalysisVisitor
private function analyzeAssignmentToReadOnlyProperty(Property $property, Node $node): void
{
$is_from_phpdoc = $property->isFromPHPDoc();
$class_fqsen = $property->getClassFQSEN();
$context = $property->getContext();
$is_from_phpdoc = $property->isFromPHPDoc();
if (!$is_from_phpdoc && $this->context->isInFunctionLikeScope()) {
$method = $this->context->getFunctionLikeInScope($this->code_base);
if ($method instanceof Method && strcasecmp($method->getName(), '__construct') === 0) {
$class_type = $method->getClassFQSEN()->asType();
if ($class_type->asExpandedTypes($this->code_base)->hasType($property->getClassFQSEN()->asType())) {
$class_type = $class_fqsen->asType();
if ($property->getClassFQSEN()->asType()->isSubtypeOf($class_type, $this->code_base)) {
// This is a constructor setting its own properties or a base class's properties.
// TODO: Could support private methods
return;
@@ -1221,7 +1336,7 @@ class AssignmentVisitor extends AnalysisVisitor
}
$this->emitIssue(
$is_from_phpdoc ? Issue::AccessReadOnlyMagicProperty : Issue::AccessReadOnlyProperty,
$node->lineno ?? 0,
$node->lineno,
$property->asPropertyFQSENString(),
$context->getFile(),
$context->getLineNumberStart()
@@ -1236,9 +1351,6 @@ class AssignmentVisitor extends AnalysisVisitor
}
$property_union_type = $property->getUnionType();
if ($property_union_type->hasTemplateTypeRecursive()) {
$property_union_type = $property_union_type->asExpandedTypes($this->code_base);
}
$mismatch_type_set = UnionType::empty();
$mismatch_expanded_types = null;
@@ -1249,9 +1361,10 @@ class AssignmentVisitor extends AnalysisVisitor
$individual_type_expanded = $type->asExpandedTypes($this->code_base);
// See if the argument can be cast to the
// parameter
// property
if (!$individual_type_expanded->canCastToUnionType(
$property_union_type
$property_union_type,
$this->code_base
)) {
$mismatch_type_set = $mismatch_type_set->withType($type);
if ($mismatch_expanded_types === null) {
@@ -1266,14 +1379,18 @@ class AssignmentVisitor extends AnalysisVisitor
// No mismatches
return;
}
if ($this->shouldSuppressIssue(Issue::TypeMismatchProperty, $node->lineno)) {
if ($this->shouldSuppressIssue(Issue::TypeMismatchPropertyReal, $node->lineno) ||
$this->shouldSuppressIssue(Issue::TypeMismatchPropertyProbablyReal, $node->lineno) ||
$this->shouldSuppressIssue(Issue::TypeMismatchProperty, $node->lineno)
) {
// TypeMismatchProperty also suppresses PhanPossiblyNullTypeMismatchProperty, etc.
return;
}
$this->emitIssue(
self::getStrictPropertyMismatchIssueType($mismatch_type_set),
$node->lineno ?? 0,
$node->lineno,
$this->getAssignedExpressionString(),
(string)$this->right_type,
$property->getRepresentationForIssue(),
(string)$property_union_type,
@@ -1341,7 +1458,7 @@ class AssignmentVisitor extends AnalysisVisitor
// Only allow compatible types to be added to declared properties.
// Allow anything to be added to dynamic properties.
// TODO: Be more permissive about declared properties without phpdoc types.
if (!$new_type->asExpandedTypes($code_base)->canCastToUnionType($original_property_types) && !$property->isDynamicProperty()) {
if (!$new_type->asPHPDocUnionType()->canCastToUnionType($original_property_types, $code_base) && !$property->isDynamicProperty()) {
continue;
}
@@ -1360,8 +1477,6 @@ class AssignmentVisitor extends AnalysisVisitor
$property->setUnionType($updated_property_types->withRealTypeSet($property->getRealUnionType()->getTypeSet()));
}
/**
* @param Property $property - The property which should have types added to it
*/
@@ -1408,7 +1523,7 @@ class AssignmentVisitor extends AnalysisVisitor
// Only allow compatible types to be added to declared properties.
// Allow anything to be added to dynamic properties.
// TODO: Be more permissive about declared properties without phpdoc types.
if (!$new_type->asExpandedTypes($this->code_base)->canCastToUnionType($original_property_types) && !$property->isDynamicProperty()) {
if (!$new_type->asPHPDocUnionType()->canCastToUnionType($original_property_types, $this->code_base) && !$property->isDynamicProperty()) {
continue;
}
@@ -1452,14 +1567,11 @@ class AssignmentVisitor extends AnalysisVisitor
$this->context,
$node->children['class']
))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, Issue::TypeExpectedObjectStaticPropAccess);
} catch (CodeBaseException $_) {
// This really shouldn't happen since the code
// parsed cleanly. This should fatal.
// throw $exception;
return $this->context;
} catch (\Exception $_) {
// If we can't figure out what kind of a class
// this is, don't worry about it
//
// Note that CodeBaseException is one possible exception due to invalid code created by the fallback parser, etc.
return $this->context;
}
@@ -1510,6 +1622,42 @@ class AssignmentVisitor extends AnalysisVisitor
return $this->context;
}
private function emitTypeModifyImmutableObjectPropertyIssue(Clazz $clazz, string $property_name, Node $node): void
{
if (!$clazz->isPHPInternal() && $clazz->hasPropertyWithName($this->code_base, $property_name)) {
try {
// Look for static properties with that $property_name
$property = $clazz->getPropertyByNameInContext(
$this->code_base,
$property_name,
$this->context,
false, // is_static
null,
true
);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
return;
}
$property_context = $property->getContext();
} else {
$property_context = $clazz->getContext();
}
$this->emitIssue(
Issue::TypeModifyImmutableObjectProperty,
$node->lineno,
$clazz->getClasslikeType(),
$clazz->getFQSEN(),
$property_name,
$property_context->getFile(),
$property_context->getLineNumberStart()
);
}
/**
* @param Node $node
* A node of type ast\AST_VAR to analyze as the target of an assignment
@@ -1559,15 +1707,9 @@ class AssignmentVisitor extends AnalysisVisitor
}
// Check to see if the variable already exists
if ($variable) {
// If the variable isn't a pass-by-reference parameter
// we clone it so as to not disturb its previous types
// We clone the variable so as to not disturb its previous types
// as we replace it.
// TODO: Do a better job of analyzing references
if ($variable instanceof Parameter) {
$variable = clone($variable);
} elseif (!($variable instanceof PassByReferenceVariable)) {
$variable = clone($variable);
}
$variable = clone($variable);
// If we're assigning to an array element then we don't
// know what the array structure of the parameter is
@@ -1603,9 +1745,9 @@ class AssignmentVisitor extends AnalysisVisitor
}
// Note that after $x[anything] = anything, $x is guaranteed not to be the empty array.
// TODO: Handle `$x = 'x'; $s[0] = '0';`
$this->analyzeSetUnionType($variable, $new_union_type->nonFalseyClone(), $node);
$this->analyzeSetUnionType($variable, $new_union_type->nonFalseyClone(), $this->assignment_node->children['expr'] ?? null);
} else {
$this->analyzeSetUnionType($variable, $this->right_type, $node);
$this->analyzeSetUnionType($variable, $this->right_type, $this->assignment_node->children['expr'] ?? null);
}
$this->context->addScopeVariable(
@@ -1655,9 +1797,7 @@ class AssignmentVisitor extends AnalysisVisitor
$this->code_base,
$this->context
);
} catch (IssueException $_) {
// Hopefully caught elsewhere
} catch (NodeException $_) {
} catch (IssueException | NodeException $_) {
// Hopefully caught elsewhere
}
}
@@ -1770,12 +1910,12 @@ class AssignmentVisitor extends AnalysisVisitor
// unless it has 1 or more array types and all are list<T>
$right_type = self::normalizeListTypesInDimAssignment($assign_type, $right_type);
if ($assign_type->isEmpty() || ($assign_type->hasGenericArray() && !$assign_type->asExpandedTypes($this->code_base)->hasArrayAccess())) {
if ($assign_type->isEmpty() || ($assign_type->hasGenericArray() && !$assign_type->hasArrayAccess($this->code_base))) {
// For empty union types or 'array', expect the provided dimension to be able to cast to int|string
if ($dim_type && !$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_or_string_type)) {
if ($dim_type && !$dim_type->isEmpty() && !$dim_type->canCastToUnionType($int_or_string_type, $this->code_base)) {
$this->emitIssue(
Issue::TypeMismatchDimAssignment,
$node->lineno ?? 0,
$node->lineno,
(string)$assign_type,
(string)$dim_type,
(string)$int_or_string_type
@@ -1783,15 +1923,15 @@ class AssignmentVisitor extends AnalysisVisitor
}
return $right_type;
}
$assign_type_expanded = $assign_type->withStaticResolvedInContext($this->context)->asExpandedTypes($this->code_base);
$assign_type_resolved = $assign_type->withStaticResolvedInContext($this->context);
//echo "$assign_type_expanded : " . json_encode($assign_type_expanded->hasArrayLike()) . "\n";
// TODO: Better heuristic to deal with false positives on ArrayAccess subclasses
if ($assign_type_expanded->hasArrayAccess() && !$assign_type_expanded->hasGenericArray()) {
if ($assign_type_resolved->hasArrayAccess($this->code_base) && !$assign_type_resolved->hasGenericArray()) {
return UnionType::empty();
}
if (!$assign_type_expanded->hasArrayLike()) {
if (!$assign_type_resolved->hasArrayLike($this->code_base)) {
if ($assign_type->hasNonNullStringType()) {
// Are we assigning to a variable/property of type 'string' (with no ArrayAccess or array types)?
if (\is_null($dim_type)) {
@@ -1810,19 +1950,20 @@ class AssignmentVisitor extends AnalysisVisitor
'int'
);
} else {
if ($right_type->canCastToUnionType($string_array_type)) {
if ($right_type->canCastToUnionType($string_array_type, $this->code_base)) {
// e.g. $a = 'aaa'; $a[0] = 'x';
// (Currently special casing this, not handling deeper dimensions)
return StringType::instance(false)->asPHPDocUnionType();
}
}
} elseif (!$assign_type->hasTypeMatchingCallback(static function (Type $type) use ($simple_xml_element_type): bool {
return !$type->isNullable() && ($type instanceof MixedType || $type === $simple_xml_element_type);
return !$type->isNullableLabeled() && ($type instanceof MixedType || $type === $simple_xml_element_type);
})) {
// Imitate the check in UnionTypeVisitor, don't warn for mixed, etc.
// Imitate the check in UnionTypeVisitor, don't warn for mixed (but warn for `?mixed`), etc.
$this->emitIssue(
Issue::TypeArraySuspicious,
$node->lineno,
ASTReverter::toShortString($node),
(string)$assign_type
);
}
+311
View File
@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace Phan\Analysis;
use ast;
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\CodeBase;
use Phan\Config;
use Phan\Issue;
use Phan\Language\Element\AddressableElementInterface;
use Phan\Language\Element\Attribute;
use Phan\Language\Element\ClassConstant;
use Phan\Language\Element\ClassElement;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Element\Parameter;
use Phan\Language\Element\Property;
use Phan\Parse\ParseVisitor;
/**
* Analyzer of the attributes of declarations.
* Emits warnings, and will eventually modify the way the element is analyzed.
* (this is why it's run before starting the analysis phase)
*
* NOTE: This runs without problems in php 7 because it uses constants from \Phan\Language\Element\Attribute, not from \Attribute
*/
class AttributeAnalyzer
{
/**
* Check function, closure, and method parameters to make sure they're valid
*
* This will also warn if method parameters are incompatible with the parameters of ancestor methods.
*/
public static function analyzeAttributesOfFunctionInterface(
CodeBase $code_base,
FunctionInterface $method
): void {
$attribute_list = $method->getAttributeList();
if ($attribute_list) {
self::checkAttributeList($code_base, $method, $method, $attribute_list);
}
foreach ($method->getRealParameterList() as $parameter) {
$attribute_list = $parameter->getAttributeList();
if ($attribute_list) {
self::checkAttributeList($code_base, $method, $parameter, $attribute_list);
}
}
}
/**
* Analyze attributes of the provided class and the attributes of the class declaration
*/
public static function analyzeAttributesOfClass(CodeBase $code_base, Clazz $class): void
{
self::analyzeAttributesOfElement($code_base, $class);
foreach ($class->getPropertyMap($code_base) as $property) {
if ($property->getFQSEN() === $property->getRealDefiningFQSEN()) {
self::analyzeAttributesOfElement($code_base, $property);
}
}
foreach ($class->getConstantMap($code_base) as $const) {
if ($const->getFQSEN() === $const->getRealDefiningFQSEN()) {
self::analyzeAttributesOfElement($code_base, $const);
}
}
foreach ($class->getMethodMap($code_base) as $method) {
if ($method->getFQSEN() === $method->getRealDefiningFQSEN()) {
self::analyzeAttributesOfElement($code_base, $method);
}
}
}
/**
* Check attributes of non-functionlikes
*
* This will also warn if method parameters are incompatible with the parameters of ancestor methods.
*/
private static function analyzeAttributesOfElement(
CodeBase $code_base,
AddressableElementInterface $element
): void {
$attribute_list = $element->getAttributeList();
if ($attribute_list) {
self::checkAttributeList($code_base, $element, $element, $attribute_list);
}
}
/**
* @param AddressableElementInterface|Parameter $element @phan-unused-param
* @param non-empty-list<Attribute> $attribute_list
*/
private static function checkAttributeList(
CodeBase $code_base,
AddressableElementInterface $declaration,
object $element,
array $attribute_list
): void {
$attribute_set = [];
foreach ($attribute_list as $attribute) {
self::checkAttribute($code_base, $declaration, $element, $attribute);
$fqsen = $attribute->getFQSEN();
$fqsen_id = \spl_object_id($fqsen);
$previous_attribute = $attribute_set[$fqsen_id] ?? null;
if ($previous_attribute instanceof Attribute) {
// This is a repeated attribute
if (!$code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$class = $code_base->getClassByFQSEN($fqsen);
if ($class->getAttributeFlags($code_base) & Attribute::IS_REPEATABLE) {
continue;
}
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::AttributeNonRepeatable,
$attribute->getLineNumberStart(),
$fqsen,
$class->getContext()->getFile(),
$class->getContext()->getLineNumberStart(),
$previous_attribute->getLineNumberStart()
);
} else {
$attribute_set[$fqsen_id] = $attribute;
}
}
}
/**
* @param AddressableElementInterface|Parameter $element @phan-unused-param
*/
private static function getTargetConstantForElement(object $element): int
{
if ($element instanceof ClassElement) {
if ($element instanceof Property) {
return Attribute::TARGET_PROPERTY;
} elseif ($element instanceof ClassConstant) {
return Attribute::TARGET_CLASS_CONSTANT;
} elseif ($element instanceof Method) {
return Attribute::TARGET_METHOD;
}
} elseif ($element instanceof Clazz) {
return Attribute::TARGET_CLASS;
} elseif ($element instanceof Func) {
return Attribute::TARGET_FUNCTION;
} elseif ($element instanceof Parameter) {
return Attribute::TARGET_PARAMETER;
}
return 0;
}
private const ATTRIBUTE_TARGET_NAME = [
0 => 'unknown',
Attribute::TARGET_CLASS => '\Attribute::TARGET_CLASS',
Attribute::TARGET_CLASS_CONSTANT => '\Attribute::TARGET_CLASS_CONSTANT',
Attribute::TARGET_PARAMETER => '\Attribute::TARGET_PARAMETER',
Attribute::TARGET_PROPERTY => '\Attribute::TARGET_PROPERTY',
Attribute::TARGET_METHOD => '\Attribute::TARGET_METHOD',
Attribute::TARGET_FUNCTION => '\Attribute::TARGET_FUNCTION',
];
/**
* Get a representation of the list of attribute target class constant names for a bitfield
*/
private static function getTargetNames(int $expected_targets): string
{
$parts = [];
foreach (self::ATTRIBUTE_TARGET_NAME as $value => $name) {
if ($value & $expected_targets) {
$parts[] = $name;
}
}
return $parts ? \implode('|', $parts) : '(no valid \Attribute::TARGET_* values)';
}
/**
* @param AddressableElementInterface|Parameter $element @phan-unused-param
*/
private static function checkAttribute(
CodeBase $code_base,
AddressableElementInterface $declaration,
object $element,
Attribute $attribute
): void {
$attribute_lineno = $attribute->getLineNumberStart();
$fqsen = $attribute->getFQSEN();
if ($code_base->hasClassWithFQSEN($fqsen)) {
$class = $code_base->getClassByFQSEN($fqsen);
if ($class->isClass() && !$class->isAbstract()) {
if (!$class->isAttribute()) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::AttributeNonAttribute,
$attribute_lineno,
$fqsen,
'#[\Attribute(...)]'
);
}
$expected_flags = $class->getAttributeFlags($code_base);
$actual_flag = self::getTargetConstantForElement($element);
if (!($actual_flag & $expected_flags)) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::AttributeWrongTarget,
$attribute_lineno,
$fqsen,
$class->getContext()->getFile(),
$class->getContext()->getLineNumberStart(),
self::getTargetNames($expected_flags),
$element,
self::getTargetNames($actual_flag)
);
}
// TODO: Pass this to the method call analyzer?
$class->addReference($declaration->getContext());
if ($class->isDeprecated()) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::DeprecatedClass,
$attribute_lineno,
(string)$class->getFQSEN(),
$class->getContext()->getFile(),
$class->getContext()->getLineNumberStart(),
$class->getDeprecationReason()
);
}
$constructor = $class->getMethodByName($code_base, '__construct');
if (!$constructor->isPublic()) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::AccessNonPublicAttribute,
$attribute_lineno,
(string)$class->getFQSEN(),
$constructor->getRepresentationForIssue(),
$class->getContext()->getFile(),
$class->getContext()->getLineNumberStart()
);
}
$attribute_node = $attribute->getNode();
if ($attribute_node) {
foreach ($attribute_node->children['args']->children ?? [] as $arg_node) {
if (!$arg_node instanceof Node) {
continue;
}
if ($arg_node->kind === ast\AST_NAMED_ARG) {
$arg_node = $arg_node->children['expr'];
}
if ($arg_node instanceof Node) {
(new ParseVisitor($code_base, $declaration->getContext()))->checkNodeIsConstExprOrWarn($arg_node, ParseVisitor::CONSTANT_EXPRESSION_IN_ATTRIBUTE);
}
}
ArgumentType::analyze($constructor, $attribute_node, $declaration->getContext(), $code_base);
}
} else {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::AttributeNonClass,
$attribute_lineno,
$fqsen,
$class->isTrait() ? 'trait' : ($class->isInterface() ? 'interface' : 'abstract class')
);
}
} else {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::UndeclaredClassAttribute,
$attribute_lineno,
$fqsen
);
}
if (Config::get_closest_minimum_target_php_version_id() < 80000) {
$attribute_group_start_lineno = $attribute->getGroupLineNumberStart();
$attribute_group_end_lineno = $attribute->getGroupLineNumberEnd();
if ($attribute_group_start_lineno === $element->getFileRef()->getLineNumberStart()) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::CompatibleAttributeGroupOnSameLine,
$attribute_group_end_lineno,
ASTReverter::toShortString($attribute->getGroup()),
$element
);
}
if ($attribute_group_end_lineno > $attribute_group_start_lineno) {
Issue::maybeEmit(
$code_base,
$declaration->getContext(),
Issue::CompatibleAttributeGroupOnMultipleLines,
$attribute_group_start_lineno,
ASTReverter::toShortString($attribute->getGroup()),
$element,
$attribute_group_end_lineno
);
}
}
}
}
@@ -356,7 +356,7 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
// TODO: Emit warning about division by zero.
return IntType::instance(false)->asRealUnionType();
}
$value = $left_value % $right_value;
$value = ((int)$left_value) % (int)($right_value);
return $make_literal_union_type(
LiteralIntType::instanceForValue($value, false),
$real_int
@@ -437,37 +437,39 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
}
/**
* @param Node $unused_node
* @param Node $node @unused-param
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryBoolAnd(Node $unused_node): UnionType
public function visitBinaryBoolAnd(Node $node): UnionType
{
// TODO: This might be useful when at least one side is a constant expression or at least one side is `never`
// e.g. `const X = Y || Z;`
return BoolType::instance(false)->asRealUnionType();
}
/**
* @param Node $node @unused-param
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryBoolXor(Node $node): UnionType
{
return BoolType::instance(false)->asRealUnionType();
}
/**
* @param Node $unused_node
* @param Node $node @unused-param
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryBoolXor(Node $unused_node): UnionType
{
return BoolType::instance(false)->asRealUnionType();
}
/**
* @param Node $unused_node
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryBoolOr(Node $unused_node): UnionType
public function visitBinaryBoolOr(Node $node): UnionType
{
return BoolType::instance(false)->asRealUnionType();
}
@@ -514,33 +516,37 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
*/
private function visitBinaryOpCommon(Node $node): UnionType
{
$code_base = $this->code_base;
$context = $this->context;
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$code_base,
$context,
$node->children['left'],
$this->should_catch_issue_exception
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$code_base,
$context,
$node->children['right'],
$this->should_catch_issue_exception
);
$left_is_array_like = $left->isExclusivelyArrayLike();
$right_is_array_like = $right->isExclusivelyArrayLike();
$left_is_array_like = $left->isExclusivelyArrayLike($code_base);
$right_is_array_like = $right->isExclusivelyArrayLike($code_base);
$left_can_cast_to_array = $left->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
ArrayType::instance(false)->asPHPDocUnionType(),
$this->code_base
);
$right_can_cast_to_array = $right->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
ArrayType::instance(false)->asPHPDocUnionType(),
$this->code_base
);
if ($left_is_array_like
&& !$right->hasArrayLike()
&& !$right->hasArrayLike($code_base)
&& !$right_can_cast_to_array
&& !$right->isEmpty()
&& !$right->containsNullable()
@@ -548,11 +554,11 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
) {
$this->emitIssue(
Issue::TypeComparisonFromArray,
$node->lineno ?? 0,
$node->lineno,
(string)$right->asNonLiteralType()
);
} elseif ($right_is_array_like
&& !$left->hasArrayLike()
&& !$left->hasArrayLike($code_base)
&& !$left_can_cast_to_array
&& !$left->isEmpty()
&& !$left->containsNullable()
@@ -560,7 +566,7 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
) {
$this->emitIssue(
Issue::TypeComparisonToArray,
$node->lineno ?? 0,
$node->lineno,
(string)$left->asNonLiteralType()
);
}
@@ -746,7 +752,7 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
// If both left and right union types are arrays, then this is array
// concatenation. (`$left + $right`)
if ($left->isGenericArray() && $right->isGenericArray()) {
self::checkInvalidArrayShapeCombination($this->code_base, $this->context, $node, $left, $right);
self::checkInvalidArrayShapeCombination($code_base, $context, $node, $left, $right);
if ($left->isEqualTo($right)) {
return $left;
}
@@ -773,12 +779,12 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
}
$left_is_array = (
!$left->genericArrayElementTypes()->isEmpty()
!$left->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $left->nonArrayTypes()->isEmpty()
) || $left->isType($array_type);
$right_is_array = (
!$right->genericArrayElementTypes()->isEmpty()
!$right->genericArrayElementTypes(false, $code_base)->isEmpty()
&& $right->nonArrayTypes()->isEmpty()
) || $right->isType($array_type);
@@ -789,7 +795,8 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
if ($left_is_array
&& !$right->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
ArrayType::instance(false)->asPHPDocUnionType(),
$code_base
)
) {
$this->emitIssue(
@@ -797,7 +804,7 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
$node->lineno ?? 0
);
return $probably_unknown_type;
} elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType())) {
} elseif ($right_is_array && !$left->canCastToUnionType($array_type->asPHPDocUnionType(), $code_base)) {
$this->emitIssue(
Issue::TypeInvalidLeftOperand,
$node->lineno ?? 0
@@ -838,8 +845,6 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
}
$common_left_fields = null;
foreach ($left->getRealTypeSet() as $type) {
// if ($type->isNullable()) { return; }
if (!$type instanceof ArrayShapeType) {
if ($type instanceof ListType) {
continue;
@@ -999,10 +1004,11 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
}
/**
* @unused-param $node
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryMod(Node $unused_node): UnionType
public function visitBinaryMod(Node $node): UnionType
{
// TODO: Warn about invalid left or right side
return IntType::instance(false)->asRealUnionType();
@@ -1037,6 +1043,9 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
$node->children['right'],
$this->should_catch_issue_exception
);
if ($right_type->isNeverType()) {
return $left_type->nonNullableClone();
}
if ($left_type->isEmpty()) {
if ($right_type->isEmpty()) {
return MixedType::instance(false)->asPHPDocUnionType();
@@ -1053,6 +1062,9 @@ final class BinaryOperatorFlagVisitor extends FlagVisitorImplementation
// On the left side, remove null and replace '?T' with 'T'
// Don't bother if the right side contains null.
if (!$right_type->isEmpty() && $left_type->containsNullable() && !$right_type->containsNullable()) {
if ($left_type->getRealUnionType()->isRealTypeNullOrUndefined()) {
return $right_type;
}
$left_type = $left_type->nonNullableClone();
}
+154 -8
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Phan\Analysis;
use AssertionError;
use ast;
use ast\Node;
use Phan\AST\Visitor\KindVisitorImplementation;
@@ -102,7 +103,7 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
{
if ($cond instanceof Node) {
// TODO: Could look up values for remaining constants and inline expressions, but doing that has low value.
if ($cond->kind === \ast\AST_CONST) {
if ($cond->kind === ast\AST_CONST) {
$cond_name_string = $cond->children['name']->children['name'] ?? null;
return \is_string($cond_name_string) && \strcasecmp($cond_name_string, 'true') === 0;
}
@@ -166,7 +167,7 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
// TODO: Could emit an issue as a side effect
// Having any sort of status in a finally statement is
// likely to have unintuitive behavior.
if ($finally_status & (~self::STATUS_THROW_OR_RETURN_BITMASK) === 0) {
if (($finally_status & (~self::STATUS_THROW_OR_RETURN_BITMASK)) === 0) {
return $finally_status;
}
} else {
@@ -236,7 +237,17 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
*/
public function visitSwitch(Node $node): int
{
return $this->visitSwitchList($node->children['stmts']);
$cond = $node->children['cond'];
if ($cond instanceof Node) {
$cond_status = $this->check($cond);
if (($cond_status & self::STATUS_PROCEED) === 0) {
// handle throw expressions, switch(exit()), etc.
return $cond_status;
}
} else {
$cond_status = self::STATUS_PROCEED;
}
return $this->visitSwitchList($node->children['stmts']) | ($cond_status & ~self::STATUS_PROCEED);
}
/**
@@ -317,6 +328,84 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
return ($status & ~self::STATUS_PROCEED) | $next_status;
}
/**
* @return int the corresponding status code
* @suppress PhanTypeMismatchArgumentNullable
*/
public function visitMatch(Node $node): int
{
$status = $node->flags & self::STATUS_BITMASK;
if ($status) {
return $status;
}
$cond_status = $this->check($node->children['cond']);
if (($cond_status & self::STATUS_PROCEED) === 0) {
return $cond_status;
}
return $this->visitMatchArmList($node->children['stmts']) | ($cond_status & ~self::STATUS_PROCEED);
}
/**
* @return int the corresponding status code for the match arm list
*/
public function visitMatchArmList(Node $node): int
{
$status = $node->flags & self::STATUS_BITMASK;
if ($status) {
return $status;
}
$status = $this->computeStatusOfMatchArmList($node);
$node->flags = $status;
return $status;
}
/**
* @return int the corresponding status code
*/
public function visitMatchArm(Node $node): int
{
$status = $node->flags & self::STATUS_BITMASK;
if ($status) {
return $status;
}
$status = $this->computeMatchArmStatus($node);
$node->flags |= $status;
return $status;
}
private function computeMatchArmStatus(Node $node): int
{
['cond' => $cond, 'expr' => $expr] = $node->children;
$cond_status = 0;
foreach ($cond->children ?? [] as $cond_expr) {
if (!$cond_expr instanceof Node) {
$cond_status |= self::STATUS_PROCEED;
continue;
}
$cond_status |= $this->check($cond_expr);
if (($cond_status & self::STATUS_PROCEED) === 0) {
return $cond_status;
}
}
return ($cond_status & ~self::STATUS_PROCEED) | ($expr instanceof Node ? $this->check($expr) : self::STATUS_PROCEED);
}
private function computeStatusOfMatchArmList(Node $node): int
{
$default_status = self::STATUS_THROW; // UnhandledMatchError if no default node exists
$combined_status = 0;
foreach ($node->children as $arm_node) {
// @phan-suppress-next-line PhanPossiblyUndeclaredProperty
$arm_cond = $arm_node->children['cond'];
if ($arm_cond === null) {
$default_status = $this->visitMatchArm($arm_node);
continue;
}
$combined_status |= $this->visitMatchArm($arm_node);
}
return $default_status | $combined_status;
}
public const UNEXITABLE_LOOP_INNER_STATUS = self::STATUS_PROCEED | self::STATUS_CONTINUE;
public const STATUS_CONTINUE_OR_BREAK = self::STATUS_CONTINUE | self::STATUS_BREAK;
@@ -326,7 +415,6 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
// We assume foreach loops are over a finite sequence, and that it's possible for that sequence to have at least one element.
$inner_status = $this->check($node->children['stmts']);
// 1. break/continue apply to the inside of a loop, not outside. Not going to analyze "break 2;", may emit an info level issue in the future.
// 2. We assume that it's possible that any given loop can have 0 iterations.
// A TODO exists above to check for special cases.
@@ -421,7 +509,7 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
public function visitUnaryOp(Node $node): int
{
// Don't modify $node->flags, use unmodified flags here
if ($node->flags !== \ast\flags\UNARY_SILENCE) {
if ($node->flags !== ast\flags\UNARY_SILENCE) {
return self::STATUS_PROCEED;
}
// Analyze exit status of `@expr` like `expr` (e.g. @trigger_error())
@@ -449,11 +537,47 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
return $status;
}
/**
* Determines the exit status of a static method call.
*
* @return int the corresponding status code
*/
public function visitStaticCall(Node $node): int
{
// TODO: The expression or arguments might unconditionally throw, though that is rare in practice.
return ($node->flags & self::STATUS_BITMASK) ?: self::STATUS_PROCEED;
}
/**
* Determines the exit status of an instance method call.
*
* @return int the corresponding status code
* @override
* @see UseReturnValueVisitor::checkIfUsingFunctionThatNeverReturns()
*/
public function visitMethodCall(Node $node): int
{
// TODO: The expression or arguments might unconditionally throw, though that is rare in practice.
return ($node->flags & self::STATUS_BITMASK) ?: self::STATUS_PROCEED;
}
/**
* Determines the exit status of an instance method call.
*
* @return int the corresponding status code
* @override
*/
public function visitNullsafeMethodCall(Node $node): int
{
// TODO: The expression or arguments might unconditionally throw, though that is rare in practice.
return ($node->flags & self::STATUS_BITMASK) ?: self::STATUS_PROCEED;
}
private static function computeStatusOfCall(Node $node): int
{
$expression = $node->children['expr'];
if ($expression instanceof Node) {
if ($expression->kind !== \ast\AST_NAME) {
if ($expression->kind !== ast\AST_NAME) {
return self::STATUS_PROCEED; // best guess
}
$function_name = $expression->children['name'];
@@ -467,8 +591,13 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
$function_name = $expression;
}
if ($function_name === '') {
// TODO: Check for all invalid fqsens, allowing 'NS\ClassName::methodName'
return self::STATUS_THROW; // nonsense such as ''();
}
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
// This is creating a closure, not calling it.
return self::STATUS_PROCEED;
}
if ($function_name[0] === '\\') {
$function_name = \substr($function_name, 1);
}
@@ -491,13 +620,14 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
if (!($constant_ast instanceof Node)) {
return self::STATUS_PROCEED;
}
if ($constant_ast->kind !== \ast\AST_CONST) {
if ($constant_ast->kind !== ast\AST_CONST) {
return self::STATUS_PROCEED;
}
$name = $constant_ast->children['name']->children['name'] ?? null;
if (!\is_string($name)) {
return self::STATUS_PROCEED;
}
// The returned code for exit() is 'return', e.g. E_USER_ERROR makes trigger_error emit an error then abort execution.
if (\in_array($name, ['E_ERROR', 'E_PARSE', 'E_CORE_ERROR', 'E_COMPILE_ERROR', 'E_USER_ERROR'], true)) {
return self::STATUS_RETURN;
}
@@ -525,6 +655,22 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
return $status;
}
/**
* An expression list has the weakest return status out of all of the (non-PROCEEDing) statements.
* @return int the corresponding status code
* @override
*/
public function visitExprList(Node $node): int
{
$status = $node->flags & self::STATUS_BITMASK;
if ($status) {
return $status;
}
$status = $this->computeStatusOfBlock($node->children);
$node->flags = $status;
return $status;
}
/**
* Analyzes a node with kind \ast\AST_IF
* @return int the exit status of a block (whether or not it would unconditionally exit, return, throw, etc.
@@ -633,7 +779,7 @@ final class BlockExitStatusChecker extends KindVisitorImplementation
}
$status = $this->check($child);
if (($status & self::STATUS_PROCEED) === 0) {
// If it's guaranteed we won't stop after this statement,
// If it's guaranteed we won't proceed after this statement,
// then skip the subsequent statements.
return $status | ($maybe_status & ~self::STATUS_PROCEED);
}

Some files were not shown because too many files have changed in this diff Show More