Install phan/phpstan local

This commit is contained in:
Clemens Schwaighofer
2023-02-08 12:02:18 +09:00
parent 53eef03387
commit f94b350ba4
1166 changed files with 298568 additions and 0 deletions

28
vendor/phan/phan/.dockerignore vendored Normal file
View File

@@ -0,0 +1,28 @@
.git/
internal/PHP_CodeSniffer
tests/docker
tests/run_all_tests_dockerized
.git
vendor
.phan/data
composer.phar
\#*#
build
report
samples
all_output.*
.vscode/
*.joblog
*.swo
*.swp
*.zip
tags
.phan/config.local.php
nohup.out
*.md.new
.idea/
src_copy
out/
phpspy.*
.phpunit.result.cache
errors.txt

View File

@@ -0,0 +1,21 @@
<?php
/**
* This is an **example** of an automatically generated baseline for Phan issues.
* Phan does not use baselines for self-analysis.
*
* When Phan is invoked with --load-baseline=path/to/baseline.php,
* The pre-existing issues listed in this file won't be emitted.
*
* This file can be updated by invoking Phan with --save-baseline=path/to/baseline.php
* (can be combined with --load-baseline)
*/
return [
// # Issue statistics:
// PhanTypeMismatchArgumentInternal : 1 occurrence
// Currently, file_suppressions and directory_suppressions are the only supported suppressions
'file_suppressions' => [
'.phan/plugins/DuplicateExpressionPlugin.php' => ['PhanTypeMismatchArgumentInternal'],
],
// 'directory_suppressions' => ['src/directory_name' => ['PhanIssueNames']] can be manually added if needed.
// (directory_suppressions will currently be ignored by subsequent calls to --save-baseline, but may be preserved in future Phan releases)
];

17
vendor/phan/phan/.phan/bin/mkfilelist vendored Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
if [[ -z $WORKSPACE ]]
then
export WORKSPACE=.
fi
cd $WORKSPACE
for dir in \
src \
tests/Phan \
vendor/phpunit/phpunit/src vendor/symfony/console
do
if [ -d "$dir" ]; then
find $dir -name '*.php'
fi
done

40
vendor/phan/phan/.phan/bin/phan vendored Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/sh
# Root directory of project
export ROOT=`git rev-parse --show-toplevel`
# Phan's directory for executables
export BIN=$ROOT/.phan/bin
# Phan's data directory
export DATA=$ROOT/.phan/data
mkdir -p $DATA;
# Go to the root of this git repo
pushd $ROOT > /dev/null
# Get the current hash of HEAD
export REV=`git rev-parse HEAD`
# Create the data directory for this run if it
# doesn't exist yet
export RUN=$DATA/$REV
mkdir -p $RUN
$BIN/mkfilelist > $RUN/files
# Run the analysis, emitting output to the console
# and using a previous state file.
phan \
--progress-bar \
--project-root-directory $ROOT \
--output $RUN/issues && exit $?
# Re-link the latest directory
rm -f $ROOT/.phan/data/latest
ln -s $RUN $DATA/latest
# Output any issues that were found
cat $RUN/issues
popd > /dev/null

680
vendor/phan/phan/.phan/config.php vendored Normal file
View File

@@ -0,0 +1,680 @@
<?php
declare(strict_types=1);
use Phan\Issue;
/**
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*
* @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
* ==================
*
* Files referenced from this file should be defined as
*
* ```
* Config::projectPath('relative_path/to/file')
* ```
*
* where the relative path is relative to the root of the
* project which is defined as either the working directory
* of the phan executable or a path passed in via the CLI
* '-d' flag.
*/
return [
// 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.
//
// Note that the **only** effect of choosing `'5.6'` is to infer that functions removed in php 7.0 exist.
// (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.
//
// NOTE: Currently, this only affects Closure::fromCallable
'pretend_newer_core_functions_exist' => true,
// If true, missing properties will be created when
// they are first seen. If false, we'll report an
// error message.
'allow_missing_properties' => false,
// Allow null to be cast as any type and for any
// type to be cast to null.
'null_casts_as_any_type' => false,
// Allow null to be cast as any array-like type
// This is an incremental step in migrating away from null_casts_as_any_type.
// If null_casts_as_any_type is true, this has no effect.
'null_casts_as_array' => false,
// Allow any array-like type to be cast to null.
// This is an incremental step in migrating away from null_casts_as_any_type.
// 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 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 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 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 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,
// If enabled, Phan will warn if **any** type of the object expression for a property access
// does not contain that property.
'strict_object_checking' => true,
// 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`.
'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.
// 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']]`
// allows casting null to a string, but not vice versa.
// (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.
// If false, Phan will convert the type of a possibly undefined array offset to the defined equivalent (without converting to nullable).
'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.
'ignore_undeclared_variables_in_global_scope' => false,
// Backwards Compatibility Checking (This is very slow)
'backward_compatibility_checks' => false,
// 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.
'check_docblock_signature_return_type_match' => true,
// If true, check to make sure the param types declared
// in the doc-block (if any) matches the param types
// declared in the method signature.
'check_docblock_signature_param_type_match' => 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*)
//
// 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.
'prefer_narrowed_phpdoc_return_type' => true,
// If enabled, check all methods that override a
// parent method to make sure its signature is
// compatible with the parent's. This check
// can add quite a bit of time to the analysis.
// This will also check if final methods are overridden, etc.
'analyze_signature_compatibility' => true,
// 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`).
// E.g. `function($x = 'val')` would make Phan infer that $x had a type of `string`, not `string|mixed`.
// Phan will not assume it knows specific types if the default value is false or null.
'guess_unknown_parameter_type_using_default' => false,
// Allow adding types to vague return types such as @return object, @return ?mixed in function/method/closure union types.
// Normally, Phan only adds inferred returned types when there is no `@return` type or real return type signature..
// This setting can be disabled on individual methods by adding `@phan-hardcode-return-type` to the doc comment.
//
// Disabled by default. This is more useful with `--analyze-twice`.
'allow_overriding_vague_return_types' => true,
// When enabled, infer that the types of the properties of `$this` are equal to their default values at the start of `__construct()`.
// This will have some false positives due to Phan not checking for setters and initializing helpers.
// This does not affect inherited properties.
'infer_default_properties_in_construct' => true,
// Set this to true to enable the plugins that Phan uses to infer more accurate return types of `implode`, `json_decode`, and many other functions.
//
// 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 is useful if a project uses phpdoc that differs from the phpdoc2 standard.
//
// 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)
//
// (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' => '']`
'phpdoc_type_mapping' => [ ],
// Set to true in order to attempt to detect dead
// (unreferenced) code. Keep in mind that the
// results will only be a guess given that classes,
// properties, constants and methods can be referenced
// 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.
//
// 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
// (functions/methods/consts/protected).
// dead_code_detection is another option which also causes references
// to be tracked.
'force_tracking_references' => false,
// Set to true in order to attempt to detect redundant and impossible conditions.
//
// This has some false positives involving loops,
// variables set in branches of loops, and global variables.
'redundant_condition_detection' => true,
// Set to true in order to attempt to detect error-prone truthiness/falsiness checks.
//
// This is not suitable for all codebases.
'error_prone_truthy_condition_detection' => true,
// Enable this to warn about harmless redundant use for classes and namespaces such as `use Foo\bar` in namespace Foo.
//
// Note: This does not affect warnings about redundant uses in the global namespace.
'warn_about_redundant_use_namespaced_class' => true,
// If true, then run a quick version of checks that takes less time.
// False by default.
'quick_mode' => false,
// If true, then before analysis, try to simplify AST into a form
// which improves Phan's type inference in edge cases.
//
// This may conflict with 'dead_code_detection'.
// When this is true, this slows down analysis slightly.
//
// E.g. rewrites `if ($a = value() && $a > 0) {...}`
// into $a = value(); if ($a) { if ($a > 0) {...}}`
'simplify_ast' => true,
// If true, Phan will read `class_alias` calls in the global scope,
// then (1) create aliases from the *parsed* files if no class definition was found,
// and (2) emit issues in the global scope if the source or target class is invalid.
// (If there are multiple possible valid original classes for an aliased class name,
// the one which will be created is unspecified.)
// NOTE: THIS IS EXPERIMENTAL, and the implementation may change.
'enable_class_alias_support' => false,
// Enable or disable support for generic templated
// class types.
'generic_types_enabled' => true,
// If enabled, warn about throw statement where the exception types
// are not documented in the PHPDoc of functions, methods, and closures.
'warn_about_undocumented_throw_statements' => true,
// If enabled (and warn_about_undocumented_throw_statements is enabled),
// warn about function/closure/method calls that have (at)throws
// without the invoking method documenting that exception.
'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => true,
// If this is a list, Phan will not warn about lack of documentation of (at)throws
// for any of the listed classes or their subclasses.
// This setting only matters when warn_about_undocumented_throw_statements is true.
// The default is the empty array (Warn about every kind of Throwable)
'exception_classes_with_optional_throws_phpdoc' => [
'LogicException',
'RuntimeException',
'InvalidArgumentException',
'AssertionError',
'TypeError',
'Phan\Exception\IssueException', // TODO: Make Phan aware that some arguments suppress certain issues
'Phan\AST\TolerantASTConverter\InvalidNodeException', // This is used internally in TolerantASTConverter
// TODO: Undo the suppressions for the below categories of issues:
'Phan\Exception\CodeBaseException',
// phpunit
'PHPUnit\Framework\ExpectationFailedException',
'SebastianBergmann\RecursionContext\InvalidArgumentException',
],
// Increase this to properly analyze require_once statements
'max_literal_string_type_length' => 1000,
// Setting this to true makes the process assignment for file analysis
// as predictable as possible, using consistent hashing.
// Even if files are added or removed, or process counts change,
// relatively few files will move to a different group.
// (use when the number of files is much larger than the process count)
// NOTE: If you rely on Phan parsing files/directories in the order
// that they were provided in this config, don't use this)
// See https://github.com/phan/phan/wiki/Different-Issue-Sets-On-Different-Numbers-of-CPUs
'consistent_hashing_file_order' => false,
// If enabled, Phan will act as though it's certain of real return types of a subset of internal functions,
// even if those return types aren't available in reflection (real types were taken from php 7.3 or 8.0-dev, depending on target_php_version).
//
// Note that with php 7 and earlier, php would return null or false for many internal functions if the argument types or counts were incorrect.
// As a result, enabling this setting with target_php_version 8.0 may result in false positives for `--redundant-condition-detection` when codebases also support php 7.x.
'assume_real_types_for_internal_functions' => true,
// Override to hardcode existence and types of (non-builtin) globals.
// Class names should be prefixed with '\\'.
// (E.g. ['_FOO' => '\\FooClass', 'page' => '\\PageClass', 'userId' => 'int'])
'globals_type_map' => [],
// The minimum severity level to report on. This can be
// set to Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL or
// Issue::SEVERITY_CRITICAL.
'minimum_severity' => Issue::SEVERITY_LOW,
// 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',
'PhanPluginDescriptionlessCommentOnProtectedMethod',
'PhanPluginNoCommentOnPrivateMethod',
'PhanPluginDescriptionlessCommentOnPrivateMethod',
'PhanPluginDescriptionlessCommentOnPrivateProperty',
// TODO: Fix edge cases in --automatic-fix for PhanPluginRedundantClosureComment
'PhanPluginRedundantClosureComment',
'PhanPluginPossiblyStaticPublicMethod',
'PhanPluginPossiblyStaticProtectedMethod',
// The types of ast\Node->children are all possibly unset.
'PhanTypePossiblyInvalidDimOffset',
// TODO: Fix PhanParamNameIndicatingUnusedInClosure instances (low priority)
'PhanParamNameIndicatingUnusedInClosure',
],
// 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.
//
// Phan is capable of detecting hundreds of types of issues.
// Projects should almost always use `suppress_issue_types` instead.
'whitelist_issue_types' => [
// 'PhanUndeclaredClass',
],
// A list of files to include in analysis
'file_list' => [
'phan',
'phan_client',
'plugins/codeclimate/engine',
'tool/analyze_phpt',
'tool/make_stubs',
'tool/pdep',
'tool/phantasm',
'tool/phoogle',
'tool/phan_repl_helpers.php',
'internal/dump_fallback_ast.php',
'internal/dump_html_styles.php',
'internal/emit_signature_map_for_php_version.php',
'internal/extract_arg_info.php',
'internal/flatten_signature_map.php',
'internal/internalsignatures.php',
'internal/line_deleter.php',
'internal/package.php',
'internal/reflection_completeness_check.php',
'internal/sanitycheck.php',
'internal/sort_signature_map.php',
'vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlock.php',
// 'vendor/phpunit/phpunit/src/Framework/TestCase.php',
],
// A regular expression to match files to be excluded
// from parsing and analysis and will not be read at all.
//
// This is useful for excluding groups of test or example
// directories/files, unanalyzable files, or files that
// can't be removed for whatever reason.
// (e.g. '@Test\.php$@', or '@vendor/.*/(tests|Tests)/@')
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
// Enable this to enable checks of require/include statements referring to valid paths.
'enable_include_path_checks' => true,
// A list of include paths to check when checking if `require_once`, `include`, etc. are valid.
//
// To refer to the directory of the file being analyzed, use `'.'`
// To refer to the project root directory, you must use \Phan\Config::getProjectRootDirectory()
//
// (E.g. `['.', \Phan\Config::getProjectRootDirectory() . '/src/folder-added-to-include_path']`)
'include_paths' => ['.'],
// Enable this to warn about the use of relative paths in `require_once`, `include`, etc.
// Relative paths are harder to reason about, and opcache may have issues with relative paths in edge cases.
'warn_about_relative_include_statement' => true,
// A list of files that will be excluded from parsing and analysis
// and will not be read at all.
//
// This is useful for excluding hopelessly unanalyzable
// files that can't be removed for whatever reason.
'exclude_file_list' => [
'internal/Sniffs/ValidUnderscoreVariableNameSniff.php',
],
// The number of processes to fork off during the analysis
// phase.
'processes' => 1,
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'internal/lib',
'src',
'tests/Phan',
'vendor/composer/semver/src',
'vendor/composer/xdebug-handler/src',
'vendor/felixfbecker/advanced-json-rpc/lib',
'vendor/microsoft/tolerant-php-parser/src',
'vendor/netresearch/jsonmapper/src',
'vendor/phpunit/phpunit/src',
'vendor/psr/log', // subdirectory depends on dependency version
'vendor/sabre/event/lib',
'vendor/symfony/console',
'vendor/symfony/polyfill-php80',
'vendor/tysonandre/var_representation_polyfill/src',
'.phan/plugins',
'.phan/stubs',
],
// List of case-insensitive file extensions supported by Phan.
// (e.g. php, html, htm)
'analyzed_file_extensions' => ['php'],
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as 'vendor/') in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to the `directory_list` as
// to `exclude_analysis_directory_list`.
'exclude_analysis_directory_list' => [
'vendor/'
],
// 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)
'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 '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)
//
// If this is true(default), then Phan will not warn.
//
// Even when this is false, Phan will still infer return values and check parameters of internal functions
// if Phan has the signatures.
'ignore_undeclared_functions_with_known_signatures' => false,
'plugin_config' => [
// A list of 1 or more PHP binaries (Absolute path or program name found in $PATH)
// to use to analyze your files with PHP's native `--syntax-check`.
//
// This can be used to simultaneously run PHP's syntax checks with multiple PHP versions.
// e.g. `'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]`
// if all of those programs can be found in $PATH
// 'php_native_syntax_check_binaries' => [PHP_BINARY],
// The maximum number of `php --syntax-check` processes to run at any point in time (Minimum: 1).
// 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,
// 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)
// This may not apply to all code bases,
// but is useful in avoiding copied and pasted descriptions that may be inapplicable or too vague.
'has_phpdoc_check_duplicates' => true,
// If true, then never allow empty statement lists, even if there is a TODO/FIXME/"deliberately empty" comment.
'empty_statement_list_ignore_todos' => true,
// 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', // i.e. '.phan/plugin/AlwaysReturnPlugin.php' in phan itself
'DollarDollarPlugin',
'UnreachableCodePlugin',
'DuplicateArrayKeyPlugin',
'PregRegexCheckerPlugin',
'PrintfCheckerPlugin',
'PHPUnitAssertionPlugin', // analyze assertSame/assertInstanceof/assertTrue/assertFalse
'UseReturnValuePlugin',
// UnknownElementTypePlugin warns about unknown types in element signatures.
'UnknownElementTypePlugin',
'DuplicateExpressionPlugin',
// warns about carriage returns("\r"), trailing whitespace, and tabs in PHP files.
'WhitespacePlugin',
// Warn about inline HTML anywhere in the files.
'InlineHTMLPlugin',
////////////////////////////////////////////////////////////////////////
// Plugins for Phan's self-analysis
////////////////////////////////////////////////////////////////////////
// Warns about the usage of assert() for Phan's self-analysis. See https://github.com/phan/phan/issues/288
'NoAssertPlugin',
'PossiblyStaticMethodPlugin',
'HasPHPDocPlugin',
'PHPDocToRealTypesPlugin', // suggests replacing (at)return void with `: void` in the declaration, etc.
'PHPDocRedundantPlugin',
'PreferNamespaceUsePlugin',
'EmptyStatementListPlugin',
// Report empty (not overridden or overriding) methods and functions
// 'EmptyMethodAndFunctionPlugin',
// This should only be enabled if the code being analyzed contains Phan plugins.
'PhanSelfCheckPlugin',
// Warn about using the same loop variable name as a loop variable of an outer loop.
'LoopVariableReusePlugin',
// Warn about assigning the value the variable already had to that variable.
'RedundantAssignmentPlugin',
// 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'`
'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',
// Suggest '@return never'
'.phan/plugins/AddNeverReturnTypePlugin.php',
// Still have false positives to suppress
// '.phan/plugins/StaticVariableMisusePlugin.php',
////////////////////////////////////////////////////////////////////////
// End plugins for Phan's self-analysis
////////////////////////////////////////////////////////////////////////
// 'SleepCheckerPlugin' is useful for projects which heavily use the __sleep() method. Phan doesn't use __sleep().
// InvokePHPNativeSyntaxCheckPlugin invokes 'php --no-php-ini --syntax-check ${abs_path_to_analyzed_file}.php' and reports any error messages.
// Using this can cause phan's overall analysis time to more than double.
// 'InvokePHPNativeSyntaxCheckPlugin',
// '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',
],
];

View File

@@ -0,0 +1,4 @@
This folder will eventually contain stubs for the latest versions of various extensions.
If the extension is loaded in the binary to run phan, then phan will do nothing.
The plan is to make phan load these files and act as though internal classes, constants,
and functions existed with the same signatures as these php files.

View File

@@ -0,0 +1,234 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ast@1.0.12
namespace ast {
class Metadata {
// properties
public $flags;
public $flagsCombinable;
public $kind;
public $name;
}
#[\AllowDynamicProperties]
class Node {
// properties
public $children;
public $endLineno; // not actually a declared property but created for declarations by php-ast.
public $flags;
public $kind;
public $lineno;
// methods
public function __construct(?int $kind = null, ?int $flags = null, ?array $children = null, ?int $lineno = null) {}
}
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;
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;
const AST_CAST = 261;
const AST_CATCH = 772;
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;
const AST_CLOSURE_USES = 137;
const AST_CLOSURE_VAR = 2049;
const AST_CONDITIONAL = 770;
const AST_CONST = 257;
const AST_CONST_DECL = 139;
const AST_CONST_ELEM = 775;
const AST_CONTINUE = 287;
const AST_DECLARE = 537;
const AST_DIM = 512;
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;
const AST_FOREACH = 1025;
const AST_FUNC_DECL = 67;
const AST_GLOBAL = 277;
const AST_GOTO = 285;
const AST_GROUP_USE = 544;
const AST_HALT_COMPILER = 282;
const AST_IF = 133;
const AST_IF_ELEM = 534;
const AST_INCLUDE_OR_EVAL = 269;
const AST_INSTANCEOF = 527;
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;
const AST_POST_INC = 273;
const AST_PRE_DEC = 272;
const AST_PRE_INC = 271;
const AST_PRINT = 268;
const AST_PROP = 513;
const AST_PROP_DECL = 138;
const AST_PROP_ELEM = 774;
const AST_PROP_GROUP = 545;
const AST_REF = 281;
const AST_RETURN = 279;
const AST_SHELL_EXEC = 265;
const AST_STATIC = 531;
const AST_STATIC_CALL = 769;
const AST_STATIC_PROP = 514;
const AST_STMT_LIST = 132;
const AST_SWITCH = 535;
const AST_SWITCH_CASE = 536;
const AST_SWITCH_LIST = 134;
const AST_THROW = 284;
const AST_TRAIT_ADAPTATIONS = 142;
const AST_TRAIT_ALIAS = 543;
const AST_TRAIT_PRECEDENCE = 539;
const AST_TRY = 771;
const AST_TYPE = 1;
const AST_TYPE_UNION = 254;
const AST_UNARY_OP = 270;
const AST_UNPACK = 258;
const AST_UNSET = 278;
const AST_USE = 143;
const AST_USE_ELEM = 542;
const AST_USE_TRAIT = 538;
const AST_VAR = 256;
const AST_WHILE = 532;
const AST_YIELD = 528;
const AST_YIELD_FROM = 275;
}
namespace ast\flags {
const ARRAY_ELEM_REF = 1;
const ARRAY_SYNTAX_LIST = 1;
const ARRAY_SYNTAX_LONG = 2;
const ARRAY_SYNTAX_SHORT = 3;
const BINARY_ADD = 1;
const BINARY_BITWISE_AND = 10;
const BINARY_BITWISE_OR = 9;
const BINARY_BITWISE_XOR = 11;
const BINARY_BOOL_AND = 259;
const BINARY_BOOL_OR = 258;
const BINARY_BOOL_XOR = 15;
const BINARY_COALESCE = 260;
const BINARY_CONCAT = 8;
const BINARY_DIV = 4;
const BINARY_IS_EQUAL = 18;
const BINARY_IS_GREATER = 256;
const BINARY_IS_GREATER_OR_EQUAL = 257;
const BINARY_IS_IDENTICAL = 16;
const BINARY_IS_NOT_EQUAL = 19;
const BINARY_IS_NOT_IDENTICAL = 17;
const BINARY_IS_SMALLER = 20;
const BINARY_IS_SMALLER_OR_EQUAL = 21;
const BINARY_MOD = 5;
const BINARY_MUL = 3;
const BINARY_POW = 12;
const BINARY_SHIFT_LEFT = 6;
const BINARY_SHIFT_RIGHT = 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;
const CLOSURE_USE_REF = 1;
const DIM_ALTERNATIVE_SYNTAX = 2;
const EXEC_EVAL = 1;
const EXEC_INCLUDE = 2;
const EXEC_INCLUDE_ONCE = 4;
const EXEC_REQUIRE = 8;
const EXEC_REQUIRE_ONCE = 16;
const FUNC_GENERATOR = 16777216;
const FUNC_RETURNS_REF = 4096;
const MAGIC_CLASS = 376;
const MAGIC_DIR = 375;
const MAGIC_FILE = 374;
const MAGIC_FUNCTION = 379;
const MAGIC_LINE = 373;
const MAGIC_METHOD = 378;
const MAGIC_NAMESPACE = 392;
const MAGIC_TRAIT = 377;
const MODIFIER_ABSTRACT = 64;
const MODIFIER_FINAL = 32;
const MODIFIER_PRIVATE = 4;
const MODIFIER_PROTECTED = 2;
const MODIFIER_PUBLIC = 1;
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;
const RETURNS_REF = 4096;
const TYPE_ARRAY = 7;
const TYPE_BOOL = 16;
const TYPE_CALLABLE = 17;
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;
const TYPE_STRING = 6;
const TYPE_VOID = 19;
const UNARY_BITWISE_NOT = 13;
const UNARY_BOOL_NOT = 14;
const UNARY_MINUS = 262;
const UNARY_PLUS = 261;
const UNARY_SILENCE = 260;
const USE_CONST = 4;
const USE_FUNCTION = 2;
const USE_NORMAL = 1;
}

View File

@@ -0,0 +1,17 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ctype@7.4.19-dev
namespace {
function ctype_alnum($text) {}
function ctype_alpha($text) {}
function ctype_cntrl($text) {}
function ctype_digit($text) {}
function ctype_graph($text) {}
function ctype_lower($text) {}
function ctype_print($text) {}
function ctype_punct($text) {}
function ctype_space($text) {}
function ctype_upper($text) {}
function ctype_xdigit($text) {}
}

View File

@@ -0,0 +1,8 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension igbinary@3.2.2
namespace {
function igbinary_serialize($value) {}
function igbinary_unserialize($str) {}
}

View File

@@ -0,0 +1,91 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension mbstring@7.4.19-dev
namespace {
function mb_check_encoding($var = null, $encoding = null) {}
function mb_chr($cp, $encoding = null) {}
function mb_convert_case($sourcestring, $mode, $encoding = null) {}
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, $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) {}
function mb_encode_numericentity($string, $convmap, $encoding = null, $is_hex = null) {}
function mb_encoding_aliases($encoding) {}
function mb_ereg($pattern, $string, &$registers = null) {}
function mb_ereg_match($pattern, $string, $option = null) {}
function mb_ereg_replace($pattern, $replacement, $string, $option = null) {}
function mb_ereg_replace_callback($pattern, $callback, $string, $option = null) {}
function mb_ereg_search($pattern = null, $option = null) {}
function mb_ereg_search_getpos() {}
function mb_ereg_search_getregs() {}
function mb_ereg_search_init($string, $pattern = null, $option = null) {}
function mb_ereg_search_pos($pattern = null, $option = null) {}
function mb_ereg_search_regs($pattern = null, $option = null) {}
function mb_ereg_search_setpos($position) {}
function mb_eregi($pattern, $string, &$registers = null) {}
function mb_eregi_replace($pattern, $replacement, $string, $option = null) {}
function mb_get_info($type = null) {}
function mb_http_input($type = null) {}
function mb_http_output($encoding = null) {}
function mb_internal_encoding($encoding = null) {}
function mb_language($language = null) {}
function mb_list_encodings() {}
function mb_ord($str, $encoding = null) {}
function mb_output_handler($contents, $status) {}
function mb_parse_str($encoded_string, &$result = null) {}
function mb_preferred_mime_name($encoding) {}
function mb_regex_encoding($encoding = null) {}
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) {}
function mb_stristr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strlen($str, $encoding = null) {}
function mb_strpos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strrchr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strrichr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strripos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strrpos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strstr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strtolower($sourcestring, $encoding = null) {}
function mb_strtoupper($sourcestring, $encoding = null) {}
function mb_strwidth($str, $encoding = null) {}
function mb_substitute_character($substchar = null) {}
function mb_substr($str, $start, $length = null, $encoding = null) {}
function mb_substr_count($haystack, $needle, $encoding = null) {}
function mbereg($pattern, $string, &$registers = null) {}
function mbereg_match($pattern, $string, $option = null) {}
function mbereg_replace($pattern, $replacement, $string, $option = null) {}
function mbereg_search($pattern = null, $option = null) {}
function mbereg_search_getpos() {}
function mbereg_search_getregs() {}
function mbereg_search_init($string, $pattern = null, $option = null) {}
function mbereg_search_pos($pattern = null, $option = null) {}
function mbereg_search_regs($pattern = null, $option = null) {}
function mbereg_search_setpos($position) {}
function mberegi($pattern, $string, &$registers = null) {}
function mberegi_replace($pattern, $replacement, $string, $option = null) {}
function mbregex_encoding($encoding = null) {}
function mbsplit($pattern, $string, $limit = null) {}
const MB_CASE_FOLD = 3;
const MB_CASE_FOLD_SIMPLE = 7;
const MB_CASE_LOWER = 1;
const MB_CASE_LOWER_SIMPLE = 5;
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;
}

View File

@@ -0,0 +1,153 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension pcntl@7.4.19-dev
namespace {
function pcntl_alarm($seconds) {}
function pcntl_async_signals($on) {}
function pcntl_errno() {}
function pcntl_exec($path, $args = null, $envs = null) {}
function pcntl_fork() {}
function pcntl_get_last_error() {}
function pcntl_getpriority($pid = null, $process_identifier = null) {}
function pcntl_setpriority($priority, $pid = null, $process_identifier = null) {}
function pcntl_signal($signo, $handler, $restart_syscalls = null) {}
function pcntl_signal_dispatch() {}
function pcntl_signal_get_handler($signo) {}
function pcntl_sigprocmask($how, $set, &$oldset = null) {}
function pcntl_sigtimedwait($set, &$info = null, $seconds = null, $nanoseconds = null) {}
function pcntl_sigwaitinfo($set, &$info = null) {}
function pcntl_strerror($errno) {}
function pcntl_unshare($flags) {}
function pcntl_wait(&$status, $options = null, &$rusage = null) {}
function pcntl_waitpid($pid, &$status, $options = null, &$rusage = null) {}
function pcntl_wexitstatus($status) {}
function pcntl_wifcontinued($status) {}
function pcntl_wifexited($status) {}
function pcntl_wifsignaled($status) {}
function pcntl_wifstopped($status) {}
function pcntl_wstopsig($status) {}
function pcntl_wtermsig($status) {}
const BUS_ADRALN = 1;
const BUS_ADRERR = 2;
const BUS_OBJERR = 3;
const CLD_CONTINUED = 6;
const CLD_DUMPED = 3;
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;
const CLONE_NEWPID = 536870912;
const CLONE_NEWUSER = 268435456;
const CLONE_NEWUTS = 67108864;
const FPE_FLTDIV = 3;
const FPE_FLTINV = 7;
const FPE_FLTOVF = 4;
const FPE_FLTRES = 6;
const FPE_FLTSUB = 8;
const FPE_FLTUND = 7;
const FPE_INTDIV = 1;
const FPE_INTOVF = 2;
const ILL_BADSTK = 8;
const ILL_COPROC = 7;
const ILL_ILLADR = 3;
const ILL_ILLOPC = 1;
const ILL_ILLOPN = 2;
const ILL_ILLTRP = 4;
const ILL_PRVOPC = 5;
const ILL_PRVREG = 6;
const PCNTL_E2BIG = 7;
const PCNTL_EACCES = 13;
const PCNTL_EAGAIN = 11;
const PCNTL_ECHILD = 10;
const PCNTL_EFAULT = 14;
const PCNTL_EINTR = 4;
const PCNTL_EINVAL = 22;
const PCNTL_EIO = 5;
const PCNTL_EISDIR = 21;
const PCNTL_ELIBBAD = 80;
const PCNTL_ELOOP = 40;
const PCNTL_EMFILE = 24;
const PCNTL_ENAMETOOLONG = 36;
const PCNTL_ENFILE = 23;
const PCNTL_ENOENT = 2;
const PCNTL_ENOEXEC = 8;
const PCNTL_ENOMEM = 12;
const PCNTL_ENOSPC = 28;
const PCNTL_ENOTDIR = 20;
const PCNTL_EPERM = 1;
const PCNTL_ESRCH = 3;
const PCNTL_ETXTBSY = 26;
const PCNTL_EUSERS = 87;
const POLL_ERR = 4;
const POLL_HUP = 6;
const POLL_IN = 1;
const POLL_MSG = 3;
const POLL_OUT = 2;
const POLL_PRI = 5;
const PRIO_PGRP = 1;
const PRIO_PROCESS = 0;
const PRIO_USER = 2;
const SEGV_ACCERR = 2;
const SEGV_MAPERR = 1;
const SIGABRT = 6;
const SIGALRM = 14;
const SIGBABY = 31;
const SIGBUS = 7;
const SIGCHLD = 17;
const SIGCLD = 17;
const SIGCONT = 18;
const SIGFPE = 8;
const SIGHUP = 1;
const SIGILL = 4;
const SIGINT = 2;
const SIGIO = 29;
const SIGIOT = 6;
const SIGKILL = 9;
const SIGPIPE = 13;
const SIGPOLL = 29;
const SIGPROF = 27;
const SIGPWR = 30;
const SIGQUIT = 3;
const SIGRTMAX = 64;
const SIGRTMIN = 34;
const SIGSEGV = 11;
const SIGSTKFLT = 16;
const SIGSTOP = 19;
const SIGSYS = 31;
const SIGTERM = 15;
const SIGTRAP = 5;
const SIGTSTP = 20;
const SIGTTIN = 21;
const SIGTTOU = 22;
const SIGURG = 23;
const SIGUSR1 = 10;
const SIGUSR2 = 12;
const SIGVTALRM = 26;
const SIGWINCH = 28;
const SIGXCPU = 24;
const SIGXFSZ = 25;
const SIG_BLOCK = 0;
const SIG_DFL = 0;
const SIG_ERR = -1;
const SIG_IGN = 1;
const SIG_SETMASK = 2;
const SIG_UNBLOCK = 1;
const SI_ASYNCIO = -4;
const SI_KERNEL = 128;
const SI_MESGQ = -3;
const SI_QUEUE = -1;
const SI_SIGIO = -5;
const SI_TIMER = -2;
const SI_TKILL = -6;
const SI_USER = 0;
const TRAP_BRKPT = 1;
const TRAP_TRACE = 2;
const WCONTINUED = 8;
const WNOHANG = 1;
const WUNTRACED = 2;
}

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) {}
}
}

View File

@@ -0,0 +1,69 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension posix@7.4.19-dev
namespace {
function posix_access($file, $mode = null) {}
function posix_ctermid() {}
function posix_errno() {}
function posix_get_last_error() {}
function posix_getcwd() {}
function posix_getegid() {}
function posix_geteuid() {}
function posix_getgid() {}
function posix_getgrgid($gid) {}
function posix_getgrnam($name) {}
function posix_getgroups() {}
function posix_getlogin() {}
function posix_getpgid($pid) {}
function posix_getpgrp() {}
function posix_getpid() {}
function posix_getppid() {}
function posix_getpwnam($username) {}
function posix_getpwuid($uid) {}
function posix_getrlimit() {}
function posix_getsid($pid) {}
function posix_getuid() {}
function posix_initgroups($name, $base_group_id) {}
function posix_isatty($fd) {}
function posix_kill($pid, $sig) {}
function posix_mkfifo($pathname, $mode) {}
function posix_mknod($pathname, $mode, $major = null, $minor = null) {}
function posix_setegid($gid) {}
function posix_seteuid($uid) {}
function posix_setgid($gid) {}
function posix_setpgid($pid, $pgid) {}
function posix_setrlimit($resource, $softlimit, $hardlimit) {}
function posix_setsid() {}
function posix_setuid($uid) {}
function posix_strerror($errno) {}
function posix_times() {}
function posix_ttyname($fd) {}
function posix_uname() {}
const POSIX_F_OK = 0;
const POSIX_RLIMIT_AS = 9;
const POSIX_RLIMIT_CORE = 4;
const POSIX_RLIMIT_CPU = 0;
const POSIX_RLIMIT_DATA = 2;
const POSIX_RLIMIT_FSIZE = 1;
const POSIX_RLIMIT_INFINITY = -1;
const POSIX_RLIMIT_LOCKS = 10;
const POSIX_RLIMIT_MEMLOCK = 8;
const POSIX_RLIMIT_MSGQUEUE = 12;
const POSIX_RLIMIT_NICE = 13;
const POSIX_RLIMIT_NOFILE = 7;
const POSIX_RLIMIT_NPROC = 6;
const POSIX_RLIMIT_RSS = 5;
const POSIX_RLIMIT_RTPRIO = 14;
const POSIX_RLIMIT_RTTIME = 15;
const POSIX_RLIMIT_SIGPENDING = 11;
const POSIX_RLIMIT_STACK = 3;
const POSIX_R_OK = 4;
const POSIX_S_IFBLK = 24576;
const POSIX_S_IFCHR = 8192;
const POSIX_S_IFIFO = 4096;
const POSIX_S_IFREG = 32768;
const POSIX_S_IFSOCK = 49152;
const POSIX_W_OK = 2;
const POSIX_X_OK = 1;
}

View File

@@ -0,0 +1,20 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension readline@7.4.19-dev
namespace {
function readline($prompt = null) {}
function readline_add_history($prompt) {}
function readline_callback_handler_install($prompt, $callback) {}
function readline_callback_handler_remove() {}
function readline_callback_read_char() {}
function readline_clear_history() {}
function readline_completion_function($funcname) {}
function readline_info($varname = null, $newvalue = null) {}
function readline_list_history() {}
function readline_on_new_line() {}
function readline_read_history($filename = null) {}
function readline_redisplay() {}
function readline_write_history($filename = null) {}
const READLINE_LIB = 'readline';
}

View File

@@ -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) {}
}

View File

@@ -0,0 +1,18 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvmsg@7.4.19-dev
namespace {
function msg_get_queue($key, $perms = null) {}
function msg_queue_exists($key) {}
function msg_receive($queue, $desiredmsgtype, &$msgtype, $maxsize, &$message, $unserialize = null, $flags = null, &$errorcode = null) {}
function msg_remove_queue($queue) {}
function msg_send($queue, $msgtype, $message, $serialize = null, $blocking = null, &$errorcode = null) {}
function msg_set_queue($queue, $data) {}
function msg_stat_queue($queue) {}
const MSG_EAGAIN = 11;
const MSG_ENOMSG = 42;
const MSG_EXCEPT = 4;
const MSG_IPC_NOWAIT = 1;
const MSG_NOERROR = 2;
}

View File

@@ -0,0 +1,10 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvsem@7.4.19-dev
namespace {
function sem_acquire($sem_identifier, $nowait = null) {}
function sem_get($key, $max_acquire = null, $perm = null, $auto_release = null) {}
function sem_release($sem_identifier) {}
function sem_remove($sem_identifier) {}
}

View File

@@ -0,0 +1,13 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvshm@7.4.19-dev
namespace {
function shm_attach($key, $memsize = null, $perm = null) {}
function shm_detach($shm_identifier) {}
function shm_get_var($id, $variable_key) {}
function shm_has_var($id, $variable_key) {}
function shm_put_var($shm_identifier, $variable_key, $variable) {}
function shm_remove($shm_identifier) {}
function shm_remove_var($id, $variable_key) {}
}

View File

@@ -0,0 +1,65 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension xdebug@2.9.4
// (The xdebug stub is included with Phan for use by users affected by phan restarting with xdebug unavailable)
namespace {
function xdebug_break() {}
function xdebug_call_class($depth = null) {}
function xdebug_call_file($depth = null) {}
function xdebug_call_function($depth = null) {}
function xdebug_call_line($depth = null) {}
function xdebug_code_coverage_started() {}
function xdebug_debug_zval($var) {}
function xdebug_debug_zval_stdout($var) {}
function xdebug_disable() {}
function xdebug_dump_superglobals() {}
function xdebug_enable() {}
function xdebug_get_code_coverage() {}
function xdebug_get_collected_errors($clear = null) {}
function xdebug_get_declared_vars() {}
function xdebug_get_formatted_function_stack() {}
function xdebug_get_function_count() {}
function xdebug_get_function_stack() {}
function xdebug_get_gc_run_count() {}
function xdebug_get_gc_total_collected_roots() {}
function xdebug_get_gcstats_filename() {}
function xdebug_get_headers() {}
function xdebug_get_monitored_functions($clear = null) {}
function xdebug_get_profiler_filename() {}
function xdebug_get_stack_depth() {}
function xdebug_get_tracefile_name() {}
function xdebug_is_debugger_active() {}
function xdebug_is_enabled() {}
function xdebug_memory_usage() {}
function xdebug_peak_memory_usage() {}
function xdebug_print_function_stack($message = null, $options = null) {}
function xdebug_set_filter($filter_group, $filter_type, $array_of_filters) {}
function xdebug_start_code_coverage($options = null) {}
function xdebug_start_error_collection() {}
function xdebug_start_function_monitor($functions_to_monitor) {}
function xdebug_start_gcstats($fname = null) {}
function xdebug_start_trace($fname = null, $options = null) {}
function xdebug_stop_code_coverage($cleanup = null) {}
function xdebug_stop_error_collection() {}
function xdebug_stop_function_monitor() {}
function xdebug_stop_gcstats() {}
function xdebug_stop_trace() {}
function xdebug_time_index() {}
function xdebug_var_dump($var) {}
const XDEBUG_CC_BRANCH_CHECK = 4;
const XDEBUG_CC_DEAD_CODE = 2;
const XDEBUG_CC_UNUSED = 1;
const XDEBUG_FILTER_CODE_COVERAGE = 512;
const XDEBUG_FILTER_NONE = 0;
const XDEBUG_FILTER_TRACING = 256;
const XDEBUG_NAMESPACE_BLACKLIST = 18;
const XDEBUG_NAMESPACE_WHITELIST = 17;
const XDEBUG_PATH_BLACKLIST = 2;
const XDEBUG_PATH_WHITELIST = 1;
const XDEBUG_STACK_NO_DESC = 1;
const XDEBUG_TRACE_APPEND = 1;
const XDEBUG_TRACE_COMPUTERIZED = 2;
const XDEBUG_TRACE_HTML = 4;
const XDEBUG_TRACE_NAKED_FILENAME = 8;
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\BlockExitStatusChecker;
use Phan\CodeBase;
use Phan\Language\Element\Func;
use Phan\Language\Element\Method;
use Phan\Language\Type\NeverType;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
/**
* This plugin checks if a function or method will not return (and has no overrides).
* If the function doesn't have a return type of never.
* then this plugin will emit an issue.
*
* It hooks into two events:
*
* - analyzeMethod
* Once all methods are parsed, this method will be called
* on every method in the code base
*
* - analyzeFunction
* Once all functions have been parsed, this method will
* be called on every function in the code base.
*
* 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
*/
final class NeverReturnPlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability
{
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
*
* @override
*/
public function analyzeMethod(
CodeBase $code_base,
Method $method
): void {
$stmts_list = self::getStatementListToAnalyze($method);
if ($stmts_list === null) {
// check for abstract methods, generators, etc.
return;
}
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
// Check if this was inherited by a descendant class.
return;
}
if ($method->getUnionType()->hasType(NeverType::instance(false))) {
return;
}
if ($method->isOverriddenByAnother()) {
return;
}
// This modifies the nodes in place, check this last
if (!BlockExitStatusChecker::willUnconditionallyNeverReturn($stmts_list)) {
return;
}
self::emitIssue(
$code_base,
$method->getContext(),
'PhanPluginNeverReturnMethod',
"Method {METHOD} never returns and has a return type of {TYPE}, but phpdoc type {TYPE} could be used instead",
[$method->getRepresentationForIssue(), $method->getUnionType(), 'never']
);
}
/**
* @param CodeBase $code_base
* The code base in which the function exists
*
* @param Func $function
* A function or closure being analyzed
*
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
$stmts_list = self::getStatementListToAnalyze($function);
if ($stmts_list === null) {
// check for abstract methods, generators, etc.
return;
}
if ($function->getUnionType()->hasType(NeverType::instance(false))) {
return;
}
// This modifies the nodes in place, check this last
if (!BlockExitStatusChecker::willUnconditionallyNeverReturn($stmts_list)) {
return;
}
self::emitIssue(
$code_base,
$function->getContext(),
'PhanPluginNeverReturnFunction',
"Function {FUNCTION} never returns and has a return type of {TYPE}, but phpdoc type {TYPE} could be used instead",
[$function->getRepresentationForIssue(), $function->getUnionType(), 'never']
);
}
/**
* @param Func|Method $func
* @return ?Node - returns null if there's no statement list to analyze
*/
private static function getStatementListToAnalyze($func): ?Node
{
if (!$func->hasNode()) {
return null;
} elseif ($func->hasYield()) {
// generators always return Generator.
return null;
}
$node = $func->getNode();
if (!$node) {
return null;
}
return $node->children['stmts'];
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new NeverReturnPlugin();

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\BlockExitStatusChecker;
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;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
/**
* This file checks if a function, closure or method unconditionally returns.
* If the function doesn't have a void return type,
* then this plugin will emit an issue.
*
* It hooks into two events:
*
* - analyzeMethod
* Once all methods are parsed, this method will be called
* on every method in the code base
*
* - analyzeFunction
* Once all functions have been parsed, this method will
* be called on every function in the code base.
*
* 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
*/
final class AlwaysReturnPlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability
{
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
*
* @override
*/
public function analyzeMethod(
CodeBase $code_base,
Method $method
): void {
$stmts_list = self::getStatementListToAnalyze($method);
if ($stmts_list === null) {
// check for abstract methods, generators, etc.
return;
}
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
// Check if this was inherited by a descendant class.
return;
}
if (self::returnTypeOfFunctionLikeAllowsNull($method)) {
// This has at least one return statement with an expression
if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) {
if ($method->getUnionType()->isEmpty() && $method->hasReturn()) {
if (!$method->checkHasSuppressIssueAndIncrementCount('PhanPluginInconsistentReturnMethod')) {
self::emitIssue(
$code_base,
$method->getContext(),
'PhanPluginInconsistentReturnMethod',
"Method {METHOD} has no return type and will inconsistently return or not return",
[(string)$method->getFQSEN()]
);
}
}
}
return;
}
if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) {
if (!$method->checkHasSuppressIssueAndIncrementCount('PhanPluginAlwaysReturnMethod')) {
self::emitIssue(
$code_base,
$method->getContext(),
'PhanPluginAlwaysReturnMethod',
"Method {METHOD} has a return type of {TYPE}, but may fail to return a value",
[(string)$method->getFQSEN(), (string)$method->getUnionType()]
);
}
}
}
/**
* @param CodeBase $code_base
* The code base in which the function exists
*
* @param Func $function
* A function or closure being analyzed
*
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
$stmts_list = self::getStatementListToAnalyze($function);
if ($stmts_list === null) {
// check for generators, etc.
return;
}
if (self::returnTypeOfFunctionLikeAllowsNull($function)) {
if ($function->getUnionType()->isEmpty() && $function->hasReturn()) {
// This has at least one return statement with an expression
if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) {
if (!$function->checkHasSuppressIssueAndIncrementCount('PhanPluginInconsistentReturnFunction')) {
self::emitIssue(
$code_base,
$function->getContext(),
'PhanPluginInconsistentReturnFunction',
"Function {FUNCTION} has no return type and will inconsistently return or not return",
[(string)$function->getFQSEN()]
);
}
}
}
return;
}
if (!BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_list)) {
if (!$function->checkHasSuppressIssueAndIncrementCount('PhanPluginAlwaysReturnFunction')) {
self::emitIssue(
$code_base,
$function->getContext(),
'PhanPluginAlwaysReturnFunction',
"Function {FUNCTION} has a return type of {TYPE}, but may fail to return a value",
[(string)$function->getFQSEN(), (string)$function->getUnionType()]
);
}
}
}
/**
* @param Func|Method $func
* @return ?Node - returns null if there's no statement list to analyze
*/
private static function getStatementListToAnalyze($func): ?Node
{
if (!$func->hasNode()) {
return null;
} elseif ($func->hasYield()) {
// generators always return Generator.
return null;
}
$node = $func->getNode();
if (!$node) {
return null;
}
return $node->children['stmts'];
}
/**
* @param FunctionInterface $func
* @return bool - Is void(absence of a return type) an acceptable return type.
* NOTE: projects can customize this as needed.
*/
private static function returnTypeOfFunctionLikeAllowsNull(FunctionInterface $func): bool
{
$real_return_type = $func->getRealReturnType();
if (!$real_return_type->isEmpty() && !$real_return_type->isType(VoidType::instance(false))) {
return false;
}
$return_type = $func->getUnionType();
return ($return_type->isEmpty()
|| $return_type->containsNullableLabeled()
|| $return_type->hasType(NeverType::instance(false))
|| $return_type->hasType(VoidType::instance(false))
|| $return_type->hasType(NullType::instance(false)));
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new AlwaysReturnPlugin();

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\ConditionVisitor;
use Phan\AST\ASTReverter;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for uses of getters that can be avoided inside of a class.
*
* - E.g. `$this->getProperty()` when the property is accessible, and the getter is not overridden.
*/
class AvoidableGetterPlugin extends PluginV3 implements
PostAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return AvoidableGetterVisitor::class;
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class AvoidableGetterVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @var array<string,string> maps getter method names to property names.
*/
private $getter_to_property_map = [];
public function visitClass(Node $node): void
{
if (!$this->context->isInClassScope()) {
// should be impossible
return;
}
$code_base = $this->code_base;
$class = $this->context->getClassInScope($code_base);
$getters = $class->getGettersMap($code_base);
if (!$getters) {
return;
}
$getter_to_property_map = [];
foreach ($getters as $prop_name => $methods) {
$prop_name = (string)$prop_name;
if (!$class->hasPropertyWithName($code_base, $prop_name)) {
continue;
}
if (!$class->getPropertyByName($code_base, $prop_name)->isAccessibleFromClass($code_base, $class->getFQSEN())) {
continue;
}
foreach ($methods as $method) {
if ($method->isOverriddenByAnother()) {
continue;
}
$getter_to_property_map[$method->getName()] = $prop_name;
}
}
if (!$getter_to_property_map) {
return;
}
$this->getter_to_property_map = $getter_to_property_map;
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
$this->recursivelyCheck($node->children['stmts']);
}
private function recursivelyCheck(Node $node): void
{
switch ($node->kind) {
// TODO: Handle phan-closure-scope.
// case ast\AST_CLOSURE:
// case ast\AST_ARROW_FUNC:
case ast\AST_FUNC_DECL:
case ast\AST_CLASS:
return;
// This only supports instance method getters, not static getters (AST_STATIC_CALL)
case ast\AST_METHOD_CALL:
if (!ConditionVisitor::isThisVarNode($node->children['expr'])) {
break;
}
$method_name = $node->children['method'];
if (is_string($method_name)) {
$property_name = $this->getter_to_property_map[$method_name] ?? null;
if ($property_name !== null) {
$this->warnCanReplaceGetterWithProperty($node, $property_name);
return;
}
}
break;
}
foreach ($node->children as $child_node) {
if ($child_node instanceof Node) {
$this->recursivelyCheck($child_node);
}
}
}
private function warnCanReplaceGetterWithProperty(Node $node, string $property_name): void
{
$class = $this->context->getClassInScope($this->code_base);
if ($class->isTrait()) {
$issue_name = 'PhanPluginAvoidableGetterInTrait';
} else {
$issue_name = 'PhanPluginAvoidableGetter';
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
$issue_name,
"Can replace {METHOD} with {PROPERTY}",
[ASTReverter::toShortString($node), '$this->' . $property_name]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new AvoidableGetterPlugin();

View File

@@ -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();

View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\Method;
use Phan\Language\Element\Property;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeClassCapability;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AnalyzePropertyCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This file demonstrates plugins for Phan.
* This Plugin hooks into five events;
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class that is called on every AST node from every
* file being analyzed
*
* - analyzeClass
* Once all classes have been parsed, this method will be
* called on every class that is found in the code base
*
* - analyzeMethod
* Once all methods are parsed, this method will be called
* on every method in the code base
*
* - analyzeFunction
* Once all functions have been parsed, this method will
* be called on every function in the code base.
*
* - analyzeProperty
* Once all functions have been parsed, this method will
* be called on every property in the code base.
*
* A plugin file must
*
* - Contain a class that inherits from \Phan\PluginV3
* and implements one or more `Capability`s.
*
* - 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 DemoPlugin extends PluginV3 implements
AnalyzeClassCapability,
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
PostAnalyzeNodeCapability,
AnalyzePropertyCapability
{
/**
* @return string - The name of the visitor that will be called (formerly analyzeNode)
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return DemoNodeVisitor::class;
}
/**
* @param CodeBase $code_base
* The code base in which the class exists
*
* @param Clazz $class
* A class being analyzed
*
* @override
*/
public function analyzeClass(
CodeBase $code_base,
Clazz $class
): void {
// As an example, we test to see if the name of
// the class is `Class`, and emit an issue explaining that
// the name is not allowed.
// NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace
if ($class->getName() === 'Class') {
self::emitIssue(
$code_base,
$class->getContext(),
'DemoPluginClassName',
"Class {CLASS} cannot be called `Class`",
[(string)$class->getFQSEN()]
);
}
}
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
*
* @override
*/
public function analyzeMethod(
CodeBase $code_base,
Method $method
): void {
// As an example, we test to see if the name of the
// method is `function`, and emit an issue if it is.
// NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace
if ($method->getName() === 'function') {
self::emitIssue(
$code_base,
$method->getContext(),
'DemoPluginMethodName',
"Method {METHOD} cannot be called `function`",
[(string)$method->getFQSEN()]
);
}
}
/**
* @param CodeBase $code_base
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
*
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
// As an example, we test to see if the name of the
// function is `function`, and emit an issue if it is.
if ($function->getName() === 'function') {
self::emitIssue(
$code_base,
$function->getContext(),
'DemoPluginFunctionName',
"Function {FUNCTION} cannot be called `function`",
[(string)$function->getFQSEN()]
);
}
}
/**
* @param CodeBase $code_base
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
*
* @override
*/
public function analyzeProperty(
CodeBase $code_base,
Property $property
): void {
// As an example, we test to see if the name of the
// property is `property`, and emit an issue if it is.
if ($property->getName() === 'property') {
self::emitIssue(
$code_base,
$property->getContext(),
'DemoPluginPropertyName',
"Property {PROPERTY} should not be called `property`",
[(string)$property->getFQSEN()]
);
}
}
}
/**
* 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 DemoNodeVisitor extends PluginAwarePostAnalysisVisitor
{
// Subclasses should declare protected $parent_node_list as an instance property if they need to know the list.
// @var list<Node> - Set after the constructor is called if an instance property with this name is declared
// protected $parent_node_list;
// A plugin's visitors should NOT implement visit(), unless they need to.
/**
* @param Node $node
* A node of kind ast\AST_INSTANCEOF to analyze
*
* @override
*/
public function visitInstanceof(Node $node): void
{
// Debug::printNode($node);
$class_name = $node->children['class']->children['name'] ?? null;
// If we can't figure out the name of the class, don't
// bother continuing.
if (!is_string($class_name)) {
return;
}
// As an example, enforce that we cannot call
// instanceof against 'object'.
if ($class_name === 'object') {
$this->emit(
'PhanPluginInstanceOfObject',
"Cannot call instanceof against `object`"
);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DemoPlugin();

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();

View File

@@ -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
);
});

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for occurrences of `$$x`,
* which may be a typo, or behave differently in php 5 vs 7, or be hard to analyze code.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DollarDollarPlugin 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 DollarDollarPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return DollarDollarVisitor::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 DollarDollarVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitVar(Node $node): void
{
if ($node->children['name'] instanceof Node) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDollarDollar',
"$$ Variables are not allowed.",
[]
);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DollarDollarPlugin();

View File

@@ -0,0 +1,376 @@
<?php
declare(strict_types=1);
use ast\Node;
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;
/**
* Checks for duplicate/equivalent array keys and case statements, as well as arrays mixing `key => value, with `value,`.
*
* @see DollarDollarPlugin for generic plugin documentation.
*/
class DuplicateArrayKeyPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return DuplicateArrayKeyVisitor::class;
}
}
/**
* This class has visitArray called on all array literals in files to check for potential problems with keys.
*
* 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 DuplicateArrayKeyVisitor extends PluginAwarePostAnalysisVisitor
{
private const HASH_PREFIX = "\x00__phan_dnu_";
// Do not define the visit() method unless a plugin has code and needs to visit most/all node types.
/**
* @param Node $node
* A switch statement's case statement(AST_SWITCH_LIST) node to analyze
* @override
*/
public function visitSwitchList(Node $node): void
{
$children = $node->children;
if (count($children) <= 1) {
// This plugin will never emit errors if there are 0 or 1 elements.
return;
}
$case_constant_set = [];
$values_to_check = [];
foreach ($children as $i => $case_node) {
if (!$case_node instanceof Node) {
throw new AssertionError("Switch list must contain nodes");
}
$case_cond = $case_node->children['cond'];
if ($case_cond === null) {
continue; // This is `default:`. php --syntax-check already checks for duplicates.
}
// 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)) {
$case_cond = $original_case_cond;
}
}
if (is_string($case_cond)) {
$cond_key = "s$case_cond";
$values_to_check[$i] = $case_cond;
} 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 {
// 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 = is_object($case_cond) ? ASTReverter::toShortString($case_cond) : self::normalizeSwitchKey($case_cond);
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($case_node->lineno),
'PhanPluginDuplicateSwitchCase',
"Duplicate/Equivalent switch case({STRING_LITERAL}) detected in switch statement - the later entry will be ignored in favor of case {CODE} at line {LINE}.",
[$normalized_case_cond, ASTReverter::toShortString($case_constant_set[$cond_key]->children['cond']), $case_constant_set[$cond_key]->lineno],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15071
);
// Add a fake value to indicate loose equality checks are redundant
$values_to_check[-1] = true;
}
$case_constant_set[$cond_key] = $case_node;
}
if (!isset($values_to_check[-1]) && count($values_to_check) > 1 && !self::areAllSwitchCasesTheSameType($values_to_check)) {
// @phan-suppress-next-line PhanPartialTypeMismatchArgument array keys are integers for switch
$this->extendedLooseEqualityCheck($values_to_check, $children);
}
}
/**
* @param array<mixed,mixed> $values_to_check scalar constant values of case statements
*/
private static function areAllSwitchCasesTheSameType(array $values_to_check): bool
{
$categories = 0;
foreach ($values_to_check as $value) {
if (is_int($value)) {
$categories |= 1;
if ($categories !== 1) {
return false;
}
} elseif (is_string($value)) {
if (is_numeric($value)) {
// This includes float-like strings such as `"1e0"`, which adds ambiguity ("1e0" == "1")
return false;
}
$categories |= 2;
if ($categories !== 2) {
return false;
}
} else {
return false;
}
}
return true;
}
/**
* Perform a heuristic check if any element is `==` a previous element.
*
* This is intended to perform well for large arrays.
*
* TODO: Do a better job for small arrays.
* @param array<mixed, mixed> $values_to_check
* @param list<mixed> $children an array of scalars
*/
private function extendedLooseEqualityCheck(array $values_to_check, array $children): void
{
$numeric_set = [];
$fuzzy_numeric_set = [];
foreach ($values_to_check as $i => $value) {
if (is_numeric($value)) {
if (is_int($value)) {
$old_index = $numeric_set[$value] ?? $fuzzy_numeric_set[$value] ?? null;
$numeric_set[$value] = $i;
} else {
// For `"1"`, search for `"1foo"`, `"1bar"`, etc.
$original_value = $value;
$value = is_float($value) ? (string)$value : (string)filter_var($value, FILTER_VALIDATE_FLOAT);
$old_index = $numeric_set[$value] ?? null;
if ($value === (string)$original_value) {
$old_index = $old_index ?? $fuzzy_numeric_set[$value] ?? null;
$numeric_set[$value] = $i;
} else {
$fuzzy_numeric_set[$value] = $i;
}
}
} else {
$value = (float)$value;
// For `"1foo"`, search for `1` but not `"1bar"`
$old_index = $numeric_set[$value] ?? null;
// @phan-suppress-next-line PhanTypeMismatchDimAssignment
$fuzzy_numeric_set[$value] = $i;
}
if ($old_index !== null) {
$this->emitPluginIssue(
$this->code_base,
(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])],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15072
);
}
}
}
/**
* @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
* @override
*/
public function visitArray(Node $node): void
{
$children = $node->children;
if (count($children) <= 1) {
// This plugin will never emit errors if there are 0 or 1 elements.
return;
}
$has_entry_without_key = false;
$key_set = [];
foreach ($children as $entry) {
if (!($entry instanceof Node)) {
continue; // Triggered by code such as `list(, $a) = $expr`. In php 7.1, the array and list() syntax was unified.
}
$key = $entry->children['key'] ?? null;
// Skip array entries without literal keys. (Do it before resolving the key value)
if (is_null($key)) {
$has_entry_without_key = true;
continue;
}
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']);
}
}
if (isset($key_set[$key])) {
// @phan-suppress-next-line PhanTypeMismatchDimFetchNullable
$this->warnAboutDuplicateArrayKey($entry, $key, $key_set[$key]);
}
// @phan-suppress-next-line PhanTypeMismatchDimAssignment
$key_set[$key] = $entry;
}
if ($has_entry_without_key && count($key_set) > 0) {
// This is probably a typo in most codebases. (e.g. ['foo' => 'bar', 'baz'])
// In phan, InternalFunctionSignatureMap.php does this deliberately with the first parameter being the return type.
$this->emit(
'PhanPluginMixedKeyNoKey',
"Should not mix array entries of the form [key => value,] with entries of the form [value,].",
[],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15071
);
}
}
/**
* @param int|string|float|bool|null $key
*/
private function warnAboutDuplicateArrayKey(Node $entry, $key, Node $old_entry): void
{
if (is_string($key) && strncmp($key, self::HASH_PREFIX, strlen(self::HASH_PREFIX)) === 0) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($entry->lineno),
'PhanPluginDuplicateArrayKeyExpression',
"Duplicate dynamic array key expression ({CODE}) detected in array - the earlier entry at line {LINE} will be ignored if the expression had the same value.",
[ASTReverter::toShortString($entry->children['key']), $old_entry->lineno],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15071
);
return;
}
$normalized_key = self::normalizeKey($key);
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($entry->lineno),
'PhanPluginDuplicateArrayKey',
"Duplicate/Equivalent array key value({STRING_LITERAL}) detected in array - the earlier entry {CODE} at line {LINE} will be ignored.",
[$normalized_key, ASTReverter::toShortString($old_entry->children['key']), $old_entry->lineno],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_A,
15071
);
}
/**
* Converts a key to the value it would be if used as a case.
* E.g. 0, 0.5, and "0" all become the same value(0) when used as an array key.
*
* @param int|string|float|bool|null $key - The array key literal to be normalized.
* @return string - The normalized representation.
*/
private static function normalizeSwitchKey($key): string
{
if (is_int($key)) {
return (string)$key;
} elseif (!is_string($key)) {
return (string)json_encode($key);
}
$tmp = [$key => true];
return ASTReverter::toShortString(key($tmp));
}
/**
* Converts a key to the value it would be if used as an array key.
* E.g. 0, 0.5, and "0" all become the same value(0) when used as an array key.
*
* @param int|string|float|bool|null $key - The array key literal to be normalized.
* @return string - The normalized representation.
*/
private static function normalizeKey($key): string
{
if (is_int($key)) {
return (string)$key;
}
$tmp = [$key => true];
return ASTReverter::toShortString(key($tmp));
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DuplicateArrayKeyPlugin();

View File

@@ -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();

View File

@@ -0,0 +1,575 @@
<?php
declare(strict_types=1);
use ast\flags;
use ast\Node;
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;
use Phan\PluginV3\PostAnalyzeNodeCapability;
use Phan\PluginV3\PreAnalyzeNodeCapability;
/**
* This plugin checks for duplicate expressions in a statement
* that are likely to be a bug.
*
* - E.g. `expr1 == expr1`
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DuplicateExpressionPlugin hooks into two events:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
* - getPreAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in pre-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 DuplicateExpressionPlugin extends PluginV3 implements
PostAnalyzeNodeCapability,
PreAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return RedundantNodePostAnalysisVisitor::class;
}
/**
* @return class-string - name of PluginAwarePreAnalysisVisitor subclass
*/
public static function getPreAnalyzeNodeVisitorClassName(): string
{
return RedundantNodePreAnalysisVisitor::class;
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class RedundantNodePostAnalysisVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* These are types of binary operations for which it is
* likely to be a typo if both the left and right-hand sides
* of the operation are the same.
*/
private const REDUNDANT_BINARY_OP_SET = [
flags\BINARY_BOOL_AND => true,
flags\BINARY_BOOL_OR => true,
flags\BINARY_BOOL_XOR => true,
flags\BINARY_BITWISE_OR => true,
flags\BINARY_BITWISE_AND => true,
flags\BINARY_BITWISE_XOR => true,
flags\BINARY_SUB => true,
flags\BINARY_DIV => true,
flags\BINARY_MOD => true,
flags\BINARY_IS_IDENTICAL => true,
flags\BINARY_IS_NOT_IDENTICAL => true,
flags\BINARY_IS_EQUAL => true,
flags\BINARY_IS_NOT_EQUAL => true,
flags\BINARY_IS_SMALLER => true,
flags\BINARY_IS_SMALLER_OR_EQUAL => true,
flags\BINARY_IS_GREATER => true,
flags\BINARY_IS_GREATER_OR_EQUAL => true,
flags\BINARY_SPACESHIP => true,
flags\BINARY_COALESCE => true,
];
/**
* A subset of REDUNDANT_BINARY_OP_SET.
*
* These binary operations will make this plugin warn if both sides are literals.
*/
private const BINARY_OP_BOTH_LITERAL_WARN_SET = [
flags\BINARY_BOOL_AND => true,
flags\BINARY_BOOL_OR => true,
flags\BINARY_BOOL_XOR => true,
flags\BINARY_IS_IDENTICAL => true,
flags\BINARY_IS_NOT_IDENTICAL => true,
flags\BINARY_IS_EQUAL => true,
flags\BINARY_IS_NOT_EQUAL => true,
flags\BINARY_IS_SMALLER => true,
flags\BINARY_IS_SMALLER_OR_EQUAL => true,
flags\BINARY_IS_GREATER => true,
flags\BINARY_IS_GREATER_OR_EQUAL => true,
flags\BINARY_SPACESHIP => true,
flags\BINARY_COALESCE => true,
];
/**
* @param Node $node
* A binary operation node to analyze
* @override
* @suppress PhanAccessClassConstantInternal
*/
public function visitBinaryOp(Node $node): void
{
$flags = $node->flags;
if (!\array_key_exists($flags, self::REDUNDANT_BINARY_OP_SET)) {
// Nothing to warn about
return;
}
$left = $node->children['left'];
$right = $node->children['right'];
if (ASTHasher::hash($left) === ASTHasher::hash($right)) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDuplicateExpressionBinaryOp',
'Both sides of the binary operator {OPERATOR} are the same: {CODE}',
[
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags],
ASTReverter::toShortString($left),
]
);
return;
}
if (!\array_key_exists($flags, self::BINARY_OP_BOTH_LITERAL_WARN_SET)) {
return;
}
if ($left instanceof Node) {
$left = self::resolveLiteralValue($left);
if ($left instanceof Node) {
return;
}
}
if ($right instanceof Node) {
$right = self::resolveLiteralValue($right);
if ($right instanceof Node) {
return;
}
}
try {
// @phan-suppress-next-line PhanPartialTypeMismatchArgument TODO: handle
$result_representation = ASTReverter::toShortString(InferValue::computeBinaryOpResult($left, $right, $flags));
} catch (Error $_) {
$result_representation = '(unknown)';
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginBothLiteralsBinaryOp',
'Suspicious usage of a binary operator where both operands are literals. Expression: {CODE} {OPERATOR} {CODE} (result is {CODE})',
[
ASTReverter::toShortString($left),
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$flags],
ASTReverter::toShortString($right),
$result_representation,
]
);
}
/**
* @param Node $node
* An assignment operation node to analyze
* @override
*/
public function visitAssignRef(Node $node): void
{
$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
* @override
*/
public function visitAssign(Node $node): void
{
$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,
$this->context,
'PhanPluginDuplicateExpressionAssignment',
'Both sides of the assignment {OPERATOR} are the same: {CODE}',
[
$node->kind === ast\AST_ASSIGN_REF ? '=&' : '=',
ASTReverter::toShortString($var),
]
);
return;
}
}
/**
* @return bool|null|Node the resolved value of $node, or $node if it could not be resolved
* This could be more permissive about what constants are allowed (e.g. user-defined constants, real constants like PI, etc.),
* but that may cause more false positives.
*/
private static function resolveLiteralValue(Node $node)
{
if ($node->kind !== ast\AST_CONST) {
return $node;
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
switch (\strtolower($node->children['name']->children['name'] ?? null)) {
case 'false':
return false;
case 'true':
return true;
case 'null':
return null;
default:
return $node;
}
}
/**
* @param Node $node
* A binary operation node to analyze
* @override
*/
public function visitConditional(Node $node): void
{
$cond_node = $node->children['cond'];
$true_node_hash = ASTHasher::hash($node->children['true']);
if (ASTHasher::hash($cond_node) === $true_node_hash) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDuplicateConditionalTernaryDuplication',
'"X ? X : Y" can usually be simplified to "X ?: Y". The duplicated expression X was {CODE}',
[ASTReverter::toShortString($cond_node)]
);
return;
}
$false_node_hash = ASTHasher::hash($node->children['false']);
if ($true_node_hash === $false_node_hash) {
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDuplicateConditionalUnnecessary',
'"X ? Y : Y" results in the same expression Y no matter what X evaluates to. Y was {CODE}',
[ASTReverter::toShortString($cond_node)]
);
return;
}
if (!$cond_node instanceof Node) {
return;
}
switch ($cond_node->kind) {
case ast\AST_ISSET:
if (ASTHasher::hash($cond_node->children['var']) === $true_node_hash) {
$this->warnDuplicateConditionalNullCoalescing('isset(X) ? X : Y', $node->children['true']);
}
break;
case ast\AST_BINARY_OP:
$this->checkBinaryOpOfConditional($cond_node, $true_node_hash);
break;
case ast\AST_UNARY_OP:
$this->checkUnaryOpOfConditional($cond_node, $true_node_hash);
break;
}
}
/**
* @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
*/
private function checkBinaryOpOfConditional(Node $cond_node, $true_node_hash): void
{
if ($cond_node->flags !== ast\flags\BINARY_IS_NOT_IDENTICAL) {
return;
}
$left_node = $cond_node->children['left'];
$right_node = $cond_node->children['right'];
if (self::isNullConstantNode($left_node)) {
if (ASTHasher::hash($right_node) === $true_node_hash) {
$this->warnDuplicateConditionalNullCoalescing('null !== X ? X : Y', $right_node);
}
} elseif (self::isNullConstantNode($right_node)) {
if (ASTHasher::hash($left_node) === $true_node_hash) {
$this->warnDuplicateConditionalNullCoalescing('X !== null ? X : Y', $left_node);
}
}
}
/**
* @param int|string $true_node_hash
*/
private function checkUnaryOpOfConditional(Node $cond_node, $true_node_hash): void
{
if ($cond_node->flags !== ast\flags\UNARY_BOOL_NOT) {
return;
}
$expr = $cond_node->children['expr'];
if (!$expr instanceof Node) {
return;
}
if ($expr->kind === ast\AST_CALL) {
$function = $expr->children['expr'];
if (!$function instanceof Node ||
$function->kind !== ast\AST_NAME ||
strcasecmp((string)($function->children['name'] ?? ''), 'is_null') !== 0
) {
return;
}
$args = $expr->children['args']->children;
if (count($args) !== 1) {
return;
}
if (ASTHasher::hash($args[0]) === $true_node_hash) {
$this->warnDuplicateConditionalNullCoalescing('!is_null(X) ? X : Y', $args[0]);
}
}
}
/**
* @param Node|mixed $node
*/
private static function isNullConstantNode($node): bool
{
if (!$node instanceof Node) {
return false;
}
return $node->kind === ast\AST_CONST && strcasecmp((string)($node->children['name']->children['name'] ?? ''), 'null') === 0;
}
/**
* @param ?(Node|string|int|float) $x_node
*/
private function warnDuplicateConditionalNullCoalescing(string $expr, $x_node): void
{
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginDuplicateConditionalNullCoalescing',
'"' . $expr . '" can usually be simplified to "X ?? Y" in PHP 7. The duplicated expression X was {CODE}',
[ASTReverter::toShortString($x_node)]
);
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in pre-order.
*/
class RedundantNodePreAnalysisVisitor extends PluginAwarePreAnalysisVisitor
{
/**
* @override
*/
public function visitIf(Node $node): void
{
if (count($node->children) <= 1) {
// There can't be any duplicates.
return;
}
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($node->is_inside_else)) {
return;
}
$children = self::extractIfElseifChain($node);
// The checks of visitIf are done in pre-order (parent nodes analyzed before child nodes)
// so that checked_duplicate_if can be set, to avoid redundant work.
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($node->checked_duplicate_if)) {
return;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$node->checked_duplicate_if = true;
['cond' => $prev_cond /*, 'stmts' => $prev_stmts */] = $children[0]->children;
// $prev_stmts_hash = ASTHasher::hash($prev_cond);
$condition_set = [ASTHasher::hash($prev_cond) => true];
$N = count($children);
for ($i = 1; $i < $N; $i++) {
['cond' => $cond /*, 'stmts' => $stmts */] = $children[$i]->children;
$cond_hash = ASTHasher::hash($cond);
if (isset($condition_set[$cond_hash])) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($cond->lineno ?? $children[$i]->lineno),
'PhanPluginDuplicateIfCondition',
'Saw the same condition {CODE} in an earlier if/elseif statement',
[ASTReverter::toShortString($cond)]
);
} else {
$condition_set[$cond_hash] = true;
}
}
if (!isset($cond)) {
$stmts = $children[$N - 1]->children['stmts'];
if (($stmts->children ?? null) && ASTHasher::hash($stmts) === ASTHasher::hash($children[$N - 2]->children['stmts'])) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($children[$N - 1]->lineno),
'PhanPluginDuplicateIfStatements',
'The statements of the else duplicate the statements of the previous if/elseif statement with condition {CODE}',
[ASTReverter::toShortString($children[$N - 2]->children['cond'])]
);
}
}
}
/**
* 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.
* @suppress PhanPartialTypeMismatchReturn
*/
private static function extractIfElseifChain(Node $node): array
{
$children = $node->children;
if (count($children) <= 1) {
return $children;
}
$last_child = \end($children);
// Loop over the `} else {` blocks.
// @phan-suppress-next-line PhanPossiblyUndeclaredProperty
while ($last_child->children['cond'] === null) {
$first_stmt = $last_child->children['stmts']->children[0] ?? null;
if (!($first_stmt instanceof Node)) {
break;
}
if ($first_stmt->kind !== ast\AST_IF) {
break;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$first_stmt->is_inside_else = true;
\array_pop($children);
$if_elems = $first_stmt->children;
foreach ($if_elems as $elem) {
$children[] = $elem;
}
$last_child = \end($children);
}
return $children;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new DuplicateExpressionPlugin();

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
use ast\Node;
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;
/**
* Plugin which looks for empty methods/functions
*
* This Plugin hooks into one event;
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class that is called on every AST node from every
* file being analyzed
*/
final class EmptyMethodAndFunctionPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return EmptyMethodAndFunctionVisitor::class;
}
}
/**
* Visit method/function/closure
*/
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) {
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->getRepresentationForIssue()
);
}
}
public function visitFuncDecl(Node $node): void
{
$this->analyzeFunction($node);
}
public function visitClosure(Node $node): void
{
$this->analyzeFunction($node);
}
// No need for visitArrowFunc.
// By design, `fn($args) => expr` can't have an empty statement list because it must have an expression.
// It's always equivalent to `return expr;`
private function analyzeFunction(Node $node): void
{
$stmts_node = $node->children['stmts'] ?? null;
if ($stmts_node && !$stmts_node->children) {
$function = $this->context->getFunctionLikeInScope($this->code_base);
if (!($function instanceof Func)) {
throw new AssertionError("Expected $function to be Func\n");
}
if (!$function->isDeprecated()) {
$this->emitIssue(
$function->isClosure() ? Issue::EmptyClosure : Issue::EmptyFunction,
$node->lineno,
$function->getRepresentationForIssue()
);
}
}
}
private static function getIssueTypeForEmptyMethod(FunctionInterface $method): string
{
if (!$method instanceof Method) {
throw new \InvalidArgumentException("\$method is not an instance of Method");
}
if ($method->isPrivate()) {
return Issue::EmptyPrivateMethod;
}
if ($method->isProtected()) {
return Issue::EmptyProtectedMethod;
}
return Issue::EmptyPublicMethod;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new EmptyMethodAndFunctionPlugin();

View File

@@ -0,0 +1,370 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\InferPureAndNoThrowVisitor;
use Phan\Config;
use Phan\Library\FileCache;
use Phan\Parse\ParseVisitor;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This file checks for empty statement lists in loops/branches.
* Due to Phan's AST rewriting for easier analysis, this may miss some edge cases.
*
* It hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class 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
*/
final class EmptyStatementListPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* If true, then never allow empty statement lists, even if there is a TODO/FIXME/"deliberately empty" comment.
* @var bool
* @internal
*/
public static $ignore_todos = false;
public function __construct()
{
self::$ignore_todos = (bool) (Config::getValue('plugin_config')['empty_statement_list_ignore_todos'] ?? false);
}
/**
* @return string - The name of the visitor that will be called.
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return EmptyStatementListVisitor::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.
*/
final class EmptyStatementListVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @var list<Node> set by plugin framework
* @suppress PhanReadOnlyProtectedProperty
*/
protected $parent_node_list;
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitIf(Node $node): void
{
// @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);
}
if (!$last_if_elem instanceof Node) {
// probably impossible
return;
}
$stmts_node = $last_if_elem->children['stmts'];
if (!$stmts_node instanceof Node) {
// probably impossible
return;
}
if ($stmts_node->children) {
// the last if element has statements
return;
}
if ($last_if_elem->children['cond'] === null) {
// Don't bother warning about else
return;
}
if ($this->hasTODOComment($stmts_node->lineno, $node)) {
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($last_if_elem->children['stmts']->lineno ?? $last_if_elem->lineno),
'PhanPluginEmptyStatementIf',
'Empty statement list statement detected for the last if/elseif statement',
[]
);
}
private function hasTODOComment(int $lineno, Node $analyzed_node, ?int $end_lineno = null): bool
{
if (EmptyStatementListPlugin::$ignore_todos) {
return false;
}
$file = FileCache::getOrReadEntry($this->context->getFile());
$lines = $file->getLines();
$end_lineno = max($lineno, $end_lineno ?? $this->findEndLine($lineno, $analyzed_node));
for ($i = $lineno; $i <= $end_lineno; $i++) {
$line = $lines[$i] ?? null;
if (!is_string($line)) {
break;
}
if (preg_match('/todo|fixme|deliberately empty/i', $line) > 0) {
return true;
}
}
return false;
}
private function findEndLine(int $lineno, Node $search_node): int
{
for ($node_index = count($this->parent_node_list) - 1; $node_index >= 0; $node_index--) {
$node = $this->parent_node_list[$node_index] ?? null;
if (!$node) {
continue;
}
if (isset($node->endLineno)) {
// Return the end line of the function declaration.
return $node->endLineno;
}
if ($node->kind === ast\AST_STMT_LIST) {
foreach ($node->children as $i => $c) {
if ($c === $search_node) {
$next_node = $node->children[$i + 1] ?? null;
if ($next_node instanceof Node) {
return $next_node->lineno - 1;
}
break;
}
}
}
$search_node = $node;
}
// Give up and guess.
return $lineno + 5;
}
/**
* @param Node $node
* A node of kind ast\AST_FOR to analyze
* @override
*/
public function visitFor(Node $node): void
{
$stmts_node = $node->children['stmts'];
if (!$stmts_node instanceof Node) {
// impossible
return;
}
if ($stmts_node->children || ($node->children['loop']->children ?? null)) {
// the for loop has statements, in the body and/or in the loop condition.
return;
}
if ($this->hasTODOComment($stmts_node->lineno, $node)) {
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($stmts_node->lineno ?? $node->lineno),
'PhanPluginEmptyStatementForLoop',
'Empty statement list statement detected for the for loop',
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitWhile(Node $node): void
{
$stmts_node = $node->children['stmts'];
if (!$stmts_node instanceof Node) {
return; // impossible
}
if ($stmts_node->children) {
// the while loop has statements
return;
}
if ($this->hasTODOComment($stmts_node->lineno, $node)) {
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($stmts_node->lineno ?? $node->lineno),
'PhanPluginEmptyStatementWhileLoop',
'Empty statement list statement detected for the while loop',
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitDoWhile(Node $node): void
{
$stmts_node = $node->children['stmts'];
if (!$stmts_node instanceof Node) {
return; // impossible
}
if ($stmts_node->children ?? null) {
// the while loop has statements
return;
}
if ($this->hasTODOComment($stmts_node->lineno, $node)) {
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($stmts_node->lineno),
'PhanPluginEmptyStatementDoWhileLoop',
'Empty statement list statement detected for the do-while loop',
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitForeach(Node $node): void
{
$stmts_node = $node->children['stmts'];
if (!$stmts_node instanceof Node) {
// impossible
return;
}
if ($stmts_node->children) {
// the while loop has statements
return;
}
if ($this->hasTODOComment($stmts_node->lineno, $node)) {
// Don't warn if there is a FIXME/TODO comment in/around the empty statement list
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($stmts_node->lineno),
'PhanPluginEmptyStatementForeachLoop',
'Empty statement list statement detected for the foreach loop',
[]
);
}
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitTry(Node $node): void
{
['try' => $try_node, 'finally' => $finally_node] = $node->children;
if (!$try_node->children) {
if (!$this->hasTODOComment($try_node->lineno, $node, $node->children['catches']->children[0]->lineno ?? $finally_node->lineno ?? null)) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($try_node->lineno),
'PhanPluginEmptyStatementTryBody',
'Empty statement list statement detected for the try statement\'s body',
[]
);
}
} elseif (InferPureAndNoThrowVisitor::isUnlikelyToThrow($this->code_base, $this->context, $try_node)) {
if (!$this->hasTODOComment($try_node->lineno, $node, $node->children['catches']->children[0]->lineno ?? $finally_node->lineno ?? null)) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($try_node->lineno),
'PhanPluginEmptyStatementPossiblyNonThrowingTryBody',
'Found a try block that looks like it might not throw. Note that this check is a heuristic prone to false positives, especially because error handlers, signal handlers, destructors, and other things may all lead to throwing.'
);
}
}
if ($finally_node instanceof Node && !$finally_node->children) {
if (!$this->hasTODOComment($finally_node->lineno, $node)) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($finally_node->lineno),
'PhanPluginEmptyStatementTryFinally',
'Empty statement list statement detected for the try\'s finally body',
[]
);
}
}
}
/**
* @param Node $node
* A node of kind ast\AST_SWITCH to analyze
* @override
*/
public function visitSwitch(Node $node): void
{
// Check all case statements and return if something that isn't a no-op is seen.
foreach ($node->children['stmts']->children ?? [] as $c) {
if (!$c instanceof Node) {
// impossible
continue;
}
$children = $c->children['stmts']->children ?? null;
if ($children) {
if (count($children) > 1) {
return;
}
$only_node = $children[0];
if ($only_node instanceof Node) {
if (!in_array($only_node->kind, [ast\AST_CONTINUE, ast\AST_BREAK], true)) {
return;
}
if (($only_node->children['depth'] ?? 1) !== 1) {
// not a no-op
return;
}
}
}
if (!ParseVisitor::isConstExpr($c->children['cond'], ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
return;
}
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
'PhanPluginEmptyStatementSwitch',
'No side effects seen for any cases of this switch statement',
[]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new EmptyStatementListPlugin();

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Language\Element\Variable;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PluginAwarePreAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
use Phan\PluginV3\PreAnalyzeNodeCapability;
/**
* This plugin modifies Phan's analysis of code using FFI\CData variables.
*
* 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.
*
* - This plugin does mangle state because FFI\CData is typical
*
* Note: When adding new plugins,
* add them to the corresponding section of README.md
*/
class FFIAnalysisPlugin extends PluginV3 implements PostAnalyzeNodeCapability, PreAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
* @override
*/
public static function getPreAnalyzeNodeVisitorClassName(): string
{
return FFIPreAnalysisVisitor::class;
}
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return FFIPostAnalysisVisitor::class;
}
}
/**
* This visitor records FFI\CData types if the original value was FFI\CData
*/
class FFIPreAnalysisVisitor extends PluginAwarePreAnalysisVisitor
{
/**
* @override
* @param Node $node a node of kind ast\AST_ASSIGN
*/
public function visitAssign(Node $node): void
{
$left = $node->children['var'];
if (!($left instanceof Node)) {
return;
}
if ($left->kind !== ast\AST_VAR) {
return;
}
$var_name = $left->children['name'];
if (!is_string($var_name)) {
return;
}
$scope = $this->context->getScope();
if (!$scope->hasVariableWithName($var_name)) {
return;
}
$var = $scope->getVariableByName($var_name);
$category = self::containsFFICDataType($var->getUnionType());
if (!$category) {
return;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$node->is_ffi = $category;
}
public const PARTIALLY_FFI_CDATA = 1;
public const ENTIRELY_FFI_CDATA = 2;
/**
* Check if the type contains FFI\CData
*/
private static function containsFFICDataType(UnionType $union_type): int
{
foreach ($union_type->getTypeSet() as $type) {
if (strcasecmp('\FFI', $type->getNamespace()) !== 0) {
continue;
}
if (strcasecmp('CData', $type->getName()) !== 0) {
continue;
}
if ($type->isNullable()) {
return self::PARTIALLY_FFI_CDATA;
}
if ($union_type->typeCount() > 1) {
return self::PARTIALLY_FFI_CDATA;
}
return self::ENTIRELY_FFI_CDATA;
}
return 0;
}
}
/**
* This visitor restores FFI\CData types after assignments if the original value was FFI\CData
*/
class FFIPostAnalysisVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @override
*/
public function visitAssign(Node $node): void
{
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($node->is_ffi)) {
$this->analyzeFFIAssign($node);
}
}
private function analyzeFFIAssign(Node $node): void
{
$var_name = $node->children['var']->children['name'] ?? null;
if (!is_string($var_name)) {
return;
}
$cdata_type = UnionType::fromFullyQualifiedPHPDocString('\FFI\CData');
$scope = $this->context->getScope();
// @phan-suppress-next-line PhanUndeclaredProperty
if ($node->is_ffi !== FFIPreAnalysisVisitor::ENTIRELY_FFI_CDATA) {
if ($scope->hasVariableWithName($var_name)) {
$cdata_type = $cdata_type->withUnionType($scope->getVariableByName($var_name)->getUnionType());
}
}
$this->context->getScope()->addVariable(
new Variable($this->context, $var_name, $cdata_type, 0)
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new FFIAnalysisPlugin();

View File

@@ -0,0 +1,402 @@
<?php
declare(strict_types=1);
namespace HasPHPDocPlugin;
use AssertionError;
use ast;
use ast\Node;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Element\ClassElement;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Comment;
use Phan\Language\Element\Func;
use Phan\Language\Element\MarkupDescription;
use Phan\Library\StringUtil;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeClassCapability;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
use function array_shift;
use function count;
use function gettype;
use function is_string;
use function json_encode;
use function ltrim;
use function preg_match;
use function strpos;
use function ucfirst;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
/**
* This file checks if an element (class or property) has a PHPDoc comment,
* and that Phan can extract a plaintext summary/description from that comment.
*
* (e.g. for generating a hover description in the language server)
*
* It hooks into these events:
*
* - analyzeClass
* Once all classes are parsed, this method will be called
* on every method in the code base
*
* - analyzeProperty
* Once all properties have been parsed, this method will
* be called on every property in the code base.
* - analyzeMethod
* Once all methods have been parsed, this method will
* be called on every method in the code base.
* - analyzeFunction
* Once all functions have been parsed, this method will
* be called on every function/closure in the code base.
*
* 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
* @internal
*/
final class HasPHPDocPlugin extends PluginV3 implements
AnalyzeClassCapability,
AnalyzeFunctionCapability,
PostAnalyzeNodeCapability
{
/** @var ?string a regex to use to exclude methods from phpdoc checks. */
public static $method_filter;
public function __construct()
{
$plugin_config = Config::getValue('plugin_config');
self::$method_filter = $plugin_config['has_phpdoc_method_ignore_regex'] ?? null;
}
/**
* @param CodeBase $code_base
* The code base in which the class exists
*
* @param Clazz $class
* A class being analyzed
* @override
*/
public function analyzeClass(
CodeBase $code_base,
Clazz $class
): void {
if ($class->isAnonymous()) {
// Probably not useful in many cases to document a short anonymous class.
return;
}
$doc_comment = $class->getDocComment();
if (!StringUtil::isNonZeroLengthString($doc_comment)) {
self::emitIssue(
$code_base,
$class->getContext(),
'PhanPluginNoCommentOnClass',
'Class {CLASS} has no doc comment',
[$class->getFQSEN()]
);
return;
}
$description = MarkupDescription::extractDescriptionFromDocComment($class);
if (!StringUtil::isNonZeroLengthString($description)) {
if (strpos($doc_comment, '@deprecated') !== false) {
return;
}
self::emitIssue(
$code_base,
$class->getContext(),
'PhanPluginDescriptionlessCommentOnClass',
'Class {CLASS} has no readable description: {STRING_LITERAL}',
[$class->getFQSEN(), self::getDocCommentRepresentation($doc_comment)]
);
return;
}
}
/**
* @param CodeBase $code_base
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
if ($function->isPHPInternal()) {
// This isn't user-defined, there's no reason to warn or way to change it.
return;
}
if ($function->isNSInternal($code_base)) {
// (at)internal are internal to the library, and there's less of a need to document them
return;
}
if ($function->isClosure()) {
// Probably not useful in many cases to document a short closure passed to array_map, etc.
return;
}
$doc_comment = $function->getDocComment();
if (!StringUtil::isNonZeroLengthString($doc_comment)) {
self::emitIssue(
$code_base,
$function->getContext(),
"PhanPluginNoCommentOnFunction",
"Function {FUNCTION} has no doc comment",
[$function->getFQSEN()]
);
return;
}
$description = MarkupDescription::extractDescriptionFromDocComment($function);
if (!StringUtil::isNonZeroLengthString($description)) {
self::emitIssue(
$code_base,
$function->getContext(),
"PhanPluginDescriptionlessCommentOnFunction",
"Function {FUNCTION} has no readable description: {STRING_LITERAL}",
[$function->getFQSEN(), self::getDocCommentRepresentation($doc_comment)]
);
return;
}
}
/**
* Encode the doc comment in a one-line form that can be used in Phan's issue message.
* @internal
*/
public static function getDocCommentRepresentation(string $doc_comment): string
{
return (string)json_encode(MarkupDescription::getDocCommentWithoutWhitespace($doc_comment), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return (bool)(Config::getValue('plugin_config')['has_phpdoc_check_duplicates'] ?? false)
? DuplicatePHPDocCheckerPlugin::class
: BasePHPDocCheckerPlugin::class;
}
}
/** Infer property and class doc comments and warn */
class BasePHPDocCheckerPlugin extends PluginAwarePostAnalysisVisitor
{
/** @return array{0:list<ClassElementEntry>,1:list<ClassElementEntry>} */
public function visitClass(Node $node): array
{
$class = $this->context->getClassInScope($this->code_base);
$property_descriptions = [];
$method_descriptions = [];
foreach ($node->children['stmts']->children ?? [] as $element) {
if (!($element instanceof Node)) {
throw new AssertionError("All properties of ast\AST_CLASS's statement list must be nodes, saw " . gettype($element));
}
switch ($element->kind) {
case ast\AST_METHOD:
$entry = $this->checkMethodDescription($class, $element);
if ($entry) {
$method_descriptions[] = $entry;
}
break;
case ast\AST_PROP_GROUP:
$entry = $this->checkPropGroupDescription($class, $element);
if ($entry) {
$property_descriptions[] = $entry;
}
break;
}
}
return [$property_descriptions, $method_descriptions];
}
/**
* @param Node $node a node of kind ast\AST_METHOD
*/
private function checkMethodDescription(Clazz $class, Node $node): ?ClassElementEntry
{
$method_name = (string)$node->children['name'];
$method = $class->getMethodByName($this->code_base, $method_name);
if ($method->isMagic()) {
// Ignore construct
return null;
}
if ($method->isOverride()) {
return null;
}
$method_filter = HasPHPDocPlugin::$method_filter;
if (is_string($method_filter)) {
$fqsen_string = ltrim((string)$method->getFQSEN(), '\\');
if (preg_match($method_filter, $fqsen_string) > 0) {
return null;
}
}
$doc_comment = $method->getDocComment();
if (!StringUtil::isNonZeroLengthString($doc_comment)) {
$visibility_upper = ucfirst($method->getVisibilityName());
self::emitPluginIssue(
$this->code_base,
$method->getContext(),
"PhanPluginNoCommentOn{$visibility_upper}Method",
"$visibility_upper method {METHOD} has no doc comment",
[$method->getFQSEN()]
);
return null;
}
$description = MarkupDescription::extractDescriptionFromDocComment($method);
if (!StringUtil::isNonZeroLengthString($description)) {
$visibility_upper = ucfirst($method->getVisibilityName());
self::emitPluginIssue(
$this->code_base,
$method->getContext(),
"PhanPluginDescriptionlessCommentOn{$visibility_upper}Method",
"$visibility_upper method {METHOD} has no readable description: {STRING_LITERAL}",
[$method->getFQSEN(), HasPHPDocPlugin::getDocCommentRepresentation($doc_comment)]
);
return null;
}
return new ClassElementEntry($method, \trim(\preg_replace('/\s+/', ' ', $description)));
}
/**
* @param Node $node a node of type ast\AST_PROP_GROUP
*/
private function checkPropGroupDescription(Clazz $class, Node $node): ?ClassElementEntry
{
$property_name = $node->children['props']->children[0]->children['name'] ?? null;
if (!is_string($property_name)) {
return null;
}
$property = $class->getPropertyByName($this->code_base, $property_name);
$doc_comment = $property->getDocComment();
if (!StringUtil::isNonZeroLengthString($doc_comment)) {
$visibility_upper = ucfirst($property->getVisibilityName());
self::emitPluginIssue(
$this->code_base,
$property->getContext(),
"PhanPluginNoCommentOn{$visibility_upper}Property",
"$visibility_upper property {PROPERTY} has no doc comment",
[$property->getRepresentationForIssue()]
);
return null;
}
// @phan-suppress-next-line PhanAccessMethodInternal
$description = MarkupDescription::extractDocComment($doc_comment, Comment::ON_PROPERTY, null, true);
if (!StringUtil::isNonZeroLengthString($description)) {
$visibility_upper = ucfirst($property->getVisibilityName());
self::emitPluginIssue(
$this->code_base,
$property->getContext(),
"PhanPluginDescriptionlessCommentOn{$visibility_upper}Property",
"$visibility_upper property {PROPERTY} has no readable description: {STRING_LITERAL}",
[$property->getRepresentationForIssue(), HasPHPDocPlugin::getDocCommentRepresentation($doc_comment)]
);
return null;
}
return new ClassElementEntry($property, \trim(\preg_replace('/\s+/', ' ', $description)));
}
}
/**
* Describes a property group or a method node and the associated description
* @phan-immutable
* @internal
*/
final class ClassElementEntry
{
/** @var ClassElement the element (or element group) */
public $element;
/** @var string the phpdoc description */
public $description;
public function __construct(ClassElement $element, string $description)
{
$this->element = $element;
$this->description = $description;
}
}
/**
* Check if phpdoc of property groups and methods are duplicated
* @internal
*/
final class DuplicatePHPDocCheckerPlugin extends BasePHPDocCheckerPlugin
{
/** No-op */
public function visitClass(Node $node): array
{
[$property_descriptions, $method_descriptions] = parent::visitClass($node);
foreach (self::findGroups($property_descriptions) as $entries) {
$first_entry = array_shift($entries);
if (!$first_entry instanceof ClassElementEntry) {
throw new AssertionError('Expected $entries of $property_descriptions to be a group of 1 or more entries');
}
$first_property = $first_entry->element;
foreach ($entries as $entry) {
$property = $entry->element;
self::emitPluginIssue(
$this->code_base,
$property->getContext(),
"PhanPluginDuplicatePropertyDescription",
"Property {PROPERTY} has the same description as the property \${PROPERTY} on line {LINE}: {COMMENT}",
[$property->getRepresentationForIssue(), $first_property->getName(), $first_property->getContext()->getLineNumberStart(), $first_entry->description]
);
}
}
foreach (self::findGroups($method_descriptions) as $entries) {
$first_entry = array_shift($entries);
if (!$first_entry instanceof ClassElementEntry) {
throw new AssertionError('Expected $entries of $property_descriptions to be a group of 1 or more entries');
}
$first_method = $first_entry->element;
foreach ($entries as $entry) {
$method = $entry->element;
self::emitPluginIssue(
$this->code_base,
$method->getContext(),
"PhanPluginDuplicateMethodDescription",
"Method {METHOD} has the same description as the method {METHOD} on line {LINE}: {COMMENT}",
[$method->getRepresentationForIssue(), $first_method->getName() . '()', $first_method->getContext()->getLineNumberStart(), $first_entry->description]
);
}
}
return [$property_descriptions, $method_descriptions];
}
/**
* @param list<ClassElementEntry> $values
* @return array<string, list<ClassElementEntry>>
*/
private static function findGroups(array $values): array
{
$result = [];
foreach ($values as $v) {
if ($v->element->isDeprecated()) {
continue;
}
$result[$v->description][] = $v;
}
foreach ($result as $description => $keys) {
if (count($keys) <= 1) {
unset($result[$description]);
}
}
return $result;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new HasPHPDocPlugin();

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\Parser;
use Phan\CLI;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Library\StringUtil;
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.
* Note that this is slow due to needing token_get_all.
*
* TODO: Cache and reuse the results
*/
class InlineHTMLPlugin extends PluginV3 implements
AfterAnalyzeFileCapability,
PostAnalyzeNodeCapability
{
private const InlineHTML = 'PhanPluginInlineHTML';
private const InlineHTMLLeading = 'PhanPluginInlineHTMLLeading';
private const InlineHTMLTrailing = 'PhanPluginInlineHTMLTrailing';
/** @var array<string,true> set of files that have echo statements */
public static $file_set_to_analyze = [];
/** @var ?string */
private $whitelist_regex;
/** @var ?string */
private $blacklist_regex;
public function __construct()
{
$plugin_config = Config::getValue('plugin_config');
$this->whitelist_regex = $plugin_config['inline_html_whitelist_regex'] ?? null;
$this->blacklist_regex = $plugin_config['inline_html_blacklist_regex'] ?? null;
}
private function shouldCheckFile(string $path): bool
{
if (is_string($this->blacklist_regex)) {
if (CLI::isPathMatchedByRegex($this->blacklist_regex, $path)) {
return false;
}
}
if (is_string($this->whitelist_regex)) {
return CLI::isPathMatchedByRegex($this->whitelist_regex, $path);
}
return true;
}
/**
* @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 {
$file = $context->getFile();
if (!isset(self::$file_set_to_analyze[$file])) {
// token_get_all is noticeably slow when there are a lot of files, so we check for the existence of echo statements in the parsed AST as a heuristic to avoid calling token_get_all.
return;
}
if (!self::shouldCheckFile($file)) {
return;
}
$file_contents = Parser::removeShebang($file_contents);
$tokens = token_get_all($file_contents);
foreach ($tokens as $i => $token) {
if (!is_array($token)) {
continue;
}
if ($token[0] !== T_INLINE_HTML) {
continue;
}
$N = count($tokens);
$this->warnAboutInlineHTML($code_base, $context, $token, $i, $N);
if ($i < $N - 1) {
// Make sure to always check if the last token is inline HTML
$token = $tokens[$N - 1] ?? null;
if (!is_array($token)) {
break;
}
if ($token[0] !== T_INLINE_HTML) {
break;
}
$this->warnAboutInlineHTML($code_base, $context, $token, $N - 1, $N);
}
break;
}
}
/**
* @param array{0:int,1:string,2:int} $token a token from token_get_all
*/
private function warnAboutInlineHTML(CodeBase $code_base, Context $context, array $token, int $i, int $n): void
{
if ($i === 0) {
$issue = self::InlineHTMLLeading;
$message = 'Saw inline HTML at the start of the file: {STRING_LITERAL}';
} elseif ($i >= $n - 1) {
$issue = self::InlineHTMLTrailing;
$message = 'Saw inline HTML at the end of the file: {STRING_LITERAL}';
} else {
$issue = self::InlineHTML;
$message = 'Saw inline HTML between the first and last token: {STRING_LITERAL}';
}
$this->emitIssue(
$code_base,
(clone $context)->withLineNumberStart($token[2]),
$issue,
$message,
[StringUtil::jsonEncode(self::truncate($token[1]))]
);
}
private static function truncate(string $token): string
{
if (strlen($token) > 20) {
return mb_substr($token, 0, 20) . "...";
}
return $token;
}
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return InlineHTMLVisitor::class;
}
}
/**
* Records existence of AST_ECHO within a file, marking the file as one that should be checked.
*
* php-ast (and the underlying AST implementation) doesn't provide a way to distinguish inline HTML from other types of echos.
*/
class InlineHTMLVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @override
* @param Node $node @unused-param
* @return void
*/
public function visitEcho(Node $node)
{
InlineHTMLPlugin::$file_set_to_analyze[$this->context->getFile()] = true;
}
}
// 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();

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\Element\Variable;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin detects undeclared variables within isset() checks.
*/
class InvalidVariableIssetPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return InvalidVariableIssetVisitor::class;
}
}
/**
* This plugin checks isset nodes (\ast\AST_ISSET) to see if they contain undeclared variables
*/
class InvalidVariableIssetVisitor extends PluginAwarePostAnalysisVisitor
{
/** define classes to parse */
private const CLASSES = [
ast\AST_STATIC_CALL,
ast\AST_STATIC_PROP,
];
/** define expression to parse */
private const EXPRESSIONS = [
ast\AST_CALL,
ast\AST_DIM,
ast\AST_INSTANCEOF,
ast\AST_METHOD_CALL,
ast\AST_PROP,
];
// A plugin's visitors should not override visit() unless they need to.
/** @override */
public function visitIsset(Node $node): Context
{
$argument = $node->children['var'];
$variable = $argument;
// get variable name from argument
while (!isset($variable->children['name'])) {
if (!$variable instanceof Node) {
// e.g. 'foo' in `isset('foo'[$i])` or `isset('foo'->bar)`.
$this->emit(
'PhanPluginInvalidVariableIsset',
"Unexpected expression in isset()",
[]
);
return $this->context;
}
if (in_array($variable->kind, self::EXPRESSIONS, true)) {
$variable = $variable->children['expr'];
} elseif (in_array($variable->kind, self::CLASSES, true)) {
$variable = $variable->children['class'];
} else {
return $this->context;
}
}
if (!$variable instanceof Node) {
$this->emit(
'PhanPluginUnexpectedExpressionIsset',
"Unexpected expression in isset()",
[]
);
return $this->context;
}
$name = $variable->children['name'] ?? null;
// emit issue if name is not declared
// Check for edge cases such as isset($$var)
if (is_string($name)) {
if ($variable->kind !== ast\AST_VAR) {
// e.g. ast\AST_NAME of an ast\AST_CONST
return $this->context;
}
if (!Variable::isHardcodedVariableInScopeWithName($name, $this->context->isInGlobalScope())
&& !$this->context->getScope()->hasVariableWithName($name)
&& !(
$this->context->isInGlobalScope() && Config::getValue('ignore_undeclared_variables_in_global_scope')
)
) {
$this->emit(
'PhanPluginUndeclaredVariableIsset',
'undeclared variable ${VARIABLE} in isset()',
[$name]
);
}
} elseif ($variable->kind !== ast\AST_VAR) {
// emit issue if argument is not array access
$this->emit(
'PhanPluginInvalidVariableIsset',
"non array/property access in isset()",
[]
);
return $this->context;
} elseif (!is_string($name)) {
// emit issue if argument is not array access
$this->emit(
'PhanPluginComplexVariableInIsset',
"Unanalyzable complex variable expression in isset",
[]
);
}
return $this->context;
}
}
return new InvalidVariableIssetPlugin();

View File

@@ -0,0 +1,448 @@
<?php
declare(strict_types=1);
use ast\Node;
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;
use Phan\PluginV3\BeforeAnalyzeFileCapability;
use Phan\PluginV3\FinalizeProcessCapability;
/**
* This plugin invokes the equivalent of `php --no-php-ini --syntax-check $analyzed_file_path`.
*
* php-ast reports syntax errors, but does not report all **semantic** errors that `php --syntax-check` would detect.
*
* Note that loading PHP modules would slow down analysis, so this plugin adds `--no-php-ini`.
*
* NOTE: This may not work in languages other than english.
* NOTE: .phan/config.php can contain a config to override the PHP binary/binaries used
* This can replace the default binary (PHP_BINARY) with an array of absolute path or program names(in $PATH)
* E.g. have 'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]
* Note: This may cause Phan to take over twice as long. This is recommended for use with `--processes N`.
*
* Known issues:
* - short_open_tags may make php --syntax-check --no-php-ini behave differently from php --syntax-check, e.g. for '<?phpinvalid;'
*
* @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
*/
class InvokePHPNativeSyntaxCheckPlugin extends PluginV3 implements
AfterAnalyzeFileCapability,
BeforeAnalyzeFileCapability,
FinalizeProcessCapability
{
private const LINE_NUMBER_REGEX = "@ on line ([1-9][0-9]*)$@D";
private const STDIN_FILENAME_REGEX = "@ in (Standard input code|-)@";
/**
* @var list<InvokeExecutionPromise>
* A list of invoked processes that this plugin created.
* This plugin creates 0 or more processes(up to a maximum number can run at a time)
* and then waits for the execution of those processes to finish.
*/
private $processes = [];
/**
* @param CodeBase $code_base @phan-unused-param
* The code base in which the node exists
*
* @param Context $context
* 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
*/
public function beforeAnalyzeFile(
CodeBase $code_base,
Context $context,
string $file_contents,
Node $node
): void {
$php_binaries = (Config::getValue('plugin_config')['php_native_syntax_check_binaries'] ?? null) ?: [PHP_BINARY];
foreach ($php_binaries as $binary) {
$this->processes[] = new InvokeExecutionPromise($binary, $file_contents, $context);
}
}
/**
* @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 {
$configured_max_incomplete_processes = (int)(Config::getValue('plugin_config')['php_native_syntax_check_max_processes'] ?? 1) - 1;
$max_incomplete_processes = max(0, $configured_max_incomplete_processes);
$this->awaitIncompleteProcesses($code_base, $max_incomplete_processes);
}
/**
* @throws Error if a syntax check process fails to shut down
*/
private function awaitIncompleteProcesses(CodeBase $code_base, int $max_incomplete_processes): void
{
foreach ($this->processes as $i => $process) {
if (!$process->read()) {
continue;
}
unset($this->processes[$i]);
self::handleError($code_base, $process);
}
$max_incomplete_processes = max(0, $max_incomplete_processes);
while (count($this->processes) > $max_incomplete_processes) {
$process = array_pop($this->processes);
if (!$process) {
throw new AssertionError("Process list should be non-empty");
}
$process->blockingRead();
self::handleError($code_base, $process);
}
}
/**
* @override
* @throws Error if a syntax check process fails to shut down.
*/
public function finalizeProcess(CodeBase $code_base): void
{
$this->awaitIncompleteProcesses($code_base, 0);
}
private static function handleError(CodeBase $code_base, InvokeExecutionPromise $process): void
{
$check_error_message = $process->getError();
if (!is_string($check_error_message)) {
return;
}
$context = $process->getContext();
$binary = $process->getBinary();
$lineno = 1;
if (preg_match(self::LINE_NUMBER_REGEX, $check_error_message, $matches)) {
$lineno = (int)$matches[1];
$check_error_message = trim(preg_replace(self::LINE_NUMBER_REGEX, '', $check_error_message));
}
$check_error_message = preg_replace(self::STDIN_FILENAME_REGEX, '', $check_error_message);
self::emitIssue(
$code_base,
(clone $context)->withLineNumberStart($lineno),
'PhanNativePHPSyntaxCheckPlugin',
'Saw error or notice for {FILE} --syntax-check: {DETAILS}',
[
$binary === PHP_BINARY ? 'php' : $binary,
json_encode($check_error_message),
],
Issue::SEVERITY_CRITICAL
);
}
}
/**
* This wraps a `php --syntax-check` process,
* and contains methods to start the process and await the result
* (and check for failures)
*/
class InvokeExecutionPromise
{
/** @var string path to the php binary invoked */
private $binary;
/** @var bool is the process finished executing */
private $done = false;
/** @var resource the result of `proc_open()` */
private $process;
/** @var array{0:resource,1:resource,2:resource} stdin, stdout, stderr */
private $pipes;
/** @var ?string an error message */
private $error = null;
/** @var string the raw bytes from stdout with serialized data */
private $raw_stdout = '';
/** @var string */
private $fallback_error = '';
/** @var Context has the file name being analyzed */
private $context;
/** @var ?string the temporary path, if needed for Windows. */
private $tmp_path;
public function __construct(string $binary, string $file_contents, Context $context)
{
$this->context = clone $context;
$new_file_contents = Parser::removeShebang($file_contents);
// TODO: Use symfony process
// Note: We might have invalid utf-8, ensure that the streams are opened in binary mode.
// I'm not sure if this is necessary.
if (DIRECTORY_SEPARATOR === "\\") {
$cmd = escapeshellarg($binary) . ' --syntax-check --no-php-ini';
$abs_path = $this->getAbsPathForFileContents($new_file_contents, $file_contents !== $new_file_contents);
if (!is_string($abs_path)) {
// The helper function has set the error and done flags
return;
}
// Possibly https://bugs.php.net/bug.php?id=51800
// NOTE: Work around this by writing from the original file. This may not work as expected in LSP mode
$abs_path = str_replace("/", "\\", $abs_path);
$cmd .= ' < ' . escapeshellarg($abs_path);
$descriptorspec = [
1 => ['pipe', 'wb'],
];
$this->binary = $binary;
// https://superuser.com/questions/1213094/how-to-escape-in-cmd-exe-c-parameters/1213100#1213100
//
// > Otherwise, old behavior is to see if the first character is
// > a quote character and if so, strip the leading character and
// > remove the last quote character on the command line, preserving
// > any text after the last quote character.
//
// e.g. `""C:\php 7.4.3\php.exe" --syntax-check --no-php-ini < "C:\some project\test.php""`
// gets unescaped as `"C:\php 7.4.3\php.exe" --syntax-check --no-php-ini < "C:\some project\test.php"`
if (PHP_VERSION_ID < 80000) {
// In PHP 8.0.0, proc_open started always escaping arguments with additional quotes, so doing it twice would be a bug.
$cmd = "\"$cmd\"";
}
$process = proc_open("$cmd", $descriptorspec, $pipes);
if (!is_resource($process)) {
$this->done = true;
$this->error = "Failed to run proc_open in " . __METHOD__;
return;
}
$this->process = $process;
} else {
$cmd = [$binary, '--syntax-check', '--no-php-ini'];
if (PHP_VERSION_ID < 70400) {
$cmd = implode(' ', array_map('escapeshellarg', $cmd));
}
$descriptorspec = [
['pipe', 'rb'],
['pipe', 'wb'],
];
$this->binary = $binary;
// @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;
$this->error = "Failed to run proc_open in " . __METHOD__;
return;
}
$this->process = $process;
error_clear_last();
if (!self::streamPutContents($pipes[0], $new_file_contents)) {
$this->fallback_error = \error_get_last()['message'] ?? '';
}
}
$this->pipes = $pipes;
if (!stream_set_blocking($pipes[1], false)) {
$this->error = "unable to set read stdout to non-blocking";
}
}
private function getAbsPathForFileContents(string $new_file_contents, bool $force_tmp_file): ?string
{
$file_name = $this->context->getFile();
if ($force_tmp_file || CLI::isDaemonOrLanguageServer()) {
// This is inefficient, but
// - Windows has problems with using stdio/stdout at the same time
// - During regular analysis, we won't need to create temporary files.
$tmp_path = tempnam(sys_get_temp_dir(), 'phan');
if (!is_string($tmp_path)) {
$this->done = true;
$this->error = "Could not create temporary path for $file_name";
return null;
}
file_put_contents($tmp_path, $new_file_contents);
$this->tmp_path = $tmp_path;
return $tmp_path;
}
$abs_path = Config::projectPath($file_name);
if (!file_exists($abs_path)) {
$this->done = true;
$this->error = "File does not exist";
return null;
}
return $abs_path;
}
/**
* @param resource $stream stream to write $file_contents to before fclose()
* @param string $file_contents
* See https://bugs.php.net/bug.php?id=39598
*/
private static function streamPutContents($stream, string $file_contents): bool
{
try {
while (strlen($file_contents) > 0) {
$bytes_written = with_disabled_phan_error_handler(/** @return int|false */ static function () use ($stream, $file_contents) {
return @fwrite($stream, $file_contents);
});
if ($bytes_written === false) {
CLI::printWarningToStderr('failed to write in ' . __METHOD__);
return false;
}
if ($bytes_written === 0) {
$read_streams = [];
$write_streams = [$stream];
$except_streams = [];
// Wait for the stream to be available for write with a timeout of 1 second.
stream_select($read_streams, $write_streams, $except_streams, 1);
if (!$write_streams) {
usleep(1000); // Probably unnecessary, but leaving it in anyway
// This is blocked?
continue;
}
// $stream is ready to be written to?
$bytes_written = fwrite($stream, $file_contents);
if (!$bytes_written) {
CLI::printToStderr('failed to write in ' . __METHOD__ . ' but the stream should be ready');
return false;
}
}
if ($bytes_written > 0) {
$file_contents = \substr($file_contents, $bytes_written);
}
}
} finally {
fclose($stream);
}
return true;
}
/**
* @return bool false if an error was encountered when trying to read more output from the syntax check process.
*/
public function read(): bool
{
if ($this->done) {
return true;
}
$stdout = $this->pipes[1];
while (!feof($stdout)) {
$bytes = fread($stdout, 4096);
if ($bytes === false) {
break;
}
if (strlen($bytes) === 0) {
break;
}
$this->raw_stdout .= $bytes;
}
if (!feof($stdout)) {
return false;
}
fclose($stdout);
$this->done = true;
$exit_code = proc_close($this->process);
if ($exit_code === 0) {
$this->error = null;
return true;
}
$output = str_replace("\r", "", trim($this->raw_stdout));
$first_line = explode("\n", $output)[0];
$this->error = $first_line;
return true;
}
/**
* @throws Error if reading failed
*/
public function blockingRead(): void
{
if ($this->done) {
return;
}
if (!stream_set_blocking($this->pipes[1], true)) {
throw new Error("Unable to make stdout blocking");
}
if (!$this->read()) {
throw new Error("Failed to read");
}
}
/**
* @throws RangeException if this was called before the process finished
*/
public function getError(): ?string
{
if (!$this->done) {
throw new RangeException("Called " . __METHOD__ . " too early");
}
if ($this->error === '') {
// There was an error running the process, but no output to stdout.
$result = "No output was detected. Is " . var_representation($this->binary) . " a relative or absolute path to an executable PHP binary?";
if ($this->fallback_error !== '') {
$result .= ' Error sending file contents to syntax check: ' . $this->fallback_error;
}
return $result;
}
return $this->error;
}
/**
* Returns the context containing the name of the file being syntax checked
*/
public function getContext(): Context
{
return $this->context;
}
/**
* @return string the path to the PHP interpreter binary. (e.g. `/usr/bin/php`)
*/
public function getBinary(): string
{
return $this->binary;
}
/**
* @return never
*/
public function __wakeup()
{
$this->tmp_path = null;
throw new RuntimeException("Cannot unserialize");
}
public function __destruct()
{
// We created a temporary path for Windows
if (is_string($this->tmp_path)) {
unlink($this->tmp_path);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new InvokePHPNativeSyntaxCheckPlugin();

View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
use Phan\Plugin\Internal\LoopVariableReusePlugin;
return new LoopVariableReusePlugin();

View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN;
use Phan\Language\UnionType;
use Phan\Library\Map;
use Phan\Library\Set;
use Phan\PluginV3;
use Phan\PluginV3\FinalizeProcessCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for return types that can be made more specific.
*
* - E.g. `/** (at)return object (*)/ function () { return new ArrayObject(); }`
* could be documented as returning an ArrayObject instead.
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* MoreSpecificElementTypePlugin hooks into two events:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
* - finalizeProcess
* This is called after the other forms of analysis are finished running.
*
* 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
*
* TODO: Account for methods in traits being possibly overrides
*
* TODO: This does not support intersection types
*/
class MoreSpecificElementTypePlugin extends PluginV3 implements
PostAnalyzeNodeCapability,
FinalizeProcessCapability
{
/** @var Map<FQSEN,ElementTypeInfo> maps function/method/closure FQSEN to function info and the set of union types they return */
public static $method_return_types;
/** @var Set<FQSEN> the set of function/method/closure FQSENs that don't need to be more specific. */
public static $method_blacklist;
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return MoreSpecificElementTypeVisitor::class;
}
/**
* Record that $function contains a return statement which returns an expression of type $return_type.
*
* This may be called multiple times for the same return statement (Phan recursively analyzes functions with underspecified param types by default)
*/
public static function recordType(FunctionInterface $function, UnionType $return_type): void
{
$fqsen = $function->getFQSEN();
if (self::$method_blacklist->offsetExists($fqsen)) {
return;
}
if ($return_type->isEmpty()) {
self::$method_blacklist->attach($fqsen);
self::$method_return_types->offsetUnset($fqsen);
return;
}
if (self::$method_return_types->offsetExists($fqsen)) {
self::$method_return_types->offsetGet($fqsen)->types->attach($return_type);
} else {
self::$method_return_types->offsetSet($fqsen, new ElementTypeInfo($function, [$return_type]));
}
}
private static function shouldWarnAboutMoreSpecificType(CodeBase $code_base, UnionType $actual_type, UnionType $declared_return_type): bool
{
if ($declared_return_type->isEmpty()) {
// There was no phpdoc type declaration, so let UnknownElementTypePlugin warn about that instead of this.
// This plugin warns about `@return mixed` but not the absence of a declaration because the former normally prevents phan from inferring something more specific.
return false;
}
if ($declared_return_type->containsNullable() && !$actual_type->containsNullable()) {
// Warn about `Subclass1|Subclass2` being the real return type of `?BaseClass`
// because the actual returned type is non-null
return true;
}
if ($declared_return_type->typeCount() === 1) {
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;
}
}
}
if ($declared_return_type->isStrictSubtypeOf($code_base, $actual_type)) {
return false;
}
if (!$actual_type->isStrictSubtypeOf($code_base, $declared_return_type)) {
return false;
}
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;
}
if ($declared_return_type->hasTopLevelArrayShapeTypeInstances()) {
return false;
}
$real_actual_type = $actual_type->getRealUnionType();
if (!$real_actual_type->isEmpty() && $declared_return_type->isStrictSubtypeOf($code_base, $real_actual_type)) {
// TODO: Provide a way to disable this heuristic.
return false;
}
return true;
}
private static function containsObjectWithKnownFQSEN(UnionType $union_type): bool
{
foreach ($union_type->getTypesRecursively() as $type) {
if ($type->hasObjectWithKnownFQSEN()) {
return true;
}
}
return false;
}
/**
* After all return statements are gathered, suggest a more specific type for the various functions.
*/
public function finalizeProcess(CodeBase $code_base): void
{
foreach (self::$method_return_types as $type_info) {
$function = $type_info->function;
$function_context = $function->getContext();
// TODO: Do a better job for Traversable<MyClass> and iterable<MyClass>
$actual_type = UnionType::merge($type_info->types->toArray())->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes();
$declared_return_type = $function->getOriginalReturnType()->withStaticResolvedInContext($function_context)->eraseTemplatesRecursive()->asNormalizedTypes();
if (!self::shouldWarnAboutMoreSpecificType($code_base, $actual_type, $declared_return_type)) {
continue;
}
if (self::containsObjectWithKnownFQSEN($actual_type) && !self::containsObjectWithKnownFQSEN($declared_return_type)) {
$issue_type = 'PhanPluginMoreSpecificActualReturnTypeContainsFQSEN';
$issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} (without an FQSEN) returns the more specific type {TYPE} (with an FQSEN)';
} else {
$issue_type = 'PhanPluginMoreSpecificActualReturnType';
$issue_message = 'Phan inferred that {FUNCTION} documented to have return type {TYPE} returns the more specific type {TYPE}';
}
$this->emitIssue(
$code_base,
$function->getContext(),
$issue_type,
$issue_message,
[
$function->getRepresentationForIssue(),
$declared_return_type,
$actual_type->getDebugRepresentation()
]
);
}
}
}
/**
* Represents the actual return types seen during analysis
* (including recursive analysis)
*/
class ElementTypeInfo
{
/** @var FunctionInterface the function with the return values*/
public $function;
/** @var Set<UnionType> the set of observed return types */
public $types;
/**
* @param list<UnionType> $return_types
*/
public function __construct(FunctionInterface $function, array $return_types)
{
$this->function = $function;
$this->types = new Set($return_types);
}
}
MoreSpecificElementTypePlugin::$method_blacklist = new Set();
MoreSpecificElementTypePlugin::$method_return_types = new Map();
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class MoreSpecificElementTypeVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node a node of kind ast\AST_RETURN, representing a return statement.
*/
public function visitReturn(Node $node): void
{
if (!$this->context->isInFunctionLikeScope()) {
return;
}
try {
$function = $this->context->getFunctionLikeInScope($this->code_base);
} catch (Exception $_) {
return;
}
if ($function->hasYield()) {
// TODO: Support analyzing yield key/value types of generators?
return;
}
if ($function instanceof Method) {
// Skip functions that are overrides or are overridden.
// They may be documenting a less specific return type to deal with the inheritance hierarchy.
if ($function->isOverride() || $function->isOverriddenByAnother()) {
return;
}
}
try {
// Fetch the list of valid classes, and warn about any undefined classes.
// (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*)
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
} catch (Exception $_) {
// Phan should already throw for this
return;
}
MoreSpecificElementTypePlugin::recordType($function, $union_type->withFlattenedArrayShapeOrLiteralTypeInstances());
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new MoreSpecificElementTypePlugin();

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for occurrences of `assert(cond)` for Phan's self-analysis.
* It is not suitable for some projects.
* See https://github.com/phan/phan/issues/288
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* NoAssertPlugin 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 NoAssertPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return NoAssertVisitor::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 NoAssertVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitCall(Node $node): void
{
$name = $node->children['expr']->children['name'] ?? null;
if (!is_string($name)) {
return;
}
if (strcasecmp($name, 'assert') !== 0) {
return;
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginNoAssert',
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
'assert() is discouraged. Although phan supports using assert() for type annotations, PHP\'s documentation recommends assertions only for debugging, and assert() has surprising behaviors.',
[]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new NoAssertPlugin();

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
// .phan/plugins/NonBoolBranchPlugin.php
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\Exception\IssueException;
use Phan\Language\Context;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePreAnalysisVisitor;
use Phan\PluginV3\PreAnalyzeNodeCapability;
/**
* This plugin warns if an expression which has types other than `bool` is used in an if/else if.
*
* Note that the 'simplify_ast' setting's default of true will interfere with this plugin.
*/
class NonBoolBranchPlugin extends PluginV3 implements PreAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePreAnalysisVisitor subclass
*
* @override
*/
public static function getPreAnalyzeNodeVisitorClassName(): string
{
return NonBoolBranchVisitor::class;
}
}
/**
* This visitor checks if statements for conditions ('cond') that are non-booleans.
*/
class NonBoolBranchVisitor extends PluginAwarePreAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @override
*/
public function visitIf(Node $node): Context
{
// Here, we visit the group of if/elseif/else instead of the individuals (visitIfElem)
// so that we have the Union types of the variables **before** the PreOrderAnalysisVisitor makes inferences
foreach ($node->children as $if_node) {
if (!$if_node instanceof Node) {
throw new AssertionError("Expected if statement to be a node");
}
$condition = $if_node->children['cond'];
// dig nodes to avoid the NOT('!') operation converting its value to a boolean type.
// Also, use right-hand side of assignments such as `$x = (expr)`
while (($condition instanceof Node) && (
($condition->flags === ast\flags\UNARY_BOOL_NOT && $condition->kind === ast\AST_UNARY_OP)
|| (\in_array($condition->kind, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true)))
) {
$condition = $condition->children['expr'];
}
if ($condition === null) {
// $condition === null will be appeared in else-clause, then avoid them
continue;
}
if ($condition instanceof Node) {
$this->context = $this->context->withLineNumberStart($condition->lineno);
}
// evaluate the type of conditional expression
try {
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $condition);
} catch (IssueException $_) {
return $this->context;
}
if (!$union_type->isEmpty() && !$union_type->isExclusivelyBoolTypes()) {
$this->emit(
'PhanPluginNonBoolBranch',
'Non bool value of type {TYPE} evaluated in if clause',
[(string)$union_type]
);
}
}
return $this->context;
}
}
return new NonBoolBranchPlugin();

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\Language\Context;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for non-booleans in either side of logical arithmetic operators
* (e.g. &&, ||, xor)
*/
class NonBoolInLogicalArithPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return NonBoolInLogicalArithVisitor::class;
}
}
/**
* This visitor checks boolean logical arithmetic operations for non-boolean expressions on either side.
*/
class NonBoolInLogicalArithVisitor extends PluginAwarePostAnalysisVisitor
{
/** define boolean operator list */
private const BINARY_BOOL_OPERATORS = [
ast\flags\BINARY_BOOL_OR,
ast\flags\BINARY_BOOL_AND,
ast\flags\BINARY_BOOL_XOR,
];
// A plugin's visitors should not override visit() unless they need to.
/**
* @override
*/
public function visitBinaryOp(Node $node): Context
{
// check every boolean binary operation
if (in_array($node->flags, self::BINARY_BOOL_OPERATORS, true)) {
// get left node and parse it
// (dig nodes to avoid NOT('!') operator's converting its value to boolean type)
$left_node = $node->children['left'];
while (isset($left_node->flags) && $left_node->flags === ast\flags\UNARY_BOOL_NOT) {
$left_node = $left_node->children['expr'];
}
// get right node and parse it
$right_node = $node->children['right'];
while (isset($right_node->flags) && $right_node->flags === ast\flags\UNARY_BOOL_NOT) {
$right_node = $right_node->children['expr'];
}
// get the type of two nodes
$left_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_node);
$right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right_node);
// if left or right type is NOT boolean, emit issue
if (!$left_type->isExclusivelyBoolTypes()) {
if ($left_node instanceof Node) {
$this->context = $this->context->withLineNumberStart($left_node->lineno);
}
$this->emit(
'PhanPluginNonBoolInLogicalArith',
'Non bool value of type {TYPE} in logical arithmetic',
[(string)$left_type]
);
}
if (!$right_type->isExclusivelyBoolTypes()) {
if ($right_node instanceof Node) {
$this->context = $this->context->withLineNumberStart($right_node->lineno);
}
$this->emit(
'PhanPluginNonBoolInLogicalArith',
'Non bool value of type {TYPE} in logical arithmetic',
[(string)$right_type]
);
}
}
return $this->context;
}
}
return new NonBoolInLogicalArithPlugin();

View File

@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Config;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This warns if references to global functions or global constants are not fully qualified.
*
* This Plugin hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class that is called on every AST node from every
* file being analyzed
*/
class NotFullyQualifiedUsagePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - The name of the visitor that will be called (formerly analyzeNode)
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return NotFullyQualifiedUsageVisitor::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 NotFullyQualifiedUsageVisitor extends PluginAwarePostAnalysisVisitor
{
// Subclasses should declare protected $parent_node_list as an instance property if they need to know the list.
// @var list<Node> - Set after the constructor is called if an instance property with this name is declared
// protected $parent_node_list;
// A plugin's visitors should NOT implement visit(), unless they need to.
// phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
public const NotFullyQualifiedFunctionCall = 'PhanPluginNotFullyQualifiedFunctionCall';
public const NotFullyQualifiedOptimizableFunctionCall = 'PhanPluginNotFullyQualifiedOptimizableFunctionCall';
public const NotFullyQualifiedGlobalConstant = 'PhanPluginNotFullyQualifiedGlobalConstant';
// phpcs:enable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
/**
* Source of functions: `zend_try_compile_special_func` from https://github.com/php/php-src/blob/master/Zend/zend_compile.c
*/
private const OPTIMIZABLE_FUNCTIONS = [
'array_key_exists' => true,
'array_slice' => true,
'boolval' => true,
'call_user_func' => true,
'call_user_func_array' => true,
'chr' => true,
'count' => true,
'defined' => true,
'doubleval' => true,
'floatval' => true,
'func_get_args' => true,
'func_num_args' => true,
'get_called_class' => true,
'get_class' => true,
'gettype' => true,
'in_array' => true,
'intval' => true,
'is_array' => true,
'is_bool' => true,
'is_double' => true,
'is_float' => true,
'is_int' => true,
'is_integer' => true,
'is_long' => true,
'is_null' => true,
'is_object' => true,
'is_real' => true,
'is_resource' => true,
'is_string' => true,
'ord' => true,
'strlen' => true,
'strval' => true,
];
/**
* @param Node $node
* A node to analyze of type ast\AST_CALL (call to a global function)
* @override
*/
public function visitCall(Node $node): void
{
$expression = $node->children['expr'];
if (!($expression instanceof Node) || $expression->kind !== ast\AST_NAME) {
return;
}
if (($expression->flags & ast\flags\NAME_NOT_FQ) !== ast\flags\NAME_NOT_FQ) {
// This is namespace\foo() or \NS\foo()
return;
}
if ($this->context->getNamespace() === '\\') {
// This is in the global namespace and is always fully qualified
return;
}
$function_name = $expression->children['name'];
if (!is_string($function_name)) {
// Possibly redundant.
return;
}
// TODO: Probably wrong for ast\parse_code - should check namespace map of USE_NORMAL for 'ast' there.
// Same for ContextNode->getFunction()
if ($this->context->hasNamespaceMapFor(\ast\flags\USE_FUNCTION, $function_name)) {
return;
}
$this->warnNotFullyQualifiedFunctionCall($function_name, $expression);
}
private function warnNotFullyQualifiedFunctionCall(string $function_name, Node $expression): void
{
if (array_key_exists(strtolower($function_name), self::OPTIMIZABLE_FUNCTIONS)) {
$issue_type = self::NotFullyQualifiedOptimizableFunctionCall;
$issue_msg = 'Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}'
. ' (opcache can optimize fully qualified calls to this function in recent php versions)';
} else {
$issue_type = self::NotFullyQualifiedFunctionCall;
$issue_msg = 'Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}';
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($expression->lineno),
$issue_type,
$issue_msg,
[$function_name, $this->context->getNamespace()]
);
}
/**
* @param Node $node
* A node to analyze of type ast\AST_CONST (reference to a constant)
* @override
*/
public function visitConst(Node $node): void
{
$expression = $node->children['name'];
if (!($expression instanceof Node) || $expression->kind !== ast\AST_NAME) {
return;
}
if (($expression->flags & ast\flags\NAME_NOT_FQ) !== ast\flags\NAME_NOT_FQ) {
// This is namespace\SOME_CONST or \NS\SOME_CONST
return;
}
if ($this->context->getNamespace() === '\\') {
// This is in the global namespace and is always fully qualified
return;
}
$constant_name = $expression->children['name'];
if (!is_string($constant_name)) {
// Possibly redundant.
return;
}
$constant_name_lower = strtolower($constant_name);
if ($constant_name_lower === 'true' || $constant_name_lower === 'false' || $constant_name_lower === 'null') {
// These are treated similarly to keywords and are either
// 1. the same in any namespace
// 2. `use somethingelse\true [as false];`
return;
}
// TODO: Probably wrong for ast\AST_NAME - should check namespace map of USE_NORMAL for 'ast' there.
// Same for ContextNode->getConst()
if ($this->context->hasNamespaceMapFor(\ast\flags\USE_CONST, $constant_name)) {
return;
}
$this->warnNotFullyQualifiedConstantUsage($constant_name, $expression);
}
private function warnNotFullyQualifiedConstantUsage(string $constant_name, Node $expression): void
{
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($expression->lineno),
self::NotFullyQualifiedGlobalConstant,
'Expected usage of {CONST} to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}',
[$constant_name, $this->context->getNamespace()]
);
}
}
if (Config::isIssueFixingPluginEnabled()) {
require_once __DIR__ . '/NotFullyQualifiedUsagePlugin/fixers.php';
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new NotFullyQualifiedUsagePlugin();

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
use Microsoft\PhpParser\Node\Expression\CallExpression;
use Microsoft\PhpParser\Node\QualifiedName;
use Microsoft\PhpParser\Node\ReservedWord;
use Microsoft\PhpParser\Token;
use Phan\AST\TolerantASTConverter\NodeUtils;
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedGlobalConstantName;
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 {
/**
* @return ?FileEditSet
*/
$fix = static function (CodeBase $code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet {
$line = $instance->getLine();
$expected_name = $instance->getTemplateParameters()[0];
$edits = [];
foreach ($contents->getNodesAtLine($line) as $node) {
if ($node instanceof QualifiedName) {
if ($node->globalSpecifier || $node->relativeSpecifier) {
IssueFixer::debug("skip already globally or relatively specified\n");
// This is already qualified
continue;
}
$actual_name = (new NodeUtils($contents->getContents()))->phpParserNameToString($node);
} elseif ($node instanceof ReservedWord) {
// A reserved word in other contexts such as 'float'
$token = $node->children;
if (!$token instanceof Token) {
continue;
}
$actual_name = (new NodeUtils($contents->getContents()))->tokenToString($token);
} else {
IssueFixer::debug("skip wrong node kind " . get_class($node) . "\n");
continue;
}
if ($actual_name !== $expected_name) {
IssueFixer::debug("skip '$actual_name' !== '$expected_name'\n");
continue;
}
$is_actual_call = $node->parent instanceof CallExpression;
$is_expected_call = $instance->getIssue()->getType() !== NotFullyQualifiedUsageVisitor::NotFullyQualifiedGlobalConstant;
if ($is_actual_call !== $is_expected_call) {
IssueFixer::debug("skip check mismatch actual expected are call vs constants\n");
// don't warn about constants with the same names as functions or vice-versa
continue;
}
try {
if ($is_expected_call) {
// Don't do this if the global function this refers to doesn't exist.
// TODO: Support namespaced functions
if (!$code_base->hasFunctionWithFQSEN(FullyQualifiedFunctionName::fromFullyQualifiedString($actual_name))) {
IssueFixer::debug("skip attempt to fix $actual_name() because function was not found in the global scope\n");
return null;
}
} else {
// Don't do this if the global function this refers to doesn't exist.
// TODO: Support namespaced functions
if (!$code_base->hasGlobalConstantWithFQSEN(FullyQualifiedGlobalConstantName::fromFullyQualifiedString($actual_name)) &&
!in_array(strtolower($actual_name), ['null', 'true', 'false'], true)) {
IssueFixer::debug("skip attempt to fix $actual_name because the constant was not found in the global scope\n");
return null;
}
}
} catch (Exception $_) {
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();
$edits[] = new FileEdit($start, $start, '\\');
}
if ($edits) {
return new FileEditSet($edits);
}
return null;
};
IssueFixer::registerFixerClosure(
NotFullyQualifiedUsageVisitor::NotFullyQualifiedGlobalConstant,
$fix
);
IssueFixer::registerFixerClosure(
NotFullyQualifiedUsageVisitor::NotFullyQualifiedFunctionCall,
$fix
);
IssueFixer::registerFixerClosure(
NotFullyQualifiedUsageVisitor::NotFullyQualifiedOptimizableFunctionCall,
$fix
);
});

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\Language\Context;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin enforces that loose equality is used for numeric operands (e.g. `2 == 2.0`),
* and that strict equality is used for non-numeric operands (e.g. `"2" === "2e0"` is false).
*/
class NumericalComparisonPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return NumericalComparisonVisitor::class;
}
}
/**
* This visitor checks binary operators to check that
* loose equality is used for numeric operands (e.g. `2 == 2.0`),
* and that strict equality is used for non-numeric operands (e.g. `"2" === "2e0"` is false).
*/
class NumericalComparisonVisitor extends PluginAwarePostAnalysisVisitor
{
/** define equal operator list */
protected const BINARY_EQUAL_OPERATORS = [
ast\flags\BINARY_IS_EQUAL,
ast\flags\BINARY_IS_NOT_EQUAL,
];
/** define identical operator list */
protected const BINARY_IDENTICAL_OPERATORS = [
ast\flags\BINARY_IS_IDENTICAL,
ast\flags\BINARY_IS_NOT_IDENTICAL,
];
// A plugin's visitors should not override visit() unless they need to.
/**
* @override
*/
public function visitBinaryOp(Node $node): Context
{
// get the types of left and right values
$left_node = $node->children['left'];
$left_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_node);
$right_node = $node->children['right'];
$right_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right_node);
// non numerical values are not allowed in the operator equal(==, !=)
if (in_array($node->flags, self::BINARY_EQUAL_OPERATORS, true)) {
if (!$left_type->isNonNullNumberType() && !$right_type->isNonNullNumberType()) {
$this->emit(
'PhanPluginNumericalComparison',
"non numerical values compared by the operators '==' or '!='",
[]
);
}
// numerical values are not allowed in the operator identical('===', '!==')
} elseif (in_array($node->flags, self::BINARY_IDENTICAL_OPERATORS, true)) {
if ($left_type->isNonNullNumberType() || $right_type->isNonNullNumberType()) {
// TODO: different name for this issue type?
$this->emit(
'PhanPluginNumericalComparison',
"numerical values compared by the operators '===' or '!=='",
[]
);
}
}
return $this->context;
}
}
return new NumericalComparisonPlugin();

View File

@@ -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();

View File

@@ -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();

View File

@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\Element\Comment\Builder;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Library\FileCacheEntry;
use Phan\Library\StringUtil;
use Phan\Phan;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AutomaticFixCapability;
use PHPDocRedundantPlugin\Fixers;
/**
* This plugin checks for redundant doc comments on functions, closures, and methods.
*
* This treats a doc comment as redundant if
*
* 1. It is exclusively annotations (0 or more), e.g. (at)return void
* 2. Every annotation repeats the real information in the signature.
*
* It does not check if the change is safe to make.
*/
class PHPDocRedundantPlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
AutomaticFixCapability
{
private const RedundantFunctionComment = 'PhanPluginRedundantFunctionComment';
private const RedundantClosureComment = 'PhanPluginRedundantClosureComment';
private const RedundantMethodComment = 'PhanPluginRedundantMethodComment';
private const RedundantReturnComment = 'PhanPluginRedundantReturnComment';
public function analyzeFunction(CodeBase $code_base, Func $function): void
{
self::analyzeFunctionLike($code_base, $function);
}
public function analyzeMethod(CodeBase $code_base, Method $method): void
{
if ($method->isMagic() || $method->isPHPInternal()) {
return;
}
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
return;
}
self::analyzeFunctionLike($code_base, $method);
}
/**
* @suppress PhanAccessClassConstantInternal
*/
private static function isRedundantFunctionComment(FunctionInterface $method, string $doc_comment): bool
{
$lines = explode("\n", $doc_comment);
foreach ($lines as $line) {
$line = trim($line, " \r\n\t*/");
if ($line === '') {
continue;
}
if ($line[0] !== '@') {
return false;
}
if (!preg_match('/^@(phan-)?(param|return)\s/', $line)) {
return false;
}
if (preg_match(Builder::PARAM_COMMENT_REGEX, $line, $matches)) {
if ($matches[0] !== $line) {
// There's a description after the (at)param annotation
return false;
}
} elseif (preg_match(Builder::RETURN_COMMENT_REGEX, $line, $matches)) {
if ($matches[0] !== $line) {
// There's a description after the (at)return annotation
return false;
}
} else {
// This is not a valid annotation. It might be documentation.
return false;
}
}
$comment = $method->getComment();
if (!$comment) {
// unparseable?
return false;
}
if ($comment->hasReturnUnionType()) {
$comment_return_type = $comment->getReturnType();
if (!$comment_return_type->isEmpty() && !$comment_return_type->asNormalizedTypes()->isEqualTo($method->getRealReturnType())) {
return false;
}
}
if (count($comment->getParameterList()) > 0) {
return false;
}
foreach ($comment->getParameterMap() as $comment_param_name => $param) {
$comment_param_type = $param->getUnionType()->asNormalizedTypes();
if ($comment_param_type->isEmpty()) {
return false;
}
foreach ($method->getRealParameterList() as $real_param) {
if ($real_param->getName() === $comment_param_name) {
if ($real_param->getUnionType()->isEqualTo($comment_param_type)) {
// This is redundant, check remaining parameters.
continue 2;
}
}
}
// could not find that comment param, Phan warns elsewhere.
// Assume this is not redundant.
return false;
}
return true;
}
private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void
{
if (Phan::isExcludedAnalysisFile($method->getContext()->getFile())) {
// This has no side effects, so we can skip files that don't need to be analyzed
return;
}
$comment = $method->getDocComment();
if (!StringUtil::isNonZeroLengthString($comment)) {
return;
}
if (!self::isRedundantFunctionComment($method, $comment)) {
self::checkIsRedundantReturn($code_base, $method, $comment);
return;
}
$encoded_comment = StringUtil::encodeValue($comment);
if ($method instanceof Method) {
self::emitIssue(
$code_base,
$method->getContext(),
self::RedundantMethodComment,
'Redundant doc comment on method {METHOD}(). Either add a description or remove the comment: {COMMENT}',
[$method->getName(), $encoded_comment]
);
} elseif ($method instanceof Func && $method->isClosure()) {
self::emitIssue(
$code_base,
$method->getContext(),
self::RedundantClosureComment,
'Redundant doc comment on closure {FUNCTION}. Either add a description or remove the comment: {COMMENT}',
[$method->getNameForIssue(), $encoded_comment]
);
} else {
self::emitIssue(
$code_base,
$method->getContext(),
self::RedundantFunctionComment,
'Redundant doc comment on function {FUNCTION}(). Either add a description or remove the comment: {COMMENT}',
[$method->getName(), $encoded_comment]
);
}
}
private static function checkIsRedundantReturn(CodeBase $code_base, FunctionInterface $method, string $doc_comment): void
{
if (strpos($doc_comment, '@return') === false) {
return;
}
$comment = $method->getComment();
if (!$comment) {
// unparseable?
return;
}
if ($method->getRealReturnType()->isEmpty()) {
return;
}
if (!$comment->hasReturnUnionType()) {
return;
}
$comment_return_type = $comment->getReturnType();
if (!$comment_return_type->asNormalizedTypes()->isEqualTo($method->getRealReturnType())) {
return;
}
$lines = explode("\n", $doc_comment);
for ($i = count($lines) - 1; $i >= 0; $i--) {
$line = $lines[$i];
$line = trim($line, " \r\n\t*/");
if ($line === '') {
continue;
}
if ($line[0] !== '@') {
return;
}
if (!preg_match('/^@(phan-)?return\s/', $line)) {
continue;
}
// @phan-suppress-next-line PhanAccessClassConstantInternal
if (!preg_match(Builder::RETURN_COMMENT_REGEX, $line, $matches)) {
return;
}
if ($matches[0] !== $line) {
// There's a description after the (at)return annotation
return;
}
self::emitIssue(
$code_base,
$method->getContext()->withLineNumberStart($comment->getReturnLineno()),
self::RedundantReturnComment,
'Redundant @return {TYPE} on function {FUNCTION}. Either add a description or remove the @return annotation: {COMMENT}',
[$comment_return_type, $method->getNameForIssue(), $line]
);
return;
}
}
/**
* @return array<string,Closure(CodeBase,FileCacheEntry,IssueInstance):(?FileEditSet)>
*/
public function getAutomaticFixers(): array
{
require_once __DIR__ . '/PHPDocRedundantPlugin/Fixers.php';
$function_like_fixer = Closure::fromCallable([Fixers::class, 'fixRedundantFunctionLikeComment']);
return [
self::RedundantFunctionComment => $function_like_fixer,
self::RedundantMethodComment => $function_like_fixer,
self::RedundantClosureComment => $function_like_fixer,
self::RedundantReturnComment => Closure::fromCallable([Fixers::class, 'fixRedundantReturnComment']),
];
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHPDocRedundantPlugin();

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace PHPDocRedundantPlugin;
use Microsoft\PhpParser;
use Microsoft\PhpParser\FunctionLike;
use Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Statement\FunctionDeclaration;
use Microsoft\PhpParser\ParseContext;
use Microsoft\PhpParser\PhpTokenizer;
use Microsoft\PhpParser\Token;
use Microsoft\PhpParser\TokenKind;
use Phan\AST\TolerantASTConverter\NodeUtils;
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\Element\Comment\Builder;
use Phan\Library\FileCacheEntry;
use Phan\Library\StringUtil;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEdit;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
/**
* This plugin implements --automatic-fix for PHPDocRedundantPlugin
*/
class Fixers
{
/**
* Remove a redundant phpdoc return type from the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixRedundantFunctionLikeComment(
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$params = $instance->getTemplateParameters();
$name = $params[0];
$encoded_comment = $params[1];
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $name);
if (!$declaration) {
return null;
}
return self::computeEditsToRemoveFunctionLikeComment($contents, $declaration, (string)$encoded_comment);
}
private static function computeEditsToRemoveFunctionLikeComment(FileCacheEntry $contents, FunctionLike $declaration, string $encoded_comment): ?FileEditSet
{
if (!$declaration instanceof PhpParser\Node) {
// impossible
return null;
}
$comment_token = self::getDocCommentToken($declaration);
if (!$comment_token) {
return null;
}
$file_contents = $contents->getContents();
$comment = $comment_token->getText($file_contents);
$actual_encoded_comment = StringUtil::encodeValue($comment);
if ($actual_encoded_comment !== $encoded_comment) {
return null;
}
return self::computeEditSetToDeleteComment($file_contents, $comment_token);
}
private static function computeEditSetToDeleteComment(string $file_contents, Token $comment_token): FileEditSet
{
// get the byte where the `)` of the argument list ends
$last_byte_index = $comment_token->getEndPosition();
$first_byte_index = $comment_token->start;
// Skip leading whitespace and the previous newline, if those were found
for (; $first_byte_index > 0; $first_byte_index--) {
$prev_byte = $file_contents[$first_byte_index - 1];
switch ($prev_byte) {
case " ":
case "\t":
// keep skipping previous bytes of whitespace
break;
case "\n":
$first_byte_index--;
if ($first_byte_index > 0 && $file_contents[$first_byte_index - 1] === "\r") {
$first_byte_index--;
}
break 2;
case "\r":
$first_byte_index--;
break 2;
default:
// This is not whitespace, so stop.
break 2;
}
}
$file_edit = new FileEdit($first_byte_index, $last_byte_index, '');
return new FileEditSet([$file_edit]);
}
/**
* Add a missing return type to the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixRedundantReturnComment(
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$lineno = $instance->getLine();
$file_lines = $contents->getLines();
$line = \trim($file_lines[$lineno]);
// @phan-suppress-next-line PhanAccessClassConstantInternal
if (!\preg_match(Builder::RETURN_COMMENT_REGEX, $line)) {
return null;
}
$first_deleted_line = $lineno;
$last_deleted_line = $lineno;
$is_blank_comment_line = static function (int $i) use ($file_lines): bool {
return \trim($file_lines[$i] ?? '') === '*';
};
while ($is_blank_comment_line($first_deleted_line - 1)) {
$first_deleted_line--;
}
while ($is_blank_comment_line($last_deleted_line + 1)) {
$last_deleted_line++;
}
$start_offset = $contents->getLineOffset($first_deleted_line);
$end_offset = $contents->getLineOffset($last_deleted_line + 1);
if (!$start_offset || !$end_offset) {
return null;
}
// Return an edit to delete the `(at)return RedundantType` and the surrounding blank comment lines
return new FileEditSet([new FileEdit($start_offset, $end_offset, '')]);
}
/**
* @suppress PhanThrowTypeAbsentForCall
* @suppress PhanUndeclaredClassMethod
* @suppress UnusedSuppression false positive for PhpTokenizer with polyfill due to https://github.com/Microsoft/tolerant-php-parser/issues/292
*/
private static function getDocCommentToken(PhpParser\Node $node): ?Token
{
$leadingTriviaText = $node->getLeadingCommentAndWhitespaceText();
$leadingTriviaTokens = PhpTokenizer::getTokensArrayFromContent(
$leadingTriviaText,
ParseContext::SourceElements,
$node->getFullStartPosition(),
false
);
for ($i = \count($leadingTriviaTokens) - 1; $i >= 0; $i--) {
$token = $leadingTriviaTokens[$i];
if ($token->kind === TokenKind::DocCommentToken) {
return $token;
}
}
return null;
}
private static function findFunctionLikeDeclaration(
FileCacheEntry $contents,
int $line,
string $name
): ?FunctionLike {
$candidates = [];
foreach ($contents->getNodesAtLine($line) as $node) {
if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) {
$name_node = $node->name;
if (!$name_node) {
continue;
}
$declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node);
if ($declaration_name === $name) {
$candidates[] = $node;
}
} elseif ($node instanceof AnonymousFunctionCreationExpression) {
if (\preg_match('/^Closure\(/', $name)) {
$candidates[] = $node;
}
}
}
if (\count($candidates) === 1) {
return $candidates[0];
}
return null;
}
}

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Library\FileCacheEntry;
use Phan\Phan;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AutomaticFixCapability;
use Phan\PluginV3\BeforeAnalyzePhaseCapability;
use PHPDocToRealTypesPlugin\Fixers;
/**
* This plugin suggests real types that can be used instead of phpdoc types.
*
* It does not check if the change is safe to make.
*
* TODO: Always use the same type representation as phpdoc if possible in this plugin
*/
class PHPDocToRealTypesPlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
AutomaticFixCapability,
BeforeAnalyzePhaseCapability
{
private const CanUsePHP71Void = 'PhanPluginCanUsePHP71Void';
private const CanUseReturnType = 'PhanPluginCanUseReturnType';
private const CanUseNullableReturnType = 'PhanPluginCanUseNullableReturnType';
private const CanUseParamType = 'PhanPluginCanUseParamType';
private const CanUseNullableParamType = 'PhanPluginCanUseNullableParamType';
/** @var array<string,Method> */
private $deferred_analysis_methods = [];
/**
* @return array<string,Closure(CodeBase,FileCacheEntry,IssueInstance):(?FileEditSet)>
*/
public function getAutomaticFixers(): array
{
require_once __DIR__ . '/PHPDocToRealTypesPlugin/Fixers.php';
$param_closure = Closure::fromCallable([Fixers::class, 'fixParamType']);
$return_closure = Closure::fromCallable([Fixers::class, 'fixReturnType']);
return [
self::CanUsePHP71Void => $return_closure,
self::CanUseReturnType => $return_closure,
self::CanUseNullableReturnType => $return_closure,
self::CanUseNullableParamType => $param_closure,
self::CanUseParamType => $param_closure,
];
}
public function analyzeFunction(CodeBase $code_base, Func $function): void
{
self::analyzeFunctionLike($code_base, $function);
}
/**
* @param CodeBase $code_base @unused-param
*/
public function analyzeMethod(CodeBase $code_base, Method $method): void
{
if ($method->isFromPHPDoc() || $method->isMagic() || $method->isPHPInternal()) {
return;
}
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
return;
}
$this->deferred_analysis_methods[$method->getFQSEN()->__toString()] = $method;
}
public function beforeAnalyzePhase(CodeBase $code_base): void
{
$ignore_overrides = (bool)getenv('PHPDOC_TO_REAL_TYPES_IGNORE_INHERITANCE');
foreach ($this->deferred_analysis_methods as $method) {
if ($method->isOverride() || $method->isOverriddenByAnother()) {
if (!$ignore_overrides) {
continue;
}
}
self::analyzeFunctionLike($code_base, $method);
}
}
private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void
{
if (Phan::isExcludedAnalysisFile($method->getContext()->getFile())) {
// This has no side effects, so we can skip files that don't need to be analyzed
return;
}
if ($method->getRealReturnType()->isEmpty()) {
self::analyzeReturnTypeOfFunctionLike($code_base, $method);
}
$phpdoc_param_list = $method->getParameterList();
foreach ($method->getRealParameterList() as $i => $parameter) {
if (!$parameter->getNonVariadicUnionType()->isEmpty()) {
continue;
}
$phpdoc_param = $phpdoc_param_list[$i];
if (!$phpdoc_param) {
continue;
}
$union_type = $phpdoc_param->getNonVariadicUnionType()->asNormalizedTypes();
if ($union_type->typeCount() !== 1) {
continue;
}
$type = $union_type->getTypeSet()[0];
if (!$type->canUseInRealSignature()) {
continue;
}
self::emitIssue(
$code_base,
$method->getContext(),
$type->isNullable() ? self::CanUseNullableParamType : self::CanUseParamType,
'Can use {TYPE} as the type of parameter ${PARAMETER} of {METHOD}',
[$type->asSignatureType(), $parameter->getName(), $method->getName()]
);
}
}
private static function analyzeReturnTypeOfFunctionLike(CodeBase $code_base, FunctionInterface $method): void
{
$union_type = $method->getUnionType();
if ($union_type->isVoidType()) {
self::emitIssue(
$code_base,
$method->getContext(),
self::CanUsePHP71Void,
'Can use php 7.1\'s {TYPE} as a return type of {METHOD}',
['void', $method->getName()]
);
return;
}
$union_type = $union_type->asNormalizedTypes();
if ($union_type->typeCount() !== 1) {
return;
}
$type = $union_type->getTypeSet()[0];
if (!$type->canUseInRealSignature()) {
return;
}
self::emitIssue(
$code_base,
$method->getContext(),
$type->isNullable() ? self::CanUseNullableReturnType : self::CanUseReturnType,
'Can use {TYPE} as a return type of {METHOD}',
[$type->asSignatureType(), $method->getName()]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHPDocToRealTypesPlugin();

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace PHPDocToRealTypesPlugin;
use Microsoft\PhpParser;
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;
use Phan\Library\FileCacheEntry;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEdit;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
/**
* This plugin implements --automatic-fix for PHPDocToRealTypesPlugin
*/
class Fixers
{
/**
* Add a missing return type to the real signature
* @param CodeBase $code_base @unused-param
*/
public static function fixReturnType(
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$params = $instance->getTemplateParameters();
$return_type = $params[0];
$name = $params[1];
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $name);
if (!$declaration) {
return null;
}
return self::computeEditsForReturnTypeDeclaration($declaration, (string)$return_type);
}
/**
* Add a missing param type to the real signature
* @unused-param $code_base
*/
public static function fixParamType(
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$params = $instance->getTemplateParameters();
$param_type = $params[0];
$param_name = $params[1];
$method_name = $params[2];
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name);
if (!$declaration) {
return null;
}
return self::computeEditsForParamTypeDeclaration($contents, $declaration, (string)$param_name, (string)$param_type);
}
private static function computeEditsForReturnTypeDeclaration(FunctionLike $declaration, string $return_type): ?FileEditSet
{
if ($return_type === '') {
return null;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$close_bracket = $declaration->anonymousFunctionUseClause->closeParen ?? $declaration->closeParen;
if (!$close_bracket instanceof Token) {
return null;
}
// get the byte where the `)` of the argument list ends
$last_byte_index = $close_bracket->getEndPosition();
$file_edit = new FileEdit($last_byte_index, $last_byte_index, " : $return_type");
return new FileEditSet([$file_edit]);
}
private static function computeEditsForParamTypeDeclaration(FileCacheEntry $contents, FunctionLike $declaration, string $param_name, string $param_type): ?FileEditSet
{
if ($param_type === '') {
return null;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$parameter_node_list = $declaration->parameters->children ?? [];
foreach ($parameter_node_list as $param) {
if (!$param instanceof PhpParser\Node\Parameter) {
continue;
}
$declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($param->variableName);
if ($declaration_name !== $param_name) {
continue;
}
$token = $param->byRefToken ?? $param->dotDotDotToken ?? $param->variableName;
$token_start_index = $token->start;
$file_edit = new FileEdit($token_start_index, $token_start_index, "$param_type ");
return new FileEditSet([$file_edit]);
}
return null;
}
private static function findFunctionLikeDeclaration(
FileCacheEntry $contents,
int $line,
string $name
): ?FunctionLike {
$candidates = [];
foreach ($contents->getNodesAtLine($line) as $node) {
if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) {
$name_node = $node->name;
if (!$name_node) {
continue;
}
$declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node);
if ($declaration_name === $name) {
$candidates[] = $node;
}
} elseif ($node instanceof AnonymousFunctionCreationExpression) {
if ($name === '{closure}') {
$candidates[] = $node;
}
}
}
if (\count($candidates) === 1) {
return $candidates[0];
}
return null;
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Language\Context;
use Phan\Language\Element\Comment\Assertion;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
/**
* Mark PHPUnit helper assertions as having side effects.
*
* - assertTrue
* - assertNull
* - assertNotNull
* - assertFalse
* - assertSame($expected, $actual)
* - assertInstanceof
*
* NOTE: This will probably be rewritten
*/
class PHPUnitAssertionPlugin extends PluginV3 implements AnalyzeFunctionCallCapability
{
/**
* @override
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$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->getClassByFQSEN($assert_class_fqsen)->getMethodMap($code_base) as $method) {
$closure = $this->createClosureForMethod($code_base, $method, $method->getName());
if (!$closure) {
continue;
}
$result[(string)$method->getFQSEN()] = $closure;
}
return $result;
}
/**
* @return ?Closure(CodeBase, Context, FunctionInterface, array, ?Node):void
* @suppress PhanAccessClassConstantInternal, PhanAccessMethodInternal
*/
private function createClosureForMethod(CodeBase $code_base, Method $method, string $name): ?Closure
{
// TODO: Add a helper method which will convert a doc comment and a stub php function source code to a closure for a param index (or indices)
switch (\strtolower($name)) {
case 'asserttrue':
case 'assertnotfalse':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_TRUE),
0
);
case 'assertfalse':
case 'assertnottrue':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_FALSE),
0
);
// TODO: Rest of https://github.com/sebastianbergmann/phpunit/issues/3368
case 'assertisstring':
// TODO: Could convert to real types?
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('string'), 'unusedParamName', Assertion::IS_OF_TYPE),
0
);
case 'assertnull':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_OF_TYPE),
0
);
case 'assertnotnull':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_NOT_OF_TYPE),
0
);
case 'assertsame':
// Sets the type of $actual to $expected
//
// This is equivalent to the side effects of the below doc comment.
// Note that the doc comment would make phan emit warnings about invalid classes, etc.
// TODO: Reuse the code for templates here
//
// (at)template T
// (at)param T $expected
// (at)param mixed $actual
// (at)phan-assert T $actual
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
static function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]);
},
Assertion::IS_OF_TYPE,
1
);
case 'assertinternaltype':
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
$string = $args[0];
if ($string instanceof ast\Node) {
$string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $string))->asSingleScalarValueOrNull();
}
if (!is_string($string)) {
return UnionType::empty();
}
$original_type = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[1]));
switch ($string) {
case 'numeric':
return UnionType::fromFullyQualifiedPHPDocString('int|float|string');
case 'integer':
case 'int':
return UnionType::fromFullyQualifiedPHPDocString('int');
case 'double':
case 'float':
case 'real':
return UnionType::fromFullyQualifiedPHPDocString('float');
case 'string':
return UnionType::fromFullyQualifiedPHPDocString('string');
case 'boolean':
case 'bool':
return UnionType::fromFullyQualifiedPHPDocString('bool');
case 'null':
return UnionType::fromFullyQualifiedPHPDocString('null');
case 'array':
$result = $original_type->arrayTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('array');
}
return $result;
case 'object':
$result = $original_type->objectTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('object');
}
return $result;
case 'resource':
return UnionType::fromFullyQualifiedPHPDocString('resource');
case 'scalar':
$result = $original_type->scalarTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('int|string|float|bool');
}
return $result;
case 'callable':
$result = $original_type->callableTypes($code_base);
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('callable');
}
return $result;
}
// Warn about possibly invalid assertion
// NOTE: This is only emitted for variables
$this->emitPluginIssue(
$code_base,
$context,
'PhanPluginPHPUnitAssertionInvalidInternalType',
'Unknown type {STRING_LITERAL} in call to assertInternalType',
[$string]
);
return UnionType::empty();
},
Assertion::IS_OF_TYPE,
1
);
case 'assertinstanceof':
// This is equivalent to the side effects of the below doc comment.
// Note that the doc comment would make phan emit warnings about invalid classes, etc.
// TODO: Reuse the code for class-string<T> here.
//
// (at)template T
// (at)param class-string<T> $expected
// (at)param mixed $actual
// (at)phan-assert T $actual
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
static function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
$string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]))->asSingleScalarValueOrNull();
if (!is_string($string)) {
return UnionType::empty();
}
try {
return FullyQualifiedClassName::fromFullyQualifiedString($string)->asType()->asPHPDocUnionType();
} catch (\Exception $_) {
return UnionType::empty();
}
},
Assertion::IS_OF_TYPE,
1
);
}
return null;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHPUnitAssertionPlugin();

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Config;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\Type;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* Mark all phpunit test cases as used for dead code detection during Phan's self-analysis.
*
* Implements the following capabilities
* (This choice of capability makes this plugin efficiently analyze only classes that are in the analyzed file list)
*
* - public static function getPostAnalyzeNodeVisitorClassName() : string
* Returns the name of a class extending PluginAwarePostAnalysisVisitor, which will be used to analyze nodes in the analysis phase.
* If the PluginAwarePostAnalysisVisitor subclass has an instance property called parent_node_list,
* Phan will automatically set that property to the list of parent nodes (The nodes deepest in the AST are at the end of the list)
* (implement \Phan\PluginV3\PostAnalyzeNodeCapability)
*/
class PHPUnitNotDeadCodePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return PHPUnitNotDeadPluginVisitor::class;
}
}
/**
* This visitor visits classes (After all class/method definitions are parsed and analyzed)
* and, for subclasses of PHPUnit test cases,
* marks the phpunit test cases, (at)dataProviders, and special PHPUnit subclass properties as being referenced (i.e. not dead code)
*/
class PHPUnitNotDeadPluginVisitor extends PluginAwarePostAnalysisVisitor
{
/** @var FullyQualifiedClassName the class FQSEN for the base class of all PHPUnit tests */
private static $phpunit_test_case_fqsen;
/** @var Type the type of the base class of all PHPUnit tests */
private static $phpunit_test_case_type;
/** @var bool did this plugin already warn that TestCase was missing? */
private static $did_warn_missing_class = false;
/**
* 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 $node): void
{
if (!Config::get_track_references()) {
return;
}
$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;
}
return;
}
// This assumes PreOrderAnalysisVisitor->visitClass is called first.
$context = $this->context;
$class = $context->getClassInScope($code_base);
if (!$class->getFQSEN()->asType()->asExpandedTypes($code_base)->hasType(self::$phpunit_test_case_type)) {
// This isn't a phpunit test case.
return;
}
// Mark subclasses of TestCase as referenced
$class->addReference($context);
// Mark all test cases as referenced
foreach ($class->getMethodMap($code_base) as $method) {
if (static::isTestCase($method)) {
// TODO: Parse @dataProvider methodName, check for method existence,
// then mark method for dataProvider as referenced.
$method->addReference($context);
$this->markDataProvidersAsReferenced($class, $method);
}
}
// https://phpunit.de/manual/current/en/fixtures.html (PHPUnit framework checks for this override)
if ($class->hasPropertyWithName($code_base, 'backupStaticAttributesBlacklist')) {
$property = $class->getPropertyByName($code_base, 'backupStaticAttributesBlacklist');
$property->addReference($context);
$property->setHasReadReference();
}
}
/**
* This regex contains a single pattern, which matches a valid PHP identifier.
* (e.g. for variable names, magic property names, etc.
* This does not allow backslashes.
*/
private const WORD_REGEX = '([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)';
/**
* Marks all data provider methods as being referenced
*
* @param Method $method the Method representing a unit test in a test case subclass
*/
private function markDataProvidersAsReferenced(Clazz $class, Method $method): void
{
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, true)) {
$class->getMethodByName($this->code_base, $data_provider_name)->addReference($this->context);
}
}
}
/**
* @return bool true if $method is a PHPUnit test case
*/
protected static function isTestCase(Method $method): bool
{
if (!$method->isPublic()) {
return false;
}
if (preg_match('@^test@i', $method->getName())) {
return true;
}
if (preg_match('/@test\b/', $method->getNode()->children['docComment'] ?? '')) {
return true;
}
return false;
}
/**
* Static initializer for this plugin - Gets called below before any methods can be used
* @suppress PhanThrowTypeAbsentForCall this FQSEN is valid
*/
public static function init(): void
{
$fqsen = FullyQualifiedClassName::make('\\PHPUnit\Framework', 'TestCase');
self::$phpunit_test_case_fqsen = $fqsen;
self::$phpunit_test_case_type = $fqsen->asType();
}
}
PHPUnitNotDeadPluginVisitor::init();
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHPUnitNotDeadCodePlugin();

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace Phan\Plugin\PhanSelfCheckPlugin;
// Don't pollute the global namespace
use ast\Node;
use Closure;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Language\Type\ArrayShapeType;
use Phan\Library\ConversionSpec;
use Phan\Library\StringUtil;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
use function count;
use function is_string;
/**
* This plugin checks for invalid calls to emitIssue, emitPluginIssue, Issue::maybeEmit(), etc.
* This is useful for developing Phan plugins.
*
* This uses ConversionSpec as a heuristic to determine the number of arguments to format strings.
* This currently does not try to check types of arguments.
*
* NOTE: This does not check Issue::fromType($typename)(...args)
*/
class PhanSelfCheckPlugin extends PluginV3 implements AnalyzeFunctionCallCapability
{
private const TooManyArgumentsForIssue = 'PhanPluginTooManyArgumentsForIssue';
private const TooFewArgumentsForIssue = 'PhanPluginTooFewArgumentsForIssue';
private const UnknownIssueType = 'PhanPluginUnknownIssueType';
/**
* @param CodeBase $code_base @phan-unused-param
* @return Closure[]
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @return Closure(CodeBase, Context, FunctionInterface, list<mixed>):void
*/
$make_array_issue_callback = static function (int $fmt_index, int $arg_index): Closure {
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
return static function (
CodeBase $code_base,
Context $context,
FunctionInterface $unused_function,
array $args
) use (
$fmt_index,
$arg_index
): void {
if (\count($args) <= $fmt_index) {
return;
}
// TODO: Check for AST_UNPACK
$issue_message_template = $args[$fmt_index];
if ($issue_message_template instanceof Node) {
$issue_message_template = (new ContextNode($code_base, $context, $issue_message_template))->getEquivalentPHPScalarValue();
}
if (!is_string($issue_message_template)) {
return;
}
$issue_message_arg_count = self::computeArraySize($code_base, $context, $args[$arg_index] ?? null);
if ($issue_message_arg_count === null) {
return;
}
self::checkIssueTemplateUsage($code_base, $context, $issue_message_template, $issue_message_arg_count);
};
};
/**
* @param int $type_index the index of a parameter expecting an issue type (e.g. PhanParamTooMany)
* @param int $arg_index the index of an array parameter expecting sequential arguments. This is >= $type_index.
* @return Closure(CodeBase, Context, FunctionInterface, list<mixed>):void
*/
$make_type_and_parameters_callback = static function (int $type_index, int $arg_index): Closure {
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
return static function (
CodeBase $code_base,
Context $context,
FunctionInterface $function,
array $args
) use (
$type_index,
$arg_index
): void {
if (\count($args) <= $type_index) {
return;
}
// TODO: Check for AST_UNPACK
$issue_type = $args[$type_index];
if ($issue_type instanceof Node) {
$issue_type = (new ContextNode($code_base, $context, $issue_type))->getEquivalentPHPScalarValue();
}
if (!is_string($issue_type)) {
return;
}
$issue = self::getIssueOrWarn($code_base, $context, $function, $issue_type);
if (!$issue) {
return;
}
$issue_message_arg_count = self::computeArraySize($code_base, $context, $args[$arg_index] ?? null);
if ($issue_message_arg_count === null) {
return;
}
self::checkIssueTemplateUsage($code_base, $context, $issue->getTemplate(), $issue_message_arg_count);
};
};
/**
* @param int $type_index the index of a parameter expecting an issue type (e.g. PhanParamTooMany)
* @param int $arg_index the index of an array parameter expecting variable arguments. This is >= $type_index.
* @return Closure(CodeBase, Context, FunctionInterface, list<mixed>):void
*/
$make_type_and_varargs_callback = static function (int $type_index, int $arg_index): Closure {
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
return static function (
CodeBase $code_base,
Context $context,
FunctionInterface $function,
array $args
) use (
$type_index,
$arg_index
): void {
if (\count($args) <= $type_index) {
return;
}
// TODO: Check for AST_UNPACK
$issue_type = $args[$type_index];
if ($issue_type instanceof Node) {
$issue_type = (new ContextNode($code_base, $context, $issue_type))->getEquivalentPHPScalarValue();
}
if (!is_string($issue_type)) {
return;
}
$issue = self::getIssueOrWarn($code_base, $context, $function, $issue_type);
if (!$issue) {
return;
}
if ((\end($args)->kind ?? null) === \ast\AST_UNPACK) {
// give up
return;
}
// number of args passed to varargs. >= 0 if valid.
$issue_message_arg_count = count($args) - $arg_index;
if ($issue_message_arg_count < 0) {
// should already emit PhanParamTooFew
return;
}
self::checkIssueTemplateUsage($code_base, $context, $issue->getTemplate(), $issue_message_arg_count);
};
};
/**
* Analyzes a call to plugin->emitIssue($code_base, $context, $issue_type, $issue_message_fmt, $args)
*/
$short_emit_issue_callback = $make_type_and_varargs_callback(0, 2);
$results = [
'\Phan\AST\ContextNode::emitIssue' => $short_emit_issue_callback,
'\Phan\Issue::emit' => $make_type_and_varargs_callback(0, 3),
'\Phan\Issue::emitWithParameters' => $make_type_and_parameters_callback(0, 3),
'\Phan\Issue::maybeEmit' => $make_type_and_varargs_callback(2, 4),
'\Phan\Issue::maybeEmitWithParameters' => $make_type_and_parameters_callback(2, 4),
'\Phan\Analysis\BinaryOperatorFlagVisitor::emitIssue' => $short_emit_issue_callback,
'\Phan\Language\Element\Comment\Builder::emitIssue' => $make_type_and_parameters_callback(0, 2),
];
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$emit_plugin_issue_fqsen = FullyQualifiedMethodName::fromFullyQualifiedString('\Phan\PluginV3\IssueEmitter::emitPluginIssue');
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$analysis_visitor_fqsen = FullyQualifiedMethodName::fromFullyQualifiedString('\Phan\AST\AnalysisVisitor::emitIssue');
$emit_plugin_issue_callback = $make_array_issue_callback(3, 4);
foreach ($code_base->getMethodSet() as $method) {
$real_fqsen = $method->getRealDefiningFQSEN();
if ($real_fqsen === $emit_plugin_issue_fqsen) {
$results[(string)$method->getFQSEN()] = $emit_plugin_issue_callback;
} elseif ($real_fqsen === $analysis_visitor_fqsen) {
$results[(string)$method->getFQSEN()] = $short_emit_issue_callback;
}
}
return $results;
}
private static function getIssueOrWarn(CodeBase $code_base, Context $context, FunctionInterface $function, string $issue_type): ?Issue
{
// Calling Issue::fromType() would print a backtrace to stderr
$issue = Issue::issueMap()[$issue_type] ?? null;
if (!$issue) {
self::emitIssue(
$code_base,
$context,
self::UnknownIssueType,
'Unknown issue type {STRING_LITERAL} in a call to {METHOD}(). (may be a false positive - check if the version of Phan running PhanSelfCheckPlugin is the same version that the analyzed codebase is using)',
[$issue_type, $function->getFQSEN()]
);
return null;
}
return $issue;
}
private static function checkIssueTemplateUsage(CodeBase $code_base, Context $context, string $issue_message_template, int $issue_message_arg_count): void
{
$issue_message_format_string = Issue::templateToFormatString($issue_message_template);
$expected_arg_count = ConversionSpec::computeExpectedArgumentCount($issue_message_format_string);
if ($expected_arg_count === $issue_message_arg_count) {
return;
}
if ($issue_message_arg_count > $expected_arg_count) {
self::emitIssue(
$code_base,
$context,
self::TooManyArgumentsForIssue,
'Too many arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}',
[StringUtil::jsonEncode($issue_message_template), $expected_arg_count, $issue_message_arg_count],
Issue::SEVERITY_NORMAL
);
} else {
self::emitIssue(
$code_base,
$context,
self::TooFewArgumentsForIssue,
'Too few arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}',
[StringUtil::jsonEncode($issue_message_template), $expected_arg_count, $issue_message_arg_count],
Issue::SEVERITY_CRITICAL
);
}
}
/**
* @param Node|mixed $arg
*/
private static function computeArraySize(CodeBase $code_base, Context $context, $arg): ?int
{
if ($arg === null) {
return 0;
}
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $arg);
if ($union_type->typeCount() !== 1) {
return null;
}
$types = $union_type->getTypeSet();
$array_shape_type = \reset($types);
if (!$array_shape_type instanceof ArrayShapeType) {
return null;
}
$field_types = $array_shape_type->getFieldTypes();
foreach ($field_types as $field_type) {
if ($field_type->isPossiblyUndefined()) {
return null;
}
}
return count($field_types);
}
}
return new PhanSelfCheckPlugin();

View File

@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ContextNode;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Element\AddressableElement;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\ElementContext;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\FinalizeProcessCapability;
/**
* This file checks if a method can be made static without causing any errors.
*
* It hooks into these events:
*
* - analyzeMethod
* Once all classes are parsed, this method will be called
* on every method in the code base
*
* - analyzeFunction
* Once all classes and functions are parsed, this method will be called
* on every function in the code base
*
* - finalizeProcess
* Once the analysis phase is complete, this method will be called
*
* 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
*/
final class PossiblyStaticMethodPlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
FinalizeProcessCapability
{
/**
* @var array<string,FunctionInterface> a list of functions and methods where checks were postponed
*/
private $methods_for_postponed_analysis = [];
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param FunctionInterface $method
* A function or method being analyzed
*/
private static function analyzePostponedMethod(
CodeBase $code_base,
FunctionInterface $method
): void {
if ($method instanceof Method) {
if ($method->isOverride()) {
// This method can't be static unless its parent is also static.
return;
}
if ($method->isOverriddenByAnother()) {
// Changing this method causes a fatal error.
return;
}
}
$stmts_list = self::getStatementListToAnalyze($method);
if ($stmts_list === null) {
// check for abstract methods, etc.
return;
}
if (self::nodeCanBeStatic($code_base, $method, $stmts_list)) {
if ($method instanceof Method) {
$visibility_upper = ucfirst($method->getVisibilityName());
self::emitIssue(
$code_base,
$method->getContext(),
"PhanPluginPossiblyStatic{$visibility_upper}Method",
"$visibility_upper method {METHOD} can be static",
[$method->getRepresentationForIssue()]
);
} else {
self::emitIssue(
$code_base,
$method->getContext(),
"PhanPluginPossiblyStaticClosure",
"{FUNCTION} can be static",
[$method->getRepresentationForIssue()]
);
}
}
}
/**
* @param FunctionInterface $method
* @return ?Node - returns null if there's no statement list to analyze
*/
private static function getStatementListToAnalyze(FunctionInterface $method): ?Node
{
$node = $method->getNode();
if (!$node) {
return null;
}
return $node->children['stmts'];
}
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param Node|int|string|float|null $node
* @return bool - returns true if the node allows its method to be static
*/
private static function nodeCanBeStatic(CodeBase $code_base, FunctionInterface $method, $node): bool
{
if (!($node instanceof Node)) {
if (is_array($node)) {
foreach ($node as $child_node) {
if (!self::nodeCanBeStatic($code_base, $method, $child_node)) {
return false;
}
}
}
return true;
}
switch ($node->kind) {
case ast\AST_VAR:
if ($node->children['name'] === 'this') {
return false;
}
// Handle edge cases such as `${$this->varName}`
break;
case ast\AST_CLASS:
case ast\AST_FUNC_DECL:
return true;
case ast\AST_STATIC_CALL:
if (self::isSelfOrParentCallUsingObject($code_base, $method, $node)) {
return false;
}
// Check code such as `static::someMethod($this->prop)`
break;
case ast\AST_CLOSURE:
case ast\AST_ARROW_FUNC:
if ($node->flags & \ast\flags\MODIFIER_STATIC) {
return true;
}
break;
}
foreach ($node->children as $child_node) {
if (!self::nodeCanBeStatic($code_base, $method, $child_node)) {
return false;
}
}
return true;
}
/**
* @param CodeBase $code_base
* The code base in which the calling instance method exists
*
* @param Node $node a node of kind ast\AST_STATIC_CALL
* (e.g. SELF::someMethod(), parent::someMethod(), SomeClass::staticMethod())
*
* @return bool true if the AST_STATIC_CALL node is really calling an instance method
*/
private static function isSelfOrParentCallUsingObject(CodeBase $code_base, FunctionInterface $method, Node $node): bool
{
$class_node = $node->children['class'];
if (!($class_node instanceof Node && $class_node->kind === ast\AST_NAME)) {
return false;
}
$class_name = $class_node->children['name'];
if (!is_string($class_name)) {
return false;
}
if (!in_array(strtolower($class_name), ['self', 'parent'], true)) {
return false;
}
$method_name = $node->children['method'];
if (!is_string($method_name)) {
// This is uninferable
return true;
}
if (!$method instanceof AddressableElement) {
// should be impossible
return true;
}
try {
$method = (new ContextNode($code_base, new ElementContext($method), $node))->getMethod($method_name, true, false);
} catch (Exception $_) {
// This might be an instance method if we don't know what it is
return true;
}
return !$method->isStatic();
}
/**
* @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 $code_base,
Method $method
): void {
// 1. Perform any checks that can be done immediately to rule out being able
// to convert this to a static method
if ($method->isStatic()) {
// This is what we want.
return;
}
if ($method->isMagic()) {
// Magic methods can't be static.
return;
}
if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) {
// Only warn once for the original definition of this method.
// Don't warn about subclasses inheriting this method.
return;
}
$method_filter = Config::getValue('plugin_config')['possibly_static_method_ignore_regex'] ?? null;
if (is_string($method_filter)) {
$fqsen_string = ltrim((string)$method->getFQSEN(), '\\');
if (preg_match($method_filter, $fqsen_string) > 0) {
return;
}
}
if (!$method->hasNode()) {
// There's no body to check - This is abstract or can't be checked
return;
}
$fqsen = $method->getFQSEN();
// 2. Defer remaining checks until we have all the necessary information
// (is this method overridden/an override, is parent::foo() referring to a static or an instance method, etc.)
$this->methods_for_postponed_analysis[(string) $fqsen] = $method;
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
if (!$function->isClosure()) {
return;
}
if ($function->isStatic()) {
return;
}
if (!$function->hasNode()) {
// There's no body to check - This is abstract or can't be checked
return;
}
// NOTE: The possibly_static_method_ignore_regex isn't used because there's no way to apply it to closures
$fqsen = $function->getFQSEN();
// 2. Defer remaining checks until we have all the necessary information
// (is this method overridden/an override, is parent::foo() referring to a static or an instance method, etc.)
$this->methods_for_postponed_analysis[(string) $fqsen] = $function;
}
/**
* @param CodeBase $code_base
* The code base being analyzed
*
* @override
*/
public function finalizeProcess(CodeBase $code_base): void
{
foreach ($this->methods_for_postponed_analysis as $method) {
self::analyzePostponedMethod($code_base, $method);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PossiblyStaticMethodPlugin();

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Library\FileCacheEntry;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AutomaticFixCapability;
use PreferNamespaceUsePlugin\Fixers;
/**
* This plugin checks for redundant doc comments on functions, closures, and methods.
*
* This treats a doc comment as redundant if
*
* 1. It is exclusively annotations (0 or more), e.g. (at)return void
* 2. Every annotation repeats the real information in the signature.
*
* It does not check if the change is safe to make.
*/
class PreferNamespaceUsePlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
AutomaticFixCapability
{
private const PreferNamespaceUseParamType = 'PhanPluginPreferNamespaceUseParamType';
private const PreferNamespaceUseReturnType = 'PhanPluginPreferNamespaceUseReturnType';
public function analyzeFunction(CodeBase $code_base, Func $function): void
{
self::analyzeFunctionLike($code_base, $function);
}
public function analyzeMethod(CodeBase $code_base, Method $method): void
{
if ($method->isMagic() || $method->isPHPInternal()) {
return;
}
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
return;
}
self::analyzeFunctionLike($code_base, $method);
}
private static function analyzeFunctionLike(CodeBase $code_base, FunctionInterface $method): void
{
$node = $method->getNode();
if (!$node) {
return;
}
$return_type = $node->children['returnType'];
if ($return_type instanceof Node) {
self::analyzeFunctionLikeReturn($code_base, $method, $return_type);
}
foreach ($node->children['params']->children ?? [] as $param_node) {
if (!($param_node instanceof Node)) {
// impossible?
continue;
}
self::analyzeFunctionLikeParam($code_base, $method, $param_node);
}
}
private static function analyzeFunctionLikeReturn(CodeBase $code_base, FunctionInterface $method, Node $return_type): void
{
$is_nullable = false;
if ($return_type->kind === ast\AST_NULLABLE_TYPE) {
$return_type = $return_type->children['type'];
if (!($return_type instanceof Node)) {
// should not happen
return;
}
$is_nullable = true;
}
$shorter_return_type = self::determineShorterType($method->getContext(), $return_type);
if (is_string($shorter_return_type)) {
$prefix = $is_nullable ? '?' : '';
self::emitIssue(
$code_base,
$method->getContext(),
self::PreferNamespaceUseReturnType,
'Could write return type of {FUNCTION} as {TYPE} instead of {TYPE}',
[$method->getName(), $prefix . $shorter_return_type, $prefix . '\\' . $return_type->children['name']]
);
}
}
private static function analyzeFunctionLikeParam(CodeBase $code_base, FunctionInterface $method, Node $param_node): void
{
$param_type = $param_node->children['type'];
if (!$param_type instanceof Node) {
return;
}
$is_nullable = false;
if ($param_type->kind === ast\AST_NULLABLE_TYPE) {
$param_type = $param_type->children['type'];
if (!($param_type instanceof Node)) {
// should not happen
return;
}
$is_nullable = true;
}
$shorter_param_type = self::determineShorterType($method->getContext(), $param_type);
if (is_string($shorter_param_type)) {
$param_name = $param_node->children['name'];
if (!is_string($param_name)) {
// should be impossible
return;
}
$prefix = $is_nullable ? '?' : '';
self::emitIssue(
$code_base,
$method->getContext(),
self::PreferNamespaceUseParamType,
'Could write param type of ${PARAMETER} of {FUNCTION} as {TYPE} instead of {TYPE}',
[$param_name, $method->getName(), $prefix . $shorter_param_type, $prefix . '\\' . $param_type->children['name']]
);
}
}
/**
* Given a node with a parameter or return type, return a string with a shorter represented of the type (if possible), or return null if this is not possible.
*
* This does not try all possibilities, and only affects fully qualified types.
*/
private static function determineShorterType(Context $context, Node $type_node): ?string
{
if ($type_node->kind !== ast\AST_NAME) {
return null;
}
if ($type_node->flags !== ast\flags\NAME_FQ) {
return null;
}
$name = $type_node->children['name'];
if (!is_string($name)) {
return null;
}
$parts = explode('\\', $name);
$name_end = (string)array_pop($parts);
$namespace = implode('\\', $parts);
if ($context->hasNamespaceMapFor(ast\flags\USE_NORMAL, $name_end)) {
$fqsen = $context->getNamespaceMapFor(ast\flags\USE_NORMAL, $name_end);
if ($fqsen->getName() === $name_end && strcasecmp(ltrim($fqsen->getNamespace(), '\\'), $namespace) === 0) {
// found `use Bar\Something` when looking for `\Bar\Something`, so suggest `Something`
return $name_end;
}
// TODO: Could look for `use \Foo\Bar as FB;`
} elseif (strcasecmp($namespace, ltrim($context->getNamespace(), "\\")) === 0) {
// Foo\Bar\Baz in Foo\Bar is Baz unless there is another namespace use shadowing it.
return $name_end;
}
return null;
}
/**
* @return array<string,Closure(CodeBase,FileCacheEntry,IssueInstance):(?FileEditSet)>
*/
public function getAutomaticFixers(): array
{
require_once __DIR__ . '/PreferNamespaceUsePlugin/Fixers.php';
return [
self::PreferNamespaceUseReturnType => Closure::fromCallable([Fixers::class, 'fixReturnType']),
self::PreferNamespaceUseParamType => Closure::fromCallable([Fixers::class, 'fixParamType']),
//self::RedundantClosureComment => $function_like_fixer,
];
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PreferNamespaceUsePlugin();

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace PreferNamespaceUsePlugin;
use Microsoft\PhpParser;
use Microsoft\PhpParser\FunctionLike;
use Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Statement\FunctionDeclaration;
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;
/**
* This plugin implements --automatic-fix for PreferNamespaceUsePlugin
*/
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 $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$params = $instance->getTemplateParameters();
$shorter_return_type = \ltrim((string)$params[1], '?');
$method_name = $params[0];
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name);
if (!$declaration) {
return null;
}
return self::computeEditsForReturnTypeDeclaration($declaration, $shorter_return_type);
}
/**
* Generate an edit to replace a fully qualified param type with a shorter equivalent representation.
* @unused-param $code_base
*/
public static function fixParamType(
CodeBase $code_base,
FileCacheEntry $contents,
IssueInstance $instance
): ?FileEditSet {
$params = $instance->getTemplateParameters();
$shorter_return_type = \ltrim((string)$params[2], '?');
$method_name = (string)$params[1];
$param_name = (string)$params[0];
$declaration = self::findFunctionLikeDeclaration($contents, $instance->getLine(), $method_name);
if (!$declaration) {
return null;
}
return self::computeEditsForParamTypeDeclaration($contents, $declaration, $param_name, $shorter_return_type);
}
/**
* @suppress PhanThrowTypeAbsentForCall
*/
private static function computeEditsForReturnTypeDeclaration(
FunctionLike $declaration,
string $shorter_return_type
): ?FileEditSet {
// @phan-suppress-next-line PhanUndeclaredProperty
$return_type_node = $declaration->returnType;
if (!$return_type_node instanceof PhpParser\Node) {
return null;
}
// 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->getStartPosition(),
$return_type_node->getEndPosition(),
$shorter_return_type
);
return new FileEditSet([$file_edit]);
}
private static function computeEditsForParamTypeDeclaration(
FileCacheEntry $contents,
FunctionLike $declaration,
string $param_name,
string $shorter_param_type
): ?FileEditSet {
// @phan-suppress-next-line PhanUndeclaredProperty
$return_type_node = $declaration->returnType;
if (!$return_type_node) {
return null;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$parameter_node_list = $declaration->parameters->children ?? [];
foreach ($parameter_node_list as $param) {
if (!$param instanceof PhpParser\Node\Parameter) {
continue;
}
$declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($param->variableName);
if ($declaration_name !== $param_name) {
continue;
}
$token = $param->typeDeclarationList;
if (!$token) {
return null;
}
// @phan-suppress-next-line PhanThrowTypeAbsentForCall php-parser is not expected to throw here
$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]);
}
return null;
}
// TODO: Move this into a reusable function
private static function findFunctionLikeDeclaration(
FileCacheEntry $contents,
int $line,
string $name
): ?FunctionLike {
$candidates = [];
foreach ($contents->getNodesAtLine($line) as $node) {
if ($node instanceof FunctionDeclaration || $node instanceof MethodDeclaration) {
$name_node = $node->name;
if (!$name_node) {
continue;
}
$declaration_name = (new NodeUtils($contents->getContents()))->tokenToString($name_node);
if ($declaration_name === $name) {
$candidates[] = $node;
}
} elseif ($node instanceof AnonymousFunctionCreationExpression) {
if ($name === '{closure}') {
$candidates[] = $node;
}
}
}
if (\count($candidates) === 1) {
return $candidates[0];
}
return null;
}
}

View File

@@ -0,0 +1,389 @@
<?php
declare(strict_types=1);
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;
use Phan\Language\Type\LiteralStringType;
use Phan\Library\RegexKeyExtractor;
use Phan\Library\StringUtil;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
/**
* This plugin checks for invalid regexes in calls to preg_match. (And all of the other internal PCRE functions).
*
* This plugin performs this check by attempting to match the empty string,
* then checking if PHP emitted a warning (Instead of failing to match)
* (PHP doesn't have preg_validate())
*
* - getAnalyzeFunctionCallClosures
* This method returns a map from function/method FQSEN to closures that are called on invocations of those closures.
*/
class PregRegexCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapability
{
// Skip over analyzing regex keys that couldn't be resolved.
// Don't try to convert values to PHP data (should be closures)
private const RESOLVE_REGEX_KEY_FLAGS = (ContextNode::RESOLVE_DEFAULT | ContextNode::RESOLVE_KEYS_SKIP_UNKNOWN_KEYS) &
~(ContextNode::RESOLVE_KEYS_SKIP_UNKNOWN_KEYS | ContextNode::RESOLVE_ARRAY_VALUES);
private static function analyzePattern(CodeBase $code_base, Context $context, Func $function, string $pattern): void
{
/**
* @suppress PhanParamSuspiciousOrder 100% deliberate use of varying regex and constant $subject for preg_match
* @return ?array<string,mixed>
*/
$err = with_disabled_phan_error_handler(static function () use ($pattern): ?array {
$old_error_reporting = error_reporting();
\error_reporting(0);
\ob_start();
\error_clear_last();
try {
// Annoyingly, preg_match would not warn about the `/e` modifier, removed in php 7.
// Use `preg_replace` instead (The eval body is empty and phan requires 7.0+ to run)
$result = @\preg_replace($pattern, '', '');
if (!\is_string($result)) {
return \error_get_last() ?? [];
}
return null;
} finally {
\ob_end_clean();
\error_reporting($old_error_reporting);
}
});
if ($err !== null) {
// TODO: scan for 'at offset %d$' and print the corresponding section of the regex. Note: Have to remove delimiters and unescape characters within the delimiters.
self::emitIssue(
$code_base,
$context,
'PhanPluginInvalidPregRegex',
'Call to {FUNCTION} was passed an invalid regex {STRING_LITERAL}: {DETAILS}',
[(string)$function->getFQSEN(), StringUtil::encodeValue($pattern), \preg_replace('@^preg_replace\(\): @', '', $err['message'] ?? 'unknown error')]
);
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;
}
}
}
/**
* @param CodeBase $code_base
* @param Context $context
* @param Node|string|int|float $pattern
* @return array<string,string>
*/
private static function extractStringsFromStringOrArray(
CodeBase $code_base,
Context $context,
$pattern
): array {
if (\is_string($pattern)) {
return [$pattern => $pattern];
}
$pattern_union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $pattern);
$result = [];
foreach ($pattern_union_type->getTypeSet() as $type) {
if ($type instanceof LiteralStringType) {
$value = $type->getValue();
$result[$value] = $value;
} elseif ($type instanceof IterableType) {
$iterable_type = $type->iterableValueUnionType($code_base);
foreach ($iterable_type ? $iterable_type->getTypeSet() : [] as $element_type) {
if ($element_type instanceof LiteralStringType) {
$value = $element_type->getValue();
$result[$value] = $value;
}
}
}
}
return $result;
}
/**
* @param non-empty-list<string> $patterns 1 or more regex patterns
* @return array<string|int,true> the set of keys in the pattern
* @throws InvalidArgumentException if any regex could not be parsed by the heuristics
*/
private static function computePatternKeys(array $patterns): array
{
$result = [];
foreach ($patterns as $regex) {
$result += RegexKeyExtractor::getKeys($regex);
}
return $result;
}
/**
* @return array<int|string,string> references to indices in the pattern
*/
private static function extractTemplateKeys(string $template): array
{
$result = [];
// > replacement may contain references of the form \\n or $n,
// ...
// > n can be from 0 to 99, and \\0 or $0 refers to the text matched by the whole pattern.
preg_match_all('/[$\\\\]([0-9]{1,2}|[^0-9{]|(?<=\$)\{[0-9]{1,2}\})/', $template, $all_matches, PREG_SET_ORDER);
foreach ($all_matches as $match) {
$key = $match[1];
if ($key[0] === '{') {
$key = (string)\substr($key, 1, -1);
}
if ($key[0] >= '0' && $key[0] <= '9') {
// Edge case: Convert '09' to 9
$result[(int)$key] = $match[0];
}
}
return $result;
}
/**
* @param string[] $patterns 1 or more regex patterns
* @param Node|string|int|float $replacement_node
*/
private static function analyzeReplacementTemplate(CodeBase $code_base, Context $context, array $patterns, $replacement_node): void
{
$replacement_templates = self::extractStringsFromStringOrArray($code_base, $context, $replacement_node);
$pattern_keys = null;
// https://secure.php.net/manual/en/function.preg-replace.php#refsect1-function.preg-replace-parameters
// > $replacement may contain references of the form \\n or $n, with the latter form being the preferred one.
try {
foreach ($replacement_templates as $replacement_template) {
$pattern_keys = $pattern_keys ?? self::computePatternKeys($patterns);
$regex_group_keys = self::extractTemplateKeys($replacement_template);
foreach ($regex_group_keys as $key => $reference_string) {
if (!isset($pattern_keys[$key])) {
usort($patterns, 'strcmp');
self::emitIssue(
$code_base,
$context,
'PhanPluginInvalidPregRegexReplacement',
'Call to {FUNCTION} was passed an invalid replacement reference {STRING_LITERAL} to pattern {STRING_LITERAL}',
['\preg_replace', StringUtil::encodeValue($reference_string), StringUtil::encodeValueList(' or ', $patterns)]
);
}
}
}
} catch (InvalidArgumentException $_) {
// TODO: Is this warned about elsewhere?
return;
}
}
/**
* @param CodeBase $code_base @phan-unused-param
* @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,
?Node $node = null
): void {
if (count($args) < 1) {
return;
}
$pattern = $args[0];
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue();
}
if (\is_string($pattern)) {
self::analyzePattern($code_base, $context, $function, $pattern);
}
};
/**
* @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,
?Node $node = null
): void {
if (count($args) < 1) {
return;
}
$pattern_node = $args[0];
foreach (self::extractStringsFromStringOrArray($code_base, $context, $pattern_node) as $pattern) {
self::analyzePattern($code_base, $context, $function, $pattern);
}
};
/**
* @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,
?Node $node = null
): void {
if (count($args) < 1) {
return;
}
$pattern_node = $args[0];
$patterns = self::extractStringsFromStringOrArray($code_base, $context, $pattern_node);
if (count($patterns) === 0) {
return;
}
foreach ($patterns as $pattern) {
self::analyzePattern($code_base, $context, $function, $pattern);
}
if (count($args) < 2) {
return;
}
self::analyzeReplacementTemplate($code_base, $context, $patterns, $args[1]);
};
/**
* @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,
?Node $node = null
): void {
if (count($args) < 1) {
return;
}
// TODO: Resolve global constants and class constants?
$pattern = $args[0];
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPValue(self::RESOLVE_REGEX_KEY_FLAGS);
}
if (\is_array($pattern)) {
foreach ($pattern as $child_pattern => $_) {
self::analyzePattern($code_base, $context, $function, (string)$child_pattern);
}
return;
}
};
// TODO: Check that the callbacks have the right signatures in another PR?
return [
// call
'preg_filter' => $preg_pattern_or_array_callback,
'preg_grep' => $preg_pattern_callback,
'preg_match' => $preg_pattern_callback,
'preg_match_all' => $preg_pattern_callback,
'preg_replace_callback_array' => $preg_replace_callback_array_callback,
'preg_replace_callback' => $preg_pattern_or_array_callback,
'preg_replace' => $preg_pattern_and_replacement_callback,
'preg_split' => $preg_pattern_callback,
];
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PregRegexCheckerPlugin();

View File

@@ -0,0 +1,786 @@
<?php
declare(strict_types=1);
namespace Phan\Plugin\PrintfCheckerPlugin;
// Don't pollute the global namespace
use ast;
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Exception\CodeBaseException;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Type;
use Phan\Language\Type\FalseType;
use Phan\Language\Type\LiteralStringType;
use Phan\Language\Type\StringType;
use Phan\Language\UnionType;
use Phan\Library\ConversionSpec;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
use Phan\PluginV3\ReturnTypeOverrideCapability;
use Throwable;
use function count;
use function implode;
use function is_object;
use function is_string;
use function strcasecmp;
use function var_export;
/**
* This plugin checks for invalid format strings and invalid uses of format strings in printf and sprintf, etc.
* e.g. for printf("literal format %s", $arg)
*
* This uses ConversionSpec as a heuristic to determine the positions used by PHP format strings.
* Some edge cases may have been overlooked.
*
* This validates strings of the form
* - constant strings, such as '%d of %s'
* - TODO: _(str) and gettext(str)
* - TODO: Better resolution of global constants and class constants
*
* This analyzes printf, sprintf, and fprintf.
*
* TODO: Add optional verbose warnings about unanalyzable strings
* TODO: Check if arg can cast to string.
*/
class PrintfCheckerPlugin extends PluginV3 implements AnalyzeFunctionCallCapability, ReturnTypeOverrideCapability
{
// Pylint error codes for emitted issues.
private const ERR_UNTRANSLATED_USE_ECHO = 1300;
private const ERR_UNTRANSLATED_NONE_USED = 1301;
private const ERR_UNTRANSLATED_NONEXISTENT = 1302;
private const ERR_UNTRANSLATED_UNUSED = 1303;
private const ERR_UNTRANSLATED_NOT_PERCENT = 1304;
private const ERR_UNTRANSLATED_INCOMPATIBLE_SPECIFIER = 1305;
private const ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT = 1306; // E.g. passing a string where an int is expected
private const ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT_WEAK = 1307; // E.g. passing an int where a string is expected
private const ERR_UNTRANSLATED_WIDTH_INSTEAD_OF_POSITION = 1308; // e.g. _('%1s'). Change to _('%1$1s' if you really mean that the width is 1, add positions for others ('%2$s', etc.)
private const ERR_UNTRANSLATED_UNKNOWN_FORMAT_STRING = 1310;
private const ERR_TRANSLATED_INCOMPATIBLE = 1309;
private const ERR_TRANSLATED_HAS_MORE_ARGS = 1311;
/**
* People who have translations may subclass this plugin and return a mapping from other locales to those locales translations of $fmt_str.
* @param string $fmt_str @phan-unused-param
* @return string[] mapping locale to the translation (e.g. ['fr_FR' => 'Bonjour'] for $fmt_str == 'Hello')
*/
protected static function gettextForAllLocales(string $fmt_str): array
{
return [];
}
/**
* Convert an expression(a list of tokens) to a primitive.
* People who have custom such as methods or functions to fetch translations
* may subclass this plugin and override this method to add checks for AST_CALL (foo()), AST_METHOD_CALL(MyClass::getTranslation($id), etc.)
*
* @param CodeBase $code_base
* @param Context $context
* @param bool|int|string|float|Node|array|null $ast_node
*/
protected function astNodeToPrimitive(CodeBase $code_base, Context $context, $ast_node): ?PrimitiveValue
{
// Base case: convert primitive tokens such as numbers and strings.
if (!($ast_node instanceof Node)) {
return new PrimitiveValue($ast_node);
}
switch ($ast_node->kind) {
// TODO: Resolve class constant access when those are format strings. Same for PregRegexCheckerPlugin.
case \ast\AST_CALL:
$name_node = $ast_node->children['expr'];
if ($name_node instanceof Node && $name_node->kind === \ast\AST_NAME) {
// TODO: Use Phan's function resolution?
// TODO: ngettext?
$name = $name_node->children['name'];
if (!\is_string($name)) {
break;
}
if ($name === '_' || strcasecmp($name, 'gettext') === 0) {
$child_arg = $ast_node->children['args']->children[0] ?? null;
if ($child_arg === null) {
break;
}
$prim = self::astNodeToPrimitive($code_base, $context, $child_arg);
if ($prim === null) {
break;
}
return new PrimitiveValue($prim->value, true);
}
}
break;
case \ast\AST_BINARY_OP:
if ($ast_node->flags !== ast\flags\BINARY_CONCAT) {
break;
}
$left = $this->astNodeToPrimitive($code_base, $context, $ast_node->children['left']);
if ($left === null) {
break;
}
$right = $this->astNodeToPrimitive($code_base, $context, $ast_node->children['right']);
if ($right === null) {
break;
}
$result = self::concatenateToPrimitive($left, $right);
if ($result) {
return $result;
}
break;
}
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $ast_node);
$result = $union_type->asSingleScalarValueOrNullOrSelf();
if (!is_object($result)) {
return new PrimitiveValue($result);
}
$scalar_union_types = $union_type->asScalarValues();
if (!$scalar_union_types) {
// We don't know how to convert this to a primitive, give up.
// (Subclasses may add their own logic first, then call self::astNodeToPrimitive)
return null;
}
$known_specs = null;
$first_str = null;
foreach ($union_type->getTypeSet() as $type) {
if (!$type instanceof LiteralStringType || $type->isNullable()) {
return null;
}
$str = $type->getValue();
$new_specs = ConversionSpec::extractAll($str);
if (\is_array($known_specs)) {
if ($known_specs != $new_specs) {
// We have different specs, e.g. %s and %d, %1$s and %2$s, etc.
// TODO: Could allow differences in padding or alignment
return null;
}
} else {
$known_specs = $new_specs;
$first_str = $str;
}
}
return new PrimitiveValue($first_str);
}
/**
* Convert a primitive and a sequence of tokens to a primitive formed by
* concatenating strings.
*
* @param PrimitiveValue $left the value on the left.
* @param PrimitiveValue $right the value on the right.
*/
protected static function concatenateToPrimitive(PrimitiveValue $left, PrimitiveValue $right): ?PrimitiveValue
{
// Combining untranslated strings with anything will cause problems.
if ($left->is_translated) {
return null;
}
if ($right->is_translated) {
return null;
}
$str = $left->value . $right->value;
return new PrimitiveValue($str);
}
/**
* @unused-param $code_base
*/
public function getReturnTypeOverrides(CodeBase $code_base): array
{
$string_union_type = StringType::instance(false)->asPHPDocUnionType();
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$sprintf_handler = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
) use ($string_union_type): UnionType {
if (count($args) < 1) {
return FalseType::instance(false)->asRealUnionType();
}
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]);
$format_strings = [];
foreach ($union_type->getTypeSet() as $type) {
if (!$type instanceof LiteralStringType) {
return $string_union_type;
}
$format_strings[] = $type->getValue();
}
if (count($format_strings) === 0) {
return $string_union_type;
}
$result_union_type = UnionType::empty();
foreach ($format_strings as $format_string) {
$min_width = 0;
foreach (ConversionSpec::extractAll($format_string) as $spec_group) {
foreach ($spec_group as $spec) {
$min_width += ($spec->width ?: 0);
}
}
if (!LiteralStringType::canRepresentStringOfLength($min_width)) {
return $string_union_type;
}
$sprintf_args = [];
for ($i = 1; $i < count($args); $i++) {
$arg = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[$i])->asSingleScalarValueOrNullOrSelf();
if (is_object($arg)) {
return $string_union_type;
}
$sprintf_args[] = $arg;
}
try {
$result = \with_disabled_phan_error_handler(
/** @return string|false */
static function () use ($format_string, $sprintf_args) {
// @phan-suppress-next-line PhanPluginPrintfVariableFormatString
return @\vsprintf($format_string, $sprintf_args);
}
);
} catch (Throwable $e) {
// PHP 8 throws ValueError for too few arguments to vsprintf
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeErrorInInternalCall,
$args[0]->lineno ?? $context->getLineNumberStart(),
$function->getName(),
$e->getMessage()
);
// TODO: When PHP 8.0 stable is out, replace this with string?
$result = false;
}
$result_union_type = $result_union_type->withType(Type::fromObject($result));
}
return $result_union_type;
};
return [
'sprintf' => $sprintf_handler,
];
}
/**
* @param CodeBase $code_base @phan-unused-param
* @return \Closure[]
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* Analyzes a printf-like function with a format directive in the first position.
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$printf_callback = function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): void {
// TODO: Resolve global constants and class constants?
// TODO: Check for AST_UNPACK
$pattern = $args[0] ?? null;
if ($pattern === null) {
return;
}
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue();
}
$remaining_args = \array_slice($args, 1);
$this->analyzePrintfPattern($code_base, $context, $function, $pattern, $remaining_args);
};
/**
* Analyzes a printf-like function with a format directive in the first position.
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$fprintf_callback = function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): void {
if (\count($args) < 2) {
return;
}
// TODO: Resolve global constants and class constants?
// TODO: Check for AST_UNPACK
$pattern = $args[1];
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue();
}
$remaining_args = \array_slice($args, 2);
$this->analyzePrintfPattern($code_base, $context, $function, $pattern, $remaining_args);
};
/**
* Analyzes a printf-like function with a format directive in the first position.
* @param list<Node|int|string|float> $args
*/
$vprintf_callback = function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): void {
if (\count($args) < 2) {
return;
}
// TODO: Resolve global constants and class constants?
// TODO: Check for AST_UNPACK
$pattern = $args[0];
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue();
}
$format_args_node = $args[1];
$format_args = (new ContextNode($code_base, $context, $format_args_node))->getEquivalentPHPValue();
$this->analyzePrintfPattern($code_base, $context, $function, $pattern, \is_array($format_args) ? $format_args : null);
};
/**
* Analyzes a printf-like function with a format directive in the first position.
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$vfprintf_callback = function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): void {
if (\count($args) < 3) {
return;
}
// TODO: Resolve global constants and class constants?
// TODO: Check for AST_UNPACK
$pattern = $args[1];
if ($pattern instanceof Node) {
$pattern = (new ContextNode($code_base, $context, $pattern))->getEquivalentPHPScalarValue();
}
$format_args_node = $args[2];
$format_args = (new ContextNode($code_base, $context, $format_args_node))->getEquivalentPHPValue();
$this->analyzePrintfPattern($code_base, $context, $function, $pattern, \is_array($format_args) ? $format_args : null);
};
return [
// call
'printf' => $printf_callback,
'sprintf' => $printf_callback,
'fprintf' => $fprintf_callback,
'vprintf' => $vprintf_callback,
'vsprintf' => $vprintf_callback,
'vfprintf' => $vfprintf_callback,
];
}
protected static function encodeString(string $str): string
{
$result = \json_encode($str, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
if ($result !== false) {
return $result;
}
return var_export($str, true);
}
/**
* Analyzes a printf pattern, emitting issues if necessary
* @param CodeBase $code_base
* @param Context $context
* @param FunctionInterface $function
* @param Node|array|string|float|int|bool|resource|null $pattern_node
* @param ?(Node|string|int|float)[] $arg_nodes arguments following the format string. Null if the arguments could not be determined.
* @suppress PhanPartialTypeMismatchArgument TODO: refactor into smaller functions
*/
protected function analyzePrintfPattern(CodeBase $code_base, Context $context, FunctionInterface $function, $pattern_node, $arg_nodes): void
{
// Given a node, extract the printf directive and whether or not it could be translated
$primitive_for_fmtstr = $this->astNodeToPrimitive($code_base, $context, $pattern_node);
/**
* @param string $issue_type
* A name for the type of issue such as 'PhanPluginMyIssue'
*
* @param string $issue_message_format
* The complete issue message format string to emit such as
* 'class with fqsen {CLASS} is broken in some fashion' (preferred)
* or 'class with fqsen %s is broken in some fashion'
* The list of placeholders for between braces can be found
* in \Phan\Issue::uncolored_format_string_for_template.
*
* @param list<string|float|int> $issue_message_args
* The arguments for this issue format.
* If this array is empty, $issue_message_args is kept in place
*
* @param int $severity
* A value from the set {Issue::SEVERITY_LOW,
* Issue::SEVERITY_NORMAL, Issue::SEVERITY_HIGH}.
*
* @param int $issue_type_id An issue id for pylint
*/
$emit_issue = static function (string $issue_type, string $issue_message_format, array $issue_message_args, int $severity, int $issue_type_id) use ($code_base, $context): void {
self::emitIssue(
$code_base,
$context,
$issue_type,
$issue_message_format,
$issue_message_args,
$severity,
Issue::REMEDIATION_B,
$issue_type_id
);
};
if ($primitive_for_fmtstr === null) {
$emit_issue(
'PhanPluginPrintfVariableFormatString',
'Code {CODE} has a dynamic format string that could not be inferred by Phan',
[ASTReverter::toShortString($pattern_node)],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_UNKNOWN_FORMAT_STRING
);
if (\is_array($arg_nodes) && count($arg_nodes) === 0) {
$replacement_function_name = \in_array($function->getName(), ['vprintf', 'fprintf', 'vfprintf'], true) ? 'fwrite' : 'echo';
$emit_issue(
"PhanPluginPrintfNoArguments",
"No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead",
['(unknown)', $replacement_function_name],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_USE_ECHO
);
return;
}
// TODO: Add a verbose option
return;
}
// Make sure that the untranslated format string is being used correctly.
// If the format string will be translated, also check the translations.
$fmt_str = $primitive_for_fmtstr->value;
$is_translated = $primitive_for_fmtstr->is_translated;
$specs = is_string($fmt_str) ? ConversionSpec::extractAll($fmt_str) : [];
$fmt_str = (string)$fmt_str;
// Check for extra or missing arguments
if (\is_array($arg_nodes) && \count($arg_nodes) === 0) {
if (count($specs) > 0) {
$largest_positional = \max(\array_keys($specs));
$examples = [];
foreach ($specs[$largest_positional] as $example_spec) {
$examples[] = self::encodeString($example_spec->directive);
}
// 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.',
[self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)],
Issue::SEVERITY_CRITICAL,
self::ERR_UNTRANSLATED_NONEXISTENT
);
}
$replacement_function_name = \in_array($function->getName(), ['vprintf', 'fprintf', 'vfprintf'], true) ? 'fwrite' : 'echo';
$emit_issue(
"PhanPluginPrintfNoArguments",
"No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead",
[self::encodeString($fmt_str), $replacement_function_name],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_USE_ECHO
);
return;
}
if (count($specs) === 0) {
$emit_issue(
'PhanPluginPrintfNoSpecifiers',
'None of the formatting arguments passed alongside format string {STRING_LITERAL} are used',
[self::encodeString($fmt_str)],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_NONE_USED
);
return;
}
if (\is_array($arg_nodes)) {
$largest_positional = \max(\array_keys($specs));
if ($largest_positional > \count($arg_nodes)) {
$examples = [];
foreach ($specs[$largest_positional] as $example_spec) {
$examples[] = self::encodeString($example_spec->directive);
}
// 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.',
[self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)],
Issue::SEVERITY_CRITICAL,
self::ERR_UNTRANSLATED_NONEXISTENT
);
} elseif ($largest_positional < count($arg_nodes)) {
$emit_issue(
'PhanPluginPrintfUnusedArgument',
'Format string {STRING_LITERAL} does not use provided argument #{INDEX}',
[self::encodeString($fmt_str), $largest_positional + 1],
Issue::SEVERITY_NORMAL,
self::ERR_UNTRANSLATED_UNUSED
);
}
}
/** @var string[][] maps argument position to a list of possible canonical strings (e.g. '%1$d') for that argument */
$types_of_arg = [];
// Check format string alone for common signs of problems.
// E.g. "% s", "%1$d %1$s"
foreach ($specs as $i => $spec_group) {
$types = [];
foreach ($spec_group as $spec) {
$canonical = $spec->toCanonicalString();
$types[$canonical] = true;
if ((\strlen($spec->padding_char) > 0 || \strlen($spec->alignment)) && ($spec->width === '' || !$spec->position)) {
// Warn about "100% dollars" but not about "100%1$ 2dollars" (If both position and width were parsed, assume the padding was intentional)
$emit_issue(
'PhanPluginPrintfNotPercent',
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
"Format string {STRING_LITERAL} contains something that is not a percent sign, it will be treated as a format string '{STRING_LITERAL}' with padding of \"{STRING_LITERAL}\" and alignment of '{STRING_LITERAL}' but no width. Use {DETAILS} for a literal percent sign, or '{STRING_LITERAL}' to be less ambiguous",
[self::encodeString($fmt_str), $spec->directive, $spec->padding_char, $spec->alignment, '%%', $canonical],
Issue::SEVERITY_NORMAL,
self::ERR_UNTRANSLATED_NOT_PERCENT
);
}
if ($is_translated && $spec->width &&
($spec->padding_char === '' || $spec->padding_char === ' ')
) {
$intended_string = $spec->toCanonicalStringWithWidthAsPosition();
$emit_issue(
'PhanPluginPrintfWidthNotPosition',
"Format string {STRING_LITERAL} is specifying a width({STRING_LITERAL}) instead of a position({STRING_LITERAL})",
[self::encodeString($fmt_str), self::encodeString($canonical), self::encodeString($intended_string)],
Issue::SEVERITY_NORMAL,
self::ERR_UNTRANSLATED_WIDTH_INSTEAD_OF_POSITION
);
}
}
$types_of_arg[$i] = $types;
if (count($types) > 1) {
// May be an off by one error in the format string.
$emit_issue(
'PhanPluginPrintfIncompatibleSpecifier',
'Format string {STRING_LITERAL} refers to argument #{INDEX} in different ways: {DETAILS}',
[self::encodeString($fmt_str), $i, implode(',', \array_keys($types))],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_INCOMPATIBLE_SPECIFIER
);
}
}
if (\is_array($arg_nodes)) {
foreach ($specs as $i => $spec_group) {
// $arg_nodes is a 0-based array, $spec_group is 1-based.
$arg_node = $arg_nodes[$i - 1] ?? null;
if (!isset($arg_node)) {
continue;
}
$actual_union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $arg_node);
if ($actual_union_type->isEmpty()) {
// Nothing to check.
continue;
}
$expected_set = [];
foreach ($spec_group as $spec) {
$type_name = $spec->getExpectedUnionTypeName();
$expected_set[$type_name] = true;
}
$expected_union_type = UnionType::empty();
foreach ($expected_set as $type_name => $_) {
// @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, $code_base)) {
continue;
}
if (isset($expected_set['string'])) {
$can_cast_to_string = false;
// 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->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, '__toString', true)) {
$can_cast_to_string = true;
break;
}
}
} catch (CodeBaseException $_) {
// Swallow "Cannot find class", go on to emit issue.
}
if ($can_cast_to_string) {
continue;
}
}
$expected_union_type_string = (string)$expected_union_type;
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',
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
'Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected. However, {FUNCTION} was passed the type {TYPE} (which is weaker than {TYPE})',
[
self::encodeString($fmt_str),
$i,
self::getSpecStringsRepresentation($spec_group),
$expected_union_type_string,
$function->getName(),
(string)$actual_union_type,
$expected_union_type_string,
],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT_WEAK
);
} else {
// This can be resolved by casting the arg to (int) manually in printf.
$emit_issue(
'PhanPluginPrintfIncompatibleArgumentType',
'Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected, but {FUNCTION} was passed incompatible type {TYPE}',
[
self::encodeString($fmt_str),
$i,
self::getSpecStringsRepresentation($spec_group),
$expected_union_type_string,
$function->getName(),
(string)$actual_union_type,
],
Issue::SEVERITY_LOW,
self::ERR_UNTRANSLATED_INCOMPATIBLE_ARGUMENT
);
}
}
}
// Make sure the translations are compatible with this format string.
// In order to take advantage of the ability to analyze translations, override gettextForAllLocales
if ($is_translated) {
$this->validateTranslations($code_base, $context, $fmt_str, $types_of_arg);
}
}
/**
* @param ConversionSpec[] $specs
*/
private static function getSpecStringsRepresentation(array $specs): string
{
return \implode(',', \array_unique(\array_map(static function (ConversionSpec $spec): string {
return $spec->directive;
}, $specs)));
}
/**
* @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, 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, $code_base);
}
// We already allow int->float conversion
return false;
}
/**
* TODO: Finish testing this.
*
* By default, this is a no-op, unless gettextForAllLocales is overridden in a subclass
*
* Check that the translations of the format string $fmt_str
* are compatible with the untranslated format string.
*
* In virtually all cases, the conversions specifiers should be
* identical to the conversion specifier (apart from whether or not
* position is explicitly stated)
*
* Emits issues.
*
* @param CodeBase $code_base
* @param Context $context
* @param string $fmt_str
* @param associative-array<int,array<mixed,ConversionSpec|true>> $types_of_arg contains array of ConversionSpec for
* each position in the untranslated format string.
*/
protected static function validateTranslations(CodeBase $code_base, Context $context, string $fmt_str, array $types_of_arg): void
{
$translations = static::gettextForAllLocales($fmt_str);
foreach ($translations as $locale => $translated_fmt_str) {
// Skip untranslated or equal strings.
if ($translated_fmt_str === $fmt_str) {
continue;
}
// Compare the translated specs for a given position to the existing spec.
$translated_specs = ConversionSpec::extractAll($translated_fmt_str);
foreach ($translated_specs as $i => $spec_group) {
$expected = $types_of_arg[$i] ?? [];
foreach ($spec_group as $spec) {
$canonical = $spec->toCanonicalString();
if (!isset($expected[$canonical])) {
$expected_types = $expected ? implode(',', \array_keys($expected))
: 'unused';
if ($expected_types !== 'unused') {
$severity = Issue::SEVERITY_NORMAL;
$issue_type_id = self::ERR_TRANSLATED_INCOMPATIBLE;
$issue_type = 'PhanPluginPrintfTranslatedIncompatible';
} else {
$severity = Issue::SEVERITY_NORMAL;
$issue_type_id = self::ERR_TRANSLATED_HAS_MORE_ARGS;
$issue_type = 'PhanPluginPrintfTranslatedHasMoreArgs';
}
self::emitIssue(
$code_base,
$context,
$issue_type,
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
'Translated string {STRING_LITERAL} has local {DETAILS} which refers to argument #{INDEX} as {STRING_LITERAL}, but the original format string treats it as {DETAILS} (ORIGINAL: {STRING_LITERAL}, TRANSLATION: {STRING_LITERAL})',
[
self::encodeString($fmt_str),
$locale,
$i,
$canonical,
$expected_types,
self::encodeString($fmt_str),
self::encodeString($translated_fmt_str),
],
$severity,
Issue::REMEDIATION_B,
$issue_type_id
);
}
}
}
}
}
}
/**
* Represents the information we have about the result of evaluating an expression.
* Currently, used only for printf arguments.
*/
class PrimitiveValue
{
/** @var array|int|string|float|bool|null The primitive value of the expression if it could be determined. */
public $value;
/** @var bool Whether or not the expression value was translated. */
public $is_translated;
/**
* @param array|int|string|float|bool|null $value
*/
public function __construct($value, bool $is_translated = false)
{
$this->value = $value;
$this->is_translated = $is_translated;
}
}
return new PrintfCheckerPlugin();

649
vendor/phan/phan/.phan/plugins/README.md vendored Normal file
View File

@@ -0,0 +1,649 @@
Plugins
=======
The plugins in this folder can be used to add additional capabilities to phan.
Add their relative path (.phan/plugins/...) to the `plugins` entry of .phan/config.php.
Plugin Documentation
--------------------
[Wiki Article: Writing Plugins For Phan](https://github.com/phan/phan/wiki/Writing-Plugins-for-Phan)
Plugin List
-----------
This section contains short descriptions of plugin files, and lists the issue types which they emit.
They are grouped into the following sections:
1. Plugins Affecting Phan Analysis
2. General-Use Plugins
3. Plugins Specific to Code Styles
4. Demo Plugins (Plugin authors should base new plugins off of these, if they don't see a similar plugin)
### 1. Plugins Affecting Phan Analysis
(More plugins will be added later, e.g. if they add new methods, add types to Phan's analysis of a return type, etc)
#### UnusedSuppressionPlugin.php
Warns if an `@suppress` annotation is no longer needed to suppress issue types on a function, method, closure, or class.
(Suppressions may stop being needed if Phan's analysis improves/changes in a release,
or if the relevant parts of the codebase fixed the bug/added annotations)
**This must be run with exactly one worker process**
- **UnusedSuppression**: `Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it`
- **UnusedPluginSuppression**: `Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} on this line but this suppression is unused or suppressed elsewhere`
- **UnusedPluginFileSuppression**: `Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} in this file but this suppression is unused or suppressed elsewhere`
The following settings can be used in `.phan/config.php`:
- `'plugin_config' => ['unused_suppression_ignore_list' => ['FlakyPluginIssueName']]` will make this plugin avoid emitting `Unused*Suppression` for a list of issue names.
- `'plugin_config' => ['unused_suppression_whitelisted_only' => true]` will make this plugin report unused suppressions only for issues in `whitelist_issue_types`.
#### FFIAnalysisPlugin.php
This is only necessary if you are using [PHP 7.4's FFI (Foreign Function Interface) support](https://wiki.php.net/rfc/ffi)
This makes Phan infer that assignments to variables that originally contained CData will continue to be CData.
### 2. General-Use Plugins
These plugins are useful across a wide variety of code styles, and should give low false positives.
Also see [DollarDollarPlugin.php](#dollardollarpluginphp) for a meaningful real-world example.
#### AlwaysReturnPlugin.php
Checks if a function or method with a non-void return type will **unconditionally** return or throw.
This is stricter than Phan's default checks (Phan accepts a function or method that **may** return something, or functions that unconditionally throw).
- **PhanPluginInconsistentReturnMethod**: `Method {METHOD} has no return type and will inconsistently return or not return`
- **PhanPluginAlwaysReturnMethod**: `Method {METHOD} has a return type of {TYPE}, but may fail to return a value`
- **PhanPluginInconsistentReturnFunction**: `Function {FUNCTION} has no return type and will inconsistently return or not return`
- **PhanPluginAlwaysReturnFunction**: `Function {FUNCTION} has a return type of {TYPE}, but may fail to return a value`
#### DuplicateArrayKeyPlugin.php
Warns about common errors in php array keys and switch statements. Has the following checks (This is able to resolve global and class constants to their scalar values).
- **PhanPluginDuplicateArrayKey**: a duplicate or equivalent array key literal.
(E.g `[2 => "value", "other" => "s", "2" => "value2"]` duplicates the key `2`)
- **PhanPluginDuplicateArrayKeyExpression**: `Duplicate/Equivalent dynamic array key expression ({CODE}) detected in array - the earlier entry will be ignored if the expression had the same value.`
(E.g. `[$x => 'value', $y => "s", $y => "value2"]`)
- **PhanPluginDuplicateSwitchCase**: a duplicate or equivalent case statement.
(E.g `switch ($x) { case 2: echo "A\n"; break; case 2: echo "B\n"; break;}` duplicates the key `2`. The later case statements are ignored.)
- **PhanPluginDuplicateSwitchCaseLooseEquality**: a case statement that is loosely equivalent to an earlier case statement.
(E.g `switch ('foo') { case 0: echo "0\n"; break; case 'foo': echo "foo\n"; break;}` has `0 == 'foo'`, and echoes `0` because of that)
- **PhanPluginMixedKeyNoKey**: mixing array entries of the form [key => value,] with entries of the form [value,].
(E.g. `['key' => 'value', 'othervalue']` is often found in code because the key for `'othervalue'` was forgotten)
#### PregRegexCheckerPlugin
This plugin checks for invalid regexes.
This plugin is able to resolve literals, global constants, and class constants as regexes.
- **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
Checks for invalid format strings, incorrect argument counts, and unused arguments in printf calls.
Additionally, warns about incompatible union types (E.g. passing `string` for the argument corresponding to `%d`)
This plugin is able to resolve literals, global constants, and class constants as format strings.
- **PhanPluginPrintfNonexistentArgument**: `Format string {STRING_LITERAL} refers to nonexistent argument #{INDEX} in {STRING_LITERAL}`
- **PhanPluginPrintfNoArguments**: `No format string arguments are given for {STRING_LITERAL}, consider using {FUNCTION} instead`
- **PhanPluginPrintfNoSpecifiers**: `None of the formatting arguments passed alongside format string {STRING_LITERAL} are used`
- **PhanPluginPrintfUnusedArgument**: `Format string {STRING_LITERAL} does not use provided argument #{INDEX}`
- **PhanPluginPrintfNotPercent**: `Format string {STRING_LITERAL} contains something that is not a percent sign, it will be treated as a format string '{STRING_LITERAL}' with padding. Use %% for a literal percent sign, or '{STRING_LITERAL}' to be less ambiguous`
(Usually a typo, e.g. `printf("%s is 20% done", $taskName)` treats `% d` as a second argument)
- **PhanPluginPrintfWidthNotPosition**: `Format string {STRING_LITERAL} is specifying a width({STRING_LITERAL}) instead of a position({STRING_LITERAL})`
- **PhanPluginPrintfIncompatibleSpecifier**: `Format string {STRING_LITERAL} refers to argument #{INDEX} in different ways: {DETAILS}` (e.g. `"%1$s of #%1$d"`. May be an off by one error.)
- **PhanPluginPrintfIncompatibleArgumentTypeWeak**: `Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected. However, {FUNCTION} was passed the type {TYPE} (which is weaker than {TYPE})`
- **PhanPluginPrintfIncompatibleArgumentType**: `Format string {STRING_LITERAL} refers to argument #{INDEX} as {DETAILS}, so type {TYPE} is expected, but {FUNCTION} was passed incompatible type {TYPE}`
- **PhanPluginPrintfVariableFormatString**: `Code {CODE} has a dynamic format string that could not be inferred by Phan`
Note (for projects using `gettext`):
Subclassing this plugin (and overriding `gettextForAllLocales`) will allow you to analyze translations of a project for compatibility.
This will require extra work to set up.
See [PrintfCheckerPlugin's source](./PrintfCheckerPlugin.php) for details.
#### UnreachableCodePlugin.php
Checks for syntactically unreachable statements in the global scope or function bodies.
(E.g. function calls after unconditional `continue`/`break`/`throw`/`return`/`exit()` statements)
- **PhanPluginUnreachableCode**: `Unreachable statement detected`
#### Unused variable detection
This is now built into Phan itself, and can be enabled via `--unused-variable-detection`.
#### InvokePHPNativeSyntaxCheckPlugin.php
This invokes `php --no-php-ini --syntax-check $analyzed_file_path` for you. (See
This is useful for cases Phan doesn't cover (e.g. [Issue #449](https://github.com/phan/phan/issues/449) or [Issue #277](https://github.com/phan/phan/issues/277)).
Note: This may double the time Phan takes to analyze a project. This plugin can be safely used along with `--processes N`.
This does not run on files that are parsed but not analyzed.
Configuration settings can be added to `.phan/config.php`:
```php
'plugin_config' => [
// A list of 1 or more PHP binaries (Absolute path or program name found in $PATH)
// to use to analyze your files with PHP's native `--syntax-check`.
//
// This can be used to simultaneously run PHP's syntax checks with multiple PHP versions.
// e.g. `'plugin_config' => ['php_native_syntax_check_binaries' => ['php72', 'php70', 'php56']]`
// if all of those programs can be found in $PATH
// 'php_native_syntax_check_binaries' => [PHP_BINARY],
// The maximum number of `php --syntax-check` processes to run at any point in time
// (Minimum: 1. Default: 1).
// 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,
],
```
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)
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.
This is done only for the functions and methods that are not excluded from analysis,
and it isn't done for methods that override or are overridden by other methods.
Note that functions such as `fopen()` are not pure due to side effects.
UseReturnValuePlugin also warns about those because their results should be used.
* This setting is ignored in the language server or daemon mode,
due to being extremely slow and memory intensive.
Automatic inference of function purity is done recursively.
This plugin also has a dynamic mode(disabled by default and slow) where it will warn if a function or method's return value is unused.
This checks if the function/method's return value is used 98% or more of the time, then warns about the remaining places where the return value was unused.
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.
#### PHPUnitAssertionPlugin.php
This plugin will make Phan infer side effects from calls to some of the helper methods that PHPUnit provides in test cases.
- Infer that a condition is truthy from `assertTrue()` and `assertNotFalse()` (e.g. `assertTrue($x instanceof MyClass)`)
- Infer that a condition is null/not null from `assertNull()` and `assertNotNull()`
- Infer class types from `assertInstanceOf(MyClass::class, $actual)`
- Infer types from `assertInternalType($expected, $actual)`
- Infer that $actual has the exact type of $expected after calling `assertSame($expected, $actual)`
- Other methods aren't supported yet.
#### EmptyStatementListPlugin.php
This file checks for empty statement lists in loops/branches.
Due to Phan's AST rewriting for easier analysis, this may miss some edge cases for if/elseif.
By default, this plugin won't warn if it can find a TODO/FIXME/"Deliberately empty" comment around the empty statement list (case insensitive).
(This may miss some TODOs due to `php-ast` not providing the end line numbers)
The setting `'plugin_config' => ['empty_statement_list_ignore_todos' => true]` can be used to make it unconditionally warn about empty statement lists.
- **PhanPluginEmptyStatementDoWhileLoop** `Empty statement list statement detected for the do-while loop`
- **PhanPluginEmptyStatementForLoop** `Empty statement list statement detected for the for loop`
- **PhanPluginEmptyStatementForeachLoop** `Empty statement list statement detected for the foreach loop`
- **PhanPluginEmptyStatementIf**: `Empty statement list statement detected for the last if/elseif statement`
- **PhanPluginEmptyStatementSwitch** `No side effects seen for any cases of this switch statement`
- **PhanPluginEmptyStatementTryBody** `Empty statement list statement detected for the try statement's body`
- **PhanPluginEmptyStatementPossiblyNonThrowingTryBody**: `Found a try block that looks like it might not throw. Note that this check is a heuristic prone to false positives, especially because error handlers, signal handlers, destructors, and other things may all lead to throwing.`
- **PhanPluginEmptyStatementTryFinally** `Empty statement list statement detected for the try's finally body`
- **PhanPluginEmptyStatementWhileLoop** `Empty statement list statement detected for the while loop`
### LoopVariableReusePlugin.php
This plugin detects reuse of loop variables.
- **PhanPluginLoopVariableReuse** `Variable ${VARIABLE} used in loop was also used in an outer loop on line {LINE}`
### RedundantAssignmentPlugin.php
This plugin checks for assignments where the variable already
has the given value.
(E.g. `$result = false; if (cond()) { $result = false; }`)
- **PhanPluginRedundantAssignment** `Assigning {TYPE} to variable ${VARIABLE} which already has that value`
- **PhanPluginRedundantAssignmentInLoop** `Assigning {TYPE} to variable ${VARIABLE} which already has that value`
- **PhanPluginRedundantAssignmentInGlobalScope** `Assigning {TYPE} to variable ${VARIABLE} which already has that value`
### UnknownClassElementAccessPlugin.php
This plugin checks for accesses to unknown class elements that can't be type checked (which may hide potential runtime errors such as having too few parameters).
To reduce false positives, this will suppress warnings if at least one recursive analysis could infer class/interface types for the object.
- **PhanPluginUnknownObjectMethodCall**: `Phan could not infer any class/interface types for the object of the method call {CODE} - inferred a type of {TYPE}`
This works best when there is only one analysis process (the default, i.e. `--processes 1`).
`--analyze-twice` will reduce the number of issues this emits.
### MoreSpecificElementTypePlugin.php
This plugin checks for return types that can be made more specific.
**This has a large number of false positives - it can be used manually to point out comments that should be made more specific, but is not recommended as part of a build.**
- **PhanPluginMoreSpecificActualReturnType**: `Phan inferred that {FUNCTION} documented to have return type {TYPE} returns the more specific type {TYPE}`
- **PhanPluginMoreSpecificActualReturnTypeContainsFQSEN**: `Phan inferred that {FUNCTION} documented to have return type {TYPE} (without an FQSEN) returns the more specific type {TYPE} (with an FQSEN)`
It's strongly recommended to use this with a single analysis process (the default, i.e. `--processes 1`).
This uses the following heuristics to reduce the number of false positives.
- Avoids warning about methods that are overrides or are overridden.
- Avoids checking generators.
- Flattens array shapes and literals before comparing types
- 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,
but may cause false positives in large projects with different code styles.
#### NonBool
##### NonBoolBranchPlugin.php
- **PhanPluginNonBoolBranch** Warns if an expression which has types other than `bool` is used in an if/else if.
(E.g. warns about `if ($x)`, where $x is an integer. Fix by checking `if ($x != 0)`, etc.)
##### NonBoolInLogicalArithPlugin.php
- **PhanPluginNonBoolInLogicalArith** Warns if an expression where the left/right-hand side has types other than `bool` is used in a binary operation.
(E.g. warns about `if ($x && $x->fn())`, where $x is an object. Fix by checking `if (($x instanceof MyClass) && $x->fn())`)
#### HasPHPDocPlugin.php
Checks if an element (class or property) has a PHPDoc comment,
and that Phan can extract a plaintext summary/description from that comment.
- **PhanPluginNoCommentOnClass**: `Class {CLASS} has no doc comment`
- **PhanPluginDescriptionlessCommentOnClass**: `Class {CLASS} has no readable description: {STRING_LITERAL}`
- **PhanPluginNoCommentOnFunction**: `Function {FUNCTION} has no doc comment`
- **PhanPluginDescriptionlessCommentOnFunction**: `Function {FUNCTION} has no readable description: {STRING_LITERAL}`
- **PhanPluginNoCommentOnPublicProperty**: `Public property {PROPERTY} has no doc comment` (Also exists for Private and Protected)
- **PhanPluginDescriptionlessCommentOnPublicProperty**: `Public property {PROPERTY} has no readable description: {STRING_LITERAL}` (Also exists for Private and Protected)
Warnings about method verbosity also exist, many categories may need to be completely disabled due to the large number of method declarations in a typical codebase:
- Warnings are not emitted for `@internal` methods.
- Warnings are not emitted for methods that override methods in the parent class.
- Warnings can be suppressed based on the method FQSEN with `plugin_config => [..., 'has_phpdoc_method_ignore_regex' => (a PCRE regex)]`
(e.g. to suppress issues about tests, or about missing documentation about getters and setters, etc.)
- This can be used to warn about duplicate method/property descriptions with `plugin_config => [..., 'has_phpdoc_check_duplicates' => true]`
(this skips checking method overrides, magic methods, and deprecated methods/properties)
The warning types for methods are below:
- **PhanPluginNoCommentOnPublicMethod**: `Public method {METHOD} has no doc comment` (Also exists for Private and Protected)
- **PhanPluginDescriptionlessCommentOnPublicMethod**: `Public method {METHOD} has no readable description: {STRING_LITERAL}` (Also exists for Private and Protected)
- **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.
- **PhanPluginInvalidVariableIsset** : Forces all uses of `isset` to be on arrays or variables.
E.g. it will warn about `isset(foo()['key'])`, because foo() is not a variable or an array access.
- **PhanUndeclaredVariable**: Warns if `$array` is undeclared in `isset($array[$key])`
#### NoAssertPlugin.php
Discourages the usage of assert() in the analyzed project.
See https://secure.php.net/assert
- **PhanPluginNoAssert**: `assert() is discouraged. Although phan supports using assert() for type annotations, PHP's documentation recommends assertions only for debugging, and assert() has surprising behaviors.`
#### NotFullyQualifiedUsagePlugin.php
Encourages the usage of fully qualified global functions and constants (slightly faster, especially for functions such as `strlen`, `count`, etc.)
- **PhanPluginNotFullyQualifiedFunctionCall**: `Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}`
- **PhanPluginNotFullyQualifiedOptimizableFunctionCall**: `Expected function call to {FUNCTION}() to be fully qualified or have a use statement but none were found in namespace {NAMESPACE} (opcache can optimize fully qualified calls to this function in recent php versions)`
- **PhanPluginNotFullyQualifiedGlobalConstant**: `Expected usage of {CONST} to be fully qualified or have a use statement but none were found in namespace {NAMESPACE}`
#### NumericalComparisonPlugin.php
Enforces that loose equality is used for numeric operands (e.g. `2 == 2.0`), and that strict equality is used for non-numeric operands (e.g. `"2" === "2e0"` is false).
- **PhanPluginNumericalComparison**: `nonnumerical values compared by the operators '==' or '!=='; numerical values compared by the operators '===' or '!=='`
#### StrictLiteralComparisonPlugin.php
Enforces that strict equality is used for comparisons to constant/literal integers or strings.
This is used to avoid surprising behaviors such as `0 == 'a'`, `"10" == "1e1"`, etc.
*Following the advice of this plugin may subtly break existing code (e.g. break implicit null/false checks, or code relying on these unexpected behaviors).*
- **PhanPluginComparisonNotStrictForScalar**: `Expected strict equality check when comparing {TYPE} to {TYPE} in {CODE}`
Also see [`StrictComparisonPlugin`](#StrictComparisonPlugin.php) and [`NumericalComparisonPlugin`](#NumericalComparisonPlugin.php).
#### PHPUnitNotDeadCodePlugin.php
Marks unit tests and dataProviders of subclasses of PHPUnit\Framework\TestCase as referenced.
Avoids false positives when `--dead-code-detection` is enabled.
(Does not emit any issue types)
#### SleepCheckerPlugin.php
Warn about returning non-arrays in [`__sleep`](https://secure.php.net/__sleep),
as well as about returning array values with invalid property names in `__sleep`.
- **SleepCheckerInvalidReturnStatement`**: `__sleep must return an array of strings. This is definitely not an array.`
- **SleepCheckerInvalidReturnType**: `__sleep is returning {TYPE}, expected string[]`
- **SleepCheckerInvalidPropNameType**: `__sleep is returning an array with a value of type {TYPE}, expected string`
- **SleepCheckerInvalidPropName**: `__sleep is returning an array that includes {PROPERTY}, which cannot be found`
- **SleepCheckerMagicPropName**: `__sleep is returning an array that includes {PROPERTY}, which is a magic property`
- **SleepCheckerDynamicPropName**: `__sleep is returning an array that includes {PROPERTY}, which is a dynamically added property (but not a declared property)`
- **SleepCheckerPropertyMissingTransient**: `Property {PROPERTY} that is not serialized by __sleep should be annotated with @transient or @phan-transient`,
#### UnknownElementTypePlugin.php
Warns about elements containing unknown types (function/method/closure return types, parameter types)
- **PhanPluginUnknownMethodReturnType**: `Method {METHOD} has no declared or inferred return type`
- **PhanPluginUnknownMethodParamType**: `Method {METHOD} has no declared or inferred parameter type for ${PARAMETER}`
- **PhanPluginUnknownFunctionReturnType**: `Function {FUNCTION} has no declared or inferred return type`
- **PhanPluginUnknownFunctionParamType**: `Function {FUNCTION} has no declared or inferred parameter type for ${PARAMETER}`
- **PhanPluginUnknownClosureReturnType**: `Closure {FUNCTION} has no declared or inferred return type`
- **PhanPluginUnknownClosureParamType**: `Closure {FUNCTION} has no declared or inferred parameter type for ${PARAMETER}`
- **PhanPluginUnknownPropertyType**: `Property {PROPERTY} has an initial type that cannot be inferred`
#### DuplicateExpressionPlugin.php
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
This plugin checks for unexpected whitespace in PHP files.
- **PhanPluginWhitespaceCarriageReturn**: `The first occurrence of a carriage return ("\r") was seen here. Running "dos2unix" can fix that.`
- **PhanPluginWhitespaceTab**: `The first occurrence of a tab was seen here. Running "expand" can fix that.`
- **PhanPluginWhitespaceTrailing**: `The first occurrence of trailing whitespace was seen here.`
#### InlineHTMLPlugin.php
This plugin checks for unexpected inline HTML.
This can be limited to a subset of files with an `inline_html_whitelist_regex` - e.g. `@^(src/|lib/)@`.
Files can be excluded with `inline_html_blacklist_regex`, e.g. `@(^src/templates/)|(\.html$)@`
- **PhanPluginInlineHTML**: `Saw inline HTML between the first and last token: {STRING_LITERAL}`
- **PhanPluginInlineHTMLLeading**: `Saw inline HTML at the start of the file: {STRING_LITERAL}`
- **PhanPluginInlineHTMLTrailing**: `Saw inline HTML at the end of the file: {STRING_LITERAL}`
#### SuspiciousParamOrderPlugin.php
This plugin guesses if arguments to a function call are out of order, based on heuristics on the name in the expression (e.g. variable name).
This will only warn if the argument types are compatible with the alternate parameters being suggested.
This may be useful when analyzing methods with long parameter lists.
E.g. warns about invoking `function example($first, $second, $third)` as `example($mySecond, $myThird, $myFirst)`
- **PhanPluginSuspiciousParamOrder**: `Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}`
- **PhanPluginSuspiciousParamOrderInternal**: `Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS}`
#### PossiblyStaticMethodPlugin.php
Checks if a method can be made static without causing any errors.
- **PhanPluginPossiblyStaticPublicMethod**: `Public method {METHOD} can be static` (Also exists for Private and Protected)
- **PhanPluginPossiblyStaticClosure**: `{FUNCTION} can be static`
Warnings may need to be completely disabled due to the large number of method declarations in a typical codebase:
- Warnings are not emitted for methods that override methods in the parent class.
- Warnings are not emitted for methods that are overridden in child classes.
- Warnings can be suppressed based on the method FQSEN with `plugin_config => [..., 'possibly_static_method_ignore_regex' => (a PCRE regex)]`
#### PHPDocToRealTypesPlugin.php
This plugin suggests real types that can be used instead of phpdoc types.
Currently, this just checks param and return types.
Some of the suggestions made by this plugin will cause inheritance errors.
This doesn't suggest changes if classes have subclasses (but this check doesn't work when inheritance involves traits).
`PHPDOC_TO_REAL_TYPES_IGNORE_INHERITANCE=1` can be used to force this to check **all** methods and emit issues.
This also supports `--automatic-fix` to add the types to the real type signatures.
- **PhanPluginCanUseReturnType**: `Can use {TYPE} as a return type of {METHOD}`
- **PhanPluginCanUseNullableReturnType**: `Can use {TYPE} as a return type of {METHOD}` (useful if there is a minimum php version of 7.1)
- **PhanPluginCanUsePHP71Void**: `Can use php 7.1's void as a return type of {METHOD}` (useful if there is a minimum php version of 7.1)
This supports `--automatic-fix`.
- `PHPDocRedundantPlugin` will be useful for cleaning up redundant phpdoc after real types were added.
- `PreferNamespaceUsePlugin` can be used to convert types from fully qualified types back to unqualified types ()
#### PHPDocRedundantPlugin.php
This plugin warns about function/method/closure phpdoc that does nothing but repeat the information in the type signature.
E.g. this will warn about `/** @return void */ function () : void {}` and `/** */`, but not `/** @return void description of what it does or other annotations */`
This supports `--automatic-fix`
- **PhanPluginRedundantFunctionComment**: `Redundant doc comment on function {FUNCTION}(). Either add a description or remove the comment: {COMMENT}`
- **PhanPluginRedundantMethodComment**: `Redundant doc comment on method {METHOD}(). Either add a description or remove the comment: {COMMENT}`
- **PhanPluginRedundantClosureComment**: `Redundant doc comment on closure {FUNCTION}. Either add a description or remove the comment: {COMMENT}`
- **PhanPluginRedundantReturnComment**: `Redundant @return {TYPE} on function {FUNCTION}. Either add a description or remove the @return annotation: {COMMENT}`
#### PreferNamespaceUsePlugin.php
This plugin suggests using `ClassName` instead of `\My\Ns\ClassName` when there is a `use My\Ns\ClassName` annotation (or for uses in namespace `\My\Ns`)
Currently, this only checks **real** (not phpdoc) param/return annotations.
- **PhanPluginPreferNamespaceUseParamType**: `Could write param type of ${PARAMETER} of {FUNCTION} as {TYPE} instead of {TYPE}`
- **PhanPluginPreferNamespaceUseReturnType**: `Could write return type of {FUNCTION} as {TYPE} instead of {TYPE}`
##### StrictComparisonPlugin.php
This plugin warns about non-strict comparisons. It warns about the following issue types:
1. Using `in_array` and `array_search` without explicitly passing true or false to `$strict`.
2. Using equality or comparison operators when both sides are possible objects.
- **PhanPluginComparisonNotStrictInCall**: `Expected {FUNCTION} to be called with a third argument for {PARAMETER} (either true or false)`
- **PhanPluginComparisonObjectEqualityNotStrict**: `Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}`
- **PhanPluginComparisonObjectOrdering**: `Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}`
##### EmptyMethodAndFunctionPlugin.php
This plugin looks for empty methods/functions.
Note that this is not emitted for empty statement lists in functions or methods that are overrides, are overridden, or are deprecated.
- **PhanEmptyClosure**: `Empty closure`
- **PhanEmptyFunction**: `Empty function {FUNCTION}`
- **PhanEmptyPrivateMethod**: `Empty private method {METHOD}`
- **PhanEmptyProtectedMethod**: `Empty protected method {METHOD}`
- **PhanEmptyPublicMethod**: `Empty public method {METHOD}`
#### DollarDollarPlugin.php
Checks for complex variable access expressions `$$x`, which may be hard to read, and make the variable accesses hard/impossible to analyze.
- **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.
(E.g. calling `$this->getFoo()` when the property `$this->foo` is accessible, and there are no known overrides of the getter)
- **PhanPluginAvoidableGetter**: `Can replace {METHOD} with {PROPERTY}`
- **PhanPluginAvoidableGetterInTrait**: `Can replace {METHOD} with {PROPERTY}`
Note that switching to properties makes the code slightly faster,
but may break code outside of the library that overrides those getters,
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.
#### AddNeverReturnTypePlugin.php
This plugin checks if a function or method will not return (and has no overrides).
If the function doesn't have a return type of never.
then this plugin will emit an issue.
Closures and short error functions are currently not checked
- **PhanPluginNeverReturnMethod**: `Method {METHOD} never returns and has a return type of {TYPE}, but phpdoc type {TYPE} could be used instead`
- **PhanPluginNeverReturnFunction**: `Function {FUNCTION} never returns and has a return type of {TYPE}, but phpdoc type {TYPE} could be used instead`
### 4. Demo plugins:
These files demonstrate plugins for Phan.
#### DemoPlugin.php
Look at this class's documentation if you want an example to base your plugin off of.
Generates the following issue types under the types:
- **DemoPluginClassName**: a declared class isn't called 'Class'
- **DemoPluginFunctionName**: a declared function isn't called `function`
- **DemoPluginMethodName**: a declared method isn't called `function`
PHP's default checks(`php -l` would catch the class/function name types.)
- **DemoPluginInstanceof**: codebase contains `(expr) instanceof object` (usually invalid, and `is_object()` should be used instead. That would actually be a check for `class object`).
### 5. Third party plugins
- https://github.com/Drenso/PhanExtensions is a third party project with several plugins to do the following:
- Analyze Symfony doc comment annotations.
- Mark elements in inline doc comments (which Phan doesn't parse) as referencing types from `use statements` as not dead code.
- https://github.com/TysonAndre/PhanTypoCheck checks all tokens of PHP files for typos, including within string literals.
It is also able to analyze calls to `gettext()`.
### 6. Self-analysis plugins:
#### PhanSelfCheckPlugin.php
This plugin checks for invalid calls to `PluginV2::emitIssue`, `Issue::maybeEmit()`, etc.
This is useful for developing Phan and Phan plugins.
- **PhanPluginTooFewArgumentsForIssue**: `Too few arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}`
- **PhanPluginTooManyArgumentsForIssue**: `Too many arguments for issue {STRING_LITERAL}: expected {COUNT}, got {COUNT}`
- **PhanPluginUnknownIssueType**: `Unknown issue type {STRING_LITERAL} in a call to {METHOD}(). (may be a false positive - check if the version of Phan running PhanSelfCheckPlugin is the same version that the analyzed codebase is using)`

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\Element\PassByReferenceVariable;
use Phan\Parse\ParseVisitor;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePreAnalysisVisitor;
use Phan\PluginV3\PreAnalyzeNodeCapability;
/**
* This plugin checks for assignments where the variable already
* has the given value.
*
* - E.g. `$result = false; if (cond()) { $result = false; }`
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* DuplicateExpressionPlugin hooks into two events:
*
* - getPreAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in pre-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 RedundantAssignmentPlugin extends PluginV3 implements
PreAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePreAnalysisVisitor subclass
*/
public static function getPreAnalyzeNodeVisitorClassName(): string
{
return RedundantAssignmentPreAnalysisVisitor::class;
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
class RedundantAssignmentPreAnalysisVisitor extends PluginAwarePreAnalysisVisitor
{
/**
* @param Node $node
* An assignment operation node to analyze
* @override
*/
public function visitAssign(Node $node): void
{
$var = $node->children['var'];
if (!$var instanceof Node) {
return;
}
if ($var->kind !== ast\AST_VAR) {
return;
}
$var_name = $var->children['name'];
if (!is_string($var_name)) {
return;
}
$variable = $this->context->getScope()->getVariableByNameOrNull($var_name);
if (!$variable || $variable instanceof PassByReferenceVariable) {
return;
}
$variable_type = $variable->getUnionType();
if ($variable_type->isPossiblyUndefined() || count($variable_type->getRealTypeSet()) !== 1) {
return;
}
$old_value = $variable_type->getRealUnionType()->asValueOrNullOrSelf();
if (is_object($old_value)) {
return;
}
$expr = $node->children['expr'];
if (!ParseVisitor::isConstExpr($expr, ParseVisitor::CONSTANT_EXPRESSION_FORBID_NEW_EXPRESSION)) {
return;
}
try {
$expr_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr, false);
} catch (Exception $_) {
return;
}
if (count($expr_type->getRealTypeSet()) !== 1) {
return;
}
$expr_value = $expr_type->getRealUnionType()->asValueOrNullOrSelf();
if ($expr_value !== $old_value) {
return;
}
if ($this->context->hasSuppressIssue($this->code_base, 'PhanPluginRedundantAssignment')) {
// Suppressing this suppresses the more specific issues.
return;
}
if ($this->context->isInGlobalScope()) {
if ($variable->getFileRef()->getFile() !== $this->context->getFile()) {
// Don't warn if this variable was set by a different file
return;
}
if (Config::getValue('__analyze_twice') && $variable->getFileRef()->getLineNumberStart() === $this->context->getLineNumberStart()) {
// Don't warn if this variable was set by a different file
return;
}
$issue_name = 'PhanPluginRedundantAssignmentInGlobalScope';
} elseif ($this->context->isInLoop()) {
$issue_name = 'PhanPluginRedundantAssignmentInLoop';
} else {
$issue_name = 'PhanPluginRedundantAssignment';
}
if ($this->context->isInLoop()) {
$this->context->deferCheckToOutermostLoop(function (Context $context_after_loop) use ($issue_name, $var_name, $variable_type, $var): void {
$new_variable = $context_after_loop->getScope()->getVariableByNameOrNull($var_name);
if (!$new_variable) {
return;
}
$new_variable_type = $new_variable->getUnionType();
if ($new_variable_type->isPossiblyUndefined()) {
return;
}
if ($new_variable_type->getRealTypeSet() !== $variable_type->getRealTypeSet()) {
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($var->lineno),
$issue_name,
'Assigning {TYPE} to variable ${VARIABLE} which already has that value',
[$variable_type, $var_name]
);
});
return;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($var->lineno),
$issue_name,
'Assigning {TYPE} to variable ${VARIABLE} which already has that value',
[$expr_type, $var_name]
);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new RedundantAssignmentPlugin();

View File

@@ -0,0 +1,172 @@
<?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, ?Node $node): void {
if ($node) {
$context = (clone $context)->withLineNumberStart($node->lineno);
}
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 $node = null
) use ($warn_remove_debug_call): void {
if (self::shouldSuppressDebugIssues($code_base, $context)) {
return;
}
$warn_remove_debug_call($code_base, $context, $function, $node);
};
/**
* @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 $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, $node);
};
/**
* @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 $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, $node);
};
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();

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();

View File

@@ -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();

View File

@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\Config;
use Phan\Language\Type\StringType;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks uses of __sleep()
*
* It assumes that the body of the __sleep() implementation is simple,
* and just returns array literals directly.
* This plugin does not analyze building up arrays, array_merge(), variables, etc.
*
* It is assumed without being checked that plugins aren't
* mangling state within the passed code base or context.
*/
class SleepCheckerPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return SleepCheckerVisitor::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 SleepCheckerVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitMethod(Node $node): void
{
if (strcasecmp('__sleep', (string)$node->children['name']) !== 0) {
return;
}
$sleep_properties = [];
$this->analyzeStatementsOfSleep($node, $sleep_properties);
$this->warnAboutTransientSleepProperties($sleep_properties);
}
/**
* Warn about instance properties that aren't mentioned in __sleep()
* and don't have (at)transient or (at)phan-transient
*
* @param array<string,true> $sleep_properties
*/
private function warnAboutTransientSleepProperties(array $sleep_properties): void
{
if (count($sleep_properties) === 0) {
// Give up, failed to extract property names
return;
}
$class = $this->context->getClassInScope($this->code_base);
$class_fqsen = $class->getFQSEN();
foreach ($class->getPropertyMap($this->code_base) as $property_name => $property) {
if ($property->isStatic()) {
continue;
}
if ($property->isFromPHPDoc()) {
continue;
}
if ($property->isDynamicProperty()) {
continue;
}
if (isset($sleep_properties[$property_name])) {
continue;
}
if ($property->getRealDefiningFQSEN()->getFullyQualifiedClassName() !== $class_fqsen) {
continue;
}
$doc_comment = $property->getDocComment() ?? '';
$has_transient = preg_match('/@(phan-)?transient\b/', $doc_comment) > 0;
if (!$has_transient) {
$regex = Config::getValue('plugin_config')['sleep_transient_warning_blacklist_regex'] ?? null;
if (is_string($regex) && preg_match($regex, $property_name)) {
continue;
}
$this->emitPluginIssue(
$this->code_base,
$property->getContext(),
'SleepCheckerPropertyMissingTransient',
'Property {PROPERTY} that is not serialized by __sleep should be annotated with @transient or @phan-transient',
[$property->__toString()]
);
}
}
}
/**
* @param Node|int|string|float|null $node
* @param array<string,true> $sleep_properties
*/
private function analyzeStatementsOfSleep($node, array &$sleep_properties = []): void
{
if (!($node instanceof Node)) {
if (is_array($node)) {
foreach ($node as $child_node) {
$this->analyzeStatementsOfSleep($child_node, $sleep_properties);
}
}
return;
}
switch ($node->kind) {
case ast\AST_RETURN:
$this->analyzeReturnValue($node->children['expr'], $node->lineno, $sleep_properties);
return;
case ast\AST_CLASS:
case ast\AST_CLOSURE:
case ast\AST_FUNC_DECL:
return;
default:
foreach ($node->children as $child_node) {
$this->analyzeStatementsOfSleep($child_node, $sleep_properties);
}
}
}
private const RESOLVE_SETTINGS =
ContextNode::RESOLVE_ARRAYS |
ContextNode::RESOLVE_ARRAY_VALUES |
ContextNode::RESOLVE_CONSTANTS;
/**
* @param Node|string|int|float|null $expr_node
* @param int $lineno
* @param array<string,true> $sleep_properties
*/
private function analyzeReturnValue($expr_node, int $lineno, array &$sleep_properties): void
{
$context = (clone $this->context)->withLineNumberStart($lineno);
if (!($expr_node instanceof Node)) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerInvalidReturnStatement',
'__sleep must return an array of strings. This is definitely not an array.'
);
return;
}
$code_base = $this->code_base;
$union_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $expr_node);
if (!$union_type->hasArray()) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerInvalidReturnType',
'__sleep is returning {TYPE}, expected {TYPE}',
[(string)$union_type, 'string[]']
);
return;
}
if (!$context->isInClassScope()) {
return;
}
$kind = $expr_node->kind;
if (!\in_array($kind, [ast\AST_CONST, ast\AST_ARRAY, ast\AST_CLASS_CONST], true)) {
return;
}
$value = (new ContextNode($code_base, $context, $expr_node))->getEquivalentPHPValue(self::RESOLVE_SETTINGS);
if (!is_array($value)) {
return;
}
$class = $context->getClassInScope($code_base);
foreach ($value as $prop_name) {
if (!is_string($prop_name)) {
$prop_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $prop_name);
if (!$prop_type->isType(StringType::instance(false))) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerInvalidPropNameType',
'__sleep is returning an array with a value of type {TYPE}, expected {TYPE}',
[(string)$prop_type, 'string']
);
}
continue;
}
$sleep_properties[$prop_name] = true;
if (!$class->hasPropertyWithName($code_base, $prop_name)) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerInvalidPropName',
'__sleep is returning an array that includes {PROPERTY}, which cannot be found',
[$prop_name]
);
continue;
}
$prop = $class->getPropertyByName($code_base, $prop_name);
if ($prop->isFromPHPDoc()) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerMagicPropName',
'__sleep is returning an array that includes {PROPERTY}, which is a magic property',
[$prop_name]
);
continue;
}
if ($prop->isDynamicProperty()) {
$this->emitPluginIssue(
$this->code_base,
$context,
'SleepCheckerDynamicPropName',
'__sleep is returning an array that includes {PROPERTY}, which is a dynamically added property (but not a declared property)',
[$prop_name]
);
continue;
}
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new SleepCheckerPlugin();

View File

@@ -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();

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Language\Context;
use Phan\Language\Element\Func;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for uses of in_array where $strict is not true.
* This is specific to some coding styles - Some code may need to use weak comparisons to work properly.
*
* This is used in Phan for the following reasons:
*
* 1. To avoid accidentally using weak comparison on objects, which may cause issues such as stack overflow when comparing a Type to itself (Type has reference cycles).
* 2. To avoid mistakes due to weak type comparison.
* 3. For slightly better performance.
*
* This implements the following helpers:
*
* - getAnalyzeFunctionCallClosures
* This method returns a map from function/method FQSEN to closures that are called on invocations of those closures.
*/
class StrictComparisonPlugin extends PluginV3 implements
AnalyzeFunctionCallCapability,
PostAnalyzeNodeCapability
{
public const ComparisonNotStrictInCall = 'PhanPluginComparisonNotStrictInCall';
public const ComparisonObjectEqualityNotStrict = 'PhanPluginComparisonObjectEqualityNotStrict';
public const ComparisonObjectOrdering = 'PhanPluginComparisonObjectOrdering';
/**
* @param CodeBase $code_base @phan-unused-param
* @return array<string, Closure(CodeBase,Context,Func,array,?Node=):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @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,
?Node $node = null
) use (
$index,
$index_name,
$min_args
): void {
if (count($args) < $min_args) {
return;
}
$strict_node = $args[$index] ?? null;
if ($strict_node instanceof Node) {
$type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $strict_node)->asSingleScalarValueOrNullOrSelf();
if ($type === true) {
return;
} elseif ($type === false) {
return;
}
}
self::emitPluginIssue(
$code_base,
$context,
self::ComparisonNotStrictInCall,
"Expected {FUNCTION} to be called with a $index_name argument for {PARAMETER} (either true or false)",
[$func->getName(), '$strict']
);
};
};
// More functions might be added in the future
$always_warn_third_not_strict = $make_callback(2, 'third', 0);
return [
'in_array' => $always_warn_third_not_strict,
'array_search' => $always_warn_third_not_strict,
];
}
/**
* @return string - The name of the visitor that will be called (formerly analyzeNode)
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return StrictComparisonVisitor::class;
}
}
/**
* Warns about using weak comparison operators when both sides are possibly objects
*/
class StrictComparisonVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node
* A node of kind ast\AST_BINARY_OP to analyze
*
* @override
*/
public function visitBinaryOp(Node $node): void
{
switch ($node->flags) {
case ast\flags\BINARY_IS_EQUAL:
case ast\flags\BINARY_IS_NOT_EQUAL:
if ($this->bothSidesArePossiblyObjects($node)) {
// TODO: Also check arrays of objects?
$this->emit(
StrictComparisonPlugin::ComparisonObjectEqualityNotStrict,
'Saw a weak equality check on possible object types {TYPE} and {TYPE} in {CODE}',
[
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['left']),
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['right']),
ASTReverter::toShortString($node),
]
);
}
break;
case ast\flags\BINARY_IS_GREATER_OR_EQUAL:
case ast\flags\BINARY_IS_SMALLER_OR_EQUAL:
case ast\flags\BINARY_IS_GREATER:
case ast\flags\BINARY_IS_SMALLER:
case ast\flags\BINARY_SPACESHIP:
if ($this->bothSidesArePossiblyObjects($node)) {
$this->emit(
StrictComparisonPlugin::ComparisonObjectOrdering,
'Using comparison operator on possible object types {TYPE} and {TYPE} in {CODE}',
[
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['left']),
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['right']),
ASTReverter::toShortString($node),
]
);
}
break;
}
}
private function bothSidesArePossiblyObjects(Node $node): bool
{
['left' => $left, 'right' => $right] = $node->children;
if (!($left instanceof Node) || !($right instanceof Node)) {
return false;
}
return UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left)->hasObjectTypes() &&
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right)->hasObjectTypes();
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new StrictComparisonPlugin();

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\Language\Type\IntType;
use Phan\Language\Type\StringType;
use Phan\Parse\ParseVisitor;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin warns about using `==`/`!=` for string literals.
* For the vast majority of projects, this will have too many false positives to use.
* Only use this if you are sure there are no weak type comparisons.
* (e.g. strings from inputs/dbs used as numbers, floats compared to integers)
*
* Also see StrictComparisonPlugin for warning about comparing objects.
*/
class StrictLiteralComparisonPlugin extends PluginV3 implements
PostAnalyzeNodeCapability
{
/**
* @return string - The name of the visitor that will be called (formerly analyzeNode)
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return StrictLiteralComparisonVisitor::class;
}
}
/**
* Warns about using weak comparison operators when both sides are possibly objects
*/
class StrictLiteralComparisonVisitor extends PluginAwarePostAnalysisVisitor
{
/**
* @param Node $node
* A node of kind ast\AST_BINARY_OP to analyze
*
* @override
*/
public function visitBinaryOp(Node $node): void
{
if ($node->flags === ast\flags\BINARY_IS_NOT_EQUAL || $node->flags === ast\flags\BINARY_IS_EQUAL) {
$this->analyzeEqualityCheck($node);
}
}
/**
* @param Node $node
* A node of kind ast\AST_BINARY_OP for `==`/`!=` to analyze
*/
private function analyzeEqualityCheck(Node $node): void
{
['left' => $left, 'right' => $right] = $node->children;
$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;
}
$const_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left_is_const ? $left : $right);
if ($const_type->isEmpty()) {
return;
}
foreach ($const_type->getTypeSet() as $type) {
if (!($type instanceof IntType || $type instanceof StringType)) {
return;
}
}
self::emitPluginIssue(
$this->code_base,
$this->context,
'PhanPluginComparisonNotStrictForScalar',
"Expected strict equality check when comparing {TYPE} to {TYPE} in {CODE}",
[
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $left),
UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $right),
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 StrictLiteralComparisonPlugin();

View File

@@ -0,0 +1,433 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\Exception\CodeBaseException;
use Phan\Language\Element\FunctionInterface;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* A plugin that checks if calls to a function or method pass in arguments in a suspicious order.
* E.g. calling `function example($offset, $count)` as `example($count, $offset)`
*/
class SuspiciousParamOrderPlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return SuspiciousParamOrderVisitor::class;
}
}
/**
* Checks for invocations of functions/methods where the return value should be used.
* Also, gathers statistics on how often those functions/methods are used.
*/
class SuspiciousParamOrderVisitor extends PluginAwarePostAnalysisVisitor
{
// phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
// 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
/**
* @param Node $node a node of type AST_CALL
* @override
*/
public function visitCall(Node $node): void
{
$args = $node->children['args']->children;
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'];
try {
$function_list_generator = (new ContextNode(
$this->code_base,
$this->context,
$expression
))->getFunctionFromNode();
foreach ($function_list_generator as $function) {
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$this->checkCall($function, $args, $node);
}
} catch (CodeBaseException $_) {
}
}
/**
* @param Node|string|int|float|null $arg_node
*/
private static function extractName($arg_node): ?string
{
if (!$arg_node instanceof Node) {
return null;
}
switch ($arg_node->kind) {
case ast\AST_VAR:
$name = $arg_node->children['name'];
break;
/*
case ast\AST_CONST:
$name = $arg_node->children['name']->children['name'];
break;
*/
case ast\AST_PROP:
case ast\AST_STATIC_PROP:
$name = $arg_node->children['prop'];
break;
case ast\AST_METHOD_CALL:
case ast\AST_STATIC_CALL:
$name = $arg_node->children['method'];
break;
case ast\AST_CALL:
$name = $arg_node->children['expr'];
break;
default:
return null;
}
return is_string($name) ? $name : null;
}
/**
* Returns a distance in the range 0..1, inclusive.
*
* A distance of 0 means they are similar (e.g. foo and getFoo()),
* and 1 means there are no letters in common (bar and foo)
*/
private static function computeDistance(string $a, string $b): float
{
$la = strlen($a);
$lb = strlen($b);
return (levenshtein($a, $b) - abs($la - $lb)) / max(1, min($la, $lb));
}
/**
* @param list<Node|string|int|float> $args
*/
private function checkCall(FunctionInterface $function, array $args, Node $node): void
{
$arg_names = [];
foreach ($args as $i => $arg_node) {
$name = self::extractName($arg_node);
if (!is_string($name)) {
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();
$parameter_names = [];
foreach ($arg_names as $i => $_) {
if (!isset($parameters[$i])) {
unset($arg_names[$i]);
continue;
}
$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 = [];
foreach ($arg_names as $i => $name) {
// To even be considered, the distance metric must be less than 60% (100% would have nothing in common)
$best_distance = min(
0.6,
self::computeDistance($name, $parameter_names[$i])
);
$best_destination = null;
// echo "Distances for $name to $parameter_names[$i] is $best_distance\n";
foreach ($parameter_names as $j => $parameter_name_j) {
if ($j === $i) {
continue;
}
$d_swap_j = self::computeDistance($name, $parameter_name_j);
// echo "Distances for $name to $parameter_name_j is $d_swap_j\n";
if ($d_swap_j < $best_distance) {
$best_destination = $j;
$best_distance = $d_swap_j;
}
}
if ($best_destination !== null) {
$best_destination_map[$i] = $best_destination;
}
}
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->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));
$param_details = implode(' and ', array_map(static function (int $i) use ($parameters): string {
$param = $parameters[$i];
return '#' . ($i + 1) . ' (' . trim($param->getUnionType() . ' $' . $param->getName()) . ')';
}, $cycle));
if ($function->isPHPInternal()) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
self::SuspiciousParamOrderInternal,
'Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION}',
[
$arg_details,
$param_details,
$function->getRepresentationForIssue(true),
]
);
} else {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
self::SuspiciousParamOrder,
'Suspicious order for arguments named {DETAILS} - These are being passed to parameters {DETAILS} of {FUNCTION} defined at {FILE}:{LINE}',
[
$arg_details,
$param_details,
$function->getRepresentationForIssue(true),
$function->getContext()->getFile(),
$function->getContext()->getLineNumberStart(),
]
);
}
}
$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(),
]
);
}
}
}
/**
* @param list<int> $values
* @return list<int> the same values of the cycle, rearranged to start with the smallest value.
*/
private static function normalizeCycle(array $values, int $next): array
{
$pos = array_search($next, $values, true);
$values = array_slice($values, $pos ?: 0);
$min_pos = 0;
foreach ($values as $i => $value) {
if ($value < $values[$min_pos]) {
$min_pos = $values[$i];
}
}
return array_merge(array_slice($values, $min_pos), array_slice($values, 0, $min_pos));
}
/**
* Given [1 => 2, 2 => 3, 3 => 1, 4 => 5, 5 => 6, 6 => 5]], return [[1,2,3],[5,6]]
* @param array<int,int> $destination_map
* @return array<int,array<int,int>>
*/
public static function findCycles(array $destination_map): array
{
$result = [];
while (count($destination_map) > 0) {
reset($destination_map);
$key = (int) key($destination_map);
$values = [];
while (count($destination_map) > 0) {
$values[] = $key;
$next = $destination_map[$key];
unset($destination_map[$key]);
if (in_array($next, $values, true)) {
$values = self::normalizeCycle($values, $next);
if (count($values) >= 2) {
$result[] = $values;
}
$values = [];
break;
}
if (!isset($destination_map[$next])) {
break;
}
$key = $next;
}
}
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
*/
public function visitMethodCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 1) {
// Can't have a suspicious param order/position if there are no params
// (or for AST_CALLABLE_CONVERT)
return;
}
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
return;
}
try {
$method = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, false, true);
} catch (Exception $_) {
return;
}
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$this->checkCall($method, $args, $node);
}
/**
* @param Node $node a node of type AST_STATIC_CALL
* @override
*/
public function visitStaticCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 1) {
// Can't have a suspicious param order/position if there are no params
return;
}
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
return;
}
try {
$method = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, true, true);
} catch (Exception $_) {
return;
}
// @phan-suppress-next-line PhanPartialTypeMismatchArgument
$this->checkCall($method, $args, $node);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new SuspiciousParamOrderPlugin();

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\FileRef;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\FinalizeProcessCapability;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin checks for accesses to unknown class elements that can't be type checked.
*
* - E.g. `$unknown->someMethod(null)`
*
* This file demonstrates plugins for Phan. Plugins hook into various events.
* UnknownClassElementAccessPlugin hooks into two events:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a visitor that is called on every AST node from every
* file being analyzed in post-order
* - finalizeProcess
* This is called after the other forms of analysis are finished running.
*
* 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 UnknownClassElementAccessPlugin extends PluginV3 implements
PostAnalyzeNodeCapability,
FinalizeProcessCapability
{
public const UnknownObjectMethodCall = 'PhanPluginUnknownObjectMethodCall';
/**
* @var array<string,list<array{0:Context,1:string, 2:UnionType}>>
* Map from file name+line+node hash to the union type to a closure to emit the issue
*/
private static $deferred_unknown_method_issues = [];
/**
* @var array<string,true>
* Set of file name+line+node hashes where the union type is known.
*/
private static $known_method_set = [];
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return UnknownClassElementAccessVisitor::class;
}
private static function generateKey(FileRef $context, int $lineno, string $node_string): string
{
// Sadly, the node can either be from the parse phase or any analysis phase, so we can't use spl_object_id.
return $context->getFile() . ':' . $lineno . ':' . sha1($node_string);
}
/**
* Emit an issue if the object of the method call isn't found later/earlier
*/
public static function deferEmittingMethodIssue(Context $context, Node $node, UnionType $union_type): void
{
$node_string = ASTReverter::toShortString($node);
$key = self::generateKey($context, $node->lineno, $node_string);
if (isset(self::$known_method_set[$key])) {
return;
}
self::$deferred_unknown_method_issues[$key][] = [(clone $context)->withLineNumberStart($node->lineno), $node_string, $union_type];
}
/**
* Prevent this plugin from warning about $node_string at this file and line
*/
public static function preventMethodIssueWarning(Context $context, Node $node): void
{
$node_string = ASTReverter::toShortString($node);
$key = self::generateKey($context, $node->lineno, $node_string);
self::$known_method_set[$key] = true;
unset(self::$deferred_unknown_method_issues[$key]);
}
public function finalizeProcess(CodeBase $code_base): void
{
foreach (self::$deferred_unknown_method_issues as $issues) {
foreach ($issues as [$context, $node_string, $union_type]) {
$this->emitIssue(
$code_base,
$context,
self::UnknownObjectMethodCall,
'Phan could not infer any class/interface types for the object of the method call {CODE} - inferred a type of {TYPE}',
[
$node_string,
$union_type->isEmpty() ? '(empty union type)' : $union_type
]
);
}
}
}
}
/**
* This visitor analyzes node kinds that can be the root of expressions
* containing duplicate expressions, and is called on nodes in post-order.
*/
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
*/
public function visitMethodCall(Node $node): void
{
try {
// Fetch the list of valid classes, and warn about any undefined classes.
// (We have more specific issue types such as PhanNonClassMethodCall below, don't emit PhanTypeExpected*)
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr']);
} catch (Exception $_) {
// Phan should already throw for this
return;
}
foreach ($union_type->getTypeSet() as $type) {
if ($type->hasObjectWithKnownFQSEN()) {
UnknownClassElementAccessPlugin::preventMethodIssueWarning($this->context, $node);
return;
}
}
if (Issue::shouldSuppressIssue($this->code_base, $this->context, UnknownClassElementAccessPlugin::UnknownObjectMethodCall, $node->lineno, [])) {
return;
}
UnknownClassElementAccessPlugin::deferEmittingMethodIssue($this->context, $node, $union_type);
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnknownClassElementAccessPlugin();

View File

@@ -0,0 +1,398 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\Element\AddressableElement;
use Phan\Language\Element\Func;
use Phan\Language\Element\Method;
use Phan\Language\Element\Property;
use Phan\Language\FQSEN;
use Phan\Language\Type;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\NullType;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AnalyzePropertyCapability;
use Phan\PluginV3\FinalizeProcessCapability;
use Phan\Suggestion;
/**
* This file checks if any elements in the codebase have undeclared types.
*/
class UnknownElementTypePlugin extends PluginV3 implements
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
AnalyzePropertyCapability,
FinalizeProcessCapability
{
/**
* A list of closures to execute before emitting issues.
* @var array<string,Closure(CodeBase):void>
*/
private $deferred_checks = [];
/**
* Returns true for array, ?array, and array|null
*/
private static function isRegularArray(UnionType $type): bool
{
return $type->hasTypeMatchingCallback(static function (Type $type): bool {
return get_class($type) === ArrayType::class;
}) && !$type->hasTypeMatchingCallback(static function (Type $type): bool {
return get_class($type) !== ArrayType::class && !($type instanceof NullType);
});
}
/**
* @param CodeBase $code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
* @override
*/
public function analyzeMethod(
CodeBase $code_base,
Method $method
): void {
if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) {
return;
}
$this->performChecks(
$method,
'PhanPluginUnknownMethodReturnType',
'Method {METHOD} has no declared or inferred return type',
'PhanPluginUnknownArrayMethodReturnType',
'Method {METHOD} has a return type of array, but does not specify any key types or value types'
);
// NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace
$warning_closures = [];
$inferred_types = [];
foreach ($method->getParameterList() as $i => $parameter) {
if ($parameter->getUnionType()->isEmpty()) {
$warning_closures[$i] = static function () use ($code_base, $parameter, $method, $i, &$inferred_types): void {
$suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null);
self::emitIssueAndSuggestion(
$code_base,
$parameter->createContext($method),
'PhanPluginUnknownMethodParamType',
'Method {METHOD} has no declared or inferred parameter type for ${PARAMETER}',
[(string)$method->getFQSEN(), $parameter->getName()],
$suggestion
);
};
} elseif (self::isRegularArray($parameter->getUnionType())) {
$warning_closures[$i] = static function () use ($code_base, $parameter, $method, $i, &$inferred_types): void {
$suggestion = self::suggestionFromUnionTypeNotRegularArray($inferred_types[$i] ?? null);
self::emitIssueAndSuggestion(
$code_base,
$parameter->createContext($method),
'PhanPluginUnknownArrayMethodParamType',
'Method {METHOD} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types',
[(string)$method->getFQSEN(), $parameter->getName()],
$suggestion
);
};
}
}
if (!$warning_closures) {
return;
}
$this->deferred_checks[$method->getFQSEN()->__toString()] = static function (CodeBase $_) use ($warning_closures): void {
foreach ($warning_closures as $cb) {
$cb();
}
};
$method->addFunctionCallAnalyzer(
/**
* @param list<mixed> $args
*/
static function (CodeBase $code_base, Context $context, Method $unused_method, array $args, Node $unused_node) use ($warning_closures, &$inferred_types): void {
foreach ($warning_closures as $i => $_) {
$parameter = $args[$i] ?? null;
if ($parameter !== null) {
$parameter_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $parameter);
if ($parameter_type->isEmpty()) {
return;
}
$combined_type = $inferred_types[$i] ?? null;
if ($combined_type instanceof UnionType) {
$combined_type = $combined_type->withUnionType($parameter_type);
} else {
$combined_type = $parameter_type;
}
$inferred_types[$i] = $combined_type;
}
}
},
$this
);
}
private static function suggestionFromUnionType(?UnionType $type): ?Suggestion
{
if (!$type || $type->isEmpty()) {
return null;
}
$type = $type->withFlattenedArrayShapeOrLiteralTypeInstances()->asNormalizedTypes();
return Suggestion::fromString("Types inferred after analysis: $type");
}
private static function suggestionFromUnionTypeNotRegularArray(?UnionType $type): ?Suggestion
{
if (!$type || $type->isEmpty()) {
return null;
}
if (self::isRegularArray($type)) {
return null;
}
$type = $type->withFlattenedArrayShapeOrLiteralTypeInstances()->asNormalizedTypes();
return Suggestion::fromString("Types inferred after analysis: $type");
}
private function performChecks(
AddressableElement $element,
string $issue_type_for_empty,
string $message_for_empty,
string $issue_type_for_unknown_array,
string $message_for_unknown_array
): void {
$union_type = $element->getUnionType();
if ($union_type->isEmpty()) {
$issue_type = $issue_type_for_empty;
$message = $message_for_empty;
} elseif (self::isRegularArray($union_type)) {
$issue_type = $issue_type_for_unknown_array;
$message = $message_for_unknown_array;
} else {
return;
}
$this->deferred_checks[$issue_type . ':' . $element->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($element, $issue_type, $message, $issue_type_for_unknown_array): void {
$new_union_type = $element->getUnionType();
$suggestion = null;
if (!$new_union_type->isEmpty()) {
if ($issue_type !== $issue_type_for_unknown_array || !self::isRegularArray($new_union_type)) {
$suggestion = self::suggestionFromUnionType($new_union_type);
}
}
self::emitIssueAndSuggestion(
$code_base,
$element->getContext(),
$issue_type,
$message,
[$element->getRepresentationForIssue()],
$suggestion
);
};
}
/**
* @param list<string|FQSEN> $args
*/
private static function emitIssueAndSuggestion(
CodeBase $code_base,
Context $context,
string $issue_type,
string $message,
array $args,
?Suggestion $suggestion
): void {
self::emitIssue(
$code_base,
$context,
$issue_type,
$message,
$args,
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_B,
Issue::TYPE_ID_UNKNOWN,
$suggestion
);
}
/**
* @param CodeBase $code_base
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
// NOTE: Placeholders can be found in \Phan\Issue::uncolored_format_string_for_replace
if ($function->getUnionType()->isEmpty()) {
if ($function->getFQSEN()->isClosure()) {
$issue = 'PhanPluginUnknownClosureReturnType';
$message = 'Closure {FUNCTION} has no declared or inferred return type';
} else {
$issue = 'PhanPluginUnknownFunctionReturnType';
$message = 'Function {FUNCTION} has no declared or inferred return type';
}
$this->deferred_checks[$issue . ':' . $function->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($function, $issue, $message): void {
$new_union_type = $function->getUnionType();
$suggestion = self::suggestionFromUnionType($new_union_type);
self::emitIssue(
$code_base,
$function->getContext(),
$issue,
$message,
[$function->getRepresentationForIssue()],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_B,
Issue::TYPE_ID_UNKNOWN,
$suggestion
);
};
} elseif (self::isRegularArray($function->getUnionType())) {
if ($function->getFQSEN()->isClosure()) {
$issue = 'PhanPluginUnknownArrayClosureReturnType';
$message = 'Closure {FUNCTION} has a return type of array, but does not specify key or value types';
} else {
$issue = 'PhanPluginUnknownArrayFunctionReturnType';
$message = 'Function {FUNCTION} has a return type of array, but does not specify key or value types';
}
$this->deferred_checks[$issue . ':' . $function->getFQSEN()->__toString()] = static function (CodeBase $code_base) use ($function, $issue, $message): void {
$new_union_type = $function->getUnionType();
$suggestion = self::suggestionFromUnionTypeNotRegularArray($new_union_type);
self::emitIssue(
$code_base,
$function->getContext(),
$issue,
$message,
[$function->getRepresentationForIssue()],
Issue::SEVERITY_NORMAL,
Issue::REMEDIATION_B,
Issue::TYPE_ID_UNKNOWN,
$suggestion
);
};
}
$warning_closures = [];
$inferred_types = [];
foreach ($function->getParameterList() as $i => $parameter) {
if ($parameter->getUnionType()->isEmpty()) {
if ($function->getFQSEN()->isClosure()) {
$issue = 'PhanPluginUnknownClosureParamType';
$message = 'Closure {FUNCTION} has no declared or inferred parameter type for ${PARAMETER}';
} else {
$issue = 'PhanPluginUnknownFunctionParamType';
$message = 'Function {FUNCTION} has no declared or inferred parameter type for ${PARAMETER}';
}
$warning_closures[$i] = static function () use ($code_base, $issue, $message, $parameter, $function, $i, &$inferred_types): void {
$suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null);
self::emitIssueAndSuggestion(
$code_base,
$parameter->createContext($function),
$issue,
$message,
[$function->getNameForIssue(), $parameter->getName()],
$suggestion
);
};
} elseif (self::isRegularArray($parameter->getUnionType())) {
if ($function->getFQSEN()->isClosure()) {
$issue = 'PhanPluginUnknownArrayClosureParamType';
$message = 'Closure {FUNCTION} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types';
} else {
$issue = 'PhanPluginUnknownArrayFunctionParamType';
$message = 'Function {FUNCTION} has a parameter type of array for ${PARAMETER}, but does not specify any key types or value types';
}
$warning_closures[$i] = static function () use ($code_base, $issue, $message, $parameter, $function, $i, &$inferred_types): void {
$suggestion = self::suggestionFromUnionType($inferred_types[$i] ?? null);
self::emitIssueAndSuggestion(
$code_base,
$parameter->createContext($function),
$issue,
$message,
[$function->getNameForIssue(), $parameter->getName()],
$suggestion
);
};
}
}
if (!$warning_closures) {
return;
}
$this->deferred_checks[$function->getFQSEN()->__toString()] = static function (CodeBase $_) use ($warning_closures): void {
foreach ($warning_closures as $cb) {
$cb();
}
};
$function->addFunctionCallAnalyzer(
/**
* @param list<mixed> $args
*/
static function (CodeBase $code_base, Context $context, Func $unused_function, array $args, Node $unused_node) use ($warning_closures, &$inferred_types): void {
foreach ($warning_closures as $i => $_) {
$parameter = $args[$i] ?? null;
if ($parameter !== null) {
$parameter_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $parameter);
if ($parameter_type->isEmpty()) {
return;
}
$combined_type = $inferred_types[$i] ?? null;
if ($combined_type instanceof UnionType) {
$combined_type = $combined_type->withUnionType($parameter_type);
} else {
$combined_type = $parameter_type;
}
$inferred_types[$i] = $combined_type;
}
}
},
$this
);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
* @override
*/
public function analyzeProperty(
CodeBase $code_base,
Property $property
): void {
if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) {
return;
}
$this->performChecks(
$property,
'PhanPluginUnknownPropertyType',
'Property {PROPERTY} has an initial type that cannot be inferred',
'PhanPluginUnknownArrayPropertyType',
'Property {PROPERTY} has an array type, but does not specify any key types or value types'
);
}
public function finalizeProcess(CodeBase $code_base): void
{
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 = [];
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnknownElementTypePlugin();

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\BlockExitStatusChecker;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This file checks for syntactically unreachable statements in
* the global scope or function bodies.
*
* It hooks into one event:
*
* - getPostAnalyzeNodeVisitorClassName
* This method returns a class 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
*/
final class UnreachableCodePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return string - The name of the visitor that will be called (formerly analyzeNode)
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return UnreachableCodeVisitor::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.
*/
final class UnreachableCodeVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should NOT implement visit(), unless they need to.
private const DECL_KIND_SET = [
\ast\AST_CLASS => true,
\ast\AST_FUNC_DECL => true,
\ast\AST_CONST => true,
];
/**
* @param Node $node
* A node to analyze
* @override
*/
public function visitStmtList(Node $node): void
{
$child_nodes = $node->children;
$last_node_index = count($child_nodes) - 1;
foreach ($child_nodes as $i => $node) {
if (!\is_int($i)) {
throw new AssertionError("Expected integer index");
}
if ($i >= $last_node_index) {
break;
}
if (!($node instanceof Node)) {
continue;
}
if (!BlockExitStatusChecker::willUnconditionallySkipRemainingStatements($node)) {
continue;
}
// Skip over empty statements and scalar statements.
for ($j = $i + 1; array_key_exists($j, $child_nodes); $j++) {
$next_node = $child_nodes[$j];
if (!($next_node instanceof Node && $next_node->lineno > 0)) {
continue;
}
if (array_key_exists($next_node->kind, self::DECL_KIND_SET)) {
if ($this->context->isInGlobalScope()) {
continue;
}
}
if ($this->context->isInFunctionLikeScope()) {
if ($this->context->getFunctionLikeInScope($this->code_base)->checkHasSuppressIssueAndIncrementCount('PhanPluginUnreachableCode')) {
// don't emit the below issue.
break;
}
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($next_node->lineno),
'PhanPluginUnreachableCode',
'Unreachable statement detected',
[]
);
break;
}
break;
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnreachableCodePlugin();

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();

View File

@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\Element\AddressableElement;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\Method;
use Phan\Language\Element\Property;
use Phan\Plugin\ConfigPluginSet;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeClassCapability;
use Phan\PluginV3\AnalyzeFunctionCapability;
use Phan\PluginV3\AnalyzeMethodCapability;
use Phan\PluginV3\AnalyzePropertyCapability;
use Phan\PluginV3\BeforeAnalyzeFileCapability;
use Phan\PluginV3\FinalizeProcessCapability;
use Phan\PluginV3\SuppressionCapability;
/**
* Check for unused (at)suppress annotations.
*
* NOTE! This plugin only produces correct results when Phan
* is run on a single processor (via the `-j1` flag).
*/
class UnusedSuppressionPlugin extends PluginV3 implements
BeforeAnalyzeFileCapability,
AnalyzeClassCapability,
AnalyzeFunctionCapability,
AnalyzeMethodCapability,
AnalyzePropertyCapability,
FinalizeProcessCapability
{
/**
* @var AddressableElement[] - Analysis is postponed until finalizeProcess.
* Issues may have been emitted after `$this->analyze*()` were called,
* which is why those methods postpone the check until analysis is finished.
*
* Also, looping over all elements again would be slow.
*
* These are currently unique, even when quick_mode is false.
*/
private $elements_for_postponed_analysis = [];
/**
* @var string[] a list of files where checks for unused suppressions was postponed
* (Because of non-quick mode, we may emit issues in a file after analysis has run on that file)
*/
private $files_for_postponed_analysis = [];
/**
* @var array<string,array<string,array<string,array<int,int>>>> stores the suppressions for active plugins
* maps plugin class to
* file name to
* issue type to
* unique list of line numbers of suppressions
*/
private $plugin_active_suppression_list = [];
/**
* @param CodeBase $code_base
* The code base in which the element exists
*
* @param AddressableElement $element
* Any element such as function, method, class
* (which has an FQSEN)
*/
private static function analyzeAddressableElement(
CodeBase $code_base,
AddressableElement $element
): void {
// Get the set of suppressed issues on the element
$suppress_issue_list =
$element->getSuppressIssueList();
if (\array_key_exists('UnusedSuppression', $suppress_issue_list)) {
// The element's doc comment is suppressing everything emitted by this plugin.
return;
}
// Check to see if any are unused
foreach ($suppress_issue_list as $issue_type => $use_count) {
if (0 !== $use_count) {
continue;
}
if (in_array($issue_type, self::getUnusedSuppressionIgnoreList(), true)) {
continue;
}
self::emitIssue(
$code_base,
$element->getContext(),
'UnusedSuppression',
"Element {FUNCTIONLIKE} suppresses issue {ISSUETYPE} but does not use it",
[(string)$element->getFQSEN(), $issue_type]
);
}
}
private function postponeAnalysisOfElement(AddressableElement $element): void
{
if (count($element->getSuppressIssueList()) === 0) {
// There are no suppressions, so there's no reason to check this
return;
}
$this->elements_for_postponed_analysis[] = $element;
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the class exists
*
* @param Clazz $class
* A class being analyzed
* @override
*/
public function analyzeClass(
CodeBase $code_base,
Clazz $class
): void {
$this->postponeAnalysisOfElement($class);
}
/**
* @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 $code_base,
Method $method
): void {
// Ignore methods inherited by subclasses
if ($method->getFQSEN() !== $method->getRealDefiningFQSEN()) {
return;
}
$this->postponeAnalysisOfElement($method);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $code_base,
Func $function
): void {
$this->postponeAnalysisOfElement($function);
}
/**
* @param CodeBase $code_base @unused-param
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
* @override
*/
public function analyzeProperty(
CodeBase $code_base,
Property $property
): void {
if ($property->getFQSEN() !== $property->getRealDefiningFQSEN()) {
return;
}
$this->elements_for_postponed_analysis[] = $property;
}
/**
* NOTE! This plugin only produces correct results when Phan
* is run on a single processor (via the `-j1` flag).
* Putting this hook in finalizeProcess() just minimizes the incorrect result counts.
* @override
*/
public function finalizeProcess(CodeBase $code_base): void
{
foreach ($this->elements_for_postponed_analysis as $element) {
self::analyzeAddressableElement($code_base, $element);
}
$this->analyzePluginSuppressions($code_base);
}
private function analyzePluginSuppressions(CodeBase $code_base): void
{
$suppression_plugin_set = ConfigPluginSet::instance()->getSuppressionPluginSet();
if (count($suppression_plugin_set) === 0) {
return;
}
foreach ($this->files_for_postponed_analysis as $file_path) {
foreach ($suppression_plugin_set as $plugin) {
$this->analyzePluginSuppressionsForFile($code_base, $plugin, $file_path);
}
}
}
/**
* @return list<string>
*/
private static function getUnusedSuppressionIgnoreList(): array
{
return Config::getValue('plugin_config')['unused_suppression_ignore_list'] ?? [];
}
private static function getReportOnlyWhitelisted(): bool
{
return Config::getValue('plugin_config')['unused_suppression_whitelisted_only'] ?? false;
}
private static function shouldReportUnusedSuppression(string $issue_type): bool
{
$ignore_list = self::getUnusedSuppressionIgnoreList();
$only_whitelisted = self::getReportOnlyWhitelisted();
$issue_whitelist = Config::getValue('whitelist_issue_types') ?? [];
return !in_array($issue_type, $ignore_list, true) &&
(!$only_whitelisted || in_array($issue_type, $issue_whitelist, true));
}
private function analyzePluginSuppressionsForFile(CodeBase $code_base, SuppressionCapability $plugin, string $relative_file_path): void
{
$absolute_file_path = Config::projectPath($relative_file_path);
$plugin_class = \get_class($plugin);
$name_pos = \strrpos($plugin_class, '\\');
if ($name_pos !== false) {
$plugin_name = \substr($plugin_class, $name_pos + 1);
} else {
$plugin_name = $plugin_class;
}
$plugin_suppressions = $plugin->getIssueSuppressionList($code_base, $absolute_file_path);
$plugin_successful_suppressions = $this->plugin_active_suppression_list[$plugin_class][$absolute_file_path] ?? null;
foreach ($plugin_suppressions as $issue_type => $line_list) {
foreach ($line_list as $lineno => $lineno_of_comment) {
if (isset($plugin_successful_suppressions[$issue_type][$lineno])) {
continue;
}
// TODO: finish letting plugins suppress UnusedSuppression on other plugins
$issue_kind = 'UnusedPluginSuppression';
$message = 'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} on this line but this suppression is unused or suppressed elsewhere';
if ($lineno === 0) {
$issue_kind = 'UnusedPluginFileSuppression';
$message = 'Plugin {STRING_LITERAL} suppresses issue {ISSUETYPE} in this file but this suppression is unused or suppressed elsewhere';
}
if (isset($plugin_suppressions['UnusedSuppression'][$lineno_of_comment])) {
continue;
}
if (isset($plugin_suppressions[$issue_kind][$lineno_of_comment])) {
continue;
}
if (!self::shouldReportUnusedSuppression($issue_type)) {
continue;
}
self::emitIssue(
$code_base,
(new Context())->withFile($relative_file_path)->withLineNumberStart($lineno_of_comment),
$issue_kind,
$message,
[$plugin_name, $issue_type]
);
}
}
return;
}
/**
* @unused-param $code_base
* @unused-param $file_contents
* @unused-param $node
*/
public function beforeAnalyzeFile(
CodeBase $code_base,
Context $context,
string $file_contents,
Node $node
): void {
$file = $context->getFile();
$this->files_for_postponed_analysis[$file] = $file;
}
/**
* Record the fact that $plugin caused suppressions in $file_path for issue $issue_type due to an annotation around $line
* @internal
*/
public function recordPluginSuppression(
SuppressionCapability $plugin,
string $file_path,
string $issue_type,
int $line
): void {
$file_name = Config::projectPath($file_path);
$plugin_class = \get_class($plugin);
$this->plugin_active_suppression_list[$plugin_class][$file_name][$issue_type][$line] = $line;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnusedSuppressionPlugin();

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
// Moved to src/Phan/Plugin/Internal/UseReturnValuePlugin.php for autoloading convenience.
// This may become a core part of Phan.
use Phan\Plugin\Internal\UseReturnValuePlugin;
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UseReturnValuePlugin();

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\IssueInstance;
use Phan\Language\Context;
use Phan\Library\FileCacheEntry;
use Phan\Plugin\Internal\IssueFixingPlugin\FileEditSet;
use Phan\PluginV3;
use Phan\PluginV3\AfterAnalyzeFileCapability;
use Phan\PluginV3\AutomaticFixCapability;
/**
* This plugin checks the whitespace in analyzed PHP files for (1) tabs, (2) windows newlines, and (3) trailing whitespace.
*/
class WhitespacePlugin extends PluginV3 implements
AfterAnalyzeFileCapability,
AutomaticFixCapability
{
public const CarriageReturn = 'PhanPluginWhitespaceCarriageReturn';
public const Tab = 'PhanPluginWhitespaceTab';
public const WhitespaceTrailing = 'PhanPluginWhitespaceTrailing';
private static function calculateLine(string $contents, int $byte_offset): int
{
return 1 + substr_count($contents, "\n", 0, $byte_offset);
}
/**
* @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 {
if (!preg_match('/[\r\t]|[ \t]\r?$/mS', $file_contents)) {
// Typical case: no errors
return;
}
$newline_position = strpos($file_contents, "\r");
if ($newline_position !== false) {
self::emitIssue(
$code_base,
(clone $context)->withLineNumberStart(self::calculateLine($file_contents, $newline_position)),
self::CarriageReturn,
'The first occurrence of a carriage return ("\r") was seen here. Running "dos2unix" can fix that.'
);
}
$tab_position = strpos($file_contents, "\t");
if ($tab_position !== false) {
self::emitIssue(
$code_base,
(clone $context)->withLineNumberStart(self::calculateLine($file_contents, $tab_position)),
self::Tab,
'The first occurrence of a tab was seen here. Running "expand" can fix that.'
);
}
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])),
self::WhitespaceTrailing,
'The first occurrence of trailing whitespace was seen here.'
);
}
}
/**
* @return array<string,Closure(CodeBase,FileCacheEntry,IssueInstance):(?FileEditSet)>
*/
public function getAutomaticFixers(): array
{
return require(__DIR__ . '/WhitespacePlugin/fixers.php');
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new WhitespacePlugin();

View File

@@ -0,0 +1,119 @@
<?php
/**
* Fixers for --automatic-fix and WhitespacePlugin
*/
declare(strict_types=1);
use Phan\CodeBase;
use Phan\Config;
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;
return [
/**
* @return ?FileEditSet
*/
WhitespacePlugin::Tab => static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet {
$spaces_per_tab = (int)(Config::getValue('plugin_config')['spaces_per_tab'] ?? 4);
if ($spaces_per_tab <= 0) {
$spaces_per_tab = 4;
}
/**
* @return Generator<FileEdit>
*/
$compute_edits = static function (string $line_contents, int $byte_offset) use ($spaces_per_tab): Generator {
preg_match_all('/\t+/', $line_contents, $matches, PREG_OFFSET_CAPTURE);
$effective_space_count = 0;
$prev_end = 0; // byte offset of previous end of tab sequences
// run the equivalent of unix's 'unexpand'
foreach ($matches[0] as $match) {
$column = $match[1]; // 0-based column
$effective_space_count += $column - $prev_end;
$len = strlen($match[0]);
$prev_end = $column + $len;
$replacement_space_count = ($len - 1) * $spaces_per_tab + ($spaces_per_tab - ($effective_space_count % $spaces_per_tab));
$start = $byte_offset + $match[1];
yield new FileEdit($start, $start + $len, str_repeat(' ', $replacement_space_count));
}
};
IssueFixer::debug("Calling tab fixer for {$instance->getFile()}\n");
$raw_contents = $contents->getContents();
$byte_offset = 0;
$edits = [];
foreach (explode("\n", $raw_contents) as $line_contents) {
if (strpos($line_contents, "\t") !== false) {
foreach ($compute_edits(rtrim($line_contents), $byte_offset) as $edit) {
$edits[] = $edit;
}
}
$byte_offset += strlen($line_contents) + 1;
}
if (!$edits) {
return null;
}
IssueFixer::debug("Resulting edits for tab fixes: " . json_encode($edits) . "\n");
//$line = $instance->getLine();
return new FileEditSet($edits);
},
/**
* @return ?FileEditSet
*/
WhitespacePlugin::WhitespaceTrailing => static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet {
IssueFixer::debug("Calling trailing whitespace fixer {$instance->getFile()}\n");
$raw_contents = $contents->getContents();
$byte_offset = 0;
$edits = [];
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+$/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
$edits[] = new FileEdit($offset, $offset + $len);
}
$byte_offset = $new_byte_offset;
}
if (!$edits) {
return null;
}
IssueFixer::debug("Resulting edits for trailing whitespace: " . json_encode($edits) . "\n");
//$line = $instance->getLine();
return new FileEditSet($edits);
},
/**
* @return ?FileEditSet
*/
WhitespacePlugin::CarriageReturn => static function (CodeBase $unused_code_base, FileCacheEntry $contents, IssueInstance $instance): ?FileEditSet {
IssueFixer::debug("Calling trailing whitespace fixer {$instance->getFile()}\n");
$raw_contents = $contents->getContents();
$byte_offset = 0;
$edits = [];
foreach (explode("\n", $raw_contents) as $line_contents) {
if (substr($line_contents, -1) === "\r") {
$offset = $byte_offset + strlen($line_contents) - 1;
// Remove the byte with the carriage return
$edits[] = new FileEdit($offset, $offset + 1);
}
$byte_offset += strlen($line_contents) + 1;
}
if (!$edits) {
return null;
}
IssueFixer::debug("Resulting edits for trailing whitespace: " . json_encode($edits) . "\n");
//$line = $instance->getLine();
return new FileEditSet($edits);
},
];

View File

@@ -0,0 +1,2 @@
Add any stubs to this directory for code that you don't want to parse, but still want
to expose to phan while analyzing the phan codebase

View File

@@ -0,0 +1,89 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension mbstring@7.3.8-dev
namespace {
function mb_check_encoding($var = null, $encoding = null) {}
function mb_chr($cp, $encoding = null) {}
function mb_convert_case($sourcestring, $mode, $encoding = null) {}
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_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) {}
function mb_encode_numericentity($string, $convmap, $encoding = null, $is_hex = null) {}
function mb_encoding_aliases($encoding) {}
function mb_ereg($pattern, $string, &$registers = null) {}
function mb_ereg_match($pattern, $string, $option = null) {}
function mb_ereg_replace($pattern, $replacement, $string, $option = null) {}
function mb_ereg_replace_callback($pattern, $callback, $string, $option = null) {}
function mb_ereg_search($pattern = null, $option = null) {}
function mb_ereg_search_getpos() {}
function mb_ereg_search_getregs() {}
function mb_ereg_search_init($string, $pattern = null, $option = null) {}
function mb_ereg_search_pos($pattern = null, $option = null) {}
function mb_ereg_search_regs($pattern = null, $option = null) {}
function mb_ereg_search_setpos($position) {}
function mb_eregi($pattern, $string, &$registers = null) {}
function mb_eregi_replace($pattern, $replacement, $string, $option = null) {}
function mb_get_info($type = null) {}
function mb_http_input($type = null) {}
function mb_http_output($encoding = null) {}
function mb_internal_encoding($encoding = null) {}
function mb_language($language = null) {}
function mb_list_encodings() {}
function mb_ord($str, $encoding = null) {}
function mb_output_handler($contents, $status) {}
function mb_parse_str($encoded_string, &$result = null) {}
function mb_preferred_mime_name($encoding) {}
function mb_regex_encoding($encoding = null) {}
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_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) {}
function mb_stristr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strlen($str, $encoding = null) {}
function mb_strpos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strrchr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strrichr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strripos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strrpos($haystack, $needle, $offset = null, $encoding = null) {}
function mb_strstr($haystack, $needle, $part = null, $encoding = null) {}
function mb_strtolower($sourcestring, $encoding = null) {}
function mb_strtoupper($sourcestring, $encoding = null) {}
function mb_strwidth($str, $encoding = null) {}
function mb_substitute_character($substchar = null) {}
function mb_substr($str, $start, $length = null, $encoding = null) {}
function mb_substr_count($haystack, $needle, $encoding = null) {}
function mbereg($pattern, $string, &$registers = null) {}
function mbereg_match($pattern, $string, $option = null) {}
function mbereg_replace($pattern, $replacement, $string, $option = null) {}
function mbereg_search($pattern = null, $option = null) {}
function mbereg_search_getpos() {}
function mbereg_search_getregs() {}
function mbereg_search_init($string, $pattern = null, $option = null) {}
function mbereg_search_pos($pattern = null, $option = null) {}
function mbereg_search_regs($pattern = null, $option = null) {}
function mbereg_search_setpos($position) {}
function mberegi($pattern, $string, &$registers = null) {}
function mberegi_replace($pattern, $replacement, $string, $option = null) {}
function mbregex_encoding($encoding = null) {}
function mbsplit($pattern, $string, $limit = null) {}
const MB_CASE_FOLD = 3;
const MB_CASE_FOLD_SIMPLE = 7;
const MB_CASE_LOWER = 1;
const MB_CASE_LOWER_SIMPLE = 5;
const MB_CASE_TITLE = 2;
const MB_CASE_TITLE_SIMPLE = 6;
const MB_CASE_UPPER = 0;
const MB_CASE_UPPER_SIMPLE = 4;
const MB_OVERLOAD_MAIL = 1;
const MB_OVERLOAD_REGEX = 4;
const MB_OVERLOAD_STRING = 2;
}

34
vendor/phan/phan/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,34 @@
# Code of Conduct
We are committed to fostering a welcoming community. Any participant and
contributor is required to adhere to Phans Code of Conduct, which is based on [Etsys Code of Conduct](http://etsy.github.io/codeofconduct.html).
We are proud to say that we have an open source community that is welcoming, positive, and respectful to all participants,
regardless of gender, gender identity, gender presentation, sexual orientation, race, age,
disability, physical appearance, national origin, ethnicity, religion or any other protected status.
We believe our diversity makes us stronger and enriches the work we do.
In an effort to maintain a high level of respect and acceptance in our community,
we ask that anyone who participates in the Phan projects open source community (https://github.com/phan) follow this Code of Conduct.
By participating in the Phan projects open source community, you agree to:
- Treat each other with respect. This space and its related channels (such as pull requests, IRC channels, or mailing lists) may not be used to disparage another participant.
- Respect each others privacy. Do not post another participants private or personally identifiable information in public spaces without their permission.
- Respect each others boundaries. Do not use this space or any of its related channels to harass another participant.
- Be thoughtful about what you say. Do not post content that promotes, supports, or glorifies hatred.
- Be helpful. Offer constructive criticism or voice a dissenting opinion, but dont be mean. Do not post threats of violence against others or promote or encourage others to engage in violence or illegal activity.
- Be lawful. Dont post, link or attach content that violates third party intellectual property rights, has malicious intent or interferes with the security of the Phan project or contributors, or violates the law.
Examples of unacceptable behavior include, but are not limited to: offensive comments, verbal threats or demands, sexualized images, intimidation, stalking, sustained disruption of discussions, unwelcome sexual attention or approaches, violence, threats of violence, or violent or discriminatory language directed against another person or group.
The safety of everyone in the community is of paramount importance. This means this is not the appropriate forum for:
- Direct communication with a contributor once they have requested that you leave them alone (except for standard automated notifications as part of the contribution process); or,
- Engaging in inflammatory debates, doxxing or trolling, regardless of subject.
Discrimination, harassment or individual or coordinated attacks on contributors of any kind, whether directly or via code language will not be tolerated.
If you are being harassed or discriminated against, if you notice that someone else is being harassed or discriminated against, or if you have any other concerns, please contact a member of Phans team.
If a participant violates this Code of Conduct, the Phan projects members may take any action we deem appropriate,
including warning the offender,
blocking them from the current project or all Phan projects temporarily or permanently, and/or contacting law enforcement.

37
vendor/phan/phan/DCO.txt vendored Normal file
View File

@@ -0,0 +1,37 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

31
vendor/phan/phan/LICENSE vendored Normal file
View File

@@ -0,0 +1,31 @@
The MIT License (MIT)
Copyright (c) 2015 Rasmus Lerdorf
Copyright (c) 2015 Andrew Morrison
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
Third party licenses are below
--------------------------------------------------------------------------------
Parts of Phan's code were based on code from the below repositories:
- LICENSE.LANGUAGE_SERVER (ISC License) https://github.com/felixfbecker/php-language-server.
- LICENSE.PHP_PARSER https://github.com/nikic/php-parser

View File

@@ -0,0 +1,20 @@
The code implementing support for the open Language Server Protocol
is based on code from https://github.com/felixfbecker/php-language-server.
Phan's language server implementation also uses the composer dependency felixfbecker/advanced-json-rpc
ISC License
Copyright (c) 2016, Felix Frederick Becker
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,5 @@
Some of Phan's type signature info (e.g. for functions, methods, etc.)
and documentation (for classes, functions, constants, etc.)
was extracted from https://github.com/JetBrains/phpstorm-stubs
phpstorm-stubs is available under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)

34
vendor/phan/phan/LICENSE.PHP_PARSER vendored Normal file
View File

@@ -0,0 +1,34 @@
Parts of the code for this are based on nikic/php-parser (For \Phan\AST\TolerantASTConverter\String_).
Those parts have the below license:
Copyright (c) 2011-2018 by Nikita Popov.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
vendor/phan/phan/MAINTAINERS.md vendored Normal file
View File

@@ -0,0 +1,14 @@
Phan is maintained by the following people.
All maintainers must agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt).
# Maintainers
**Andrew S. Morrison**<br />
As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt).
**Rasmus Lerdorf**<br />
As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt).
**Tyson Andre**<br />
As a maintainer of Phan, I agree to the [Developer Certificate of Origin](https://github.com/phan/phan/blob/38bf1fd15e39bc668084accb8caab21f09ff75ba/DCO.txt).

4496
vendor/phan/phan/NEWS.md vendored Normal file

File diff suppressed because it is too large Load Diff

297
vendor/phan/phan/README.md vendored Normal file
View File

@@ -0,0 +1,297 @@
Phan is a static analyzer for PHP that prefers to minimize false-positives. Phan attempts to prove incorrectness rather than correctness.
Phan looks for common issues and will verify type compatibility on various operations when type
information is available or can be deduced. Phan has a good (but not comprehensive) understanding of flow control
and can track values in a few use cases (e.g. arrays, integers, and strings).
[![Build Status](https://dev.azure.com/tysonandre775/phan/_apis/build/status/phan.phan?branchName=v5)](https://dev.azure.com/tysonandre775/phan/_build/latest?definitionId=3&branchName=v5)
[![Build Status](https://github.com/phan/phan/actions/workflows/main.yml/badge.svg?branch=v5)](https://github.com/phan/phan/actions/workflows/main.yml?query=branch%3Av5)
[![Build Status (Windows)](https://ci.appveyor.com/api/projects/status/github/phan/phan?branch=v5&svg=true)](https://ci.appveyor.com/project/TysonAndre/phan/branch/v5)
[![Gitter](https://badges.gitter.im/phan/phan.svg)](https://gitter.im/phan/phan?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Latest Stable Version](https://img.shields.io/packagist/v/phan/phan.svg)](https://packagist.org/packages/phan/phan)
[![License](https://img.shields.io/packagist/l/phan/phan.svg)](https://github.com/phan/phan/blob/v5/LICENSE)
# Getting Started
The easiest way to use Phan is via Composer.
```
composer require phan/phan
```
With Phan installed, you'll want to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file) in
your project to tell Phan how to analyze your source code. Once configured, you can run it via `./vendor/bin/phan`.
Phan 5 depends on PHP 7.2+ with the [php-ast](https://github.com/nikic/php-ast) extension (1.0.16+ is preferred) and supports analyzing PHP version 7.0-8.1 syntax.
Installation instructions for php-ast can be found [here](https://github.com/nikic/php-ast#installation).
(Phan can be used without php-ast by using the CLI option `--allow-polyfill-parser`, but there are slight differences in the parsing of doc comments)
* **Alternative Installation Methods**<br />
See [Getting Started](https://github.com/phan/phan/wiki/Getting-Started) for alternative methods of using
Phan and details on how to configure Phan for your project.<br />
* **Incrementally Strengthening Analysis**<br />
Take a look at [Incrementally Strengthening Analysis](https://github.com/phan/phan/wiki/Incrementally-Strengthening-Analysis) for some tips on how to slowly ramp up the strictness of the analysis as your code becomes better equipped to be analyzed. <br />
* **Installing Dependencies**<br />
Take a look at [Installing Phan Dependencies](https://github.com/phan/phan/wiki/Getting-Started#installing-phan-dependencies) for help getting Phan's dependencies installed on your system.
The [Wiki has more information about using Phan](https://github.com/phan/phan/wiki#using-phan).
# Features
Phan is able to perform the following kinds of analysis:
* Check that all methods, functions, classes, traits, interfaces, constants, properties and variables are defined and accessible.
* Check for type safety and arity issues on method/function/closure calls.
* Check for PHP8/PHP7/PHP5 backward compatibility.
* Check for features that weren't supported in older PHP 7.x minor releases (E.g. `object`, `void`, `iterable`, `?T`, `[$x] = ...;`, negative string offsets, multiple exception catches, etc.)
* Check for sanity with array accesses.
* Check for type safety on binary operations.
* Check for valid and type safe return values on methods, functions, and closures.
* Check for No-Ops on arrays, closures, constants, properties, variables, unary operators, and binary operators.
* Check for unused/dead/[unreachable](https://github.com/phan/phan/tree/v5/.phan/plugins#unreachablecodepluginphp) code. (Pass in `--dead-code-detection`)
* Check for unused variables and parameters. (Pass in `--unused-variable-detection`)
* Check for redundant or impossible conditions and pointless casts. (Pass in `--redundant-condition-detection`)
* Check for unused `use` statements.
These and a few other issue types can be automatically fixed with `--automatic-fix`.
* Check for classes, functions and methods being redefined.
* Check for sanity with class inheritance (e.g. checks method signature compatibility).
Phan also checks for final classes/methods being overridden, that abstract methods are implemented, and that the implemented interface is really an interface (and so on).
* Supports namespaces, traits and variadics.
* Supports [Union Types](https://github.com/phan/phan/wiki/About-Union-Types).
* Supports [Generic Types (i.e. `@template`)](https://github.com/phan/phan/wiki/Generic-Types).
* Supports generic arrays such as `int[]`, `UserObject[]`, `array<int,UserObject>`, etc..
* Supports array shapes such as `array{key:string,otherKey:?stdClass}`, etc. (internally and in PHPDoc tags)
This also supports indicating that fields of an array shape are optional
via `array{requiredKey:string,optionalKey?:string}` (useful for `@param`)
* Supports phpdoc [type annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code).
* Supports inheriting phpdoc type annotations.
* Supports checking that phpdoc type annotations are a narrowed form (E.g. subclasses/subtypes) of the real type signatures
* Supports inferring types from [assert() statements](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code) and conditionals in if elements/loops.
* Supports [`@deprecated` annotation](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#deprecated) for deprecating classes, methods and functions
* Supports [`@internal` annotation](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#internal) for elements (such as a constant, function, class, class constant, property or method) as internal to the package in which it's defined.
* Supports `@suppress <ISSUE_TYPE>` annotations for [suppressing issues](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#suppress).
* Supports [magic @property annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#property) (`@property <union_type> <variable_name>`)
* Supports [magic @method annotations](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code#method) (`@method <union_type> <method_name>(<union_type> <param1_name>)`)
* Supports [`class_alias` annotations (experimental, off by default)](https://github.com/phan/phan/pull/586)
* Supports indicating the class to which a closure will be bound, via `@phan-closure-scope` ([example](tests/files/src/0264_closure_override_context.php))
* Supports analysis of closures and return types passed to `array_map`, `array_filter`, and other internal array functions.
* Offers extensive configuration for weakening the analysis to make it useful on large sloppy code bases
* Can be run on many cores. (requires `pcntl`)
* Output is emitted in text, checkstyle, json, pylint, csv, or codeclimate formats.
* Can run [user plugins on source for checks specific to your code](https://github.com/phan/phan/wiki/Writing-Plugins-for-Phan).
[Phan includes various plugins you may wish to enable for your project](https://github.com/phan/phan/tree/v5/.phan/plugins#2-general-use-plugins).
See [Phan Issue Types](https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan) for descriptions
and examples of all issues that can be detected by Phan. Take a look at the
[\Phan\Issue](https://github.com/phan/phan/blob/v5/src/Phan/Issue.php) to see the
definition of each error type.
Take a look at the [Tutorial for Analyzing a Large Sloppy Code Base](https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base) to get a sense of what the process of doing ongoing analysis might look like for you.
Phan can be used from [various editors and IDEs](https://github.com/phan/phan/wiki/Editor-Support) for its error checking, "go to definition" support, etc. via the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
Editors and tools can also request analysis of individual files in a project using the simpler [Daemon Mode](https://github.com/phan/phan/wiki/Using-Phan-Daemon-Mode).
See the [tests](https://github.com/phan/phan/blob/v5/tests/files) directory for some examples of the various checks.
Phan is imperfect and shouldn't be used to prove that your PHP-based rocket guidance system is free of defects.
## Features provided by plugins
Additional analysis features have been provided by [plugins](https://github.com/phan/phan/tree/v5/.phan/plugins#plugins).
- [Checking for syntactically unreachable statements](https://github.com/phan/phan/tree/v5/.phan/plugins#unreachablecodepluginphp) (E.g. `{ throw new Exception("Message"); return $value; }`)
- [Checking `*printf()` format strings against the provided arguments](https://github.com/phan/phan/tree/v5/.phan/plugins#printfcheckerplugin) (as well as checking for common errors)
- [Checking that PCRE regexes passed to `preg_*()` are valid](https://github.com/phan/phan/tree/v5/.phan/plugins#pregregexcheckerplugin)
- [Checking for `@suppress` annotations that are no longer needed.](https://github.com/phan/phan/tree/v5/.phan/plugins#unusedsuppressionpluginphp)
- [Checking for duplicate or missing array keys.](https://github.com/phan/phan/tree/v5/.phan/plugins#duplicatearraykeypluginphp)
- [Checking coding style conventions](https://github.com/phan/phan/tree/v5/.phan/plugins#3-plugins-specific-to-code-styles)
- [Others](https://github.com/phan/phan/tree/v5/.phan/plugins#plugins)
Example: [Phan's plugins for self-analysis.](https://github.com/phan/phan/blob/3.2.8/.phan/config.php#L601-L674)
# Usage
After [installing Phan](#getting-started), Phan needs to be configured with details on where to find code to analyze and how to analyze it. The
easiest way to tell Phan where to find source code is to [create a `.phan/config.php` file](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file).
A simple `.phan/config.php` file might look something like the following.
```php
<?php
/**
* This configuration will be read and overlaid on top of the
* default configuration. Command line arguments will be applied
* after this file is read.
*/
return [
// 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.
"target_php_version" => null,
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'src',
'vendor/symfony/console',
],
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as "vendor/") in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to the `directory_list` as
// to `exclude_analysis_directory_list`.
"exclude_analysis_directory_list" => [
'vendor/'
],
// A list of plugin files to execute.
// Plugins which are bundled with Phan can be added here by providing their name
// (e.g. 'AlwaysReturnPlugin')
//
// Documentation about available bundled plugins can be found
// at https://github.com/phan/phan/tree/v5/.phan/plugins
//
// Alternately, you can pass in the full path to a PHP file
// with the plugin's implementation.
// (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php')
'plugins' => [
// checks if a function, closure or method unconditionally returns.
// can also be written as 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'
'AlwaysReturnPlugin',
'DollarDollarPlugin',
'DuplicateArrayKeyPlugin',
'DuplicateExpressionPlugin',
'PregRegexCheckerPlugin',
'PrintfCheckerPlugin',
'SleepCheckerPlugin',
// Checks for syntactically unreachable statements in
// the global scope or function bodies.
'UnreachableCodePlugin',
'UseReturnValuePlugin',
'EmptyStatementListPlugin',
'LoopVariableReusePlugin',
],
];
```
Take a look at [Creating a Config File](https://github.com/phan/phan/wiki/Getting-Started#creating-a-config-file) and
[Incrementally Strengthening Analysis](https://github.com/phan/phan/wiki/Incrementally-Strengthening-Analysis) for
more details.
Running `phan --help` will show [usage information and command-line options](./internal/CLI-HELP.md).
## Annotating Your Source Code
Phan reads and understands most [PHPDoc](http://www.phpdoc.org/docs/latest/guides/types.html)
type annotations including [Union Types](https://github.com/phan/phan/wiki/About-Union-Types)
(like `int|MyClass|string|null`) and generic array types (like `int[]` or `string[]|MyClass[]` or `array<int,MyClass>`).
Take a look at [Annotating Your Source Code](https://github.com/phan/phan/wiki/Annotating-Your-Source-Code)
and [About Union Types](https://github.com/phan/phan/wiki/About-Union-Types) for some help
getting started with defining types in your code.
Phan supports `(int|string)[]` style annotations, and represents them internally as `int[]|string[]`
(Both annotations are treated like array which may have integers and/or strings).
When you have arrays of mixed types, just use `array`.
The following code shows off the various annotations that are supported.
```php
/**
* @return void
*/
function f() {}
/** @deprecated */
class C {
/** @var int */
const C = 42;
/** @var string[]|null */
public $p = null;
/**
* @param int|null $p
* @return string[]|null
*/
public static function f($p) {
if (is_null($p)) {
return null;
}
return array_map(
/** @param int $i */
function($i) {
return "thing $i";
},
range(0, $p)
);
}
}
```
Just like in PHP, any type can be nulled in the function declaration which also
means a null is allowed to be passed in for that parameter.
Phan checks the type of every single element of arrays (Including keys and values).
In practical terms, this means that `[$int1=>$int2,$int3=>$int4,$int5=>$str6]` is seen as `array<int,int|string>`,
which Phan represents as `array<int,int>|array<int,string>`.
`[$strKey => new MyClass(), $strKey2 => $unknown]` will be represented as
`array<string,MyClass>|array<string,mixed>`.
- Literals such as `[12,'myString']` will be represented internally as array shapes such as `array{0:12,1:'myString'}`
# Generating a file list
This static analyzer does not track includes or try to figure out autoloader magic. It treats
all the files you throw at it as one big application. For code encapsulated in classes this
works well. For code running in the global scope it gets a bit tricky because order
matters. If you have an `index.php` including a file that sets a bunch of global variables and
you then try to access those after the `include(...)` in `index.php` the static analyzer won't
know anything about these.
In practical terms this simply means that you should put your entry points and any files
setting things in the global scope at the top of your file list. If you have a `config.php`
that sets global variables that everything else needs, then you should put that first in the list followed by your
various entry points, then all your library files containing your classes.
# Development
Take a look at [Developer's Guide to Phan](https://github.com/phan/phan/wiki/Developer's-Guide-To-Phan) for help getting started hacking on Phan.
When you find an issue, please take the time to create a tiny reproducing code snippet that illustrates
the bug. And once you have done that, fix it. Then turn your code snippet into a test and add it to
[tests](tests) then `./test` and send a PR with your fix and test. Alternatively, you can open an Issue with
details.
To run Phan's unit tests, just run `./test`.
To run all of Phan's unit tests and integration tests, run `./tests/run_all_tests.sh`
# Code of Conduct
We are committed to fostering a welcoming community. Any participant and
contributor is required to adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md).
# Online Demo
**This requires an up to date version of Firefox/Chrome and at least 4 GB of free RAM.** (this is a 15 MB download)
[Run Phan entirely in your browser](https://phan.github.io/demo/).
[![Preview of analyzing PHP](https://raw.githubusercontent.com/phan/demo/master/static/preview.png)](https://phan.github.io/demo/)

23
vendor/phan/phan/azure-pipelines.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# https://aka.ms/yaml
trigger:
- master
- v4
- v5
jobs:
- template: .azure/job.yml
parameters:
configurationName: PHP_73_NTS
phpVersion: 7.3
vmImage: 'ubuntu-18.04'
- template: .azure/job.yml
parameters:
configurationName: PHP_74_NTS
phpVersion: 7.4
vmImage: 'ubuntu-20.04'
- template: .azure/job.yml
parameters:
configurationName: PHP_80_NTS
phpVersion: 8.0
vmImage: 'ubuntu-20.04'

58
vendor/phan/phan/composer.json vendored Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "phan/phan",
"description": "A static analyzer for PHP",
"keywords": ["php", "static", "analyzer"],
"type": "project",
"license": "MIT",
"authors": [
{
"name": "Tyson Andre"
},
{
"name": "Rasmus Lerdorf"
},
{
"name": "Andrew S. Morrison"
}
],
"config": {
"sort-packages": true,
"platform": {
"php": "7.2.24"
}
},
"require": {
"php": "^7.2.0|^8.0.0",
"ext-filter": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"composer/semver": "^1.4|^2.0|^3.0",
"composer/xdebug-handler": "^2.0|^3.0",
"felixfbecker/advanced-json-rpc": "^3.0.4",
"microsoft/tolerant-php-parser": "0.1.1",
"netresearch/jsonmapper": "^1.6.0|^2.0|^3.0|^4.0",
"sabre/event": "^5.1.3",
"symfony/console": "^3.2|^4.0|^5.0|^6.0",
"symfony/polyfill-mbstring": "^1.11.0",
"symfony/polyfill-php80": "^1.20.0",
"tysonandre/var_representation_polyfill": "^0.0.2|^0.1.0"
},
"suggest": {
"ext-ast": "Needed for parsing ASTs (unless --use-fallback-parser is used). 1.0.1+ is needed, 1.0.16+ is recommended.",
"ext-iconv": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8",
"ext-igbinary": "Improves performance of polyfill when ext-ast is unavailable",
"ext-mbstring": "Either iconv or mbstring is needed to ensure issue messages are valid utf-8",
"ext-tokenizer": "Needed for fallback/polyfill parser support and file/line-based suppressions.",
"ext-var_representation": "Suggested for converting values to strings in issue messages"
},
"require-dev": {
"phpunit/phpunit": "^8.5.0"
},
"autoload": {
"psr-4": {"Phan\\": "src/Phan"}
},
"autoload-dev": {
"psr-4": {"Phan\\Tests\\": "tests/Phan"}
},
"bin": ["phan", "phan_client", "tocheckstyle"]
}

3059
vendor/phan/phan/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

10
vendor/phan/phan/phan vendored Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env php
<?php
// @phan-file-suppress PhanPluginRemoveDebugAny
if (PHP_VERSION_ID < 70200) {
fwrite(STDERR, "ERROR: Phan 5.x requires PHP 7.2+, but it is being run with PHP " . PHP_VERSION . PHP_EOL);
fwrite(STDERR, "PHP 7.1 reached its end of life in December 2019." . PHP_EOL);
fwrite(STDERR, "Exiting without analyzing code." . PHP_EOL);
exit(1);
}
require_once __DIR__ . '/src/phan.php';

796
vendor/phan/phan/phan_client vendored Executable file
View File

@@ -0,0 +1,796 @@
#!/usr/bin/env php
<?php
/**
* Usage: phan_client -l path/to/file.php
* Compatible with php 5.6-8.1
* (The server itself requires a newer php version)
*
* See plugins/vim/snippet.vim for an example of a use of this program.
*
* Analyzes a single php file.
* - If it is syntactically valid, scans it with phan, and emits lines beginning with "phan error:"
* - If it is invalid, emits the output of the PHP syntax checker
*
* This is meant to be a self-contained script with no file dependencies.
*
* Not tested on windows, probably won't work, but should be easy to add.
* Enhanced substitute for php -l, when phan daemon is running in the background for that folder.
*
* Note: if the daemon is run inside of Docker, one would probably need to change the URL in src/Phan/Daemon/Request.php from 127.0.0.1 to 0.0.0.0,
* and docker run -p 127.0.0.1:4846:4846 path/to/phan --daemonize-tcp-port 4846 --quick (second port is the docker one)
*
* See one of the many dockerized phan instructions, such as https://github.com/cloudflare/docker-phan
* e.g. https://github.com/cloudflare/docker-phan/blob/master/builder/scripts/mkimage-phan.bash
* mentions how it installed php-ast, similar steps could be used for other modules.
* (Install phpVERSION-dev/pecl to install extensions from source/pecl (phpize, configure, make install/pecl install))
*
* TODO: tutorial or repo.
*
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
* @phan-file-suppress PhanPluginDuplicateConditionalNullCoalescing this can't use the `??` operator because it's compatible with php 5.6
* @phan-file-suppress PhanPluginCanUseParamType, PhanPluginCanUsePHP71Void, PhanPluginCanUseReturnType
* @phan-file-suppress PhanPluginRemoveDebugEcho
*/
class PhanPHPLinter
{
// Wait at most 3 seconds to lint a file.
const TIMEOUT_MS = 3000;
/** @var bool - Whether or not this is verbose */
public static $verbose = false;
/**
* @param string $msg
* @return void
*/
private static function debugError($msg)
{
error_log($msg);
}
/**
* @param string $msg
* @return void
*/
private static function debugInfo($msg)
{
if (self::$verbose) {
self::debugError($msg);
}
}
/**
* The main function of the phan_client binary.
* See the doc comment of this file.
*
* @return void
*/
public static function run()
{
error_reporting(E_ALL);
// TODO: check for .phan/lock to see if daemon is running?
$opts = new PhanPHPLinterOpts(); // parse options, exit on failure.
self::$verbose = $opts->verbose;
$failure_code = 0;
$temporary_file_mapping_contents = [];
// TODO: Check that path gets defined
foreach ($opts->file_list as $path) {
if (isset($opts->temporary_file_map[$path])) {
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$temporary_path = $opts->temporary_file_map[$path];
$temporary_contents = file_get_contents($temporary_path);
if ($temporary_contents === false) {
self::debugError(sprintf("Could not open temporary input file: %s", $temporary_path));
$failure_code = 1;
continue;
}
$exit_code = 0;
$output = '';
if (!$opts->use_fallback_parser) {
ob_start();
try {
system("php -l --no-php-ini " . escapeshellarg($temporary_path), $exit_code);
} finally {
$output = ob_get_clean();
}
}
if ($exit_code === 0) {
$temporary_file_mapping_contents[$path] = $temporary_contents;
}
if ($exit_code !== 0) {
echo $output;
}
} else {
// TODO: use popen instead
// TODO: add option to capture output, suppress "No syntax error"?
// --no-php-ini is a faster way to parse since php doesn't need to load multiple extensions. Assumes none of the extensions change the way php is parsed.
$exit_code = 0;
$output = '';
if (!$opts->use_fallback_parser) {
ob_start();
try {
system("php -l --no-php-ini " . escapeshellarg($path), $exit_code);
} finally {
$output = ob_get_clean();
}
}
if ($exit_code !== 0) {
echo $output;
}
}
if ($exit_code !== 0) {
// The file is syntactically invalid. Or php somehow isn't able to be invoked from this script.
$failure_code = $exit_code;
}
}
// Exit if any of the requested files are syntactically invalid.
if ($failure_code !== 0) {
self::debugError("Files were syntactically invalid\n");
exit($failure_code);
}
if (!isset($path)) {
self::debugError("Unexpectedly parsed no files\n");
exit($failure_code);
}
// TODO: Check that everything in $this->file_list is in the same path.
// $path = reset($opts->file_list);
$real = realpath($path);
if (!is_string($real)) {
self::debugError("Could not resolve $path\n");
}
// @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
$dirname = dirname($real);
$old_dirname = null;
unset($real);
// TODO: In another PR, have an alternative way to run the daemon/server on Windows (Serialize and unserialize global state?
// The server side is unsupported on Windows, due to the `pcntl` extension not being supported.
$found_phan_config = false;
while ($dirname !== $old_dirname) {
if (file_exists($dirname . '/.phan/config.php')) {
$found_phan_config = true;
break;
}
$old_dirname = $dirname;
$dirname = dirname($dirname);
}
if (!$found_phan_config) {
self::debugInfo("Not in a Phan project, nothing to do.");
exit(0);
}
$file_mapping = [];
$real_files = [];
foreach ($opts->file_list as $path) {
$real = realpath($path);
if (!is_string($real)) {
self::debugInfo("could not find real path to '$path'");
continue;
}
// Convert this to a relative path
if (in_array(substr($real, 0, strlen($dirname) + 1),
[$dirname . DIRECTORY_SEPARATOR, $dirname . '/'],
true
)) {
$real = substr($real, strlen($dirname) + 1);
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable not able to analyze. Not using coalescing because this supports php 5.
$mapped_path = isset($opts->temporary_file_map[$path]) ? $opts->temporary_file_map[$path] : $path;
// If we are analyzing a temporary file, but it's within a project, then output the path to a temporary file for consistency.
// (Tools which pass something a temporary path expect a temporary path in the output.)
$file_mapping[$real] = $mapped_path;
$real_files[] = $real;
} else {
self::debugInfo("Not in a Phan project, nothing to do.");
}
}
if (count($file_mapping) === 0) {
self::debugInfo("Not in a real project");
}
// The file is syntactically valid. Run phan.
$request = [
'method' => 'analyze_files',
'files' => $real_files,
'format' => 'json',
];
if ($opts->output_mode) {
$request['format'] = $opts->output_mode;
$request['is_user_specified_format'] = true;
}
if ($opts->color === null && (!$opts->output_mode || $opts->output_mode === 'text')) {
$opts->color = self::supportsColor(STDOUT);
}
if ($opts->color) {
$request['color'] = true;
}
if (count($temporary_file_mapping_contents) > 0) {
$request['temporary_file_mapping_contents'] = $temporary_file_mapping_contents;
}
$serialized_request = json_encode($request);
if (!is_string($serialized_request)) {
self::debugError("Could not serialize this request\n");
exit(1);
}
// TODO: check if the folder is within a folder with subdirectory .phan/config.php
// TODO: Check if there is a lock before attempting to connect?
$client = @stream_socket_client($opts->url, $errno, $errstr, 20.0);
// NOTE: Some future release of php may change stream_socket_client to return an object on success.
if (!$client) {
// TODO: This should attempt to start up the phan daemon for the given folder?
self::debugError("Phan daemon not running on " . ($opts->url));
exit(0);
}
fwrite($client, $serialized_request);
stream_set_timeout($client, (int)floor(self::TIMEOUT_MS / 1000), 1000 * (self::TIMEOUT_MS % 1000));
stream_socket_shutdown($client, STREAM_SHUT_WR);
$response_lines = [];
while (!feof($client)) {
$response_lines[] = fgets($client);
}
stream_socket_shutdown($client, STREAM_SHUT_RD);
fclose($client);
$response_bytes = implode('', $response_lines);
// This uses the 'phplike' format imitating php's error format. "%s in %s on line %d"
$response = json_decode($response_bytes, true);
if (!is_array($response)) {
self::debugError(sprintf("Invalid response from phan for %s: expected JSON object: %s", $opts->url, $response_bytes));
return;
}
$status = isset($response['status']) ? $response['status'] : null;
if ($status === 'ok') {
self::dumpJSONIssues($response, $file_mapping, $request);
} else {
self::debugError(sprintf("Invalid response from phan for %s: %s", $opts->url, $response_bytes));
}
}
/**
* @param array<string,mixed> $response
* @param string[] $file_mapping
* @param array<string,mixed> $request
* @return void
*/
private static function dumpJSONIssues(array $response, array $file_mapping, array $request)
{
$did_debug = false;
$lines = [];
// if ($response['issue_count'] > 0)
$issues = $response['issues'];
$format = $request['format'];
if ($format === 'json') {
if (!\is_array($issues)) {
if (\is_string($issues)) {
self::debugError(sprintf("Invalid issues response from phan: %s\n", $issues));
} else {
self::debugError(sprintf("Invalid type for issues response from phan: %s\n", gettype($issues)));
}
return;
}
if (isset($request['is_user_specified_format'])) {
// The user requested the raw JSON, not what `phan_client` converts it to
echo json_encode($issues) . "\n";
return;
}
} else {
// When formats other than 'json' are requested, the Phan daemon returns the issues as a raw string.
// (e.g. codeclimate returns a string with JSON separated by "\x00")
if (!\is_string($issues)) {
self::debugError(sprintf("Invalid type for issues response from phan: %s\n", gettype($issues)));
return;
}
echo $issues;
return;
}
foreach ($issues as $issue) {
if (!is_array($issue)) {
self::debugError(sprintf("Invalid type for element of issues response from phan: %s\n", gettype($issues)));
return;
}
if ($issue['type'] !== 'issue') {
continue;
}
$pathInProject = $issue['location']['path']; // relative path
if (!isset($file_mapping[$pathInProject])) {
if (!$did_debug) {
self::debugInfo(sprintf("Unexpected path for issue (expected %s): %s\n", json_encode($file_mapping) ?: 'invalid', json_encode($issue) ?: 'invalid'));
}
$did_debug = true;
continue;
}
$line = $issue['location']['lines']['begin'];
$description = $issue['description'];
$parts = explode(' ', $description, 3);
if (count($parts) === 3 && $parts[1] === $issue['check_name']) {
$description = implode(': ', $parts);
}
if (isset($issue['suggestion'])) {
$description .= ' (' . $issue['suggestion'] . ')';
}
$lines[] = sprintf("Phan error: %s in %s on line %d\n", $description, $file_mapping[$pathInProject], $line);
}
// https://github.com/neomake/neomake/issues/153
echo implode('', $lines);
}
/**
* Returns true if the output stream supports colors
*
* This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
* terminals via named pipes, so we can only check the environment.
*
* Reference: Composer\XdebugHandler\Process::supportsColor
* https://github.com/composer/xdebug-handler
* (This is internal, so it was duplicated in case their API changed)
*
* This duplicates CLI::supportsColor() so that phan_client can run as a standalone file
*
* @param resource $output A valid CLI output stream
* @return bool
* @suppress PhanUndeclaredFunction
*/
public static function supportsColor($output)
{
if ('Hyper' === getenv('TERM_PROGRAM')) {
return true;
}
if (\defined('PHP_WINDOWS_VERSION_BUILD')) {
return (\function_exists('sapi_windows_vt100_support')
&& \sapi_windows_vt100_support($output))
|| false !== \getenv('ANSICON')
|| 'ON' === \getenv('ConEmuANSI')
|| 'xterm' === \getenv('TERM');
}
if (\function_exists('stream_isatty')) {
return \stream_isatty($output);
} elseif (\function_exists('posix_isatty')) {
return \posix_isatty($output);
}
$stat = \fstat($output);
// Check if formatted mode is S_IFCHR
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
}
}
/**
* This represents the CLI options for Phan
* (and the logic to parse them and generate usage messages)
*/
class PhanPHPLinterOpts
{
/** @var string tcp:// or unix:// socket URL of the daemon. */
public $url;
/** @var list<string> - file list */
public $file_list = [];
/** @var string[]|null - optional, maps original files to temporary file path to use as a substitute. */
public $temporary_file_map = null;
/** @var bool if true, enable verbose output. */
public $verbose = false;
/** @var bool should this client request analysis from the Phan server when the file has syntax errors */
public $use_fallback_parser;
/** @var ?string the output mode to use. If null, use the default from the daemon */
public $output_mode = null;
/** @var ?bool whether to color the output on the client */
public $color = null;
/**
* @var bool should this client print a usage text if an **unexpected** error occurred.
*/
private $print_usage_on_error = true;
/**
* @param string $msg - optional message
* @param int $exit_code - process exit code.
* @return never - exits with $exit_code
*/
public function usage($msg = '', $exit_code = 0)
{
if (strlen($msg) > 0 || $this->print_usage_on_error) {
global $argv;
if (!empty($msg)) {
echo "$msg\n";
}
// TODO: Add an option to autostart the daemon if user also has global configuration to allow it for a given project folder. ($HOME/.phanconfig)
// TODO: Allow changing (adding/removing) issue suppression types for the analysis phase (would not affect the parse phase)
echo <<<EOB
Usage: {$argv[0]} [options] -l file.php [ -l file2.php]
--daemonize-socket </path/to/file.sock>
Unix socket which a Phan daemon is listening for requests on.
--daemonize-tcp-port <default|1024-65535>
TCP port which a Phan daemon is listening for JSON requests on, in daemon mode. (E.g. 'default', which is an alias for port 4846)
If no option is specified for the daemon's address, phan_client defaults to connecting on port 4846.
--use-fallback-parser
Skip the local PHP syntax check.
Use this if the daemon is also executing with --use-fallback-parser, or if the daemon runs a different PHP version from the default.
Useful if you wish to report errors while editing the file, even if the file is currently syntactically invalid.
-l, --syntax-check <file.php>
Syntax check, and if the Phan daemon is running, analyze the following file (absolute path or relative to current working directory)
This will only analyze the file if a full phan check (with .phan/config.php) would analyze the file.
-m, --output-mode <mode>
Output mode from 'phan_client' (default), 'text', 'json', 'csv', 'codeclimate', 'checkstyle', or 'pylint'
-t, --temporary-file-map '{"file.php":"/path/to/tmp/file_copy.php"}'
A json mapping from original path to absolute temporary path (E.g. of a file that is still being edited)
-f, --flycheck-file '/path/to/tmp/file_copy.php'
A simpler way to specify a file mapping when checking a single files.
Pass this after the only occurrence of --syntax-check.
-d, --disable-usage-on-error
If this option is set, don't print full usage messages for missing/inaccessible files or inaccessible daemons.
(Continue printing usage messages for invalid combinations of options.)
-v, --verbose
Whether to emit debugging output of this client.
--color, --no-color
Whether or not to colorize reported issues.
The default and text output modes are colorized by default, if the terminal supports it.
-h, --help
This help information
EOB;
}
exit($exit_code);
}
const GETOPT_SHORT_OPTIONS = 's:p:l:t:f:m:vhd';
const GETOPT_LONG_OPTIONS = [
'help',
'daemonize-socket:',
'daemonize-tcp-port:',
'disable-usage-on-error',
'syntax-check:',
'temporary-file-map:',
'use-fallback-parser',
'flycheck-file:',
'output-mode:',
'color',
'no-color',
'verbose',
];
/**
* @suppress PhanParamTooManyInternal - `getopt` added an optional third parameter in php 7.1
* @suppress UnusedSuppression
*/
public function __construct()
{
global $argv;
// Parse command line args
$optind = 0;
$getopt_reflection = new ReflectionFunction('getopt');
if ($getopt_reflection->getNumberOfParameters() >= 3) {
// optind support is only in php 7.1+.
// hhvm doesn't expect a third parameter, but reports a version of php 7.1, even in the latest version.
$opts = getopt(self::GETOPT_SHORT_OPTIONS, self::GETOPT_LONG_OPTIONS, $optind);
} else {
$opts = getopt(self::GETOPT_SHORT_OPTIONS, self::GETOPT_LONG_OPTIONS);
}
if (PHP_VERSION_ID >= 70100 && $optind < count($argv)) {
$this->usage(sprintf("Unexpected parameter %s", json_encode($argv[$optind]) ?: var_export($argv[$optind], true)));
}
// Check for this first, since the option parser may also emit debug output in the future.
if (in_array('-v', $argv, true) || in_array('--verbose', $argv, true)) {
PhanPHPLinter::$verbose = true;
$this->verbose = true;
}
$print_usage_on_error = true;
if (!is_array($opts)) {
$opts = [];
}
foreach ($opts as $key => $value) {
switch ($key) {
case 's':
case 'daemonize-socket':
$this->checkCanConnectToDaemon('unix');
if ($this->url !== null) {
$this->usage('Can specify --daemonize-socket or --daemonize-tcp-port only once', 1);
}
// Check if the socket is valid after parsing the file list.
// @phan-suppress-next-line PhanPossiblyFalseTypeArgumentInternal
$socket_dirname = dirname(realpath($value));
if (!is_string($socket_dirname) || !file_exists($socket_dirname) || !is_dir($socket_dirname)) {
// The client doesn't require that the file exists if the daemon isn't running, but we do require that the folder exists.
$msg = sprintf('Configured to connect to Unix socket server at socket %s, but folder %s does not exist', json_encode($value) ?: 'invalid', json_encode($socket_dirname) ?: 'invalid');
$this->usage($msg, 1);
} else {
$this->url = sprintf('unix://%s/%s', $socket_dirname, basename($value));
}
break;
case 'use-fallback-parser':
$this->use_fallback_parser = true;
break;
case 'f':
case 'flycheck-file':
// Add alias, for use in flycheck
if (\is_array($this->temporary_file_map)) {
$this->usage('--flycheck-file should be specified only once.', 1);
}
if (!\is_array($this->file_list) || count($this->file_list) !== 1) {
$this->usage('--flycheck-file should be specified after the first occurrence of -l.', 1);
}
if (!is_string($value)) {
$this->usage('--flycheck-file should be passed a string value', 1);
}
$this->temporary_file_map = [$this->file_list[0] => $value];
break;
case 't':
case 'temporary-file-map':
if (\is_array($this->temporary_file_map)) {
$this->usage('--temporary-file-map should be specified only once.', 1);
}
$mapping = json_decode($value, true);
if (!\is_array($mapping)) {
$this->usage('--temporary-file-map should be a JSON encoded map from source file to temporary file to analyze instead', 1);
}
$this->temporary_file_map = $mapping;
break;
case 'p':
case 'daemonize-tcp-port':
$this->checkCanConnectToDaemon('tcp');
if (strcasecmp($value, 'default') === 0) {
$port = 4846;
} else {
$port = filter_var($value, FILTER_VALIDATE_INT);
}
if ($port >= 1024 && $port <= 65535) {
$this->url = sprintf('tcp://127.0.0.1:%d', $port);
} else {
$this->usage("daemonize-tcp-port must be the string 'default' or an integer between 1024 and 65535, got '$value'", 1);
}
break;
case 'l':
case 'syntax-check':
$path = $value;
if (!is_string($path)) {
$this->print_usage_on_error = $print_usage_on_error;
$this->usage(sprintf("Error: asked to analyze path %s which is not a string", json_encode($path) ?: 'invalid'), 1);
}
if (!file_exists($path)) {
$this->print_usage_on_error = $print_usage_on_error;
$this->usage(sprintf("Error: asked to analyze file %s which does not exist", json_encode($path) ?: 'invalid'), 1);
}
$this->file_list[] = $path;
break;
case 'h':
case 'help':
// never returns
$this->usage();
case 'd':
case 'disable-usage-on-error':
$print_usage_on_error = false;
break;
case 'v':
case 'verbose':
break; // already parsed.
case 'color':
$this->color = true;
break;
case 'no-color':
$this->color = false;
break;
case 'm':
case 'output-mode':
if (!is_string($value) || !in_array($value, ['text', 'json', 'csv', 'codeclimate', 'checkstyle', 'pylint', 'phan_client'], true)) {
$this->usage("Expected --output-mode {text,json,csv,codeclimate,checkstyle,pylint}, but got " . json_encode($value), 1);
}
if ($value === 'phan_client') {
// We're requesting the default
break;
}
$this->output_mode = $value;
break;
default:
$this->usage("Unknown option '-$key'", 1);
}
}
try {
self::checkAllArgsUsed($opts, $argv);
} catch (InvalidArgumentException $e) {
$this->usage($e->getMessage(), 1);
}
if (count($this->file_list) === 0) {
// Invalid invocation, always print this message
$this->usage("This requires at least one file to analyze (with -l path/to/file", 1);
}
if (\is_array($this->temporary_file_map)) {
foreach ($this->temporary_file_map as $original_path => $unused_temporary_path) {
if (!in_array($original_path, $this->file_list, true)) {
$this->usage("Need to specify -l '$original_path' if a mapping is included", 1);
}
}
}
if ($this->url === null) {
$this->url = 'tcp://127.0.0.1:4846';
}
// In the majority of cases, apply this **after** checking sanity of CLI options
// (without actually starting the analysis).
$this->print_usage_on_error = $print_usage_on_error;
}
/**
* prints error message if php doesn't support connecting to a daemon with a given protocol.
* @param string $protocol
* @return void
*/
private function checkCanConnectToDaemon($protocol)
{
$opt = $protocol === 'unix' ? '--daemonize-socket' : '--daemonize-tcp-port';
if (!in_array($protocol, stream_get_transports(), true)) {
$this->usage("The $protocol:///path/to/file schema is not supported on this system, cannot connect to a daemon with $opt", 1);
}
if ($this->url !== null) {
$this->usage('Can specify --daemonize-socket or --daemonize-tcp-port only once', 1);
}
}
/**
* Deliberately duplicating CLI::checkAllArgsUsed()
*
* @param array<string,mixed> $opts
* @param list<string> $argv
* @return void
* @throws InvalidArgumentException
*/
private static function checkAllArgsUsed(array $opts, array &$argv)
{
$pruneargv = [];
foreach ($opts as $opt => $value) {
foreach ($argv as $key => $chunk) {
$regex = '/^' . (isset($opt[1]) ? '--' : '-') . \preg_quote((string) $opt, '/') . '/';
if (in_array($chunk, is_array($value) ? $value : [$value], true)
&& $argv[$key - 1][0] === '-'
|| \preg_match($regex, $chunk)
) {
$pruneargv[] = $key;
}
}
}
while (count($pruneargv) > 0) {
$key = \array_pop($pruneargv);
unset($argv[$key]);
}
foreach ($argv as $arg) {
if ($arg[0] === '-') {
$parts = \explode('=', $arg, 2);
$key = $parts[0];
$value = isset($parts[1]) ? $parts[1] : ''; // php getopt() treats --processes and --processes= the same way
$key = \preg_replace('/^--?/', '', $key);
if ($value === '') {
if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) {
throw new InvalidArgumentException("Missing required value for '$arg'");
}
if (strlen($key) === 1 && strlen($parts[0]) === 2) {
// @phan-suppress-next-line PhanParamSuspiciousOrder this is deliberate
if (\strpos(self::GETOPT_SHORT_OPTIONS, "$key:") !== false) {
throw new InvalidArgumentException("Missing required value for '-$key'");
}
}
}
throw new InvalidArgumentException("Unknown option '$arg'" . self::getFlagSuggestionString($key), 1);
}
}
}
/**
* Finds potentially misspelled flags and returns them as a string
*
* This will use levenshtein distance, showing the first one or two flags
* which match with a distance of <= 5
*
* @param string $key Misspelled key to attempt to correct
* @return string
* @internal
*/
public static function getFlagSuggestionString(
$key
) {
/**
* @param string $s
* @return string
*/
$trim = static function ($s) {
return \rtrim($s, ':');
};
/**
* @param string $suggestion
* @return string
*/
$generate_suggestion = static function ($suggestion) {
return (strlen($suggestion) === 1 ? '-' : '--') . $suggestion;
};
/**
* @param string $suggestion
* @param string ...$other_suggestions
* @return string
*/
$generate_suggestion_text = static function ($suggestion, ...$other_suggestions) use ($generate_suggestion) {
$suggestions = \array_merge([$suggestion], $other_suggestions);
return ' (did you mean ' . \implode(' or ', array_map($generate_suggestion, $suggestions)) . '?)';
};
$short_options = \array_filter(array_map($trim, \str_split(self::GETOPT_SHORT_OPTIONS)));
if (strlen($key) === 1) {
$alternate = \ctype_lower($key) ? \strtoupper($key) : \strtolower($key);
if (in_array($alternate, $short_options, true)) {
return $generate_suggestion_text($alternate);
}
return '';
} elseif ($key === '') {
return '';
} elseif (strlen($key) > 255) {
// levenshtein refuses to run for longer keys
return '';
}
// include short options in case a typo is made like -aa instead of -a
$known_flags = \array_merge(self::GETOPT_LONG_OPTIONS, $short_options);
$known_flags = array_map($trim, $known_flags);
$similarities = [];
$key_lower = \strtolower($key);
foreach ($known_flags as $flag) {
if (strlen($flag) === 1 && \stripos($key, $flag) === false) {
// Skip over suggestions of flags that have no common characters
continue;
}
$distance = \levenshtein($key_lower, \strtolower($flag));
// distance > 5 is too far off to be a typo
// Make sure that if two flags have the same distance, ties are sorted alphabetically
if ($distance > 5) {
continue;
}
if ($key === $flag) {
if (in_array($key . ':', self::GETOPT_LONG_OPTIONS, true)) {
return " (This option is probably missing the required value. Or this option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())";
} else {
return " (This option may not apply to a regular Phan analysis, and/or it may be unintentionally unhandled in \Phan\CLI::__construct())";
}
}
$similarities[$flag] = [$distance, "x" . \strtolower($flag), $flag];
}
\asort($similarities); // retain keys and sort descending
$similarity_values = \array_values($similarities);
if (count($similarity_values) >= 2 && ($similarity_values[1][0] <= $similarity_values[0][0] + 1)) {
// If the next-closest suggestion isn't close to as similar as the closest suggestion, just return the closest suggestion
return $generate_suggestion_text($similarity_values[0][2], $similarity_values[1][2]);
} elseif (count($similarity_values) >= 1) {
return $generate_suggestion_text($similarity_values[0][2]);
}
return '';
}
}
PhanPHPLinter::run();

2
vendor/phan/phan/prep vendored Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env php
<?php require_once 'src/prep.php';

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use ast\Node;
use function is_float;
use function is_int;
use function is_null;
use function is_object;
use function is_string;
use function md5;
/**
* This converts a PHP AST Node into a hash.
* This ignores line numbers and spacing.
*/
class ASTHasher
{
/**
* @param string|int|null $node
* @return string a 16-byte binary key for the array key
* @internal
*/
public static function hashKey($node): string
{
if (is_string($node)) {
return md5($node, true);
} elseif (is_int($node)) {
if (\PHP_INT_SIZE >= 8) {
return "\0\0\0\0\0\0\0\0" . \pack('J', $node);
} else {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node);
}
}
// This is not a valid array key, give up
return md5((string) $node, true);
}
/**
* @param Node|string|int|float|null $node
* @return string a 16-byte binary key for the Node which is unlikely to overlap for ordinary code
*/
public static function hash($node): string
{
if (!is_object($node)) {
// hashKey
if (is_string($node)) {
return md5($node, true);
} elseif (is_int($node)) {
if (\PHP_INT_SIZE >= 8) {
return "\0\0\0\0\0\0\0\0" . \pack('J', $node);
} else {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . \pack('N', $node);
}
} elseif (is_float($node)) {
return "\0\0\0\0\0\0\0\1" . \pack('e', $node);
} elseif (is_null($node)) {
return "\0\0\0\0\0\0\0\2\0\0\0\0\0\0\0\0";
}
// This is not a valid AST, give up
return md5((string) $node, true);
}
// @phan-suppress-next-line PhanUndeclaredProperty
return $node->hash ?? ($node->hash = self::computeHash($node));
}
/**
* @param Node $node
* @return string a newly computed 16-byte binary key
*/
private static function computeHash(Node $node): string
{
$str = 'N' . $node->kind . ':' . ($node->flags & 0xfffff);
foreach ($node->children as $key => $child) {
// added in PhanAnnotationAdder
if (\is_string($key) && \strncmp($key, 'phan', 4) === 0) {
continue;
}
$str .= self::hashKey($key);
$str .= self::hash($child);
}
return md5($str, true);
}
}

View File

@@ -0,0 +1,588 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use ast;
use ast\flags;
use ast\Node;
use Closure;
use Phan\Analysis\PostOrderAnalysisVisitor;
use Phan\AST\TolerantASTConverter\Shim;
use function array_map;
use function implode;
use function is_string;
use function sprintf;
use function var_representation;
use const VAR_REPRESENTATION_SINGLE_LINE;
Shim::load();
/**
* This converts a PHP AST into an approximate string representation.
* This ignores line numbers and spacing.
*
* Eventual goals:
*
* 1. Short representations of constants for LSP hover requests.
* 2. Short representations for errors (e.g. "Error at $x->foo(self::MY_CONST)")
* 3. Configuration of rendering this.
*
* Similar utilities:
*
* - https://github.com/tpunt/php-ast-reverter is a pretty printer.
* - \Phan\Debug::nodeToString() converts nodes to strings.
*/
class ASTReverter
{
public const EXEC_NODE_FLAG_NAMES = [
flags\EXEC_EVAL => 'eval',
flags\EXEC_INCLUDE => 'include',
flags\EXEC_INCLUDE_ONCE => 'include_once',
flags\EXEC_REQUIRE => 'require',
flags\EXEC_REQUIRE_ONCE => 'require_once',
];
/** @var associative-array<int,Closure(Node):string> this contains maps from node kinds to closures to convert node kinds to strings */
private static $closure_map;
/** @var Closure(Node):string this maps unknown node types to strings */
private static $noop;
// TODO: Make this configurable, copy instance properties to static properties.
public function __construct()
{
}
/**
* Convert $node to a short PHP string representing $node.
*
* This does not work for all node kinds, and may be ambiguous.
*
* @param Node|string|int|float|bool|null|resource|array $node
*/
public static function toShortString($node): string
{
if (!($node instanceof Node)) {
if (\is_resource($node)) {
return 'resource(' . \get_resource_type($node) . ')';
}
return var_representation($node, VAR_REPRESENTATION_SINGLE_LINE);
}
return (self::$closure_map[$node->kind] ?? self::$noop)($node);
}
/**
* Escapes the inner contents to be suitable for a single-line double quoted string
*
* @see https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/PrettyPrinter/Standard.php
*/
public static function escapeInnerString(string $string, string $quote = null): string
{
if (null === $quote) {
// For doc strings, don't escape newlines
$escaped = \addcslashes($string, "\t\f\v$\\");
} else {
$escaped = \addcslashes($string, "\n\r\t\f\v$" . $quote . "\\");
}
// Escape other control characters
return \preg_replace_callback('/([\0-\10\16-\37])(?=([0-7]?))/', /** @param list<string> $matches */ static function (array $matches): string {
$oct = \decoct(\ord($matches[1]));
if ($matches[2] !== '') {
// If there is a trailing digit, use the full three character form
return '\\' . \str_pad($oct, 3, '0', \STR_PAD_LEFT);
}
return '\\' . $oct;
}, $escaped);
}
/**
* Static initializer.
*/
public static function init(): void
{
self::$noop = static function (Node $_): string {
return '(unknown)';
};
self::$closure_map = [
/**
* @suppress PhanAccessClassConstantInternal
*/
ast\AST_TYPE => static function (Node $node): string {
return PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags];
},
/**
* @suppress PhanPartialTypeMismatchArgument
*/
ast\AST_TYPE_INTERSECTION => static function (Node $node): string {
return implode('&', array_map([self::class, 'toShortTypeString'], $node->children));
},
/**
* @suppress PhanPartialTypeMismatchArgument
*/
ast\AST_TYPE_UNION => static function (Node $node): string {
return implode('|', array_map([self::class, 'toShortTypeString'], $node->children));
},
/**
* @suppress PhanTypeMismatchArgumentNullable
*/
ast\AST_NULLABLE_TYPE => static function (Node $node): string {
return '?' . self::toShortTypeString($node->children['type']);
},
ast\AST_POST_INC => static function (Node $node): string {
return self::formatIncDec('%s++', $node->children['var']);
},
ast\AST_PRE_INC => static function (Node $node): string {
return self::formatIncDec('++%s', $node->children['var']);
},
ast\AST_POST_DEC => static function (Node $node): string {
return self::formatIncDec('%s--', $node->children['var']);
},
ast\AST_PRE_DEC => static function (Node $node): string {
return self::formatIncDec('--%s', $node->children['var']);
},
ast\AST_ARG_LIST => static function (Node $node): string {
return '(' . implode(', ', array_map([self::class, 'toShortString'], $node->children)) . ')';
},
ast\AST_CALLABLE_CONVERT => /** @unused-param $node */ static function (Node $node): string {
return '(...)';
},
ast\AST_ATTRIBUTE_LIST => static function (Node $node): string {
return implode(' ', array_map([self::class, 'toShortString'], $node->children));
},
ast\AST_ATTRIBUTE_GROUP => static function (Node $node): string {
return implode(', ', array_map([self::class, 'toShortString'], $node->children));
},
ast\AST_ATTRIBUTE => static function (Node $node): string {
$result = self::toShortString($node->children['class']);
$args = $node->children['args'];
if ($args) {
$result .= self::toShortString($args);
}
return $result;
},
ast\AST_NAMED_ARG => static function (Node $node): string {
return $node->children['name'] . ': ' . self::toShortString($node->children['expr']);
},
ast\AST_PARAM_LIST => static function (Node $node): string {
return '(' . implode(', ', array_map([self::class, 'toShortString'], $node->children)) . ')';
},
ast\AST_PARAM => static function (Node $node): string {
$str = '$' . $node->children['name'];
if ($node->flags & ast\flags\PARAM_VARIADIC) {
$str = "...$str";
}
if ($node->flags & ast\flags\PARAM_REF) {
$str = "&$str";
}
if (isset($node->children['type'])) {
$str = ASTReverter::toShortString($node->children['type']) . ' ' . $str;
}
if (isset($node->children['default'])) {
$str .= ' = ' . ASTReverter::toShortString($node->children['default']);
}
return $str;
},
ast\AST_EXPR_LIST => static function (Node $node): string {
return implode(', ', array_map([self::class, 'toShortString'], $node->children));
},
ast\AST_CLASS_CONST => static function (Node $node): string {
return self::toShortString($node->children['class']) . '::' . $node->children['const'];
},
ast\AST_CLASS_NAME => static function (Node $node): string {
return self::toShortString($node->children['class']) . '::class';
},
ast\AST_MAGIC_CONST => static function (Node $node): string {
return UnionTypeVisitor::MAGIC_CONST_NAME_MAP[$node->flags] ?? '(unknown)';
},
ast\AST_CONST => static function (Node $node): string {
return self::toShortString($node->children['name']);
},
ast\AST_VAR => static function (Node $node): string {
$name_node = $node->children['name'];
if (is_string($name_node)) {
return '$' . $name_node;
}
return '$' . (is_string($name_node) ? $name_node : ('{' . self::toShortString($name_node) . '}'));
},
ast\AST_DIM => static function (Node $node): string {
$expr_str = self::toShortString($node->children['expr']);
if ($expr_str === '(unknown)') {
return '(unknown)';
}
$dim = $node->children['dim'];
if ($dim !== null) {
$dim_str = self::toShortString($dim);
} else {
$dim_str = '';
}
if ($node->flags & ast\flags\DIM_ALTERNATIVE_SYNTAX) {
return "{$expr_str}{{$dim_str}}";
}
return "{$expr_str}[$dim_str]";
},
ast\AST_NAME => static function (Node $node): string {
$result = $node->children['name'];
switch ($node->flags) {
case ast\flags\NAME_FQ:
return '\\' . $result;
case ast\flags\NAME_RELATIVE:
return 'namespace\\' . $result;
default:
return (string)$result;
}
},
ast\AST_NAME_LIST => static function (Node $node): string {
return implode('|', array_map([self::class, 'toShortString'], $node->children));
},
ast\AST_ARRAY => static function (Node $node): string {
$parts = [];
foreach ($node->children as $elem) {
if (!$elem instanceof Node) {
// Should always either be a Node or null.
$parts[] = '';
continue;
}
// AST_ARRAY_ELEM or AST_UNPACK
$parts[] = self::toShortString($elem);
}
$string = implode(',', $parts);
switch ($node->flags) {
case ast\flags\ARRAY_SYNTAX_SHORT:
case ast\flags\ARRAY_SYNTAX_LONG:
default:
return "[$string]";
case ast\flags\ARRAY_SYNTAX_LIST:
return "list($string)";
}
},
/** @suppress PhanAccessClassConstantInternal */
ast\AST_BINARY_OP => static function (Node $node): string {
return sprintf(
"(%s %s %s)",
self::toShortString($node->children['left']),
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown',
self::toShortString($node->children['right'])
);
},
ast\AST_ASSIGN => static function (Node $node): string {
return sprintf(
"(%s = %s)",
self::toShortString($node->children['var']),
self::toShortString($node->children['expr'])
);
},
ast\AST_ASSIGN_REF => static function (Node $node): string {
return sprintf(
"(%s =& %s)",
self::toShortString($node->children['var']),
self::toShortString($node->children['expr'])
);
},
/** @suppress PhanAccessClassConstantInternal */
ast\AST_ASSIGN_OP => static function (Node $node): string {
return sprintf(
"(%s %s= %s)",
self::toShortString($node->children['var']),
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags] ?? 'unknown',
self::toShortString($node->children['expr'])
);
},
ast\AST_UNARY_OP => static function (Node $node): string {
$operation_name = PostOrderAnalysisVisitor::NAME_FOR_UNARY_OP[$node->flags] ?? null;
if (!$operation_name) {
return '(unknown)';
}
$expr = $node->children['expr'];
$expr_text = self::toShortString($expr);
if (($expr->kind ?? null) !== ast\AST_UNARY_OP) {
return $operation_name . $expr_text;
}
return sprintf("%s(%s)", $operation_name, $expr_text);
},
ast\AST_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return sprintf(
'%s->%s',
self::toShortString($node->children['expr']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_NULLSAFE_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return sprintf(
'%s?->%s',
self::toShortString($node->children['expr']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_STATIC_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return sprintf(
'%s::%s%s',
self::toShortString($node->children['class']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
self::toShortString($node->children['args'])
);
},
ast\AST_METHOD_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return sprintf(
'%s->%s%s',
self::toShortString($node->children['expr']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
self::toShortString($node->children['args'])
);
},
ast\AST_NULLSAFE_METHOD_CALL => static function (Node $node): string {
$method_node = $node->children['method'];
return sprintf(
'%s?->%s%s',
self::toShortString($node->children['expr']),
is_string($method_node) ? $method_node : self::toShortString($method_node),
self::toShortString($node->children['args'])
);
},
ast\AST_STATIC_PROP => static function (Node $node): string {
$prop_node = $node->children['prop'];
return sprintf(
'%s::$%s',
self::toShortString($node->children['class']),
$prop_node instanceof Node ? '{' . self::toShortString($prop_node) . '}' : (string)$prop_node
);
},
ast\AST_INSTANCEOF => static function (Node $node): string {
return sprintf(
'(%s instanceof %s)',
self::toShortString($node->children['expr']),
self::toShortString($node->children['class'])
);
},
ast\AST_CAST => static function (Node $node): string {
return sprintf(
'(%s)(%s)',
// @phan-suppress-next-line PhanAccessClassConstantInternal
PostOrderAnalysisVisitor::AST_CAST_FLAGS_LOOKUP[$node->flags] ?? 'unknown',
self::toShortString($node->children['expr'])
);
},
ast\AST_CALL => static function (Node $node): string {
return sprintf(
'%s%s',
self::toShortString($node->children['expr']),
self::toShortString($node->children['args'])
);
},
ast\AST_NEW => static function (Node $node): string {
// TODO: add parenthesis in case this is used as (new X())->method(), or properties, but only when necessary
return sprintf(
'new %s%s',
self::toShortString($node->children['class']),
self::toShortString($node->children['args'])
);
},
ast\AST_CLONE => static function (Node $node): string {
// clone($x)->someMethod() has surprising precedence,
// so surround `clone $x` with parenthesis.
return sprintf(
'(clone(%s))',
self::toShortString($node->children['expr'])
);
},
ast\AST_CONDITIONAL => static function (Node $node): string {
['cond' => $cond, 'true' => $true, 'false' => $false] = $node->children;
if ($true !== null) {
return sprintf('(%s ? %s : %s)', self::toShortString($cond), self::toShortString($true), self::toShortString($false));
}
return sprintf('(%s ?: %s)', self::toShortString($cond), self::toShortString($false));
},
/** @suppress PhanPossiblyUndeclaredProperty */
ast\AST_MATCH => static function (Node $node): string {
['cond' => $cond, 'stmts' => $stmts] = $node->children;
return sprintf('match (%s) {%s}', ASTReverter::toShortString($cond), $stmts->children ? ' ' . ASTReverter::toShortString($stmts) . ' ' : '');
},
ast\AST_MATCH_ARM_LIST => static function (Node $node): string {
return implode(', ', array_map(self::class . '::toShortString', $node->children));
},
ast\AST_MATCH_ARM => static function (Node $node): string {
['cond' => $cond, 'expr' => $expr] = $node->children;
return sprintf('%s => %s', $cond !== null ? ASTReverter::toShortString($cond) : 'default', ASTReverter::toShortString($expr));
},
ast\AST_ISSET => static function (Node $node): string {
return sprintf(
'isset(%s)',
self::toShortString($node->children['var'])
);
},
ast\AST_EMPTY => static function (Node $node): string {
return sprintf(
'empty(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_PRINT => static function (Node $node): string {
return sprintf(
'print(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_ECHO => static function (Node $node): string {
return 'echo ' . ASTReverter::toShortString($node->children['expr']) . ';';
},
ast\AST_ARRAY_ELEM => static function (Node $node): string {
$value_representation = self::toShortString($node->children['value']);
$key_node = $node->children['key'];
if ($key_node !== null) {
return self::toShortString($key_node) . '=>' . $value_representation;
}
return $value_representation;
},
ast\AST_UNPACK => static function (Node $node): string {
return sprintf(
'...(%s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_INCLUDE_OR_EVAL => static function (Node $node): string {
return sprintf(
'%s(%s)',
self::EXEC_NODE_FLAG_NAMES[$node->flags],
self::toShortString($node->children['expr'])
);
},
ast\AST_ENCAPS_LIST => static function (Node $node): string {
$parts = [];
foreach ($node->children as $c) {
if ($c instanceof Node) {
$parts[] = '{' . self::toShortString($c) . '}';
} else {
$parts[] = self::escapeInnerString((string)$c, '"');
}
}
return '"' . implode('', $parts) . '"';
},
ast\AST_SHELL_EXEC => static function (Node $node): string {
$parts = [];
$expr = $node->children['expr'];
if ($expr instanceof Node) {
foreach ($expr->children as $c) {
if ($c instanceof Node) {
$parts[] = '{' . self::toShortString($c) . '}';
} else {
$parts[] = self::escapeInnerString((string)$c, '`');
}
}
} else {
$parts[] = self::escapeInnerString((string)$expr, '`');
}
return '`' . implode('', $parts) . '`';
},
// Slightly better short placeholders than (unknown)
ast\AST_CLOSURE => static function (Node $_): string {
return '(function)';
},
ast\AST_ARROW_FUNC => static function (Node $_): string {
return '(fn)';
},
ast\AST_RETURN => static function (Node $node): string {
$expr_node = $node->children['expr'];
if ($expr_node === null) {
return 'return;';
}
return sprintf(
'return %s;',
self::toShortString($node->children['expr'])
);
},
ast\AST_THROW => static function (Node $node): string {
return sprintf(
'(throw %s)',
self::toShortString($node->children['expr'])
);
},
ast\AST_FOR => static function (Node $_): string {
return '(for loop)';
},
ast\AST_WHILE => static function (Node $_): string {
return '(while loop)';
},
ast\AST_DO_WHILE => static function (Node $_): string {
return '(do-while loop)';
},
ast\AST_FOREACH => static function (Node $_): string {
return '(foreach loop)';
},
ast\AST_IF => static function (Node $_): string {
return '(if statement)';
},
ast\AST_IF_ELEM => static function (Node $_): string {
return '(if statement element)';
},
ast\AST_TRY => static function (Node $_): string {
return '(try statement)';
},
ast\AST_SWITCH => static function (Node $_): string {
return '(switch statement)';
},
ast\AST_SWITCH_LIST => static function (Node $_): string {
return '(switch case list)';
},
ast\AST_SWITCH_CASE => static function (Node $_): string {
return '(switch case statement)';
},
ast\AST_EXIT => static function (Node $node): string {
$expr = $node->children['expr'];
return 'exit(' . (isset($expr) ? self::toShortString($expr) : '') . ')';
},
ast\AST_YIELD => static function (Node $node): string {
['value' => $value, 'key' => $key] = $node->children;
if ($value !== null) {
return '(yield)';
}
if ($key !== null) {
return sprintf('(yield %s => %s)', self::toShortString($key), self::toShortString($value));
}
return sprintf('(yield %s)', self::toShortString($value));
},
ast\AST_YIELD_FROM => static function (Node $node): string {
return '(yield from ' . self::toShortString($node->children['expr']) . ')';
},
// TODO: AST_SHELL_EXEC, AST_ENCAPS_LIST(in shell_exec or double quotes)
];
}
/**
* Returns the representation of an AST_TYPE, AST_NULLABLE_TYPE, AST_TYPE_UNION, or AST_NAME, as seen in an element signature
*/
public static function toShortTypeString(Node $node): string
{
if ($node->kind === ast\AST_NULLABLE_TYPE) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable
return '?' . self::toShortTypeString($node->children['type']);
}
if ($node->kind === ast\AST_TYPE) {
return PostOrderAnalysisVisitor::AST_TYPE_FLAGS_LOOKUP[$node->flags];
}
// Probably AST_NAME
return self::toShortString($node);
}
/**
* @param Node|string|int|float $node
*/
private static function formatIncDec(string $format, $node): string
{
$str = self::toShortString($node);
if (!($node instanceof Node && $node->kind === ast\AST_VAR)) {
$str = '(' . $str . ')';
}
// @phan-suppress-next-line PhanPluginPrintfVariableFormatString
return sprintf($format, $str);
}
}
ASTReverter::init();

View File

@@ -0,0 +1,789 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use AssertionError;
use ast;
use ast\flags;
use ast\Node;
use function array_map;
use function array_merge;
use function array_pop;
use function count;
use function in_array;
/**
* This simplifies a PHP AST into a form which is easier to analyze,
* and returns the new Node.
* The original ast\Node objects are not modified.
*
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
* @phan-file-suppress PhanPossiblyUndeclaredProperty
*/
class ASTSimplifier
{
public function __construct()
{
}
/**
* @param Node $node
* @return non-empty-list<Node> (Equivalent list of nodes to [$node], possibly a clone with modifications)
*/
private static function apply(Node $node): array
{
switch ($node->kind) {
case ast\AST_FUNC_DECL:
case ast\AST_METHOD:
case ast\AST_CLOSURE:
case ast\AST_CLASS:
case ast\AST_DO_WHILE:
case ast\AST_FOREACH:
return [self::applyToStmts($node)];
case ast\AST_FOR:
return self::normalizeForStatement($node);
case ast\AST_WHILE:
return self::normalizeWhileStatement($node);
//case ast\AST_BREAK:
//case ast\AST_CONTINUE:
//case ast\AST_RETURN:
//case ast\AST_THROW:
//case ast\AST_EXIT:
default:
return [$node];
case ast\AST_STMT_LIST:
return [self::applyToStatementList($node)];
// Conditional blocks:
case ast\AST_IF:
return self::normalizeIfStatement($node);
case ast\AST_TRY:
return [self::normalizeTryStatement($node)];
}
}
/**
* @param Node $node - A node which will have its child statements simplified.
* @return Node - The same node, or an equivalent simplified node
*/
private static function applyToStmts(Node $node): Node
{
$stmts = $node->children['stmts'];
// Can be null, a single statement, or (possibly) a scalar instead of a node?
if (!($stmts instanceof Node)) {
return $node;
}
$new_stmts = self::applyToStatementList($stmts);
if ($new_stmts === $stmts) {
return $node;
}
$new_node = clone($node);
$new_node->children['stmts'] = $new_stmts;
return $new_node;
}
/**
* @param Node $statement_list - The statement list to simplify
* @return Node - an equivalent statement list (Identical, or a clone)
*/
private static function applyToStatementList(Node $statement_list): Node
{
if ($statement_list->kind !== ast\AST_STMT_LIST) {
$statement_list = self::buildStatementList($statement_list->lineno, $statement_list);
}
$new_children = [];
foreach ($statement_list->children as $child_node) {
if ($child_node instanceof Node) {
foreach (self::apply($child_node) as $new_child_node) {
$new_children[] = $new_child_node;
}
} else {
$new_children[] = $child_node;
}
}
[$new_children, $modified] = self::normalizeStatementList($new_children);
if (!$modified && $new_children === $statement_list->children) {
return $statement_list;
}
$clone_node = clone($statement_list);
$clone_node->children = $new_children;
return $clone_node;
}
/**
* Creates a new node with kind ast\AST_STMT_LIST from a list of 0 or more child nodes.
*/
private static function buildStatementList(int $lineno, Node ...$child_nodes): Node
{
return new Node(
ast\AST_STMT_LIST,
0,
$child_nodes,
$lineno
);
}
/**
* @param list<?Node|?float|?int|?string|?float|?bool> $statements
* @return array{0:list<Node>,1:bool} - [New/old list, bool $modified] An equivalent list after simplifying (or the original list)
*/
private static function normalizeStatementList(array $statements): array
{
$modified = false;
$new_statements = [];
foreach ($statements as $stmt) {
$new_statements[] = $stmt;
if (!($stmt instanceof Node)) {
continue;
}
if ($stmt->kind !== ast\AST_IF) {
continue;
}
// Run normalizeIfStatement again.
\array_pop($new_statements);
\array_push($new_statements, ...self::normalizeIfStatement($stmt));
$modified = $modified || \end($new_statements) !== $stmt;
continue;
}
return [$modified ? $new_statements : $statements, $modified];
}
/**
* Replaces the last node in a list with a list of 0 or more nodes
* @param list<Node> $nodes
* @param Node ...$new_statements
*/
private static function replaceLastNodeWithNodeList(array &$nodes, Node...$new_statements): void
{
if (\array_pop($nodes) === false) {
throw new AssertionError("Saw an unexpected empty node list");
}
foreach ($new_statements as $stmt) {
$nodes[] = $stmt;
}
}
public const NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS = [
flags\BINARY_BOOL_XOR,
flags\BINARY_IS_IDENTICAL,
flags\BINARY_IS_NOT_IDENTICAL,
flags\BINARY_IS_EQUAL,
flags\BINARY_IS_NOT_EQUAL,
flags\BINARY_IS_SMALLER,
flags\BINARY_IS_SMALLER_OR_EQUAL,
flags\BINARY_IS_GREATER,
flags\BINARY_IS_GREATER_OR_EQUAL,
flags\BINARY_SPACESHIP,
];
/**
* If this returns true, the expression has no side effects, and can safely be reordered.
* (E.g. returns true for `MY_CONST` or `false` in `if (MY_CONST === ($x = y))`
*
* @param Node|string|float|int $node
* @internal the way this behaves may change
* @see ScopeImpactCheckingVisitor::hasPossibleImpact() for a more general check
*/
public static function isExpressionWithoutSideEffects($node): bool
{
if (!($node instanceof Node)) {
return true;
}
switch ($node->kind) {
case ast\AST_CONST:
case ast\AST_MAGIC_CONST:
case ast\AST_NAME:
return true;
case ast\AST_UNARY_OP:
return self::isExpressionWithoutSideEffects($node->children['expr']);
case ast\AST_BINARY_OP:
return self::isExpressionWithoutSideEffects($node->children['left']) &&
self::isExpressionWithoutSideEffects($node->children['right']);
case ast\AST_CLASS_CONST:
case ast\AST_CLASS_NAME:
return self::isExpressionWithoutSideEffects($node->children['class']);
default:
return false;
}
}
/**
* Converts an if statement to one which is easier for phan to analyze
* E.g. repeatedly makes these conversions
* if (A && B) {X} -> if (A) { if (B) {X}}
* if ($var = A) {X} -> $var = A; if ($var) {X}
* @return non-empty-list<Node> - One or more nodes created from $original_node.
* Will return [$original_node] if no modifications were made.
*/
private static function normalizeIfStatement(Node $original_node): array
{
$nodes = [$original_node];
// Repeatedly apply these rules
do {
$old_nodes = $nodes;
$node = $nodes[count($nodes) - 1];
$node->flags = 0;
$if_cond = $node->children[0]->children['cond'];
if (!($if_cond instanceof Node)) {
break; // No transformation rules apply here.
}
if ($if_cond->kind === ast\AST_UNARY_OP &&
$if_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $if_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfDoubleNegateReduction($node));
continue;
}
if (count($node->children) === 1) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfNegatedToIfElseReduction($node));
continue;
}
}
if ($if_cond->kind === ast\AST_BINARY_OP && in_array($if_cond->flags, self::NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS, true)) {
// if (($var = A) === B) {X} -> $var = A; if ($var === B) { X}
$if_cond_children = $if_cond->children;
if (in_array($if_cond_children['left']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) &&
($if_cond_children['left']->children['var']->kind ?? 0) === ast\AST_VAR &&
self::isExpressionWithoutSideEffects($if_cond_children['right'])) {
self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInLeftSideOfBinaryOpReduction($node));
continue;
}
if (in_array($if_cond_children['right']->kind ?? 0, [ast\AST_ASSIGN, ast\AST_ASSIGN_REF], true) &&
($if_cond_children['right']->children['var']->kind ?? 0) === ast\AST_VAR &&
self::isExpressionWithoutSideEffects($if_cond_children['left'])) {
self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInRightSideOfBinaryOpReduction($node));
continue;
}
// TODO: If the left-hand side is a constant or class constant or literal, that's safe to rearrange as well
// (But `foo($y = something()) && $x = $y` is not safe to rearrange)
}
if (count($node->children) === 1) {
if ($if_cond->kind === ast\AST_BINARY_OP &&
$if_cond->flags === flags\BINARY_BOOL_AND) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfAndReduction($node));
// if (A && B) {X} -> if (A) { if (B) {X}}
// Do this, unless there is an else statement that can be executed.
continue;
}
} elseif (count($node->children) === 2) {
if ($if_cond->kind === ast\AST_UNARY_OP &&
$if_cond->flags === flags\UNARY_BOOL_NOT &&
$node->children[1]->children['cond'] === null) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfNegateReduction($node));
continue;
}
} elseif (count($node->children) >= 3) {
self::replaceLastNodeWithNodeList($nodes, self::applyIfChainReduction($node));
continue;
}
if ($if_cond->kind === ast\AST_ASSIGN &&
($if_cond->children['var']->kind ?? null) === ast\AST_VAR) {
// if ($var = A) {X} -> $var = A; if ($var) {X}
// do this whether or not there is an else.
// TODO: Could also reduce `if (($var = A) && B) {X} else if (C) {Y} -> $var = A; ....
self::replaceLastNodeWithNodeList($nodes, ...self::applyIfAssignReduction($node));
continue;
}
} while ($old_nodes !== $nodes);
return $nodes;
}
/**
* Converts a while statement to one which is easier for phan to analyze
* E.g. repeatedly makes these conversions
* while (A && B) {X} -> while (A) { if (!B) {break;} X}
* while (!!A) {X} -> while (A) { X }
* @return array{0:Node} - An array with a single while statement
* Will return [$original_node] if no modifications were made.
*/
private static function normalizeWhileStatement(Node $original_node): array
{
$node = $original_node;
// Repeatedly apply these rules
while (true) {
$while_cond = $node->children['cond'];
if (!($while_cond instanceof Node)) {
break; // No transformation rules apply here.
}
if ($while_cond->kind === ast\AST_UNARY_OP &&
$while_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $while_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
$node = self::applyWhileDoubleNegateReduction($node);
continue;
}
break;
}
if ($while_cond->kind === ast\AST_BINARY_OP &&
$while_cond->flags === flags\BINARY_BOOL_AND) {
// TODO: Also support `and` operator.
$node = self::applyWhileAndReduction($node);
// while (A && B) {X} -> while (A) { if (!B) {break;} X}
// Do this, unless there is an else statement that can be executed.
continue;
}
break;
}
return [$node];
}
/**
* Converts a for statement to one which is easier for phan to analyze
* E.g. repeatedly makes these conversions
* for (init; !!cond; loop) -> for (init; cond; loop)
* @return array{0:Node} - An array with a single for statement.
* Will return [$node] if no modifications were made.
*/
private static function normalizeForStatement(Node $node): array
{
// Repeatedly apply these rules
while (true) {
$for_cond_list = $node->children['cond'];
if (!($for_cond_list instanceof Node)) {
break; // No transformation rules apply here.
}
$for_cond = \end($for_cond_list->children);
if (!($for_cond instanceof Node)) {
break;
}
if ($for_cond->kind === ast\AST_UNARY_OP &&
$for_cond->flags === flags\UNARY_BOOL_NOT) {
$cond_node = $for_cond->children['expr'];
if ($cond_node instanceof Node &&
$cond_node->kind === ast\AST_UNARY_OP &&
$cond_node->flags === flags\UNARY_BOOL_NOT) {
$node = self::applyForDoubleNegateReduction($node);
continue;
}
}
break;
}
return [$node];
}
/**
* if (($var = A) === B) {X} -> $var = A; if ($var === B) { X }
*
* @return array{0:Node,1:Node}
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyAssignInLeftSideOfBinaryOpReduction(Node $node): array
{
$inner_assign_statement = $node->children[0]->children['cond']->children['left'];
if (!($inner_assign_statement instanceof Node)) {
throw new AssertionError('Expected $inner_assign_statement instanceof Node');
}
$inner_assign_var = $inner_assign_statement->children['var'];
if ($inner_assign_var->kind !== ast\AST_VAR) {
throw new AssertionError('Expected $inner_assign_var->kind === ast\AST_VAR');
}
$new_node_elem = clone($node->children[0]);
$new_node_elem->children['cond']->children['left'] = $inner_assign_var;
$new_node_elem->flags = 0;
$new_node = clone($node);
$new_node->children[0] = $new_node_elem;
$new_node->lineno = $new_node_elem->lineno;
$new_node->flags = 0;
return [$inner_assign_statement, $new_node];
}
/**
* if (B === ($var = A)) {X} -> $var = A; if (B === $var) { X }
*
* @return array{0:Node,1:Node}
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyAssignInRightSideOfBinaryOpReduction(Node $node): array
{
$inner_assign_statement = $node->children[0]->children['cond']->children['right'];
$inner_assign_var = $inner_assign_statement->children['var'];
$new_node_elem = clone($node->children[0]);
$new_node_elem->children['cond']->children['right'] = $inner_assign_var;
$new_node_elem->flags = 0;
$new_node = clone($node);
$new_node->children[0] = $new_node_elem;
$new_node->lineno = $new_node_elem->lineno;
$new_node->flags = 0;
return [$inner_assign_statement, $new_node];
}
/**
* Creates a new node with kind ast\AST_IF from two branches
*/
private static function buildIfNode(Node $l, Node $r): Node
{
return new Node(
ast\AST_IF,
0,
[$l, $r],
$l->lineno
);
}
/**
* maps if (A) {X} elseif (B) {Y} else {Z} -> if (A) {Y} else { if (B) {Y} else {Z}}
*/
private static function applyIfChainReduction(Node $node): Node
{
$children = $node->children; // Copy of array of Nodes of type IF_ELEM
if (count($children) <= 2) {
return $node;
}
while (count($children) > 2) {
$r = array_pop($children);
$l = array_pop($children);
if (!($l instanceof Node && $r instanceof Node)) {
throw new AssertionError("Expected to have AST_IF_ELEM nodes");
}
$l->children['stmts']->flags = 0;
$r->children['stmts']->flags = 0;
$inner_if_node = self::buildIfNode($l, $r);
$new_r = new Node(
ast\AST_IF_ELEM,
0,
[
'cond' => null,
'stmts' => self::buildStatementList($inner_if_node->lineno, ...(self::normalizeIfStatement($inner_if_node))),
],
0
);
$children[] = $new_r;
}
// $children is an array of 2 nodes of type IF_ELEM
return new Node(ast\AST_IF, 0, $children, $node->lineno);
}
/**
* Converts if (A && B) {X}` -> `if (A) { if (B){X}}`
* @return Node simplified node logically equivalent to $node, with kind ast\AST_IF.
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyIfAndReduction(Node $node): Node
{
if (count($node->children) !== 1) {
throw new AssertionError('Expected an if statement with no else/elseif statements');
}
$inner_node_elem = clone($node->children[0]); // AST_IF_ELEM
$inner_node_elem->children['cond'] = $inner_node_elem->children['cond']->children['right'];
$inner_node_elem->flags = 0;
$inner_node_lineno = $inner_node_elem->lineno;
// Normalize code such as `if (A && (B && C)) {...}` recursively.
$inner_node_stmts = self::normalizeIfStatement(new Node(
ast\AST_IF,
0,
[$inner_node_elem],
$inner_node_lineno
));
$inner_node_stmt_list = new Node(ast\AST_STMT_LIST, 0, $inner_node_stmts, $inner_node_lineno);
$outer_node_elem = clone($node->children[0]); // AST_IF_ELEM
$outer_node_elem->children['cond'] = $node->children[0]->children['cond']->children['left'];
$outer_node_elem->children['stmts'] = $inner_node_stmt_list;
$outer_node_elem->flags = 0;
return new Node(
ast\AST_IF,
0,
[$outer_node_elem],
$node->lineno
);
}
/**
* Converts `while (A && B) {X}` -> `while (A) { if (!B) { break;} X}`
* @return Node simplified node logically equivalent to $node, with kind ast\AST_IF.
*/
private static function applyWhileAndReduction(Node $node): Node
{
$cond_node = $node->children['cond'];
$right_node = $cond_node->children['right'];
$lineno = $right_node->lineno ?? $cond_node->lineno;
$conditional_break_elem = self::makeBreakWithNegatedConditional($right_node, $lineno);
return new Node(
ast\AST_WHILE,
0,
[
'cond' => $cond_node->children['left'],
'stmts' => new Node(
ast\AST_STMT_LIST,
0,
array_merge([$conditional_break_elem], $node->children['stmts']->children),
$lineno
),
],
$node->lineno
);
}
/**
* Creates a Node for `if (!COND) { break; }`
* @param Node|string|int|float $cond_node
*/
private static function makeBreakWithNegatedConditional($cond_node, int $lineno): Node
{
$break_if_elem = new Node(
ast\AST_IF_ELEM,
0,
[
'cond' => new Node(
ast\AST_UNARY_OP,
flags\UNARY_BOOL_NOT,
['expr' => $cond_node],
$lineno
),
'stmts' => new Node(
ast\AST_STMT_LIST,
0,
[new Node(ast\AST_BREAK, 0, ['depth' => null], $lineno)],
$lineno
),
],
$lineno
);
return new Node(
ast\AST_IF,
0,
[$break_if_elem],
$lineno
);
}
/**
* Converts if ($x = A) {Y} -> $x = A; if ($x) {Y}
* This allows analyzing variables set in if blocks outside of the `if` block
* @return array{0:Node,1:Node} [$outer_assign_statement, $new_node]
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyIfAssignReduction(Node $node): array
{
$outer_assign_statement = $node->children[0]->children['cond'];
if (!($outer_assign_statement instanceof Node)) {
throw new AssertionError('Expected condition of first if statement (with assignment as condition) to be a Node');
}
$new_node_elem = clone($node->children[0]);
$new_node_elem->children['cond'] = $new_node_elem->children['cond']->children['var'];
$new_node_elem->flags = 0;
$new_node = clone($node);
$new_node->children[0] = $new_node_elem;
$new_node->lineno = $new_node_elem->lineno;
$new_node->flags = 0;
return [$outer_assign_statement, $new_node];
}
/**
* Converts if (!x) {Y} else {Z} -> if (x) {Z} else {Y}
* This improves Phan's analysis for cases such as `if (!is_string($x))`.
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyIfNegateReduction(Node $node): Node
{
if (!(
count($node->children) === 2 &&
$node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT &&
$node->children[1]->children['cond'] === null
)) {
throw new AssertionError('Failed precondition of ' . __METHOD__);
}
$new_node = clone($node);
$new_node->children = [clone($new_node->children[1]), clone($new_node->children[0])];
$new_node->children[0]->children['cond'] = $node->children[0]->children['cond']->children['expr'];
$new_node->children[1]->children['cond'] = null;
$new_node->flags = 0;
// @phan-suppress-next-line PhanUndeclaredProperty used by EmptyStatementListPlugin
$new_node->is_simplified = true;
return $new_node;
}
/**
* Converts if (!!(x)) {Y} -> if (x) {Y}
* This improves Phan's analysis for cases such as `if (!!x)`
* @suppress PhanTypePossiblyInvalidCloneNotObject this was checked by the caller.
*/
private static function applyIfDoubleNegateReduction(Node $node): Node
{
if (!(
$node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT &&
$node->children[0]->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT
)) {
throw new AssertionError('Failed precondition of ' . __METHOD__);
}
$new_cond = $node->children[0]->children['cond']->children['expr']->children['expr'];
$new_node = clone($node);
$new_node->flags = 0;
$new_node->children[0] = clone($node->children[0]);
$new_node->children[0]->flags = 0;
$new_node->children[0]->children['cond'] = $new_cond;
return $new_node;
}
/**
* Converts while (!!(x)) {Y} -> if (x) {Y}
* This improves Phan's analysis for cases such as `if (!!x)`
*/
private static function applyWhileDoubleNegateReduction(Node $node): Node
{
if (!(
$node->children['cond']->flags === flags\UNARY_BOOL_NOT &&
$node->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT
)) {
throw new AssertionError('Failed precondition of ' . __METHOD__);
}
return new Node(
ast\AST_WHILE,
0,
[
'cond' => $node->children['cond']->children['expr']->children['expr'],
'stmts' => $node->children['stmts']
],
$node->lineno
);
}
/**
* Converts for (INIT; !!(x); LOOP) {Y} -> if (INIT; x; LOOP) {Y}
* This improves Phan's analysis for cases such as `if (!!x)`
*/
private static function applyForDoubleNegateReduction(Node $node): Node
{
$children = $node->children;
$cond_node_list = $children['cond']->children;
$cond_node = array_pop($cond_node_list);
if (!(
$cond_node->flags === flags\UNARY_BOOL_NOT &&
$cond_node->children['expr']->flags === flags\UNARY_BOOL_NOT
)) {
throw new AssertionError('Failed precondition of ' . __METHOD__);
}
$cond_node_list[] = $cond_node->children['expr']->children['expr'];
$children['cond'] = new ast\Node(
ast\AST_EXPR_LIST,
0,
$cond_node_list,
$children['cond']->lineno
);
return new Node(
ast\AST_FOR,
0,
$children,
$node->lineno
);
}
private static function applyIfNegatedToIfElseReduction(Node $node): Node
{
if (count($node->children) !== 1) {
throw new AssertionError("Expected one child node");
}
$if_elem = $node->children[0];
if ($if_elem->children['cond']->flags !== flags\UNARY_BOOL_NOT) {
throw new AssertionError("Expected condition to begin with unary boolean negation operator");
}
$lineno = $if_elem->lineno;
$new_else_elem = new Node(
ast\AST_IF_ELEM,
0,
[
'cond' => null,
'stmts' => $if_elem->children['stmts'],
],
$lineno
);
$new_if_elem = new Node(
ast\AST_IF_ELEM,
0,
[
'cond' => $if_elem->children['cond']->children['expr'],
'stmts' => new Node(ast\AST_STMT_LIST, 0, [], $if_elem->lineno),
],
$lineno
);
return new Node(
ast\AST_IF,
0,
[$new_if_elem, $new_else_elem],
$node->lineno
);
}
/**
* Recurses on a list of 0 or more catch statements. (as in try/catch)
* Returns an equivalent list of catch AST nodes (or the original if no changes were made)
*/
private static function normalizeCatchesList(Node $catches): Node
{
$list = $catches->children;
$new_list = array_map(
static function (Node $node): Node {
return self::applyToStmts($node);
},
// @phan-suppress-next-line PhanPartialTypeMismatchArgument should be impossible to be float
$list
);
if ($new_list === $list) {
return $catches;
}
$new_catches = clone($catches);
$new_catches->children = $new_list;
$new_catches->flags = 0;
return $new_catches;
}
/**
* Recurses on a try/catch/finally node, applying simplifications(catch/finally are optional)
* Returns an equivalent try/catch/finally node (or the original if no changes were made)
*/
private static function normalizeTryStatement(Node $node): Node
{
$try = $node->children['try'];
$catches = $node->children['catches'];
$finally = $node->children['finally'] ?? null;
$new_try = self::applyToStatementList($try);
$new_catches = $catches ? self::normalizeCatchesList($catches) : $catches;
$new_finally = $finally ? self::applyToStatementList($finally) : $finally;
if ($new_try === $try && $new_catches === $catches && $new_finally === $finally) {
return $node;
}
$new_node = clone($node);
$new_node->children['try'] = $new_try;
$new_node->children['catches'] = $new_catches;
$new_node->children['finally'] = $new_finally;
$new_node->flags = 0;
return $new_node;
}
/**
* Returns a Node that represents $node after all of the AST simplification steps.
*
* $node is not modified. This will reuse descendant nodes that didn't change.
*/
public static function applyStatic(Node $node): Node
{
$rewriter = new self();
$nodes = $rewriter->apply($node);
if (count($nodes) !== 1) {
throw new AssertionError("Expected applying simplifier to a statement list would return an array with one statement list");
}
return $nodes[0];
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use Phan\AST\Visitor\KindVisitorImplementation;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\FQSEN;
use Phan\Language\Type;
use Phan\Language\UnionType;
use Phan\Suggestion;
/**
* A visitor used for analysis.
*
* In addition to calling the corresponding visit*() method for the passed in \ast\Node's kind,
* this contains helper methods to emit issues.
*/
abstract class AnalysisVisitor extends KindVisitorImplementation
{
/**
* @var CodeBase
* The code base within which we're operating
* @phan-read-only
*/
protected $code_base;
/**
* @var Context
* The context in which the node we're going to be looking
* at exists.
*/
protected $context;
/**
* @param CodeBase $code_base
* The code base within which we're operating
*
* @param Context $context
* The context of the parser at the node for which we'd
* like to determine a type
*/
public function __construct(
CodeBase $code_base,
Context $context
) {
$this->context = $context;
$this->code_base = $code_base;
}
/**
* @param string $issue_type
* The type of issue to emit such as Issue::ParentlessClass
*
* @param int $lineno
* The line number where the issue was found
*
* @param int|string|FQSEN|UnionType|Type ...$parameters
* Template parameters for the issue's error message
*
* @see PluginAwarePostAnalysisVisitor::emitPluginIssue if you are using this from a plugin.
*/
protected function emitIssue(
string $issue_type,
int $lineno,
...$parameters
): void {
Issue::maybeEmitWithParameters(
$this->code_base,
$this->context,
$issue_type,
$lineno,
$parameters
);
}
/**
* @param string $issue_type
* The type of issue to emit such as Issue::ParentlessClass
*
* @param int $lineno
* The line number where the issue was found
*
* @param list<int|string|FQSEN|UnionType|Type> $parameters
* Template parameters for the issue's error message
*
* @param ?Suggestion $suggestion
* A suggestion (may be null)
*/
protected function emitIssueWithSuggestion(
string $issue_type,
int $lineno,
array $parameters,
?Suggestion $suggestion
): void {
Issue::maybeEmitWithParameters(
$this->code_base,
$this->context,
$issue_type,
$lineno,
$parameters,
$suggestion
);
}
/**
* Check if an issue type (different from the one being emitted) should be suppressed.
*
* This is useful for ensuring that TypeMismatchProperty also suppresses PhanPossiblyNullTypeMismatchProperty,
* for example.
*/
protected function shouldSuppressIssue(string $issue_type, int $lineno): bool
{
return Issue::shouldSuppressIssue(
$this->code_base,
$this->context,
$issue_type,
$lineno,
[]
);
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Phan\AST;
use ast;
use ast\Node;
use InvalidArgumentException;
use function is_string;
/**
* Utilities for computing uses of an ast\AST_ARROW_FUNC node.
*/
class ArrowFunc
{
/** @var associative-array<int|string, Node> maps variable names to the first Node where the variable was used.*/
private $uses = [];
private function __construct()
{
}
/**
* Returns the set of variables used by the arrow func $n
*
* @param Node $n a Node with kind ast\AST_ARROW_FUNC
* @return associative-array<int|string, Node>
*/
public static function getUses(Node $n): array
{
if ($n->kind !== ast\AST_ARROW_FUNC) {
throw new InvalidArgumentException("Expected node kind AST_ARROW_FUNC but got " . ast\get_kind_name($n->kind));
}
// @phan-suppress-next-line PhanUndeclaredProperty
return $n->phan_arrow_uses ?? $n->phan_arrow_uses = (new self())->computeUses($n);
}
/**
* @return array<string|int,Node>
*/
private function computeUses(Node $n): array
{
$stmts = $n->children['stmts'];
if ($stmts instanceof Node) { // should always be a node
$this->buildUses($stmts);
// Iterate over the AST_PARAM nodes and remove their variables.
// They are variables used within the function, but are not uses from the outer scope.
foreach ($n->children['params']->children ?? [] as $param) {
$name = $param->children['name'] ?? null;
if (\is_string($name)) {
unset($this->uses[$name]);
}
}
}
return $this->uses;
}
/**
* @param int|string $name the name of the variable being used by this arrow func.
* may need to handle `${'0'}`?
*/
private function recordUse($name, Node $n): void
{
if ($name !== 'this') {
$this->uses[$name] = $this->uses[$name] ?? $n;
}
}
private function buildUses(Node $n): void
{
switch ($n->kind) {
case ast\AST_VAR:
$name = $n->children['name'];
if (is_string($name)) {
$this->recordUse($name, $n);
return;
}
break;
case ast\AST_ARROW_FUNC:
foreach (self::getUses($n) as $name => $child_node) {
$this->recordUse($name, $child_node);
}
return;
case ast\AST_CLOSURE:
foreach ($n->children['uses']->children ?? [] as $child_node) {
if (!$child_node instanceof Node) {
continue;
}
$name = $child_node->children['name'];
if (is_string($name)) {
$this->recordUse($name, $child_node);
}
}
return;
case ast\AST_CLASS:
foreach ($n->children['args']->children ?? [] as $child_node) {
if ($child_node instanceof Node) {
$this->buildUses($child_node);
}
}
return;
}
foreach ($n->children as $child_node) {
if ($child_node instanceof Node) {
$this->buildUses($child_node);
}
}
}
/**
* Record that variable $variable_name exists in the outer scope of the arrow function with node $n
*/
public static function recordVariableExistsInOuterScope(Node $n, string $variable_name): void
{
if ($n->kind !== ast\AST_ARROW_FUNC) {
throw new InvalidArgumentException("Expected node kind AST_ARROW_FUNC but got " . ast\get_kind_name($n->kind));
}
// @phan-suppress-next-line PhanUndeclaredProperty
$n->phan_arrow_inherited_vars[$variable_name] = true;
}
}

File diff suppressed because it is too large Load Diff

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