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
47 changes: 47 additions & 0 deletions src/Internal/UnsealedConstantArrayShape.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Sfp\PHPStan\Psr\Log\Internal;

use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;

use function method_exists;

/**
* Marks a {@see ConstantArrayType} produced via the PHPStan API as unsealed
* (the {@code array{foo?: Bar, ...}} form).
*
* PHPStan 2.2 distinguishes sealed and unsealed array shapes; user-provided
* log contexts almost always carry extra keys, so the expected context type
* must permit them. On older PHPStan releases that do not expose the unsealed
* API, the original type is returned unchanged — preserving prior behaviour.
*
* @internal
*/
final class UnsealedConstantArrayShape
{
public static function apply(ConstantArrayType $type): ConstantArrayType
{
/** @phpstan-ignore function.alreadyNarrowedType, function.impossibleType */
if (! method_exists(ConstantArrayTypeBuilder::class, 'makeUnsealed')) {
return $type;
}

$builder = ConstantArrayTypeBuilder::createFromConstantArray($type);
/** @phpstan-ignore method.notFound */
$builder->makeUnsealed(
(new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(),
new MixedType()
);

$constantArrays = $builder->getArray()->getConstantArrays();

return $constantArrays === [] ? $type : $constantArrays[0];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/TypeProvider/BigQueryContextTypeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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]);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/TypeProvider/Psr3ContextTypeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
);
));
}
}
37 changes: 31 additions & 6 deletions test/Rules/ContextTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
<<<EOF
Parameter #2 \$context of method Psr\Log\LoggerInterface::info() expects $expected, array{exception: string} given.
💡 Offset 'exception' (Throwable) does not accept type string.
EOF,
19,
Expand All @@ -82,8 +86,8 @@ public static function provideContextTypePattern(): array
'expectedErrors' => [
$expectedError,
[
<<<'EOF'
Parameter #2 $context of method Psr\Log\LoggerInterface::info() expects array{exception?: Throwable}, (array{exception: string} | array{exception: Throwable}) given.
<<<EOF
Parameter #2 \$context of method Psr\Log\LoggerInterface::info() expects $expected, (array{exception: string} | array{exception: Throwable}) given.
💡 Offset 'exception' (Throwable) does not accept type string.
EOF,
20,
Expand All @@ -100,14 +104,15 @@ public function testProcessNodeWithBigQueryContextTypeProvider(): void
{
$contextTypeProvider = new BigQueryContextTypeProvider(__DIR__ . '/../TypeProvider/data/bigQuerySchema.json', new GenericTableFieldSchemaJsonPayloadTypeMapper());
$this->contextTypeProviderResolver = new AnyScopeContextTypeProviderResolver($contextTypeProvider);
$expected = self::expectedBigQueryShape();
$this->analyse([__DIR__ . '/data/contextType.php'], [
[
sprintf(
<<<'EOF'
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,
Expand All @@ -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');
}
}