Skip to content

Commit

Permalink
qa: add some type-enhancements for static analysis
Browse files Browse the repository at this point in the history
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
  • Loading branch information
boesing committed Mar 31, 2024
1 parent e0eec68 commit be4d9ea
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 8 deletions.
9 changes: 9 additions & 0 deletions src/Normalizer/Formatter/JsonFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace CuyZ\Valinor\Normalizer\Formatter;

use CuyZ\Valinor\Normalizer\Formatter\Exception\CannotFormatInvalidTypeToJson;
use CuyZ\Valinor\Normalizer\JsonNormalizer;
use Generator;

use function array_is_list;
Expand All @@ -16,11 +17,14 @@
use function is_scalar;
use function json_encode;

use const JSON_THROW_ON_ERROR;

/** @internal */
final class JsonFormatter implements StreamFormatter
{
/**
* @param resource $resource
* @param int-mask-of<JsonNormalizer::JSON_*> $jsonEncodingOptions
*/
public function __construct(
private mixed $resource,
Expand All @@ -34,6 +38,11 @@ public function format(mixed $value): void
} elseif (is_bool($value)) {
$this->write($value ? 'true' : 'false');
} elseif (is_scalar($value)) {
assert(($this->jsonEncodingOptions & JSON_THROW_ON_ERROR) === JSON_THROW_ON_ERROR);
/**
* @phpstan-ignore-next-line / Due to the new json encoding options feature, it is not possible to let SA
* tools understand that JSON_THROW_ON_ERROR is always set.
*/
$this->write(json_encode($value, $this->jsonEncodingOptions));
} elseif (is_iterable($value)) {
// Note: when a generator is formatted, it is considered as a list
Expand Down
36 changes: 29 additions & 7 deletions src/Normalizer/JsonNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@
use function is_resource;
use function stream_get_contents;

use const JSON_HEX_QUOT;
use const JSON_HEX_TAG;
use const JSON_HEX_AMP;
use const JSON_HEX_APOS;
use const JSON_INVALID_UTF8_IGNORE;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_NUMERIC_CHECK;
use const JSON_PRESERVE_ZERO_FRACTION;
use const JSON_UNESCAPED_LINE_TERMINATORS;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use const JSON_THROW_ON_ERROR;

/**
Expand All @@ -38,8 +46,7 @@ final class JsonNormalizer implements Normalizer
public const JSON_UNESCAPED_LINE_TERMINATORS = JSON_UNESCAPED_LINE_TERMINATORS;
public const JSON_UNESCAPED_SLASHES = JSON_UNESCAPED_SLASHES;
public const JSON_UNESCAPED_UNICODE = JSON_UNESCAPED_UNICODE;
private const DEFAULT_JSON_FLAG_THROW_ON_ERROR = JSON_THROW_ON_ERROR;

public const JSON_THROW_ON_ERROR = JSON_THROW_ON_ERROR;
private const ACCEPTABLE_JSON_OPTIONS = self::JSON_HEX_QUOT
| self::JSON_HEX_TAG
| self::JSON_HEX_AMP
Expand All @@ -50,22 +57,37 @@ final class JsonNormalizer implements Normalizer
| self::JSON_PRESERVE_ZERO_FRACTION
| self::JSON_UNESCAPED_LINE_TERMINATORS
| self::JSON_UNESCAPED_SLASHES
| self::JSON_UNESCAPED_UNICODE;
| self::JSON_UNESCAPED_UNICODE
| self::JSON_THROW_ON_ERROR;

/**
* @param int-mask-of<JsonNormalizer::JSON_*|JsonNormalizer::DEFAULT_JSON_FLAG_THROW_ON_ERROR> $jsonEncodingOptions
* @param int-mask-of<JsonNormalizer::JSON_*> $jsonEncodingOptions
*/
public function __construct(
private RecursiveTransformer $transformer,
public readonly int $jsonEncodingOptions = self::DEFAULT_JSON_FLAG_THROW_ON_ERROR,
) {}
public readonly int $jsonEncodingOptions = self::JSON_THROW_ON_ERROR,
) {
assert(
($this->jsonEncodingOptions & JSON_THROW_ON_ERROR) === JSON_THROW_ON_ERROR,
'JSON encoding options always have to contain JSON_THROW_ON_ERROR.',
);
}

/**
* @param int-mask-of<JsonNormalizer::JSON_*> $options
*/
public function withOptions(int $options): self
{
return new self($this->transformer, (self::ACCEPTABLE_JSON_OPTIONS & $options) | self::DEFAULT_JSON_FLAG_THROW_ON_ERROR);
/**
* SA tools are not able to infer that we end up having only accepted options here. Therefore, inlining the
* type for now should be okayish.
* Might be fixed with https://github.com/phpstan/phpstan/issues/9384 for phpstan but psalm does have some
* (not all) issues as well.
*
* @var int-mask-of<JsonNormalizer::JSON_*> $acceptedOptions
*/
$acceptedOptions = (self::ACCEPTABLE_JSON_OPTIONS & $options) | self::JSON_THROW_ON_ERROR;
return new self($this->transformer, $acceptedOptions);
}

public function normalize(mixed $value): string
Expand Down
10 changes: 9 additions & 1 deletion tests/Integration/Normalizer/NormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum;
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
use CuyZ\Valinor\Tests\Integration\IntegrationTestCase;
use CuyZ\Valinor\Normalizer\JsonNormalizer;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
Expand All @@ -37,6 +38,7 @@ final class NormalizerTest extends IntegrationTestCase
/**
* @param array<int, list<callable>> $transformers
* @param list<class-string> $transformerAttributes
* @param int-mask-of<JsonNormalizer::JSON_*> $jsonEncodingOptions
*/
#[DataProvider('normalize_basic_values_yields_expected_output_data_provider')]
public function test_normalize_basic_values_yields_expected_output(
Expand All @@ -60,7 +62,9 @@ public function test_normalize_basic_values_yields_expected_output(
}

$arrayResult = $builder->normalizer(Format::array())->normalize($input);
$jsonResult = $builder->normalizer(Format::json())->withOptions($jsonEncodingOptions)->normalize($input);
$jsonNormalizer = $builder->normalizer(Format::json())->withOptions($jsonEncodingOptions);

$jsonResult = $jsonNormalizer->normalize($input);

self::assertSame($expectedArray, $arrayResult);
self::assertSame($expectedJson, $jsonResult);
Expand Down Expand Up @@ -1068,15 +1072,19 @@ public function test_json_transformer_will_always_throw_on_error(): void

public function test_json_transformer_only_accepts_acceptable_json_options(): void
{
// @phpstan-ignore-next-line / Verify that unaccepted flags are ignored
$normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT);
self::assertSame(JSON_THROW_ON_ERROR, $normalizer->jsonEncodingOptions);

// @phpstan-ignore-next-line / Verify that unaccepted flags are ignored
$normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR);
self::assertSame(JSON_THROW_ON_ERROR, $normalizer->jsonEncodingOptions);

// @phpstan-ignore-next-line / Verify that unaccepted flags are ignored
$normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PRETTY_PRINT);
self::assertSame(JSON_THROW_ON_ERROR, $normalizer->jsonEncodingOptions);

// @phpstan-ignore-next-line / Verify that unaccepted flags are ignored
$normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT);
self::assertSame(JSON_THROW_ON_ERROR, $normalizer->jsonEncodingOptions);
}
Expand Down

0 comments on commit be4d9ea

Please sign in to comment.