Skip to content

Commit 923e01f

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions/github-script-9
2 parents b3b4905 + 17e54d0 commit 923e01f

12 files changed

Lines changed: 244 additions & 34 deletions

.github/workflows/benchmark-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
# to avoid issues with multiline strings
153153
154154
- name: Create or update comment
155-
uses: peter-evans/create-or-update-comment@v4
155+
uses: peter-evans/create-or-update-comment@v5
156156
with:
157157
comment-id: ${{ steps.fc.outputs.comment-id }}
158158
issue-number: ${{ steps.pr.outputs.number }}

.github/workflows/claude-code-review.yml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Claude Code Review
22

33
on:
44
pull_request:
5-
types: [opened, synchronize]
5+
types: [opened, synchronize, ready_for_review, reopened]
66
# Optional: Only run on specific file changes
77
# paths:
88
# - "src/**/*.ts"
@@ -36,18 +36,9 @@ jobs:
3636
uses: anthropics/claude-code-action@v1
3737
with:
3838
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39-
prompt: |
40-
Please review this pull request and provide feedback on:
41-
- Code quality and best practices
42-
- Potential bugs or issues
43-
- Performance considerations
44-
- Security concerns
45-
- Test coverage
46-
47-
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
48-
49-
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
50-
39+
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40+
plugins: 'code-review@claude-code-plugins'
41+
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
5142
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
52-
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
53-
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
43+
# or https://code.claude.com/docs/en/cli-reference for available options
44+

.github/workflows/claude.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ on:
1313
jobs:
1414
claude:
1515
if: |
16-
github.repository_owner == github.actor &&
17-
(
18-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
19-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
20-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
21-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
22-
)
16+
github.repository_owner == github.actor &&
17+
(
18+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
19+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
20+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
21+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
22+
)
2323
runs-on: ubuntu-latest
2424
permissions:
2525
contents: read
@@ -48,5 +48,6 @@ jobs:
4848

4949
# Optional: Add claude_args to customize behavior and configuration
5050
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
51-
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
52-
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
51+
# or https://code.claude.com/docs/en/cli-reference for available options
52+
# claude_args: '--allowed-tools Bash(gh pr *)'
53+

.php-cs-fixer.dist.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
'@PhpCsFixer:risky' => true,
2323
'native_function_invocation' => false,
2424
'php_unit_test_case_static_method_calls' => false,
25+
// Tests verify implicit int/bool→string coercion matching the native bcmath
26+
// extension, so enforcing strict types would break intentional compatibility checks.
27+
'declare_strict_types' => false,
2528
])
2629
->setFinder($finder)
2730
;

lib/RoundingMode.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
*
1010
* The enum is only defined if:
1111
* - PHP version is 8.1 or higher (enum support)
12-
* - Native RoundingMode enum doesn't already exist (PHP < 8.4)
12+
* - No class/enum named RoundingMode is already declared in the global namespace.
13+
* Since PHP 8.1 `class_exists()` returns true for enums too, this single check
14+
* covers both the native PHP 8.4+ RoundingMode enum and the Rector 2.4+
15+
* scoped polyfill that exposes a class via class_alias.
1316
*/
14-
// @phpstan-ignore-next-line
15-
if (!enum_exists('RoundingMode') && version_compare(PHP_VERSION, '8.1', '>=')) {
17+
// The version_compare check is defensive runtime safety. composer.json constrains
18+
// PHP to >=8.1, so PHPStan always infers this as always-true; silence that.
19+
// @phpstan-ignore-next-line booleanAnd.rightAlwaysTrue
20+
if (!class_exists('RoundingMode', false) && version_compare(PHP_VERSION, '8.1', '>=')) {
1621
enum RoundingMode: string
1722
{
1823
case HalfAwayFromZero = 'half_away_from_zero';

lib/bcmath.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,19 @@ function bcmul(string $dividend, string $divisor, ?int $scale = null): string
8888
*
8989
* @param null|int $scale optional
9090
*/
91-
function bcpow(string $base, string $exponent, ?int $scale = null): string
91+
function bcpow(string $num, string $exponent, ?int $scale = null): string
9292
{
93-
return BCMath::pow($base, $exponent, $scale);
93+
return BCMath::pow($num, $exponent, $scale);
9494
}
9595

9696
/**
9797
* Raise an arbitrary precision number to another, reduced by a specified modulus.
9898
*
9999
* @param null|int $scale optional
100100
*/
101-
function bcpowmod(string $base, string $exponent, string $modulus, ?int $scale = null): string
101+
function bcpowmod(string $num, string $exponent, string $modulus, ?int $scale = null): string
102102
{
103-
return BCMath::powmod($base, $exponent, $modulus, $scale);
103+
return BCMath::powmod($num, $exponent, $modulus, $scale);
104104
}
105105

106106
/**

phpstan.neon.dist

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,42 @@ parameters:
1111
-
1212
identifier: expr.resultUnused
1313
path: tests/BCMathWithoutExtensionTest.php
14+
# Rector 2.4+ ships a scoped polyfill at
15+
# vendor/rector/rector/vendor/symfony/polyfill-php84/Resources/RoundingMode.php
16+
# that registers a global `class RoundingMode` with string constants
17+
# (e.g. HalfAwayFromZero = 'halfawayfromzero') via class_alias. PHPStan's
18+
# Better Reflection walks vendor recursively and binds `\RoundingMode` to
19+
# that class instead of our enum polyfill in lib/RoundingMode.php, so
20+
# references to `\RoundingMode::HalfAwayFromZero` are typed as string
21+
# rather than as an enum case. At runtime the enum in lib/RoundingMode.php
22+
# wins because it is loaded via composer files autoload before Rector's
23+
# autoload has any chance to run for the global name (and our tests
24+
# confirm the enum is the one in use). These errors are purely a
25+
# static-analysis artifact. See https://github.com/nanasess/bcmath-polyfill/issues/56
26+
-
27+
identifier: match.alwaysFalse
28+
path: src/BCMath.php
29+
message: "#Match arm comparison between RoundingMode and '(halfawayfromzero|halftowardszero|halfeven|halfodd|towardszero|awayfromzero|negativeinfinity|positiveinfinity)' is always false\\.#"
30+
-
31+
identifier: argument.type
32+
message: "#Parameter \\#3 \\$mode of (static method bcmath_compat\\\\BCMath::round\\(\\)|function bcround) expects int\\|RoundingMode, (string|int) given\\.#"
33+
paths:
34+
- src/BCMath.php
35+
- tests/BCMathTest.php
36+
- tests/BCMathWithoutExtensionTest.php
37+
-
38+
identifier: return.type
39+
path: tests/BCMathTest.php
40+
message: "#Method BCMathTest::provideRoundingModeEnumSupportCases\\(\\).*RoundingMode.*#"
41+
-
42+
identifier: function.impossibleType
43+
path: tests/BCMathTest.php
44+
message: "#Call to function in_array\\(\\) with arguments RoundingMode, array\\{.*\\} and true will always evaluate to false\\.#"
45+
-
46+
identifier: encapsedStringPart.nonString
47+
path: tests/BCMathTest.php
48+
message: "#Part \\$\\w+->name \\(mixed\\) of encapsed string cannot be cast to string\\.#"
49+
-
50+
identifier: property.nonObject
51+
path: tests/BCMathTest.php
52+
message: "#Cannot access property \\$name on string\\.#"

rector.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Rector\Config\RectorConfig;
66
use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector;
7+
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector;
78

89
return RectorConfig::configure()
910
->withPaths([
@@ -16,5 +17,8 @@
1617
->withPreparedSets(typeDeclarations: true, deadCode: true, codeQuality: true)
1718
->withSkip([
1819
UnwrapFutureCompatibleIfPhpVersionRector::class,
20+
// Tests rely on implicit int/bool→string coercion to verify native bcmath
21+
// extension compatibility; declaring strict types would break them.
22+
SafeDeclareStrictTypesRector::class,
1923
__DIR__ . '/tests/php-src',
2024
]);

src/BCMath.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ public static function comp(string $num1, string $num2, ?int $scale = null): int
631631
public static function pow(string $base, string $exponent, ?int $scale = null): string
632632
{
633633
// Phase 1: Argument validation
634-
self::validateNumberString($base, 'bcpow', 1, 'base');
634+
self::validateNumberString($base, 'bcpow', 1, 'num');
635635
self::validateNumberString($exponent, 'bcpow', 2, 'exponent');
636636

637637
// Phase 2: Scale resolution and validation
@@ -743,7 +743,7 @@ public static function powmod(string $base, string $exponent, string $modulus, ?
743743
throw new \ArgumentCountError('bcpowmod() expects at most 4 arguments, '.func_num_args().' given');
744744
}
745745

746-
self::validateNumberString($base, 'bcpowmod', 1, 'base');
746+
self::validateNumberString($base, 'bcpowmod', 1, 'num');
747747
self::validateNumberString($exponent, 'bcpowmod', 2, 'exponent');
748748
self::validateNumberString($modulus, 'bcpowmod', 3, 'modulus');
749749

tests/RoundingModeGuardTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
use PHPUnit\Framework\Attributes\CoversNothing;
4+
use PHPUnit\Framework\Attributes\RequiresPhp;
5+
use PHPUnit\Framework\TestCase;
6+
7+
/**
8+
* @internal
9+
*/
10+
#[CoversNothing]
11+
final class RoundingModeGuardTest extends TestCase
12+
{
13+
/**
14+
* Regression test for issue #56.
15+
*
16+
* Rector 2.4.x's scoped polyfill declares a global `class RoundingMode`
17+
* via `class_alias` on PHP < 8.4. The polyfill in lib/RoundingMode.php
18+
* must detect this and skip enum declaration.
19+
*/
20+
#[RequiresPhp('>= 8.1 < 8.4')]
21+
public function testPolyfillSkipsWhenRoundingModeClassAlreadyDefined(): void
22+
{
23+
$result = $this->runFixture(__DIR__.'/fixtures/rector-scenario.php');
24+
25+
$this->assertSame(0, $result['exitCode'], 'Fixture exited with non-zero status. stderr: '.$result['stderr']);
26+
$this->assertStringNotContainsString('Cannot declare enum', $result['stderr']);
27+
$this->assertStringNotContainsString('Fatal error', $result['stderr']);
28+
$this->assertStringContainsString('OK', $result['stdout']);
29+
}
30+
31+
#[RequiresPhp('>= 8.1 < 8.4')]
32+
public function testPolyfillDefinesEnumOnCleanLoad(): void
33+
{
34+
$result = $this->runFixture(__DIR__.'/fixtures/clean-load.php');
35+
36+
$this->assertSame(0, $result['exitCode'], 'Fixture exited with non-zero status. stderr: '.$result['stderr']);
37+
$this->assertStringContainsString('ENUM_DEFINED', $result['stdout']);
38+
}
39+
40+
/**
41+
* On PHP 8.4+ the native RoundingMode enum is already registered, so the polyfill
42+
* guard in lib/RoundingMode.php must short-circuit. This asserts that loading the
43+
* polyfill produces no fatal, and that the (native) enum is visible afterwards.
44+
*/
45+
#[RequiresPhp('>= 8.4')]
46+
public function testPolyfillSkipsOnPhp84WithNativeEnum(): void
47+
{
48+
$result = $this->runFixture(__DIR__.'/fixtures/clean-load.php');
49+
50+
$this->assertSame(0, $result['exitCode'], 'Fixture exited with non-zero status. stderr: '.$result['stderr']);
51+
$this->assertStringNotContainsString('Cannot declare enum', $result['stderr']);
52+
$this->assertStringNotContainsString('Fatal error', $result['stderr']);
53+
$this->assertStringContainsString('ENUM_DEFINED', $result['stdout']);
54+
}
55+
56+
/**
57+
* @return array{stdout: string, stderr: string, exitCode: int}
58+
*/
59+
private function runFixture(string $fixturePath, int $timeoutSeconds = 10): array
60+
{
61+
$descriptors = [
62+
0 => ['pipe', 'r'],
63+
1 => ['pipe', 'w'],
64+
2 => ['pipe', 'w'],
65+
];
66+
67+
$process = proc_open([PHP_BINARY, $fixturePath], $descriptors, $pipes);
68+
if (!is_resource($process)) {
69+
$this->fail('Failed to spawn PHP subprocess');
70+
}
71+
72+
fclose($pipes[0]);
73+
stream_set_blocking($pipes[1], false);
74+
stream_set_blocking($pipes[2], false);
75+
76+
$stdout = '';
77+
$stderr = '';
78+
$exitCode = -1;
79+
$deadline = microtime(true) + $timeoutSeconds;
80+
81+
while (true) {
82+
$chunkOut = stream_get_contents($pipes[1]);
83+
if ($chunkOut !== false) {
84+
$stdout .= $chunkOut;
85+
}
86+
$chunkErr = stream_get_contents($pipes[2]);
87+
if ($chunkErr !== false) {
88+
$stderr .= $chunkErr;
89+
}
90+
91+
$status = proc_get_status($process);
92+
if (!$status['running']) {
93+
// Capture exit code from status: on PHP < 8.3 calling proc_get_status()
94+
// reaps the process so the subsequent proc_close() returns -1.
95+
$exitCode = $status['exitcode'];
96+
// Drain any remaining buffered output after the process exited.
97+
$chunkOut = stream_get_contents($pipes[1]);
98+
if ($chunkOut !== false) {
99+
$stdout .= $chunkOut;
100+
}
101+
$chunkErr = stream_get_contents($pipes[2]);
102+
if ($chunkErr !== false) {
103+
$stderr .= $chunkErr;
104+
}
105+
106+
break;
107+
}
108+
109+
if (microtime(true) > $deadline) {
110+
proc_terminate($process);
111+
fclose($pipes[1]);
112+
fclose($pipes[2]);
113+
proc_close($process);
114+
$this->fail(sprintf('Subprocess for %s timed out after %ds. stderr so far: %s', $fixturePath, $timeoutSeconds, $stderr));
115+
}
116+
117+
usleep(10000);
118+
}
119+
120+
fclose($pipes[1]);
121+
fclose($pipes[2]);
122+
proc_close($process);
123+
124+
return [
125+
'stdout' => $stdout,
126+
'stderr' => $stderr,
127+
'exitCode' => $exitCode,
128+
];
129+
}
130+
}

0 commit comments

Comments
 (0)