Dep: update

주로 PHAN
This commit is contained in:
2021-08-06 22:39:09 +09:00
parent 404cd24855
commit 21e7a4d966
1098 changed files with 92137 additions and 33228 deletions
+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