From 1f00c0cdad340c2f3bb33c7e74e5b9062c33e6a8 Mon Sep 17 00:00:00 2001 From: Martin Bruna Date: Thu, 28 May 2026 12:33:50 +0200 Subject: [PATCH] Return unsealed context shapes for PHPStan 2.2 compatibility PHPStan 2.2 introduces sealed array shapes under bleedingEdge, so the sealed `array{exception?: Throwable}` returned by the Psr3 and BigQuery context type providers rejected real user contexts carrying extra keys. Route every provider result through a new `UnsealedConstantArrayShape` helper so the expected shape becomes `array{exception?: Throwable, ...}`. A `method_exists` guard keeps behaviour unchanged on PHPStan 2.1.x. --- src/Internal/UnsealedConstantArrayShape.php | 47 +++++++++++++++++++ ...cTableFieldSchemaJsonPayloadTypeMapper.php | 5 +- .../BigQueryContextTypeProvider.php | 6 ++- src/TypeProvider/Psr3ContextTypeProvider.php | 5 +- test/Rules/ContextTypeRuleTest.php | 37 ++++++++++++--- 5 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 src/Internal/UnsealedConstantArrayShape.php diff --git a/src/Internal/UnsealedConstantArrayShape.php b/src/Internal/UnsealedConstantArrayShape.php new file mode 100644 index 0000000..676a058 --- /dev/null +++ b/src/Internal/UnsealedConstantArrayShape.php @@ -0,0 +1,47 @@ +makeUnsealed( + (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), + new MixedType() + ); + + $constantArrays = $builder->getArray()->getConstantArrays(); + + return $constantArrays === [] ? $type : $constantArrays[0]; + } +} diff --git a/src/TypeMapping/BigQuery/GenericTableFieldSchemaJsonPayloadTypeMapper.php b/src/TypeMapping/BigQuery/GenericTableFieldSchemaJsonPayloadTypeMapper.php index 6af5e58..d9ac79b 100644 --- a/src/TypeMapping/BigQuery/GenericTableFieldSchemaJsonPayloadTypeMapper.php +++ b/src/TypeMapping/BigQuery/GenericTableFieldSchemaJsonPayloadTypeMapper.php @@ -15,6 +15,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Sfp\PHPStan\Psr\Log\Internal\UnsealedConstantArrayShape; use Sfp\PHPStan\Psr\Log\TypeMapping\BigQuery\Exception\UnsupportedTypeException; use UnexpectedValueException; @@ -65,7 +66,9 @@ public static function convertFieldsToTypes(array $jsonPayloadFields, int $neste // if ($nestedLevel > 2) { // continue; // } - $valueTypes[] = self::convertFieldsToTypes($item['fields'], ++$nestedLevel); + $valueTypes[] = UnsealedConstantArrayShape::apply( + self::convertFieldsToTypes($item['fields'], ++$nestedLevel) + ); } else { $valueTypes[] = $objectType; } diff --git a/src/TypeProvider/BigQueryContextTypeProvider.php b/src/TypeProvider/BigQueryContextTypeProvider.php index c67393e..565a335 100644 --- a/src/TypeProvider/BigQueryContextTypeProvider.php +++ b/src/TypeProvider/BigQueryContextTypeProvider.php @@ -11,6 +11,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use RuntimeException; +use Sfp\PHPStan\Psr\Log\Internal\UnsealedConstantArrayShape; use Sfp\PHPStan\Psr\Log\TypeMapping\BigQuery\TableFieldSchemaJsonPayloadTypeMapperInterface; use UnexpectedValueException; @@ -53,7 +54,10 @@ public function getType(): Type true ); - return $builder->getArray(); + $array = $builder->getArray(); + $constantArrays = $array->getConstantArrays(); + + return $constantArrays === [] ? $array : UnsealedConstantArrayShape::apply($constantArrays[0]); } /** diff --git a/src/TypeProvider/Psr3ContextTypeProvider.php b/src/TypeProvider/Psr3ContextTypeProvider.php index b0ac743..0e4d5b3 100644 --- a/src/TypeProvider/Psr3ContextTypeProvider.php +++ b/src/TypeProvider/Psr3ContextTypeProvider.php @@ -9,6 +9,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use Sfp\PHPStan\Psr\Log\Internal\UnsealedConstantArrayShape; use Throwable; final class Psr3ContextTypeProvider implements ContextTypeProviderInterface @@ -25,11 +26,11 @@ public function __construct(string $exceptionClass = Throwable::class) #[Override] public function getType(): Type { - return new ConstantArrayType( + return UnsealedConstantArrayShape::apply(new ConstantArrayType( [new ConstantStringType('exception')], [new ObjectType($this->exceptionClass)], [0], [0] - ); + )); } } diff --git a/test/Rules/ContextTypeRuleTest.php b/test/Rules/ContextTypeRuleTest.php index 9c9e80a..0f92959 100644 --- a/test/Rules/ContextTypeRuleTest.php +++ b/test/Rules/ContextTypeRuleTest.php @@ -7,12 +7,14 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use Sfp\PHPStan\Psr\Log\Rules\ContextTypeRule; use Sfp\PHPStan\Psr\Log\TypeMapping\BigQuery\GenericTableFieldSchemaJsonPayloadTypeMapper; use Sfp\PHPStan\Psr\Log\TypeProvider\BigQueryContextTypeProvider; use Sfp\PHPStan\Psr\Log\TypeProviderResolver\AnyScopeContextTypeProviderResolver; use Sfp\PHPStan\Psr\Log\TypeProviderResolver\ContextTypeProviderResolverInterface; +use function method_exists; use function sprintf; /** @@ -62,9 +64,11 @@ public function testProcessNode(bool $checkUnionTypes, array $expectedErrors): v */ public static function provideContextTypePattern(): array { + $expected = self::expectedContextShape(); + $expectedError = [ - <<<'EOF' -Parameter #2 $context of method Psr\Log\LoggerInterface::info() expects array{exception?: Throwable}, array{exception: string} given. + << [ $expectedError, [ - <<<'EOF' -Parameter #2 $context of method Psr\Log\LoggerInterface::info() expects array{exception?: Throwable}, (array{exception: string} | array{exception: Throwable}) given. + <<contextTypeProviderResolver = new AnyScopeContextTypeProviderResolver($contextTypeProvider); + $expected = self::expectedBigQueryShape(); $this->analyse([__DIR__ . '/data/contextType.php'], [ [ sprintf( @@ -107,7 +112,7 @@ public function testProcessNodeWithBigQueryContextTypeProvider(): void Parameter #2 $context of method Psr\Log\LoggerInterface::info() expects %s, %s given. 💡 Offset 'exception' (Throwable) does not accept type string. EOF, - 'array{first_name?: string, product?: array{id?: string}, cancellation_reason?: (float | int | numeric-string), cancellation_date?: \DateTimeInterface, exception?: \Throwable}', + $expected, 'array{exception: string}' ), 19, @@ -118,11 +123,31 @@ public function testProcessNodeWithBigQueryContextTypeProvider(): void Parameter #2 $context of method Psr\Log\LoggerInterface::info() expects %s, %s given. 💡 Offset 'exception' (Throwable) does not accept type string. EOF, - 'array{first_name?: string, product?: array{id?: string}, cancellation_reason?: (float | int | numeric-string), cancellation_date?: \DateTimeInterface, exception?: \Throwable}', + $expected, '(array{exception: string} | array{exception: Throwable})' ), 20, ], ]); } + + private static function expectedContextShape(): string + { + return self::unsealedShapesSupported() + ? 'array{exception?: Throwable, ...}' + : 'array{exception?: Throwable}'; + } + + private static function expectedBigQueryShape(): string + { + return self::unsealedShapesSupported() + ? 'array{first_name?: string, product?: array{id?: string, ...}, cancellation_reason?: (float | int | numeric-string), cancellation_date?: \DateTimeInterface, exception?: \Throwable, ...}' + : 'array{first_name?: string, product?: array{id?: string}, cancellation_reason?: (float | int | numeric-string), cancellation_date?: \DateTimeInterface, exception?: \Throwable}'; + } + + private static function unsealedShapesSupported(): bool + { + /** @phpstan-ignore function.alreadyNarrowedType, function.impossibleType */ + return method_exists(ConstantArrayTypeBuilder::class, 'makeUnsealed'); + } }