phan 설치
This commit is contained in:
+21
@@ -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
@@ -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
|
||||
Vendored
+40
@@ -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
|
||||
Vendored
+583
@@ -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',
|
||||
],
|
||||
];
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user