Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/.github/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore

/math/phpcs.xml.dist export-ignore
/math/phpstan.neon export-ignore

/money/phpcs.xml.dist export-ignore
/money/phpstan.neon export-ignore
42 changes: 42 additions & 0 deletions .github/workflows/coding-standards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: "Coding Standards"

on:
pull_request:
push:
branches:
- "main"

jobs:
coding-standards:
name: "Coding Standards - ${{ matrix.package }}"
runs-on: "ubuntu-24.04"

strategy:
fail-fast: false
matrix:
package:
- "math"
- "money"

defaults:
run:
working-directory: "${{ matrix.package }}"

steps:
- name: "Checkout"
uses: "actions/checkout@v6"

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.4"
tools: "cs2pr"

- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
working-directory: "${{ matrix.package }}"

- name: "Run PHP_CodeSniffer"
run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr"
42 changes: 42 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: "Static Analysis"

on:
pull_request:
push:
branches:
- "main"

jobs:
static-analysis-phpstan:
name: "PHPStan - ${{ matrix.package }}"
runs-on: "ubuntu-24.04"

strategy:
fail-fast: false
matrix:
package:
- "math"
- "money"

defaults:
run:
working-directory: "${{ matrix.package }}"

steps:
- name: "Checkout code"
uses: "actions/checkout@v6"

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.4"
tools: "cs2pr"

- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v3"
with:
working-directory: "${{ matrix.package }}"

- name: "Run a static analysis with phpstan/phpstan"
run: "vendor/bin/phpstan analyse --error-format=checkstyle | cs2pr"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# math
/math/vendor/
/math/composer.lock
/math/.phpcs-cache

# money
/money/vendor/
/money/composer.lock
/money/.phpcs-cache
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# PHPStan Brick Extensions

PHPStan extensions that narrow throw types for [brick/math](https://github.com/brick/math) and [brick/money](https://github.com/brick/money).

## Packages

### `simpod/phpstan-brick-math`

```
composer require --dev simpod/phpstan-brick-math
```

If you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you're all set.

Otherwise, include in your `phpstan.neon`:

```neon
includes:
- vendor/simpod/phpstan-brick-math/extension.neon
```

### `simpod/phpstan-brick-money`

```
composer require --dev simpod/phpstan-brick-money
```

If you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you're all set.

Otherwise, include in your `phpstan.neon`:

```neon
includes:
- vendor/simpod/phpstan-brick-money/extension.neon
```
31 changes: 31 additions & 0 deletions math/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "simpod/phpstan-brick-math",
"description": "PHPStan dynamic throw type extensions for brick/math",
"license": "MIT",
"type": "phpstan-extension",
"require": {
"php": "^8.4",
"brick/math": "^0.15",
"phpstan/phpstan": "^2.1"
},
"require-dev": {
"cdn77/coding-standard": "^7.4"
},
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"autoload": {
"psr-4": {
"Brick\\Math\\PHPStan\\": "src/"
}
}
}
20 changes: 20 additions & 0 deletions math/extension.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
-
class: Brick\Math\PHPStan\BigNumberOfThrowTypeExtension
tags:
- phpstan.dynamicStaticMethodThrowTypeExtension

-
class: Brick\Math\PHPStan\BigNumberConversionThrowTypeExtension
tags:
- phpstan.dynamicMethodThrowTypeExtension

-
class: Brick\Math\PHPStan\BigNumberOperationThrowTypeExtension
tags:
- phpstan.dynamicMethodThrowTypeExtension

-
class: Brick\Math\PHPStan\RoundingModeThrowTypeExtension
tags:
- phpstan.dynamicMethodThrowTypeExtension
16 changes: 16 additions & 0 deletions math/phpcs.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="." />
<arg name="extensions" value="php" />
<arg name="parallel" value="80" />
<arg name="cache" value=".phpcs-cache" />
<arg name="colors" />

<config name="php_version" value="80400"/>

<arg value="nps" />

<file>src</file>

<rule ref="Cdn77" />
</ruleset>
7 changes: 7 additions & 0 deletions math/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
includes:
- extension.neon

parameters:
level: max
paths:
- src
73 changes: 73 additions & 0 deletions math/src/BigNumberConversionThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Brick\Math\PHPStan;

use Brick\Math\BigDecimal;
use Brick\Math\BigInteger;
use Brick\Math\BigNumber;
use Brick\Math\BigRational;
use Brick\Math\Exception\IntegerOverflowException;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodThrowTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

/**
* Narrows the throw type of toBigInteger(), toBigDecimal(), toBigRational(), and toInt().
*
* When the caller is already the target type, toBigInteger/toBigDecimal/toBigRational are no-ops and cannot throw.
* When toInt() is called on {@see BigInteger}, only {@see IntegerOverflowException} can be thrown
* (no {@see RoundingNecessaryException}).
*/
final class BigNumberConversionThrowTypeExtension implements DynamicMethodThrowTypeExtension
{
private const array MethodToClass = [
'toBigInteger' => BigInteger::class,
'toBigDecimal' => BigDecimal::class,
'toBigRational' => BigRational::class,
];

public function isMethodSupported(MethodReflection $methodReflection): bool
{
$className = $methodReflection->getDeclaringClass()->getName();
$isBigNumber = $className === BigNumber::class
|| $methodReflection->getDeclaringClass()->isSubclassOf(BigNumber::class);

if (! $isBigNumber) {
return false;
}

return isset(self::MethodToClass[$methodReflection->getName()])
|| $methodReflection->getName() === 'toInt';
}

public function getThrowTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): Type|null {
$callerType = $scope->getType($methodCall->var);
$methodName = $methodReflection->getName();

if ($methodName === 'toInt') {
// BigInteger::toInt() can only throw IntegerOverflowException (no RoundingNecessaryException).
if ((new ObjectType(BigInteger::class))->isSuperTypeOf($callerType)->yes()) {
return new ObjectType(IntegerOverflowException::class);
}

return $methodReflection->getThrowType();
}

$targetClass = self::MethodToClass[$methodName];

if ((new ObjectType($targetClass))->isSuperTypeOf($callerType)->yes()) {
return null;
}

return $methodReflection->getThrowType();
}
}
Loading