From b29fc6ad3c499d65cf63d08688b7b5b08220640f Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 10:59:37 -0600 Subject: [PATCH 1/4] Allow operator overloading for Decimal extension (fixes #3938). --- src/Psalm/Config.php | 5 + .../BinaryOp/ArithmeticOpAnalyzer.php | 44 ++ stubs/decimal.phpstub | 492 ++++++++++++++++++ tests/BinaryOperationTest.php | 69 +++ 4 files changed, 610 insertions(+) create mode 100644 stubs/decimal.phpstub diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index aa98e01872d..0bc3792864c 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1969,6 +1969,11 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): $this->internal_stubs[] = $ext_mysqli_path; } + if (extension_loaded('decimal')) { + $ext_decimal_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'decimal.phpstub'; + $this->internal_stubs[] = $ext_decimal_path; + } + foreach ($this->internal_stubs as $stub_path) { if (!file_exists($stub_path)) { throw new UnexpectedValueException('Cannot locate ' . $stub_path); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index e95c17b3368..a04595bd38f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; +use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TPositiveInt; use Psalm\Type\Atomic\TTemplateParam; @@ -611,6 +612,49 @@ private static function analyzeOperands( return null; } + if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Plus + || $parent instanceof PhpParser\Node\Expr\BinaryOp\Minus + || $parent instanceof PhpParser\Node\Expr\BinaryOp\Mul + || $parent instanceof PhpParser\Node\Expr\BinaryOp\Div + || $parent instanceof PhpParser\Node\Expr\BinaryOp\Mod + || $parent instanceof PhpParser\Node\Expr\BinaryOp\Pow + ) { + $non_decimal_type = null; + if ($left_type_part instanceof TNamedObject + && strtolower($left_type_part->value) === "decimal\\decimal" + ) { + $non_decimal_type = $right_type_part; + } elseif ($right_type_part instanceof TNamedObject + && strtolower($right_type_part->value) === "decimal\\decimal" + ) { + $non_decimal_type = $left_type_part; + } + if ($non_decimal_type !== null) { + if ($non_decimal_type instanceof TInt + || $non_decimal_type instanceof TNumericString + || $non_decimal_type instanceof TNamedObject + && strtolower($non_decimal_type->value) === "decimal\\decimal" + ) { + $result_type = Type::combineUnionTypes( + new Type\Union([new TNamedObject("Decimal\\Decimal")]), + $result_type + ); + } else { + if ($statements_source && IssueBuffer::accepts( + new InvalidOperand( + "Cannot add Decimal\\Decimal to {$non_decimal_type->getId()}", + new CodeLocation($statements_source, $parent) + ), + $statements_source->getSuppressedIssues() + )) { + // fall through + } + } + + return null; + } + } + if ($left_type_part instanceof Type\Atomic\TLiteralString) { if (preg_match('/^\-?\d+$/', $left_type_part->value)) { $left_type_part = new Type\Atomic\TLiteralInt((int) $left_type_part->value); diff --git a/stubs/decimal.phpstub b/stubs/decimal.phpstub new file mode 100644 index 00000000000..90b329dab7f --- /dev/null +++ b/stubs/decimal.phpstub @@ -0,0 +1,492 @@ +` operator. + * + * @param mixed $other + * + * @return int 0 if this decimal is considered is equal to $other, + * -1 if this decimal should be placed before $other, + * 1 if this decimal should be placed after $other. + */ + public function compareTo($other): int {} + + /** + * String representation. + * + * This method is equivalent to a cast to string, as well as `toString`. + * + * @return string the value of this decimal represented exactly, in either + * fixed or scientific form, depending on the value. + */ + public function __toString(): string {} + + /** + * JSON + * + * This method is only here to honour the interface, and is equivalent to + * `toString`. JSON does not have a decimal type so all decimals are encoded + * as strings in the same format as `toString`. + * + * @return string + */ + public function jsonSerialize() {} +} diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 26f1dd05b5c..bf9d485d7d0 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -85,6 +85,75 @@ public function testGMPOperations(): void $this->assertSame($assertions, $actual_vars); } + public function testDecimalOperations(): void + { + if (!class_exists('Decimal\\Decimal')) { + $this->markTestSkipped('Cannot run test, base class "Decimal\\Decimal" does not exist!'); + } + + $this->addFile( + 'somefile.php', + ' 'Decimal\\Decimal', + '$b' => 'Decimal\\Decimal', + '$c' => 'Decimal\\Decimal', + '$d' => 'Decimal\\Decimal', + '$f' => 'Decimal\\Decimal', + '$g' => 'Decimal\\Decimal', + '$h' => 'Decimal\\Decimal', + '$i' => 'Decimal\\Decimal', + '$j' => 'Decimal\\Decimal', + '$k' => 'Decimal\\Decimal', + '$l' => 'Decimal\\Decimal', + '$m' => 'Decimal\\Decimal', + '$n' => 'Decimal\\Decimal', + '$o' => 'Decimal\\Decimal', + '$p' => 'Decimal\\Decimal', + '$q' => 'Decimal\\Decimal', + '$r' => 'Decimal\\Decimal', + '$s' => 'Decimal\\Decimal', + '$t' => 'Decimal\\Decimal', + ]; + + $context = new Context(); + + $this->analyzeFile('somefile.php', $context); + + $actual_vars = []; + foreach ($assertions as $var => $_) { + if (isset($context->vars_in_scope[$var])) { + $actual_vars[$var] = (string)$context->vars_in_scope[$var]; + } + } + + $this->assertSame($assertions, $actual_vars); + } + public function testStrictTrueEquivalence(): void { $config = Config::getInstance(); From 5808d4ea830908a83c20bb3b74d97c0505fd9fe2 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 12:10:31 -0600 Subject: [PATCH 2/4] Add decimal extension to CI. --- .github/workflows/ci.yml | 1 + .github/workflows/windows-ci.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a691c6d11c3..8ae030d4f46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,7 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none + extensions: decimal - uses: actions/checkout@v2 diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index f59e4b38471..2af25710067 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -23,6 +23,7 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none + extensions: decimal - uses: actions/checkout@v2 From ba881c80bf243617c96ee05b2bbbf39d74848c73 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 12:15:32 -0600 Subject: [PATCH 3/4] Use maybeAdd instead of accepts. --- .../Expression/BinaryOp/ArithmeticOpAnalyzer.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index a04595bd38f..afba17ce14a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -640,14 +640,14 @@ private static function analyzeOperands( $result_type ); } else { - if ($statements_source && IssueBuffer::accepts( - new InvalidOperand( - "Cannot add Decimal\\Decimal to {$non_decimal_type->getId()}", - new CodeLocation($statements_source, $parent) - ), - $statements_source->getSuppressedIssues() - )) { - // fall through + if ($statements_source) { + IssueBuffer::maybeAdd( + new InvalidOperand( + "Cannot add Decimal\\Decimal to {$non_decimal_type->getId()}", + new CodeLocation($statements_source, $parent) + ), + $statements_source->getSuppressedIssues() + ); } } From 466696cda493c35f57bd4c88326afc30358321df Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 13:39:13 -0600 Subject: [PATCH 4/4] Remove decimal extension from Windows CI. --- .github/workflows/windows-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 2af25710067..f59e4b38471 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -23,7 +23,6 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none - extensions: decimal - uses: actions/checkout@v2