Skip to content

Commit b67fb59

Browse files
authored
Merge pull request #1 from simPod/feat/initial-monorepo
feat: add PHPStan throw type extensions for brick/math and brick/money
2 parents 81c9872 + 2cda506 commit b67fb59

23 files changed

+1858
-0
lines changed

.gitattributes

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/.github/ export-ignore
2+
/.gitattributes export-ignore
3+
/.gitignore export-ignore
4+
5+
/math/phpcs.xml.dist export-ignore
6+
/math/phpstan.neon export-ignore
7+
8+
/money/phpcs.xml.dist export-ignore
9+
/money/phpstan.neon export-ignore
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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@v3"
38+
with:
39+
working-directory: "${{ matrix.package }}"
40+
41+
- name: "Run PHP_CodeSniffer"
42+
run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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@v3"
38+
with:
39+
working-directory: "${{ matrix.package }}"
40+
41+
- name: "Run a static analysis with phpstan/phpstan"
42+
run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr"

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# math
2+
/math/vendor/
3+
/math/composer.lock
4+
/math/.phpcs-cache
5+
6+
# money
7+
/money/vendor/
8+
/money/composer.lock
9+
/money/.phpcs-cache

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# PHPStan Brick Extensions
2+
3+
PHPStan extensions that narrow throw types for [brick/math](https://github.com/brick/math) and [brick/money](https://github.com/brick/money).
4+
5+
## Packages
6+
7+
### `simpod/phpstan-brick-math`
8+
9+
```
10+
composer require --dev simpod/phpstan-brick-math
11+
```
12+
13+
If you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you're all set.
14+
15+
Otherwise, include in your `phpstan.neon`:
16+
17+
```neon
18+
includes:
19+
- vendor/simpod/phpstan-brick-math/extension.neon
20+
```
21+
22+
### `simpod/phpstan-brick-money`
23+
24+
```
25+
composer require --dev simpod/phpstan-brick-money
26+
```
27+
28+
If you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you're all set.
29+
30+
Otherwise, include in your `phpstan.neon`:
31+
32+
```neon
33+
includes:
34+
- vendor/simpod/phpstan-brick-money/extension.neon
35+
```

math/composer.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
"require-dev": {
12+
"cdn77/coding-standard": "^7.4"
13+
},
14+
"extra": {
15+
"phpstan": {
16+
"includes": [
17+
"extension.neon"
18+
]
19+
}
20+
},
21+
"config": {
22+
"allow-plugins": {
23+
"dealerdirect/phpcodesniffer-composer-installer": true
24+
}
25+
},
26+
"autoload": {
27+
"psr-4": {
28+
"Brick\\Math\\PHPStan\\": "src/"
29+
}
30+
}
31+
}

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/phpcs.xml.dist

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0"?>
2+
<ruleset>
3+
<arg name="basepath" value="." />
4+
<arg name="extensions" value="php" />
5+
<arg name="parallel" value="80" />
6+
<arg name="cache" value=".phpcs-cache" />
7+
<arg name="colors" />
8+
9+
<config name="php_version" value="80400"/>
10+
11+
<arg value="nps" />
12+
13+
<file>src</file>
14+
15+
<rule ref="Cdn77" />
16+
</ruleset>

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

0 commit comments

Comments
 (0)