From a8896da551ddd42d9961b46791e36fca7d060c45 Mon Sep 17 00:00:00 2001 From: orklah Date: Thu, 3 Dec 2020 23:46:52 +0100 Subject: [PATCH] First commit --- .editorconfig | 13 ++++ .php_cs | 126 +++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++ Plugin.php | 18 ++++++ README.md | 87 +++++++++++++++++++++++++ composer.json | 31 +++++++++ hooks/InsaneComparison.php | 96 ++++++++++++++++++++++++++++ psalm.xml | 16 +++++ 8 files changed, 408 insertions(+) create mode 100644 .editorconfig create mode 100644 .php_cs create mode 100644 LICENSE create mode 100644 Plugin.php create mode 100644 README.md create mode 100644 composer.json create mode 100644 hooks/InsaneComparison.php create mode 100644 psalm.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef4017a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + + +[*.{yaml,yml}] +indent_size = 2 diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..34da580 --- /dev/null +++ b/.php_cs @@ -0,0 +1,126 @@ +in('hooks') + ->notPath(['Kernel.php']); + +return PhpCsFixer\Config::create() + ->setRules( + [ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'dir_constant' => true, + 'heredoc_to_nowdoc' => true, + 'linebreak_after_opening_tag' => true, + 'modernize_types_casting' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'ordered_class_elements' => true, + 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], + 'phpdoc_order' => true, + 'declare_strict_types' => true, + 'doctrine_annotation_braces' => true, + 'doctrine_annotation_indentation' => true, + 'doctrine_annotation_spaces' => true, + 'psr4' => true, + 'no_php4_constructor' => true, + 'no_short_echo_tag' => true, + 'semicolon_after_instruction' => true, + 'align_multiline_comment' => true, + 'doctrine_annotation_array_assignment' => true, + 'general_phpdoc_annotation_remove' => ['annotations' => ["author", "package"]], + 'list_syntax' => ["syntax" => "short"], + 'phpdoc_types_order' => ['null_adjustment' => 'always_last'], + 'single_line_comment_style' => true, + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_return' => true, + 'cast_spaces' => true, + 'combine_consecutive_unsets' => true, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'declare_equal_normalize' => [ + 'space' => 'none', + ], + 'function_to_constant' => true, + 'include' => true, + 'lowercase_cast' => true, + 'magic_constant_casing' => true, + 'mb_str_functions' => true, + 'method_separation' => true, + 'native_function_casing' => true, + 'new_with_braces' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'non_printable_character' => true, + 'normalize_index_brace' => true, + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_fqcn_annotation' => true, + 'php_unit_strict' => true, + 'php_unit_test_class_requires_covers' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'pow_to_exponentiation' => true, + 'increment_style' => ['style' => 'post'], + 'protected_to_private' => true, + 'psr0' => true, + 'random_api_migration' => true, + 'return_type_declaration' => [ + 'space_before' => 'one', + ], + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'silenced_deprecation_error' => true, + 'simplified_null_return' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'strict_comparison' => true, + 'strict_param' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + ] + ) + ->setFinder($finder); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46eba0b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 orklah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Plugin.php b/Plugin.php new file mode 100644 index 0000000..80a15ae --- /dev/null +++ b/Plugin.php @@ -0,0 +1,18 @@ +registerHooksFromClass(InsaneComparison::class); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c42735 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# psalm-insane-comparison +A [Psalm](https://github.com/vimeo/psalm) plugin to detect code susceptible to change behaviour with the introduction of [PHP RFC: Saner string to number comparisons](https://wiki.php.net/rfc/string_to_number_comparison) + +Installation: + +```console +$ composer require --dev orklah/psalm-insane-comparison +$ vendor/bin/psalm-plugin enable orklah/psalm-insane-comparison +``` + +Usage: + +Run your usual Psalm command: +```console +$ vendor/bin/psalm +``` + +Explanation: + +Before PHP8, comparison between a non-empty-string and the literal int 0 resulted in `true`. This is no longer the case with the [PHP RFC: Saner string to number comparisons](https://wiki.php.net/rfc/string_to_number_comparison). +```php +$a = 'banana'; +$b = 0; +if($a == $b){ + echo 'PHP 7 will display this'; +} +else{ + echo 'PHP 8 will display this instead'; +} +``` +This plugin helps identify those case to check them before migrating. + +You can solve this issue in a lot of ways: +- use strict equality: +```php +$a = 'banana'; +$b = 0; +if($a == $b){ + echo 'This is impossible'; +} +else{ + echo 'PHP 7 and 8 will both display this'; +} +``` +- use a cast to make both operands the same type: +```php +$a = 'banana'; +$b = 0; +if((int)$a == $b){ + echo 'PHP 7 and 8 will both display this'; +} +else{ + echo 'This is impossible'; +} +``` +```php +$a = 'banana'; +$b = 0; +if($a == (string)$b){ + echo 'This is impossible'; +} +else{ + echo 'PHP 7 and 8 will both display this'; +} +``` +- Make psalm understand you're working with positive-ints when the int operand is not a literal: +```php +$a = 'banana'; +/** @var positive-int $b */ +if($a == $b){ + echo 'This is impossible'; +} +else{ + echo 'PHP 7 and 8 will both display this'; +} +``` +- Make psalm understand you're working with numeric-strings when the string operand is not a literal: +```php +/** @var numeric-string $a */ +$b = 0; +if($a == $b){ + echo 'PHP 7 and 8 will both display this depending on the value of $a'; +} +else{ + echo 'PHP 7 and 8 will both display this depending on the value of $a'; +} +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..990721d --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "orklah/psalm-insane-comparison", + "description": "Detects possible insane comparison (\"string\" == 0) to help migrate to PHP8", + "type": "psalm-plugin", + "minimum-stability": "stable", + "license": "MIT", + "authors": [ + { + "name": "orklah" + } + ], + "extra": { + "psalm" : { + "pluginClass": "Orklah\\PsalmInsaneComparison\\Plugin" + } + }, + "require": { + "php": "^7.3", + "ext-simplexml": "*" + }, + "autoload": { + "psr-4": { + "Orklah\\PsalmInsaneComparison\\": ["."], + "Orklah\\PsalmInsaneComparison\\Hooks\\": ["hooks"] + } + }, + "require-dev": { + "vimeo/psalm": "^4.0", + "nikic/php-parser": "^4.0" + } +} diff --git a/hooks/InsaneComparison.php b/hooks/InsaneComparison.php new file mode 100644 index 0000000..ef77032 --- /dev/null +++ b/hooks/InsaneComparison.php @@ -0,0 +1,96 @@ +getNodeTypeProvider()->getType($expr->left); + $right_type = $statements_source->getNodeTypeProvider()->getType($expr->right); + + if($left_type === null || $right_type === null){ + return true; + } + + if($left_type->isString() && $right_type->isInt()){ + $string_type = $left_type; + $int_type = $right_type; + } elseif($left_type->isInt() && $right_type->isString()) { + $string_type = $right_type; + $int_type = $left_type; + } + else{ + //probably false negatives here because lots of union types get through? + return true; + } + + if( + $int_type instanceof TPositiveInt || + ($int_type instanceof TLiteralInt && $int_type->value !== 0) + ){ + // not interested, we search for literal 0 + return true; + } + + if( + $string_type instanceof TNumericString || + ($string_type instanceof TLiteralString && !preg_match('#[a-zA-Z]#', $string_type->value[0] ?? '')) || + ($string_type instanceof TSingleLetter && !preg_match('#[a-zA-Z]#', $string_type->value[0] ?? '')) + ){ + // not interested, we search strings that begins with a letter + return true; + } + + if (IssueBuffer::accepts( + new InsaneComparison( + 'Possible Insane Comparison between ' . $string_type->getKey() . ' and ' . $int_type->getKey(), + new CodeLocation($statements_source, $expr) + ), + $statements_source->getSuppressedIssues() + ) + ) { + // continue + } + + return true; + } +} + + +class InsaneComparison extends PluginIssue +{ +} diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..5f6badc --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + +