phan 설치

This commit is contained in:
2020-05-01 15:26:54 +09:00
parent a529eba693
commit 51c96b4d04
991 changed files with 239131 additions and 14 deletions
+21
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
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
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
+583
View File
@@ -0,0 +1,583 @@
<?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 src/Phan/Config.php
* See Config for all configurable options.
*
* 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 [
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `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,
// 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's object expression
// is definitely not an object,
// or if **any** type in an invoked expression is not a callable.
// Setting this to true will introduce numerous false positives
// (and reveal some bugs).
'strict_method_checking' => true,
// If enabled, Phan will warn if **any** type in the argument's type
// cannot be cast to a type in the parameter's expected type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check)
'strict_param_checking' => true,
// If enabled, Phan will warn if **any** type in a property assignment's type
// cannot be cast to a type in the property's expected type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// (For self-analysis, Phan has a large number of suppressions and file-level suppressions, due to \ast\Node being difficult to type check)
'strict_property_checking' => true,
// If enabled, Phan will warn if **any** type in the return statement's union type
// cannot be cast to a type in the method's declared return type.
// Setting this to true will introduce a large number of false positives (and some bugs).
// (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. This process is
// slow.
'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,
// (*Requires check_docblock_signature_param_type_match to be true*)
// If true, make narrowed types from phpdoc params override
// the real types from the signature, when real types exist.
// (E.g. allows specifying desired lists of subclasses,
// or to indicate a preference for non-nullable types over nullable types)
// Affects analysis of the body of the method and the param types passed in by callers.
'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 allow contravariance in real parameter types of method overrides (Introduced in php 7.2)
// See https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening
// (Users may enable this if analyzing projects that support only php 7.2+)
// This is false by default. (Will warn if real parameter types are omitted in an override)
'allow_method_param_type_widening' => false,
// Set this to true to make Phan guess that undocumented parameter types
// (for optional parameters) have the same type as default values
// (Instead of combining that type with `mixed`).
// 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, Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
// If the corresponding value is not empty, Phan will act as though it saw the corresponding union type when the keys show up in a UnionType of @param, @return, @var, @property, etc.
//
// 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.
'dead_code_detection' => false,
// Set to true in order to attempt to detect unused variables.
// dead_code_detection will also enable unused variable detection.
'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')
// here 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',
],
// If empty, no filter against issues types will be applied.
// If 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/make_stubs',
'tool/pdep',
'tool/phantasm',
'tool/phoogle',
'internal/dump_fallback_ast.php',
'internal/dump_html_styles.php',
'internal/extract_arg_info.php',
'internal/internalsignatures.php',
'internal/package.php',
'internal/reflection_completeness_check.php',
'internal/sanitycheck.php',
'internal/update_wiki_config_types.php',
'internal/update_wiki_issue_types.php',
'vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php',
'vendor/phpdocumentor/reflection-docblock/src/DocBlock.php',
// '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/Psr',
'vendor/sabre/event/lib',
'vendor/symfony/console',
'.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 'mkstubs' script can be used to generate your own stubs (compatible with php 7.0+ right now)
'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',
'posix' => '.phan/internal_stubs/posix.phan_php',
'readline' => '.phan/internal_stubs/readline.phan_php',
'sysvmsg' => '.phan/internal_stubs/sysvmsg.phan_php',
'sysvsem' => '.phan/internal_stubs/sysvsem.phan_php',
'sysvshm' => '.phan/internal_stubs/sysvshm.phan_php',
],
// 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,
// blacklist of methods to warn 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,
],
// 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',
'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'`
'.phan/plugins/StrictLiteralComparisonPlugin.php',
// 'UnknownClassElementAccessPlugin' is more useful with batch analysis than in an editor.
// It's used in tests/run_test __FakeSelfFallbackTest
////////////////////////////////////////////////////////////////////////
// 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.
// NOTE: This plugin only produces correct results when
// Phan is run on a single core (-j1).
// 'UnusedSuppressionPlugin',
],
];
+4
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.
+216
View File
@@ -0,0 +1,216 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ast@1.0.6
namespace ast {
class Metadata {
// properties
public $flags;
public $flagsCombinable;
public $kind;
public $name;
}
class Node {
// properties
public $children;
public $endLineno;
public $flags;
public $kind;
public $lineno;
// methods
public function __construct($kind = null, $flags = null, ?array $children = null, $lineno = null) {}
}
function get_kind_name($kind) {}
function get_metadata() {}
function get_supported_versions($exclude_deprecated = null) {}
function kind_uses_flags($kind) {}
function parse_code($code, $version, $filename = null) {}
function parse_file($filename, $version) {}
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_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_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_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_METHOD = 69;
const AST_METHOD_CALL = 768;
const AST_METHOD_REFERENCE = 540;
const AST_NAME = 2048;
const AST_NAMESPACE = 541;
const AST_NAME_LIST = 141;
const AST_NEW = 526;
const AST_NULLABLE_TYPE = 2050;
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_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_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_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;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension ctype@7.4.3-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) {}
}
@@ -0,0 +1,8 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension igbinary@3.1.2
namespace {
function igbinary_serialize($value) {}
function igbinary_unserialize($str) {}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension mbstring@7.3.8
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;
}
+152
View File
@@ -0,0 +1,152 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension pcntl@7.4.3-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_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;
}
+69
View File
@@ -0,0 +1,69 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension posix@7.4.3-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;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension readline@7.4.3-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';
}
+18
View File
@@ -0,0 +1,18 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvmsg@7.4.3-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;
}
+10
View File
@@ -0,0 +1,10 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvsem@7.4.3-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) {}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
// These stubs were generated by the phan stub generator.
// @phan-stub-for-extension sysvshm@7.4.3-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) {}
}
+65
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;
}
+163
View File
@@ -0,0 +1,163 @@
<?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\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)) {
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 abstract methods, generators, etc.
return;
}
if (self::returnTypeOfFunctionLikeAllowsNull($function)) {
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->containsNullable()
|| $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();
+130
View File
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Analysis\ConditionVisitorUtil;
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 (!ConditionVisitorUtil::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();
+228
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();
+78
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();
@@ -0,0 +1,302 @@
<?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\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)) {
$case_cond = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $case_cond)->asSingleScalarValueOrNullOrSelf();
if (is_object($case_cond)) {
// Skip non-literal keys.
continue;
}
}
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;
} else {
$cond_key = json_encode($case_cond);
if (is_scalar($case_cond)) {
$values_to_check[$i] = $case_cond;
}
}
if (isset($case_constant_set[$cond_key])) {
$normalized_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)) {
// For `"1"`, search for `"1foo"`, `"1bar"`, etc.
$value = is_float($value) ? $value : filter_var($value, FILTER_VALIDATE_FLOAT);
$old_index = $numeric_set[$value] ?? $fuzzy_numeric_set[$value] ?? null;
$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
* 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 ($key === null) {
$has_entry_without_key = true;
continue;
}
if (!is_scalar($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();
@@ -0,0 +1,464 @@
<?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\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);
}
/**
* @param Node $node
* An assignment operation node to analyze
* @override
*/
public function visitAssign(Node $node): void
{
$var = $node->children['var'];
$expr = $node->children['expr'];
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 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'])]
);
}
}
}
/**
* @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);
\array_push($children, ...$first_stmt->children);
$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();
@@ -0,0 +1,121 @@
<?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\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
{
public function visitMethod(Node $node): void
{
$stmts_node = $node->children['stmts'] ?? null;
if ($stmts_node && !$stmts_node->children) {
$method = $this->context->getFunctionLikeInScope($this->code_base);
if (!($method instanceof Method)) {
throw new AssertionError("Expected $method to be a method");
}
if (!$method->isOverriddenByAnother()
&& !$method->isOverride()
&& !$method->isDeprecated()
) {
$this->emitIssue(
self::getIssueTypeForEmptyMethod($method),
$node->lineno,
$method->getName()
);
}
}
}
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()) {
if (!$function->isClosure()) {
$this->emitIssue(
Issue::EmptyFunction,
$node->lineno,
$function->getName()
);
} else {
$this->emitIssue(
Issue::EmptyClosure,
$node->lineno
);
}
}
}
}
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();
@@ -0,0 +1,356 @@
<?php
declare(strict_types=1);
use ast\Node;
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)) {
$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',
[]
);
}
}
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'])) {
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();
+153
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();
+402
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();
+173
View File
@@ -0,0 +1,173 @@
<?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;
/**
* 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
* @return void
*/
public function visitEcho(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.
return new InlineHTMLPlugin();
@@ -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();
@@ -0,0 +1,428 @@
<?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\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]*)$@";
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),
]
);
}
}
/**
* 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 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
$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;
self::streamPutContents($pipes[0], $new_file_contents);
}
$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
* @return void
* See https://bugs.php.net/bug.php?id=39598
*/
private static function streamPutContents($stream, string $file_contents): void
{
try {
while (strlen($file_contents) > 0) {
$bytes_written = fwrite($stream, $file_contents);
if ($bytes_written === false) {
error_log('failed to write in ' . __METHOD__);
return;
}
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) {
error_log('failed to write in ' . __METHOD__ . ' but the stream should be ready');
return;
}
}
if ($bytes_written > 0) {
$file_contents = \substr($file_contents, $bytes_written);
}
}
} finally {
fclose($stream);
}
}
/**
* @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");
}
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;
}
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();
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\Language\Context;
use Phan\Language\Element\Variable;
use Phan\Plugin\Internal\RedundantConditionLoopCheck;
use Phan\PluginV3;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use Phan\PluginV3\PostAnalyzeNodeCapability;
/**
* This plugin detects reuse of loop variables
*/
class LoopVariableReusePlugin extends PluginV3 implements PostAnalyzeNodeCapability
{
/**
* @return class-string - name of PluginAwarePostAnalysisVisitor subclass
*
* @override
*/
public static function getPostAnalyzeNodeVisitorClassName(): string
{
return LoopVariableReuseVisitor::class;
}
}
/**
* This visitor implements the checks for reuse of loop variables.
*/
class LoopVariableReuseVisitor extends PluginAwarePostAnalysisVisitor
{
// A plugin's visitors should not override visit() unless they need to.
/**
* @var list<Node> set by plugin framework
* @suppress PhanReadOnlyProtectedProperty
*/
protected $parent_node_list;
/**
* @override checks for reuse of variables in a node of kind ast\AST_FOREACH
*/
public function visitForeach(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfForeach($node));
return $this->context;
}
/**
* @return array<string|int,Node>
*/
private function extractLoopVariablesOfForeach(Node $node): array
{
return $this->extractVariables($node->children['key']) + $this->extractVariables($node->children['value']);
}
/**
* @override checks for reuse of variables in a node of kind ast\AST_FOR
*/
public function visitFor(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfFor($node));
return $this->context;
}
/**
* @param Node $node a node of kind ast\AST_FOR
* @return array<string|int,Node>
* @suppress PhanAccessMethodInternal
*/
private function extractLoopVariablesOfFor(Node $node): array
{
$directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']) +
RedundantConditionLoopCheck::extractIncrementDirections($this->code_base, $this->context, $node->children['loop']);
if (!$directions) {
return [];
}
$variables = self::extractVariables($node->children['cond']) + self::extractVariables($node->children['loop']);
return array_intersect_key($variables, $directions);
}
/**
* @override checks for reuse of variables in a node of kind ast\AST_WHILE
*/
public function visitWhile(Node $node): Context
{
$this->findVariableReuse($this->extractLoopVariablesOfWhile($node));
return $this->context;
}
/**
* @param Node $node a node of kind ast\AST_WHILE
* @return array<string|int,Node>
* @suppress PhanAccessMethodInternal
*/
private function extractLoopVariablesOfWhile(Node $node): array
{
$directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']);
if (!$directions) {
return [];
}
return array_intersect_key(self::extractVariables($node->children['cond']), $directions);
}
/**
* @param array<string|int,Node> $variables
*/
private function findVariableReuse(array $variables): void
{
if (!$variables) {
return;
}
for ($i = count($this->parent_node_list) - 1; $i >= 0; $i--) {
$parent_node = $this->parent_node_list[$i];
$outer_variables = [];
switch ($parent_node->kind) {
case ast\AST_FOREACH:
$outer_variables = $this->extractLoopVariablesOfForeach($parent_node);
break;
case ast\AST_FOR:
$outer_variables = $this->extractLoopVariablesOfFor($parent_node);
break;
case ast\AST_WHILE:
$outer_variables = $this->extractLoopVariablesOfWhile($parent_node);
break;
case ast\AST_FUNC_DECL:
case ast\AST_CLOSURE:
case ast\AST_ARROW_FUNC:
case ast\AST_METHOD:
case ast\AST_CLASS:
return;
default:
continue 2;
}
$common_outer_variables = array_intersect_key($outer_variables, $variables);
if ($common_outer_variables) {
$this->warnCommonOuterVariables($variables, $common_outer_variables);
return;
}
}
}
/**
* @param array<string|int,Node> $variables
* @param array<string|int,Node> $common_outer_variables
*/
private function warnCommonOuterVariables(array $variables, array $common_outer_variables): void
{
foreach ($common_outer_variables as $variable_name => $node) {
$inner_node = $variables[$variable_name];
$this->emitPluginIssue(
$this->code_base,
(clone($this->context))->withLineNumberStart($inner_node->lineno),
'PhanPluginLoopVariableReuse',
'Variable ${VARIABLE} used in loop was also used in an outer loop on line {LINE}',
[$variable_name, $node->lineno]
);
}
}
/**
* @param Node|string|int|float|null $node
* @return array<int|string,Node> a list of all variable nodes in this foreach
*/
private function extractVariables($node): array
{
if (!$node instanceof Node) {
return [];
}
switch ($node->kind) {
case ast\AST_VAR:
if ($node->kind === ast\AST_VAR) {
$var_name = $node->children['name'];
if (is_string($var_name)) {
if (in_array($var_name, ['this', '_'], true) || Variable::isHardcodedVariableInScopeWithName($var_name, $this->context->isInGlobalScope())) {
return [];
}
return [$var_name => $node];
}
}
break;
// Kinds of nodes we don't bother checking
case ast\AST_STATIC_PROP:
case ast\AST_PROP:
// Kinds of declarations creating a new scope.
case ast\AST_FUNC_DECL:
case ast\AST_CLOSURE:
case ast\AST_ARROW_FUNC:
case ast\AST_METHOD:
case ast\AST_CLASS:
// FUNC_DECL and METHOD are probably unreachable.
return [];
}
$result = [];
foreach ($node->children as $child_node) {
$result += self::extractVariables($child_node);
}
return $result;
}
}
return new LoopVariableReusePlugin();
@@ -0,0 +1,241 @@
<?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
*/
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]->isObjectWithKnownFQSEN()) {
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->asExpandedTypes($code_base)->canCastToUnionType($declared_return_type)) {
// 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->isObjectWithKnownFQSEN()) {
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();
+85
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();
+88
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();
@@ -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();
@@ -0,0 +1,200 @@
<?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 keywords and are the same in any namespace
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();
@@ -0,0 +1,94 @@
<?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\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) {
continue;
}
if ($node->globalSpecifier || $node->relativeSpecifier) {
// This is already qualified
continue;
}
$actual_name = (new NodeUtils($contents->getContents()))->phpParserNameToString($node);
if ($actual_name !== $expected_name) {
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))) {
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->getStart();
$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
);
});
@@ -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();
+235
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();
@@ -0,0 +1,185 @@
<?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
*/
public static function fixRedundantFunctionLikeComment(
CodeBase $unused_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
*/
public static function fixRedundantReturnComment(
CodeBase $unused_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->getFullStart(),
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;
}
}
@@ -0,0 +1,158 @@
<?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);
}
public function analyzeMethod(CodeBase $unused_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();
@@ -0,0 +1,131 @@
<?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
*/
public static function fixReturnType(
CodeBase $unused_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
*/
public static function fixParamType(
CodeBase $unused_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;
}
}
@@ -0,0 +1,245 @@
<?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')) {
fwrite(STDERR, "PHPUnitAssertionPlugin failed to find class PHPUnit\Framework\Assert, giving up (set environment variable PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET=1 to ignore this)\n");
}
return [];
}
$result = [];
foreach ($code_base->getMethodSet() as $method) {
$method_fqsen = $method->getDefiningFQSEN();
$class_fqsen = $method_fqsen->getFullyQualifiedClassName();
if ($class_fqsen !== $assert_class_fqsen) {
continue;
}
$closure = $this->createClosureForMethod($code_base, $method, $method_fqsen->getName());
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();
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();
@@ -0,0 +1,152 @@
<?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
*/
public function visitClass(Node $unused_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) {
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)) {
$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();
+271
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();
@@ -0,0 +1,301 @@
<?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 $unused_code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
* @override
*/
public function analyzeMethod(
CodeBase $unused_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 $unused_code_base
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $unused_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();
@@ -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();
@@ -0,0 +1,147 @@
<?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 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 PreferNamespaceUsePlugin
*/
class Fixers
{
/**
* Generate an edit to replace a fully qualified return type with a shorter equivalent representation.
*/
public static function fixReturnType(
CodeBase $unused_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.
*/
public static function fixParamType(
CodeBase $unused_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->getStart(),
$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->typeDeclaration;
if (!$token) {
return null;
}
// @phan-suppress-next-line PhanThrowTypeAbsentForCall php-parser is not expected to throw here
$start = $token instanceof Token ? $token->start : $token->getStart();
$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;
}
}
@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\ContextNode;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
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;
}
}
/**
* @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):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @param list<Node|string|int|float> $args the nodes for the arguments to the invocation
*/
$preg_pattern_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): 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
*/
$preg_pattern_or_array_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): 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
*/
$preg_pattern_and_replacement_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): 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
*/
$preg_replace_callback_array_callback = static function (
CodeBase $code_base,
Context $context,
Func $function,
array $args
): 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();
+783
View File
@@ -0,0 +1,783 @@
<?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);
}
public function getReturnTypeOverrides(CodeBase $unused_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}',
[self::encodeString($fmt_str), $largest_positional, \implode(',', $examples)],
Issue::SEVERITY_NORMAL,
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)) {
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->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, '__toString')) {
$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)) {
// 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): 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);
}
// 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 ConversionSpec[][] $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();
+547
View File
@@ -0,0 +1,547 @@
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).
#### 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)`)
#### 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)
#### UseReturnValuePlugin.php
This plugin warns when code fails to use the return value of internal functions/methods such as `sprintf` or `array_merge` or `Exception->getCode()`.
(functions/methods where the return value should almost always be used)
- **PhanPluginUseReturnValueInternalKnown**: `Expected to use the return value of the internal function/method {FUNCTION}`,
`'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`,
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`
- **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`)
### 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}`
#### 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 return type for ${PARAMETER}`
- **PhanPluginUnknownClosureReturnType**: `Closure {FUNCTION} has no declared or inferred return type`
- **PhanPluginUnknownClosureParamType**: `Closure {FUNCTION} has no declared or inferred return 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`)
- **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}`
#### 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.
#### 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.
### 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)`
@@ -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)) {
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): 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,
$this->context,
$issue_name,
'Assigning {TYPE} to variable ${VARIABLE} which already has that value',
[$variable_type, $var_name]
);
});
return;
}
$this->emitPluginIssue(
$this->code_base,
$this->context,
$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();
+243
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();
@@ -0,0 +1,164 @@
<?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):void>
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
/**
* @return Closure(CodeBase,Context,Func,array):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
*/
return static function (
CodeBase $code_base,
Context $context,
Func $func,
array $args
) 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();
@@ -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);
$right_is_const = ParseVisitor::isConstExpr($right);
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();
@@ -0,0 +1,331 @@
<?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';
// 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) < 2) {
// Can't have a suspicious param order if there are less than 2 params
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|null> $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)) {
return;
}
$arg_names[$i] = strtolower($name);
}
if (count($arg_names) < 2) {
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) {
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) {
return;
}
foreach (self::findCycles($best_destination_map) as $cycle) {
// To reduce false positives, don't warn unless we know the parameter $j would be compatible with what was used at $i
foreach ($cycle as $array_index => $i) {
$j = $cycle[($array_index + 1) % count($cycle)];
$type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $args[$i]);
// echo "Checking if $type can cast to $parameters[$j]\n";
if (!$type->asExpandedTypes($this->code_base)->canCastToUnionType($parameters[$j]->getUnionType())) {
continue 2;
}
}
$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(),
]
);
}
}
}
/**
* @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_METHOD_CALL
* @override
*/
public function visitMethodCall(Node $node): void
{
$args = $node->children['args']->children;
if (count($args) < 2) {
// Can't have a suspicious param order if there are less than 2 params
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);
} 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) < 2) {
// Can't have a suspicious param order if there are less than 2 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();
@@ -0,0 +1,152 @@
<?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 blacklistMethodIssue(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_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->isObjectWithKnownFQSEN()) {
UnknownClassElementAccessPlugin::blacklistMethodIssue($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();
@@ -0,0 +1,391 @@
<?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;
}
}
}
);
}
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 return type for ${PARAMETER}';
} else {
$issue = 'PhanPluginUnknownFunctionParamType';
$message = 'Function {FUNCTION} has no declared or inferred return 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;
}
}
}
);
}
/**
* @param CodeBase $_
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
* @override
*/
public function analyzeProperty(
CodeBase $_,
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
{
foreach ($this->deferred_checks as $check) {
$check($code_base);
}
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new UnknownElementTypePlugin();
+118
View File
@@ -0,0 +1,118 @@
<?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;
}
}
$context = clone($this->context)->withLineNumberStart($next_node->lineno);
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,
$context,
'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();
@@ -0,0 +1,304 @@
<?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 $unused_code_base
* The code base in which the class exists
*
* @param Clazz $class
* A class being analyzed
* @override
*/
public function analyzeClass(
CodeBase $unused_code_base,
Clazz $class
): void {
$this->postponeAnalysisOfElement($class);
}
/**
* @param CodeBase $unused_code_base
* The code base in which the method exists
*
* @param Method $method
* A method being analyzed
* @override
*/
public function analyzeMethod(
CodeBase $unused_code_base,
Method $method
): void {
// Ignore methods inherited by subclasses
if ($method->getFQSEN() !== $method->getDefiningFQSEN()) {
return;
}
$this->postponeAnalysisOfElement($method);
}
/**
* @param CodeBase $unused_code_base
* The code base in which the function exists
*
* @param Func $function
* A function being analyzed
* @override
*/
public function analyzeFunction(
CodeBase $unused_code_base,
Func $function
): void {
$this->postponeAnalysisOfElement($function);
}
/**
* @param CodeBase $unused_code_base
* The code base in which the property exists
*
* @param Property $property
* A property being analyzed
* @override
*/
public function analyzeProperty(
CodeBase $unused_code_base,
Property $property
): void {
$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;
}
public function beforeAnalyzeFile(
CodeBase $unused_code_base,
Context $context,
string $unused_file_contents,
Node $unused_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();
+11
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();
+92
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?$/m', $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?$/m', $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();
@@ -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+$/', $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);
},
];
+2
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
+89
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;
}