Skip to content

Commit ecbeade

Browse files
committed
feat: add PHPStan dynamic throw type extensions for brick/math and brick/money
Monorepo with two independent Composer packages: - simpod/phpstan-brick-math: narrows throw types for BigNumber factory, conversion, operation, and rounding-mode methods - simpod/phpstan-brick-money: narrows throw types for Money/RationalMoney factory, arithmetic, comparison, conversion, and CurrencyConverter methods Extracted from brick/math#112 and brick/money#116.
1 parent 81c9872 commit ecbeade

19 files changed

+1746
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: "Coding Standards"
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- "main"
8+
9+
jobs:
10+
coding-standards:
11+
name: "Coding Standards - ${{ matrix.package }}"
12+
runs-on: "ubuntu-24.04"
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
package:
18+
- "math"
19+
- "money"
20+
21+
defaults:
22+
run:
23+
working-directory: "${{ matrix.package }}"
24+
25+
steps:
26+
- name: "Checkout"
27+
uses: "actions/checkout@v6"
28+
29+
- name: "Install PHP"
30+
uses: "shivammathur/setup-php@v2"
31+
with:
32+
coverage: "none"
33+
php-version: "8.4"
34+
tools: "cs2pr"
35+
36+
- name: "Install dependencies with Composer"
37+
uses: "ramsey/composer-install@v2"
38+
39+
- name: "Run PHP_CodeSniffer"
40+
run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: "Static Analysis"
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- "main"
8+
9+
jobs:
10+
static-analysis-phpstan:
11+
name: "PHPStan - ${{ matrix.package }}"
12+
runs-on: "ubuntu-24.04"
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
package:
18+
- "math"
19+
- "money"
20+
21+
defaults:
22+
run:
23+
working-directory: "${{ matrix.package }}"
24+
25+
steps:
26+
- name: "Checkout code"
27+
uses: "actions/checkout@v6"
28+
29+
- name: "Install PHP"
30+
uses: "shivammathur/setup-php@v2"
31+
with:
32+
coverage: "none"
33+
php-version: "8.4"
34+
tools: "cs2pr"
35+
36+
- name: "Install dependencies with Composer"
37+
uses: "ramsey/composer-install@v2"
38+
39+
- name: "Run a static analysis with phpstan/phpstan"
40+
run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/math/vendor/
2+
/math/composer.lock
3+
/money/vendor/
4+
/money/composer.lock

math/composer.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "simpod/phpstan-brick-math",
3+
"description": "PHPStan dynamic throw type extensions for brick/math",
4+
"license": "MIT",
5+
"type": "phpstan-extension",
6+
"require": {
7+
"php": "^8.4",
8+
"brick/math": "^0.15",
9+
"phpstan/phpstan": "^2.1"
10+
},
11+
"extra": {
12+
"phpstan": {
13+
"includes": [
14+
"extension.neon"
15+
]
16+
}
17+
},
18+
"autoload": {
19+
"psr-4": {
20+
"Brick\\Math\\PHPStan\\": "src/"
21+
}
22+
}
23+
}

math/extension.neon

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
-
3+
class: Brick\Math\PHPStan\BigNumberOfThrowTypeExtension
4+
tags:
5+
- phpstan.dynamicStaticMethodThrowTypeExtension
6+
7+
-
8+
class: Brick\Math\PHPStan\BigNumberConversionThrowTypeExtension
9+
tags:
10+
- phpstan.dynamicMethodThrowTypeExtension
11+
12+
-
13+
class: Brick\Math\PHPStan\BigNumberOperationThrowTypeExtension
14+
tags:
15+
- phpstan.dynamicMethodThrowTypeExtension
16+
17+
-
18+
class: Brick\Math\PHPStan\RoundingModeThrowTypeExtension
19+
tags:
20+
- phpstan.dynamicMethodThrowTypeExtension

math/phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
includes:
2+
- extension.neon
3+
4+
parameters:
5+
level: 10
6+
paths:
7+
- src
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brick\Math\PHPStan;
6+
7+
use Brick\Math\BigDecimal;
8+
use Brick\Math\BigInteger;
9+
use Brick\Math\BigNumber;
10+
use Brick\Math\BigRational;
11+
use Brick\Math\Exception\IntegerOverflowException;
12+
use Brick\Math\Exception\RoundingNecessaryException;
13+
use PhpParser\Node\Expr\MethodCall;
14+
use PHPStan\Analyser\Scope;
15+
use PHPStan\Reflection\MethodReflection;
16+
use PHPStan\Type\DynamicMethodThrowTypeExtension;
17+
use PHPStan\Type\ObjectType;
18+
use PHPStan\Type\Type;
19+
20+
/**
21+
* Narrows the throw type of toBigInteger(), toBigDecimal(), toBigRational(), and toInt().
22+
*
23+
* When the caller is already the target type, toBigInteger/toBigDecimal/toBigRational are no-ops and cannot throw.
24+
* When toInt() is called on {@see BigInteger}, only {@see IntegerOverflowException} can be thrown
25+
* (no {@see RoundingNecessaryException}).
26+
*/
27+
final class BigNumberConversionThrowTypeExtension implements DynamicMethodThrowTypeExtension
28+
{
29+
private const METHOD_TO_CLASS = [
30+
'toBigInteger' => BigInteger::class,
31+
'toBigDecimal' => BigDecimal::class,
32+
'toBigRational' => BigRational::class,
33+
];
34+
35+
public function isMethodSupported(MethodReflection $methodReflection): bool
36+
{
37+
$className = $methodReflection->getDeclaringClass()->getName();
38+
$isBigNumber = $className === BigNumber::class
39+
|| $methodReflection->getDeclaringClass()->isSubclassOf(BigNumber::class);
40+
41+
if (! $isBigNumber) {
42+
return false;
43+
}
44+
45+
return isset(self::METHOD_TO_CLASS[$methodReflection->getName()])
46+
|| $methodReflection->getName() === 'toInt';
47+
}
48+
49+
public function getThrowTypeFromMethodCall(
50+
MethodReflection $methodReflection,
51+
MethodCall $methodCall,
52+
Scope $scope,
53+
): ?Type {
54+
$callerType = $scope->getType($methodCall->var);
55+
$methodName = $methodReflection->getName();
56+
57+
if ($methodName === 'toInt') {
58+
// BigInteger::toInt() can only throw IntegerOverflowException (no RoundingNecessaryException).
59+
if ((new ObjectType(BigInteger::class))->isSuperTypeOf($callerType)->yes()) {
60+
return new ObjectType(IntegerOverflowException::class);
61+
}
62+
63+
return $methodReflection->getThrowType();
64+
}
65+
66+
$targetClass = self::METHOD_TO_CLASS[$methodName];
67+
68+
if ((new ObjectType($targetClass))->isSuperTypeOf($callerType)->yes()) {
69+
return null;
70+
}
71+
72+
return $methodReflection->getThrowType();
73+
}
74+
}

0 commit comments

Comments
 (0)