mirror of
https://github.com/danog/psalm-insane-comparison.git
synced 2024-11-26 20:14:46 +01:00
First commit
This commit is contained in:
commit
a8896da551
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@ -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
|
126
.php_cs
Normal file
126
.php_cs
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->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);
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
18
Plugin.php
Normal file
18
Plugin.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Orklah\PsalmStrictVisibility;
|
||||
|
||||
use Orklah\PsalmInsaneComparison\Hooks\InsaneComparison;
|
||||
use SimpleXMLElement;
|
||||
use Psalm\Plugin\PluginEntryPointInterface;
|
||||
use Psalm\Plugin\RegistrationInterface;
|
||||
|
||||
class Plugin implements PluginEntryPointInterface
|
||||
{
|
||||
/** @return void */
|
||||
public function __invoke(RegistrationInterface $psalm, ?SimpleXMLElement $config = null): void
|
||||
{
|
||||
if(class_exists(InsaneComparison::class)){
|
||||
$psalm->registerHooksFromClass(InsaneComparison::class);
|
||||
}
|
||||
}
|
||||
}
|
87
README.md
Normal file
87
README.md
Normal file
@ -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';
|
||||
}
|
||||
```
|
31
composer.json
Normal file
31
composer.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
96
hooks/InsaneComparison.php
Normal file
96
hooks/InsaneComparison.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Orklah\PsalmInsaneComparison\Hooks;
|
||||
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Scalar\DNumber;
|
||||
use PhpParser\Node\Scalar\LNumber;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Issue\PluginIssue;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\Hook\AfterExpressionAnalysisInterface;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Type\Atomic\TCallableString;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TLiteralClassString;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TNumericString;
|
||||
use Psalm\Type\Atomic\TPositiveInt;
|
||||
use Psalm\Type\Atomic\TSingleLetter;
|
||||
use Psalm\Type\Atomic\TTraitString;
|
||||
|
||||
class InsaneComparisonAnalyzer implements AfterExpressionAnalysisInterface
|
||||
{
|
||||
public static function afterExpressionAnalysis(
|
||||
Expr $expr,
|
||||
Context $context,
|
||||
StatementsSource $statements_source,
|
||||
Codebase $codebase,
|
||||
array &$file_replacements = []
|
||||
): ?bool {
|
||||
if(!$expr instanceof Expr\BinaryOp\Equal && !$expr instanceof Expr\BinaryOp\NotEqual){
|
||||
return true;
|
||||
}
|
||||
|
||||
$left_type = $statements_source->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
|
||||
{
|
||||
}
|
16
psalm.xml
Normal file
16
psalm.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="false"
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<file name="sandbox.php"/>
|
||||
</projectFiles>
|
||||
<plugins>
|
||||
<plugin filename="hooks/InsaneComparison.php" />
|
||||
</plugins>
|
||||
</psalm>
|
Loading…
Reference in New Issue
Block a user