From 25c073e15db1344de936c87921fdc073ebf7af1e Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Wed, 30 Oct 2024 22:48:06 +0100 Subject: [PATCH 1/5] Replace string with int normalization and update dependencies Refactor tests and implementation to use integers for normalization instead of strings, including Unsigned and Signed Integer Tests and associated data providers. Migrate dependencies in composer.json and update various configurations for PHP 8.2 syntax support and PHPUnit 11.0. --- .gitignore | 1 + castor.php | 222 ++++++++++++++++++ composer.json | 14 +- phpstan-baseline.neon | 90 ++++--- rector.php | 7 +- src/AbstractCBORObject.php | 2 +- src/ByteStringObject.php | 4 +- src/Decoder.php | 19 +- src/Encoder.php | 115 +++++++++ src/EncoderInterface.php | 10 + src/MapItem.php | 4 +- src/NegativeIntegerObject.php | 18 +- .../DoublePrecisionFloatObject.php | 15 ++ src/OtherObject/OtherObjectManager.php | 6 +- .../SinglePrecisionFloatObject.php | 15 ++ src/StringStream.php | 1 + src/Tag/BigFloatTag.php | 2 +- src/Tag/DecimalFractionTag.php | 2 +- src/Tag/TimestampTag.php | 2 +- src/TextStringObject.php | 2 +- src/UnsignedIntegerObject.php | 19 +- tests/DoublePrecisionFloat.php | 21 -- tests/DoublePrecisionFloatTest.php | 30 +++ tests/FloatTest.php | 2 +- tests/ListObjectTest.php | 8 +- tests/MapObjectTest.php | 16 +- tests/SignedIntegerTest.php | 24 +- tests/Tag/DatetimeTagTest.php | 2 +- tests/UnsignedIntegerTest.php | 32 +-- tests/VectorTest.php | 2 +- 30 files changed, 567 insertions(+), 140 deletions(-) create mode 100644 castor.php create mode 100644 src/Encoder.php create mode 100644 src/EncoderInterface.php delete mode 100644 tests/DoublePrecisionFloat.php create mode 100644 tests/DoublePrecisionFloatTest.php diff --git a/.gitignore b/.gitignore index 8da2f5c..ec1249c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ yarn-error.log /composer.lock /vendor infection.txt +/.castor.stub.php diff --git a/castor.php b/castor.php new file mode 100644 index 0000000..a55a3fb --- /dev/null +++ b/castor.php @@ -0,0 +1,222 @@ +title('Running infection'); + $nproc = run('nproc', quiet: true); + if (! $nproc->isSuccessful()) { + io()->error('Cannot determine the number of processors'); + return; + } + $threads = (int) $nproc->getOutput(); + $command = [ + 'php', + 'vendor/bin/infection', + sprintf('--min-msi=%s', $minMsi), + sprintf('--min-covered-msi=%s', $minCoveredMsi), + sprintf('--threads=%s', $threads), + ]; + if ($ci) { + $command[] = '--logger-github'; + $command[] = '-s'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run tests')] +function test(bool $coverageHtml = false, bool $coverageText = false, null|string $group = null): void +{ + io()->title('Running tests'); + $command = ['php', 'vendor/bin/phpunit', '--color']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + if ($coverageHtml) { + $command[] = '--coverage-html=build/coverage'; + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + } + if ($coverageText) { + $command[] = '--coverage-text'; + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + } + if ($group !== null) { + $command[] = sprintf('--group=%s', $group); + } + run($command, context: $context); +} + +#[AsTask(description: 'Coding standards check')] +function cs( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running coding standards check'); + $command = ['php', 'vendor/bin/ecs', 'check']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + if ($fix) { + $command[] = '--fix'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + run($command, context: $context); +} + +#[AsTask(description: 'Running PHPStan')] +function stan(#[AsOption(description: 'Generate baseline')] bool $baseline = false): void +{ + io()->title('Running PHPStan'); + $command = ['php', 'vendor/bin/phpstan', 'analyse']; + if ($baseline) { + $command[] = '--generate-baseline'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Validate Composer configuration')] +function validate(): void +{ + io()->title('Validating Composer configuration'); + $command = ['composer', 'validate', '--strict']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); + + $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; + run($command, context: $context); +} + +/** + * @param array $allowedLicenses + */ +#[AsTask(description: 'Check licenses')] +function checkLicenses( + array $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'] +): void { + io()->title('Checking licenses'); + $allowedExceptions = []; + $command = ['composer', 'licenses', '-f', 'json']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + $context = $context->withQuiet(); + $result = run($command, context: $context); + if (! $result->isSuccessful()) { + io()->error('Cannot determine licenses'); + exit(1); + } + $licenses = json_decode((string) $result->getOutput(), true); + $disallowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => ! in_array($name, $allowedExceptions, true) + && count(array_diff($info['license'], $allowedLicenses)) === 1, + \ARRAY_FILTER_USE_BOTH + ); + $allowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => in_array($name, $allowedExceptions, true) + || count(array_diff($info['license'], $allowedLicenses)) === 0, + \ARRAY_FILTER_USE_BOTH + ); + if (count($disallowed) > 0) { + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($disallowed), + $disallowed + ) + ); + io() + ->error('Disallowed licenses found'); + exit(1); + } + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($allowed), + $allowed + ) + ); + io() + ->success('All licenses are allowed'); +} + +#[AsTask(description: 'Run Rector')] +function rector( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/rector', 'process', '--ansi']; + if (! $fix) { + $command[] = '--dry-run'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run Rector')] +function deptrac(): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run Linter')] +function lint(): void +{ + io()->title('Running Linter'); + $command = ['composer', 'exec', '--', 'parallel-lint', __DIR__ . '/src/', __DIR__ . '/tests/']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} diff --git a/composer.json b/composer.json index 13925d3..fbeac99 100644 --- a/composer.json +++ b/composer.json @@ -24,27 +24,27 @@ } }, "require": { - "php": ">=8.0", + "php": ">=8.2", "ext-mbstring": "*", "brick/math": "^0.9|^0.10|^0.11|^0.12" }, "require-dev": { "ext-json": "*", - "ekino/phpstan-banned-code": "^1.0", - "infection/infection": "^0.27", + "ekino/phpstan-banned-code": "^2.0", + "infection/infection": "^0.29", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-beberlei-assert": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^10.1", - "rector/rector": "^0.19", + "phpunit/phpunit": "^10.1|^11.0", + "rector/rector": "^1.0", "roave/security-advisories": "dev-latest", - "symfony/var-dumper": "^6.0|^7.0", + "symfony/var-dumper": "^6.4|^7.1", "symplify/easy-coding-standard": "^12.0", "php-parallel-lint/php-parallel-lint": "^1.3", - "qossmic/deptrac-shim": "^1.0" + "qossmic/deptrac": "^2.0" }, "config": { "sort-packages": true, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8de4af4..cc7dd93 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,36 +1,56 @@ parameters: - ignoreErrors: - - - message: "#^Instanceof between CBOR\\\\MapItem and CBOR\\\\MapItem will always evaluate to true\\.$#" - count: 1 - path: src/MapObject.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/DoublePrecisionFloatObject.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/HalfPrecisionFloatObject.php - - - - message: "#^PHPDoc tag @var with type CBOR\\\\OtherObject is not subtype of native type string\\.$#" - count: 1 - path: src/OtherObject/OtherObjectManager.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/SinglePrecisionFloatObject.php - - - - message: "#^PHPDoc tag @var with type CBOR\\\\Tag is not subtype of native type string\\.$#" - count: 1 - path: src/Tag/TagManager.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToInt\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Tag/TagManager.php + ignoreErrors: + - + message: "#^Instanceof between CBOR\\\\MapItem and CBOR\\\\MapItem will always evaluate to true\\.$#" + count: 1 + path: src/MapObject.php + + - + message: "#^Method CBOR\\\\NegativeIntegerObject\\:\\:getValue\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" + count: 1 + path: src/NegativeIntegerObject.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/DoublePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/HalfPrecisionFloatObject.php + + - + message: "#^PHPDoc tag @var with type CBOR\\\\OtherObject is not subtype of native type string\\.$#" + count: 1 + path: src/OtherObject/OtherObjectManager.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/SinglePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$num1 of function bcmul expects numeric\\-string, string given\\.$#" + count: 1 + path: src/Tag/BigFloatTag.php + + - + message: "#^Parameter \\#1 \\$num1 of function bcmul expects numeric\\-string, string given\\.$#" + count: 1 + path: src/Tag/DecimalFractionTag.php + + - + message: "#^PHPDoc tag @var with type CBOR\\\\Tag is not subtype of native type string\\.$#" + count: 1 + path: src/Tag/TagManager.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToInt\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Tag/TagManager.php + + - + message: "#^Method CBOR\\\\UnsignedIntegerObject\\:\\:getValue\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" + count: 1 + path: src/UnsignedIntegerObject.php diff --git a/rector.php b/rector.php index 0e6ac9a..c78a24e 100644 --- a/rector.php +++ b/rector.php @@ -4,7 +4,6 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; -use Rector\PHPUnit\Set\PHPUnitLevelSetList; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; @@ -13,9 +12,9 @@ return static function (RectorConfig $config): void { $config->import(SetList::DEAD_CODE); - $config->import(LevelSetList::UP_TO_PHP_80); + $config->import(LevelSetList::UP_TO_PHP_82); $config->import(SymfonySetList::SYMFONY_CODE_QUALITY); - $config->import(PHPUnitLevelSetList::UP_TO_PHPUNIT_100); + $config->import(PHPUnitSetList::PHPUNIT_110); $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); $config->parallel(); $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); @@ -26,7 +25,7 @@ PreferPHPUnitThisCallRector::class, ] ); - $config->phpVersion(PhpVersion::PHP_80); + $config->phpVersion(PhpVersion::PHP_82); $config->importNames(); $config->importShortClasses(); }; diff --git a/src/AbstractCBORObject.php b/src/AbstractCBORObject.php index 0e0351d..e680b3a 100644 --- a/src/AbstractCBORObject.php +++ b/src/AbstractCBORObject.php @@ -10,7 +10,7 @@ abstract class AbstractCBORObject implements CBORObject, Stringable { public function __construct( - private int $majorType, + private readonly int $majorType, protected int $additionalInformation ) { } diff --git a/src/ByteStringObject.php b/src/ByteStringObject.php index b7bb01f..da2b507 100644 --- a/src/ByteStringObject.php +++ b/src/ByteStringObject.php @@ -11,9 +11,9 @@ final class ByteStringObject extends AbstractCBORObject implements Normalizable { private const MAJOR_TYPE = self::MAJOR_TYPE_BYTE_STRING; - private string $value; + private readonly string $value; - private ?string $length = null; + private readonly ?string $length; public function __construct(string $data) { diff --git a/src/Decoder.php b/src/Decoder.php index 67e98f7..d63896d 100644 --- a/src/Decoder.php +++ b/src/Decoder.php @@ -35,9 +35,10 @@ use InvalidArgumentException; use RuntimeException; use function ord; +use function sprintf; use const STR_PAD_LEFT; -final class Decoder implements DecoderInterface +final readonly class Decoder implements DecoderInterface { private TagManagerInterface $tagObjectManager; @@ -150,7 +151,7 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_TEXT_STRING : //3 + case CBORObject::MAJOR_TYPE_TEXT_STRING: //3 $object = IndefiniteLengthTextStringObject::create(); while (! ($it = $this->process($stream, true)) instanceof BreakObject) { if (! $it instanceof TextStringObject) { @@ -162,7 +163,7 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_LIST : //4 + case CBORObject::MAJOR_TYPE_LIST: //4 $object = IndefiniteLengthListObject::create(); $it = $this->process($stream, true); while (! $it instanceof BreakObject) { @@ -171,23 +172,23 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_MAP : //5 + case CBORObject::MAJOR_TYPE_MAP: //5 $object = IndefiniteLengthMapObject::create(); while (! ($it = $this->process($stream, true)) instanceof BreakObject) { $object->add($it, $this->process($stream, false)); } return $object; - case CBORObject::MAJOR_TYPE_OTHER_TYPE : //7 + case CBORObject::MAJOR_TYPE_OTHER_TYPE: //7 if (! $breakable) { throw new InvalidArgumentException('Cannot parse the data. No enclosing indefinite.'); } return BreakObject::create(); - case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER : //0 - case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER : //1 - case CBORObject::MAJOR_TYPE_TAG : //6 - default : + case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER: //0 + case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER: //1 + case CBORObject::MAJOR_TYPE_TAG: //6 + default: throw new InvalidArgumentException(sprintf( 'Cannot parse the data. Found infinite length for Major Type "%s" (%d).', str_pad(decbin($mt), 5, '0', STR_PAD_LEFT), diff --git a/src/Encoder.php b/src/Encoder.php new file mode 100644 index 0000000..512e019 --- /dev/null +++ b/src/Encoder.php @@ -0,0 +1,115 @@ +processData($data, $options) + ->__toString(); + } + + private function processData(mixed $data, int $option): CBORObject + { + return match (true) { + $data instanceof CBORObject => $data, + is_string($data) => preg_match('//u', $data) === 1 ? $this->processTextString( + $data, + $option + ) : $this->processByteString($data, $option), + is_array($data) => array_is_list($data) ? $this->processList($data, $option) : $this->processMap( + $data, + $option + ), + is_int($data) => $data < 0 ? NegativeIntegerObject::create($data) : UnsignedIntegerObject::create($data), + is_float($data) => $this->processFloat($data, $option), + $data === null => NullObject::create(), + $data === false => FalseObject::create(), + $data === true => TrueObject::create(), + default => throw new InvalidArgumentException('Unsupported data type'), + }; + } + + /** + * @param array $data + */ + private function processList(array $data, int $option): ListObject|IndefiniteLengthListObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_LIST_LENGTH); + $list = $isIndefinite ? IndefiniteLengthListObject::create() : ListObject::create(); + foreach ($data as $item) { + $list->add($this->processData($item, $option)); + } + + return $list; + } + + /** + * @param array $data + */ + private function processMap(array $data, int $option): MapObject|IndefiniteLengthMapObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_MAP_LENGTH); + $map = $isIndefinite ? IndefiniteLengthMapObject::create() : MapObject::create(); + foreach ($data as $key => $value) { + $map->add($this->processData($key, $option), $this->processData($value, $option)); + } + + return $map; + } + + private function processFloat(float $data, int $option): SinglePrecisionFloatObject|DoublePrecisionFloatObject + { + $isSinglePrecisionFloat = 0 !== ($option & self::FLOAT_FORMAT_SINGLE_PRECISION); + + return match (true) { + $isSinglePrecisionFloat => SinglePrecisionFloatObject::createFromFloat($data), + default => DoublePrecisionFloatObject::createFromFloat($data), + }; + } + + private function processTextString(string $data, int $option): TextStringObject|IndefiniteLengthTextStringObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_TEXT_STRING_LENGTH); + + return $isIndefinite ? IndefiniteLengthTextStringObject::create()->add( + TextStringObject::create($data) + ) : TextStringObject::create( + $data + ); + } + + private function processByteString(string $data, int $option): ByteStringObject|IndefiniteLengthByteStringObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_BYTE_STRING_LENGTH); + return $isIndefinite ? IndefiniteLengthByteStringObject::create()->add( + ByteStringObject::create($data) + ) : ByteStringObject::create( + $data + ); + } +} diff --git a/src/EncoderInterface.php b/src/EncoderInterface.php new file mode 100644 index 0000000..f9c18ee --- /dev/null +++ b/src/EncoderInterface.php @@ -0,0 +1,10 @@ +data === null) { - return (string) (-1 - $this->additionalInformation); + return -1 - $this->additionalInformation; } $result = Utils::binToBigInteger($this->data); $minusOne = BigInteger::of(-1); - return $minusOne->minus($result) + $valueAsString = $minusOne->minus($result) ->toBase(10) ; + $valueAsInt = (int) $valueAsString; + return (string) $valueAsInt === $valueAsString ? $valueAsInt : $valueAsString; } - public function normalize(): string + /** + * @return numeric-string|int + */ + public function normalize(): string|int { return $this->getValue(); } diff --git a/src/OtherObject/DoublePrecisionFloatObject.php b/src/OtherObject/DoublePrecisionFloatObject.php index db3a1d2..7debc1e 100644 --- a/src/OtherObject/DoublePrecisionFloatObject.php +++ b/src/OtherObject/DoublePrecisionFloatObject.php @@ -19,6 +19,21 @@ public static function supportedAdditionalInformation(): array return [self::OBJECT_DOUBLE_PRECISION_FLOAT]; } + public static function createFromFloat(float $number): self + { + $value = match (true) { + is_nan($number) => hex2bin('7FF8000000000000'), + is_infinite($number) && $number > 0 => hex2bin('7FF0000000000000'), + is_infinite($number) && $number < 0 => hex2bin('FFF0000000000000'), + default => (fn (): string => unpack('S', "\x01\x00")[1] === 1 ? strrev(pack('d', $number)) : pack( + 'd', + $number + ))(), + }; + + return new self(self::OBJECT_DOUBLE_PRECISION_FLOAT, $value); + } + public static function createFromLoadedData(int $additionalInformation, ?string $data): Base { return new self($additionalInformation, $data); diff --git a/src/OtherObject/OtherObjectManager.php b/src/OtherObject/OtherObjectManager.php index e691c3d..738fd4c 100644 --- a/src/OtherObject/OtherObjectManager.php +++ b/src/OtherObject/OtherObjectManager.php @@ -4,11 +4,12 @@ namespace CBOR\OtherObject; +use CBOR\CBORObject; use CBOR\OtherObject; use InvalidArgumentException; use function array_key_exists; -class OtherObjectManager implements OtherObjectManagerInterface +final class OtherObjectManager implements OtherObjectManagerInterface { /** * @var string[] @@ -20,6 +21,9 @@ public static function create(): self return new self(); } + /** + * @param class-string $class + */ public function add(string $class): self { foreach ($class::supportedAdditionalInformation() as $ai) { diff --git a/src/OtherObject/SinglePrecisionFloatObject.php b/src/OtherObject/SinglePrecisionFloatObject.php index d47cd30..a5a44fb 100644 --- a/src/OtherObject/SinglePrecisionFloatObject.php +++ b/src/OtherObject/SinglePrecisionFloatObject.php @@ -18,6 +18,21 @@ public static function supportedAdditionalInformation(): array return [self::OBJECT_SINGLE_PRECISION_FLOAT]; } + public static function createFromFloat(float $number): self + { + $value = match (true) { + is_nan($number) => hex2bin('7FC00000'), + is_infinite($number) && $number > 0 => hex2bin('7F800000'), + is_infinite($number) && $number < 0 => hex2bin('FF800000'), + default => (fn (): string => unpack('S', "\x01\x00")[1] === 1 ? strrev(pack('f', $number)) : pack( + 'f', + $number + ))(), + }; + + return new self(self::OBJECT_DOUBLE_PRECISION_FLOAT, $value); + } + public static function createFromLoadedData(int $additionalInformation, ?string $data): Base { return new self($additionalInformation, $data); diff --git a/src/StringStream.php b/src/StringStream.php index d522813..3dbf1da 100644 --- a/src/StringStream.php +++ b/src/StringStream.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use RuntimeException; +use function sprintf; final class StringStream implements Stream { diff --git a/src/Tag/BigFloatTag.php b/src/Tag/BigFloatTag.php index 1de2cbe..088fba6 100644 --- a/src/Tag/BigFloatTag.php +++ b/src/Tag/BigFloatTag.php @@ -78,6 +78,6 @@ public function normalize() /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ $m = $object->get(1); - return rtrim(bcmul($m->normalize(), bcpow('2', $e->normalize(), 100), 100), '0'); + return rtrim(bcmul((string) $m->normalize(), bcpow('2', (string) $e->normalize(), 100), 100), '0'); } } diff --git a/src/Tag/DecimalFractionTag.php b/src/Tag/DecimalFractionTag.php index 9eafd25..27980e9 100644 --- a/src/Tag/DecimalFractionTag.php +++ b/src/Tag/DecimalFractionTag.php @@ -77,6 +77,6 @@ public function normalize() /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ $m = $object->get(1); - return rtrim(bcmul($m->normalize(), bcpow('10', $e->normalize(), 100), 100), '0'); + return rtrim(bcmul((string) $m->normalize(), bcpow('10', (string) $e->normalize(), 100), 100), '0'); } } diff --git a/src/Tag/TimestampTag.php b/src/Tag/TimestampTag.php index afdec9f..4723f5a 100644 --- a/src/Tag/TimestampTag.php +++ b/src/Tag/TimestampTag.php @@ -51,7 +51,7 @@ public function normalize(): DateTimeInterface switch (true) { case $object instanceof UnsignedIntegerObject: case $object instanceof NegativeIntegerObject: - $formatted = DateTimeImmutable::createFromFormat('U', $object->normalize()); + $formatted = DateTimeImmutable::createFromFormat('U', (string) $object->normalize()); break; case $object instanceof HalfPrecisionFloatObject: diff --git a/src/TextStringObject.php b/src/TextStringObject.php index a0ca87a..80ab334 100644 --- a/src/TextStringObject.php +++ b/src/TextStringObject.php @@ -13,7 +13,7 @@ final class TextStringObject extends AbstractCBORObject implements Normalizable private ?string $length = null; - private string $data; + private readonly string $data; public function __construct(string $data) { diff --git a/src/UnsignedIntegerObject.php b/src/UnsignedIntegerObject.php index 3483334..95c4159 100644 --- a/src/UnsignedIntegerObject.php +++ b/src/UnsignedIntegerObject.php @@ -14,7 +14,7 @@ final class UnsignedIntegerObject extends AbstractCBORObject implements Normaliz public function __construct( int $additionalInformation, - private ?string $data + private readonly ?string $data ) { parent::__construct(self::MAJOR_TYPE, $additionalInformation); } @@ -58,18 +58,25 @@ public function getMajorType(): int return self::MAJOR_TYPE; } - public function getValue(): string + /** + * @return numeric-string|int + */ + public function getValue(): string|int { if ($this->data === null) { - return (string) $this->additionalInformation; + return $this->additionalInformation; } $integer = BigInteger::fromBase(bin2hex($this->data), 16); - - return $integer->toBase(10); + $valueAsString = $integer->toBase(10); + $valueAsInt = (int) $valueAsString; + return (string) $valueAsInt === $valueAsString ? $valueAsInt : $valueAsString; } - public function normalize(): string + /** + * @return numeric-string|int + */ + public function normalize(): string|int { return $this->getValue(); } diff --git a/tests/DoublePrecisionFloat.php b/tests/DoublePrecisionFloat.php deleted file mode 100644 index 3dcf68f..0000000 --- a/tests/DoublePrecisionFloat.php +++ /dev/null @@ -1,21 +0,0 @@ -normalize()); - } -} diff --git a/tests/DoublePrecisionFloatTest.php b/tests/DoublePrecisionFloatTest.php new file mode 100644 index 0000000..0234eb9 --- /dev/null +++ b/tests/DoublePrecisionFloatTest.php @@ -0,0 +1,30 @@ +normalize()); + static::assertSame(hex2bin('fb3fd5555555555555'), $obj->__toString()); + } + + #[Test] + public function aDoublePrecisionObjectCanBeCreatedFromFloat(): void + { + $obj = DoublePrecisionFloatObject::createFromFloat(1 / 3); + static::assertSame(1 / 3, $obj->normalize()); + static::assertSame(hex2bin('fb3fd5555555555555'), $obj->__toString()); + } +} diff --git a/tests/FloatTest.php b/tests/FloatTest.php index fba3096..94f1d4f 100644 --- a/tests/FloatTest.php +++ b/tests/FloatTest.php @@ -16,7 +16,7 @@ final class FloatTest extends CBORTestCase { #[DataProvider('getDataSet')] #[Test] - public function aFloatCanBeParsed(string $data): void + public function aFloatCanBeParsed(int|string $data): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() diff --git a/tests/ListObjectTest.php b/tests/ListObjectTest.php index 1bdb7b1..9dadd40 100644 --- a/tests/ListObjectTest.php +++ b/tests/ListObjectTest.php @@ -37,7 +37,7 @@ public function aListActsAsAnArray(): void static::assertCount(3, $object1); static::assertCount(3, $object2); - static::assertSame(['Hello', 'World', '3'], $object2->normalize()); + static::assertSame(['Hello', 'World', 3], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); static::assertArrayHasKey(0, $object2); @@ -45,7 +45,7 @@ public function aListActsAsAnArray(): void static::assertArrayHasKey(2, $object2); static::assertSame($object2[0]->normalize(), 'Hello'); static::assertSame($object2[1]->normalize(), 'World'); - static::assertSame($object2[2]->normalize(), '3'); + static::assertSame($object2[2]->normalize(), 3); } #[Test] @@ -70,7 +70,7 @@ public function anIndefiniteLengthListActsAsAnArray(): void static::assertCount(3, $object1); static::assertCount(3, $object2); - static::assertSame(['Hello', 'World', '3'], $object2->normalize()); + static::assertSame(['Hello', 'World', 3], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); static::assertArrayHasKey(0, $object2); @@ -78,6 +78,6 @@ public function anIndefiniteLengthListActsAsAnArray(): void static::assertArrayHasKey(2, $object2); static::assertSame($object2[0]->normalize(), 'Hello'); static::assertSame($object2[1]->normalize(), 'World'); - static::assertSame($object2[2]->normalize(), '3'); + static::assertSame($object2[2]->normalize(), 3); } } diff --git a/tests/MapObjectTest.php b/tests/MapObjectTest.php index 527b694..0407e69 100644 --- a/tests/MapObjectTest.php +++ b/tests/MapObjectTest.php @@ -43,8 +43,8 @@ public function aMapActsAsAnArray(): void static::assertSame([ 10 => 'Hello', -150 => 'World', - 'AZERTY' => '1', - 'Test' => '3', + 'AZERTY' => 1, + 'Test' => 3, ], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); @@ -54,8 +54,8 @@ public function aMapActsAsAnArray(): void static::assertArrayHasKey('Test', $object2); static::assertSame($object2[10]->normalize(), 'Hello'); static::assertSame($object2[-150]->normalize(), 'World'); - static::assertSame($object2['AZERTY']->normalize(), '1'); - static::assertSame($object2['Test']->normalize(), '3'); + static::assertSame($object2['AZERTY']->normalize(), 1); + static::assertSame($object2['Test']->normalize(), 3); } #[Test] @@ -83,8 +83,8 @@ public function anIndefiniteLengthMapActsAsAnArray(): void static::assertSame([ 10 => 'Hello', -150 => 'World', - 'AZERTY' => '1', - 'Test' => '3', + 'AZERTY' => 1, + 'Test' => 3, ], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); @@ -94,7 +94,7 @@ public function anIndefiniteLengthMapActsAsAnArray(): void static::assertArrayHasKey('Test', $object2); static::assertSame($object2[10]->normalize(), 'Hello'); static::assertSame($object2[-150]->normalize(), 'World'); - static::assertSame($object2['AZERTY']->normalize(), '1'); - static::assertSame($object2['Test']->normalize(), '3'); + static::assertSame($object2['AZERTY']->normalize(), 1); + static::assertSame($object2['Test']->normalize(), 3); } } diff --git a/tests/SignedIntegerTest.php b/tests/SignedIntegerTest.php index 6e2b57e..0d765b7 100644 --- a/tests/SignedIntegerTest.php +++ b/tests/SignedIntegerTest.php @@ -20,7 +20,7 @@ final class SignedIntegerTest extends CBORTestCase #[Test] public function createOnValidValue( int $intValue, - string $expectedIntValue, + int $expectedIntValue, int $expectedMajorType, int $expectedAdditionalInformation ): void { @@ -32,15 +32,15 @@ public function createOnValidValue( public static function getValidValue(): Iterator { - yield [-12_345_678, '-12345678', 1, 26]; - yield [-255, '-255', 1, 24]; - yield [-254, '-254', 1, 24]; - yield [-65535, '-65535', 1, 25]; - yield [-18, '-18', 1, 17]; + yield [-12_345_678, -12345678, 1, 26]; + yield [-255, -255, 1, 24]; + yield [-254, -254, 1, 24]; + yield [-65535, -65535, 1, 25]; + yield [-18, -18, 1, 17]; } #[Test] - public function ceateOnNegativeValue(): void + public function createOnNegativeValue(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The value must be a negative integer.'); @@ -59,7 +59,7 @@ public function createOnOutOfRangeValue(): void #[DataProvider('getDataSet')] #[Test] - public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, string $expectedNormalizedData): void + public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, int|string $expectedNormalizedData): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() @@ -72,10 +72,10 @@ public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, string $ex public static function getDataSet(): Iterator { - yield ['20', '-1']; - yield ['29', '-10']; - yield ['3863', '-100']; - yield ['3903e7', '-1000']; + yield ['20', -1]; + yield ['29', -10]; + yield ['3863', -100]; + yield ['3903e7', -1000]; yield ['c349010000000000000000', '-18446744073709551617']; yield ['3bffffffffffffffff', '-18446744073709551616']; } diff --git a/tests/Tag/DatetimeTagTest.php b/tests/Tag/DatetimeTagTest.php index 42d18dc..567bae7 100644 --- a/tests/Tag/DatetimeTagTest.php +++ b/tests/Tag/DatetimeTagTest.php @@ -53,7 +53,7 @@ public function createValidTimestampTagWithUnsignedInteger(): void public function createValidTimestampTagWithNegativeInteger(): void { $tag = TimestampTag::create(NegativeIntegerObject::create(-10)); - static::assertSame('-10.000000', $tag->normalize()->format('U.u')); + static::assertEqualsWithDelta(-10.0, $tag->normalize()->format('U.u'), 0.00001); } #[Test] diff --git a/tests/UnsignedIntegerTest.php b/tests/UnsignedIntegerTest.php index 75b65e9..5933238 100644 --- a/tests/UnsignedIntegerTest.php +++ b/tests/UnsignedIntegerTest.php @@ -20,7 +20,7 @@ final class UnsignedIntegerTest extends CBORTestCase #[Test] public function createOnValidValue( int $intValue, - string $expectedIntValue, + int $expectedIntValue, int $expectedMajorType, int $expectedAdditionalInformation ): void { @@ -32,10 +32,10 @@ public function createOnValidValue( public static function getValidValue(): Iterator { - yield [12_345_678, '12345678', 0, 26]; - yield [255, '255', 0, 25]; - yield [254, '254', 0, 24]; - yield [18, '18', 0, 18]; + yield [12_345_678, 12345678, 0, 26]; + yield [255, 255, 0, 25]; + yield [254, 254, 0, 24]; + yield [18, 18, 0, 18]; } #[Test] @@ -58,7 +58,7 @@ public function createOnOutOfRangeValue(): void #[DataProvider('getDataSet')] #[Test] - public function anUnsignedIntegerCanBeParsed(string $data, string $expectedNormalizedData): void + public function anUnsignedIntegerCanBeParsed(string $data, int|string $expectedNormalizedData): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() @@ -70,16 +70,16 @@ public function anUnsignedIntegerCanBeParsed(string $data, string $expectedNorma public static function getDataSet(): Iterator { - yield ['00', '0']; - yield ['01', '1']; - yield ['0a', '10']; - yield ['17', '23']; - yield ['1818', '24']; - yield ['1819', '25']; - yield ['1864', '100']; - yield ['1903e8', '1000']; - yield ['1a000f4240', '1000000']; - yield ['1b000000e8d4a51000', '1000000000000']; + yield ['00', 0]; + yield ['01', 1]; + yield ['0a', 10]; + yield ['17', 23]; + yield ['1818', 24]; + yield ['1819', 25]; + yield ['1864', 100]; + yield ['1903e8', 1000]; + yield ['1a000f4240', 1000000]; + yield ['1b000000e8d4a51000', 1000000000000]; yield ['1bffffffffffffffff', '18446744073709551615']; yield ['c249010000000000000000', '18446744073709551616']; } diff --git a/tests/VectorTest.php b/tests/VectorTest.php index e633375..26a88ea 100644 --- a/tests/VectorTest.php +++ b/tests/VectorTest.php @@ -14,8 +14,8 @@ */ final class VectorTest extends CBORTestCase { - #[DataProvider('getVectors')] #[Test] + #[DataProvider('getVectors')] public function createOnValidValue(string $cbor, string $hex): void { $stream = StringStream::create(base64_decode($cbor, true)); From c9f6906d67683b82b00813b752897a68d40f58ba Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 31 Oct 2024 16:43:13 +0100 Subject: [PATCH 2/5] Refactor code for clarity and performance Refactored encode and process methods for clarity in Encoder.php. Simplified TagManager and OtherObjectManager by using constructors and arrays. Improved string conversion logic in several CBOR classes. Optimized vector tests to reduce complexity. Removed redundant PHPStan baseline errors and updated ECS configuration for precise directory paths. --- ecs.php | 5 +--- phpstan-baseline.neon | 15 ----------- src/AbstractCBORObject.php | 3 +-- src/ByteStringObject.php | 4 +-- src/CBORObject.php | 6 ++--- src/Decoder.php | 34 ++++++++++++------------ src/Encoder.php | 26 +++++++++--------- src/IndefiniteLengthByteStringObject.php | 2 +- src/ListObject.php | 4 +-- src/MapObject.php | 20 +++----------- src/OtherObject/OtherObjectManager.php | 22 +++++++++------ src/Tag/TagManager.php | 22 ++++++++++----- src/TextStringObject.php | 4 +-- tests/VectorTest.php | 7 +++-- 14 files changed, 79 insertions(+), 95 deletions(-) diff --git a/ecs.php b/ecs.php index 2d4767b..6f25433 100644 --- a/ecs.php +++ b/ecs.php @@ -85,8 +85,5 @@ ]); $config->parallel(); - $config->paths([__DIR__]); - $config->skip( - [__DIR__ . '/.github', __DIR__ . '/build', __DIR__ . '/vendor', PhpUnitTestClassRequiresCoversFixer::class] - ); + $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); }; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc7dd93..ac344c7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Instanceof between CBOR\\\\MapItem and CBOR\\\\MapItem will always evaluate to true\\.$#" - count: 1 - path: src/MapObject.php - - message: "#^Method CBOR\\\\NegativeIntegerObject\\:\\:getValue\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" count: 1 @@ -20,11 +15,6 @@ parameters: count: 3 path: src/OtherObject/HalfPrecisionFloatObject.php - - - message: "#^PHPDoc tag @var with type CBOR\\\\OtherObject is not subtype of native type string\\.$#" - count: 1 - path: src/OtherObject/OtherObjectManager.php - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" count: 3 @@ -40,11 +30,6 @@ parameters: count: 1 path: src/Tag/DecimalFractionTag.php - - - message: "#^PHPDoc tag @var with type CBOR\\\\Tag is not subtype of native type string\\.$#" - count: 1 - path: src/Tag/TagManager.php - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToInt\\(\\) expects string, string\\|null given\\.$#" count: 1 diff --git a/src/AbstractCBORObject.php b/src/AbstractCBORObject.php index e680b3a..e04cf3e 100644 --- a/src/AbstractCBORObject.php +++ b/src/AbstractCBORObject.php @@ -4,10 +4,9 @@ namespace CBOR; -use Stringable; use function chr; -abstract class AbstractCBORObject implements CBORObject, Stringable +abstract class AbstractCBORObject implements CBORObject { public function __construct( private readonly int $majorType, diff --git a/src/ByteStringObject.php b/src/ByteStringObject.php index da2b507..1a91f2f 100644 --- a/src/ByteStringObject.php +++ b/src/ByteStringObject.php @@ -27,9 +27,7 @@ public function __construct(string $data) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; return $result . $this->value; } diff --git a/src/CBORObject.php b/src/CBORObject.php index 2da9f8a..816b86a 100644 --- a/src/CBORObject.php +++ b/src/CBORObject.php @@ -4,7 +4,9 @@ namespace CBOR; -interface CBORObject +use Stringable; + +interface CBORObject extends Stringable { public const MAJOR_TYPE_UNSIGNED_INTEGER = 0b000; @@ -86,8 +88,6 @@ interface CBORObject public const TAG_CBOR = 55799; - public function __toString(): string; - public function getMajorType(): int; public function getAdditionalInformation(): int; diff --git a/src/Decoder.php b/src/Decoder.php index d63896d..0c8b556 100644 --- a/src/Decoder.php +++ b/src/Decoder.php @@ -199,28 +199,28 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR private function generateTagManager(): TagManagerInterface { - return TagManager::create() - ->add(DatetimeTag::class) - ->add(TimestampTag::class) + return TagManager::create([ + DatetimeTag::class, + TimestampTag::class, - ->add(UnsignedBigIntegerTag::class) - ->add(NegativeBigIntegerTag::class) + UnsignedBigIntegerTag::class, + NegativeBigIntegerTag::class, - ->add(DecimalFractionTag::class) - ->add(BigFloatTag::class) + DecimalFractionTag::class, + BigFloatTag::class, - ->add(Base64UrlEncodingTag::class) - ->add(Base64EncodingTag::class) - ->add(Base16EncodingTag::class) - ->add(CBOREncodingTag::class) + Base64UrlEncodingTag::class, + Base64EncodingTag::class, + Base16EncodingTag::class, + CBOREncodingTag::class, - ->add(UriTag::class) - ->add(Base64UrlTag::class) - ->add(Base64Tag::class) - ->add(MimeTag::class) + UriTag::class, + Base64UrlTag::class, + Base64Tag::class, + MimeTag::class, - ->add(CBORTag::class) - ; + CBORTag::class, + ]); } private function generateOtherObjectManager(): OtherObjectManagerInterface diff --git a/src/Encoder.php b/src/Encoder.php index 512e019..d8ed60e 100644 --- a/src/Encoder.php +++ b/src/Encoder.php @@ -29,8 +29,7 @@ final class Encoder implements EncoderInterface public function encode(mixed $data, int $options = 0): string { - return $this->processData($data, $options) - ->__toString(); + return (string) $this->processData($data, $options); } private function processData(mixed $data, int $option): CBORObject @@ -95,21 +94,24 @@ private function processFloat(float $data, int $option): SinglePrecisionFloatObj private function processTextString(string $data, int $option): TextStringObject|IndefiniteLengthTextStringObject { $isIndefinite = 0 !== ($option & self::INDEFINITE_TEXT_STRING_LENGTH); + $cbor = TextStringObject::create($data); - return $isIndefinite ? IndefiniteLengthTextStringObject::create()->add( - TextStringObject::create($data) - ) : TextStringObject::create( - $data - ); + if (! $isIndefinite) { + return $cbor; + } + + return IndefiniteLengthTextStringObject::create()->add($cbor); } private function processByteString(string $data, int $option): ByteStringObject|IndefiniteLengthByteStringObject { $isIndefinite = 0 !== ($option & self::INDEFINITE_BYTE_STRING_LENGTH); - return $isIndefinite ? IndefiniteLengthByteStringObject::create()->add( - ByteStringObject::create($data) - ) : ByteStringObject::create( - $data - ); + $cbor = ByteStringObject::create($data); + + if (! $isIndefinite) { + return $cbor; + } + + return IndefiniteLengthByteStringObject::create()->add($cbor); } } diff --git a/src/IndefiniteLengthByteStringObject.php b/src/IndefiniteLengthByteStringObject.php index ebe97c1..49da2f3 100644 --- a/src/IndefiniteLengthByteStringObject.php +++ b/src/IndefiniteLengthByteStringObject.php @@ -27,7 +27,7 @@ public function __toString(): string { $result = parent::__toString(); foreach ($this->chunks as $chunk) { - $result .= $chunk->__toString(); + $result .= (string) $chunk; } return $result . "\xFF"; diff --git a/src/ListObject.php b/src/ListObject.php index 4f8da72..06f7f26 100644 --- a/src/ListObject.php +++ b/src/ListObject.php @@ -46,9 +46,7 @@ public function __construct(array $data = []) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; foreach ($this->data as $object) { $result .= (string) $object; } diff --git a/src/MapObject.php b/src/MapObject.php index 72a7431..a7c1139 100644 --- a/src/MapObject.php +++ b/src/MapObject.php @@ -26,7 +26,7 @@ final class MapObject extends AbstractCBORObject implements Countable, IteratorA */ private array $data; - private ?string $length = null; + private ?string $length; /** * @param MapItem[] $data @@ -34,12 +34,6 @@ final class MapObject extends AbstractCBORObject implements Countable, IteratorA public function __construct(array $data = []) { [$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data); - array_map(static function ($item): void { - if (! $item instanceof MapItem) { - throw new InvalidArgumentException('The list must contain only MapItem objects.'); - } - }, $data); - parent::__construct(self::MAJOR_TYPE, $additionalInformation); $this->data = $data; $this->length = $length; @@ -48,16 +42,10 @@ public function __construct(array $data = []) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; foreach ($this->data as $object) { - $result .= $object->getKey() - ->__toString() - ; - $result .= $object->getValue() - ->__toString() - ; + $result .= (string) $object->getKey(); + $result .= (string) $object->getValue(); } return $result; diff --git a/src/OtherObject/OtherObjectManager.php b/src/OtherObject/OtherObjectManager.php index 738fd4c..ababcbb 100644 --- a/src/OtherObject/OtherObjectManager.php +++ b/src/OtherObject/OtherObjectManager.php @@ -4,25 +4,29 @@ namespace CBOR\OtherObject; -use CBOR\CBORObject; -use CBOR\OtherObject; use InvalidArgumentException; use function array_key_exists; final class OtherObjectManager implements OtherObjectManagerInterface { /** - * @var string[] + * @param class-string[] $classes */ - private array $classes = []; + public function __construct( + private array $classes = [], + ) { + } - public static function create(): self + /** + * @param class-string[] $classes + */ + public static function create(array $classes = []): self { - return new self(); + return new self($classes); } /** - * @param class-string $class + * @param class-string $class */ public function add(string $class): self { @@ -36,6 +40,9 @@ public function add(string $class): self return $this; } + /** + * @return class-string + */ public function getClassForValue(int $value): string { return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericObject::class; @@ -43,7 +50,6 @@ public function getClassForValue(int $value): string public function createObjectForValue(int $value, ?string $data): OtherObjectInterface { - /** @var OtherObject $class */ $class = $this->getClassForValue($value); return $class::createFromLoadedData($value, $data); diff --git a/src/Tag/TagManager.php b/src/Tag/TagManager.php index 0d3eb6d..40c0a8c 100644 --- a/src/Tag/TagManager.php +++ b/src/Tag/TagManager.php @@ -5,7 +5,6 @@ namespace CBOR\Tag; use CBOR\CBORObject; -use CBOR\Tag; use CBOR\Utils; use InvalidArgumentException; use function array_key_exists; @@ -13,15 +12,24 @@ final class TagManager implements TagManagerInterface { /** - * @var string[] + * @param class-string[] $classes */ - private array $classes = []; + public function __construct( + private array $classes = [] + ) { + } - public static function create(): self + /** + * @param array> $classes + */ + public static function create(array $classes = []): self { - return new self(); + return new self($classes); } + /** + * @param class-string $class + */ public function add(string $class): self { if ($class::getTagId() < 0) { @@ -32,6 +40,9 @@ public function add(string $class): self return $this; } + /** + * @return class-string + */ public function getClassForValue(int $value): string { return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericTag::class; @@ -44,7 +55,6 @@ public function createObjectForValue(int $additionalInformation, ?string $data, Utils::assertString($data, 'Invalid data'); $value = Utils::binToInt($data); } - /** @var Tag $class */ $class = $this->getClassForValue($value); return $class::createFromLoadedData($additionalInformation, $data, $object); diff --git a/src/TextStringObject.php b/src/TextStringObject.php index 80ab334..dfe77e5 100644 --- a/src/TextStringObject.php +++ b/src/TextStringObject.php @@ -27,9 +27,7 @@ public function __construct(string $data) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; return $result . $this->data; } diff --git a/tests/VectorTest.php b/tests/VectorTest.php index 26a88ea..18ab715 100644 --- a/tests/VectorTest.php +++ b/tests/VectorTest.php @@ -26,8 +26,11 @@ public function createOnValidValue(string $cbor, string $hex): void static::assertSame(hex2bin($hex), (string) $result); } - public static function getVectors(): array + public static function getVectors(): iterable { - return json_decode(file_get_contents(__DIR__ . '/vectors.json'), true, 512, JSON_THROW_ON_ERROR); + $data = json_decode(file_get_contents(__DIR__ . '/vectors.json'), true, 512, JSON_THROW_ON_ERROR); + foreach ($data as $datum) { + yield [$datum['cbor'], $datum['hex']]; + } } } From 9139a317c10e0ad732350dcebd96e6caf4124384 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 31 Oct 2024 17:34:19 +0100 Subject: [PATCH 3/5] Remove redundant import and improve Rector setup Removed the unused `PhpUnitTestClassRequiresCoversFixer` import from `ecs.php`. Enhanced the Rector configuration in `rector.php` by adding Symfony sets for code quality, constructor injection, and validator attributes, while keeping imports organized and removing unused imports. --- ecs.php | 2 +- rector.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ecs.php b/ecs.php index 6f25433..3c327bf 100644 --- a/ecs.php +++ b/ecs.php @@ -19,7 +19,6 @@ use PhpCsFixer\Fixer\PhpTag\LinebreakAfterOpeningTagFixer; use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestAnnotationFixer; use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer; -use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestClassRequiresCoversFixer; use PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer; use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer; use PhpCsFixer\Fixer\Strict\StrictComparisonFixer; @@ -31,6 +30,7 @@ $header = ''; return static function (ECSConfig $config) use ($header): void { + $header = ''; $config->import(SetList::PSR_12); $config->import(SetList::CLEAN_CODE); $config->import(SetList::DOCTRINE_ANNOTATIONS); diff --git a/rector.php b/rector.php index c78a24e..1ad732b 100644 --- a/rector.php +++ b/rector.php @@ -13,19 +13,26 @@ return static function (RectorConfig $config): void { $config->import(SetList::DEAD_CODE); $config->import(LevelSetList::UP_TO_PHP_82); + $config->import(SymfonySetList::SYMFONY_64); + $config->import(SymfonySetList::SYMFONY_50_TYPES); + $config->import(SymfonySetList::SYMFONY_52_VALIDATOR_ATTRIBUTES); $config->import(SymfonySetList::SYMFONY_CODE_QUALITY); - $config->import(PHPUnitSetList::PHPUNIT_110); + $config->import(SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION); + $config->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES); $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); + $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); + $config->import(PHPUnitSetList::PHPUNIT_110); $config->parallel(); $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); $config->skip( [ - __DIR__ . '/src/IndefiniteLengthMapObject.php', __DIR__ . '/src/MapObject.php', PreferPHPUnitThisCallRector::class, ] ); $config->phpVersion(PhpVersion::PHP_82); + $config->parallel(); $config->importNames(); $config->importShortClasses(); + $config->removeUnusedImports(); }; From c8181d22900842d28501fbccb168075fbbc1bf7e Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 31 Oct 2024 18:25:29 +0100 Subject: [PATCH 4/5] Remove Makefile and update CI workflows Deleted the Makefile and incorporated related CI tasks into GitHub workflows. Updated the workflows to use Castor for tasks previously handled by make commands. Added new workflows for dependency review, issue locking, and scorecards analysis. Adjusted phpstan baseline and deptrac configurations accordingly. --- .gitattributes | 2 +- .github/workflows/dependency-review.yml | 14 +++++ .github/workflows/infection.yml | 34 ++++++++++ .github/workflows/integrate.yml | 41 ++---------- .github/workflows/lock-closed-issues.yml | 23 +++++++ .github/workflows/merge-me.yml | 28 --------- .../workflows/release-on-milestone-closed.yml | 2 - .github/workflows/scorecards.yml | 62 +++++++++++++++++++ Makefile | 50 --------------- deptrac.yaml | 6 +- phpstan-baseline.neon | 20 ++++++ 11 files changed, 162 insertions(+), 120 deletions(-) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/infection.yml create mode 100644 .github/workflows/lock-closed-issues.yml delete mode 100644 .github/workflows/merge-me.yml create mode 100644 .github/workflows/scorecards.yml delete mode 100644 Makefile diff --git a/.gitattributes b/.gitattributes index 06f47c0..d47f0d5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,8 +8,8 @@ /CODE_OF_CONDUCT.md export-ignore /deptrac.yaml export-ignore /ecs.php export-ignore +/castor.php export-ignore /infection.json.dist export-ignore -/Makefile export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..b9d6d20 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml new file mode 100644 index 0000000..97bdab5 --- /dev/null +++ b/.github/workflows/infection.yml @@ -0,0 +1,34 @@ +name: "Integrate" + +on: + push: + branches: + - "*.*.x" + +jobs: + mutation_testing: + name: "5️⃣ Mutation Testing" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" + coverage: "xdebug" + + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Fetch Git base reference" + run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + + - name: "Execute Infection" + run: "castor infect" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 98731bc..91effc2 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -55,7 +55,6 @@ jobs: operating-system: - "ubuntu-latest" php-version: - - "8.1" - "8.2" - "8.3" dependencies: @@ -80,7 +79,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute tests (PHP)" - run: "make ci-cc" + run: "castor test" # - name: Send coverage to Coveralls # if: "matrix.php-version == '8.1' && matrix.dependencies == 'highest'" @@ -120,7 +119,7 @@ jobs: run: "composer dump-autoload --optimize --strict-psr" - name: "Execute static analysis" - run: "make st" + run: "castor stan" coding_standards: name: "4️⃣ Coding Standards" @@ -146,40 +145,10 @@ jobs: composer-options: "--optimize-autoloader" - name: "Check coding style" - run: "make ci-cs" + run: "castor cs" - name: "Deptrac" - run: | - vendor/bin/deptrac analyse --fail-on-uncovered --no-cache - - mutation_testing: - name: "5️⃣ Mutation Testing" - needs: - - "byte_level" - - "syntax_errors" - runs-on: "ubuntu-latest" - steps: - - name: "Set up PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "8.3" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" - coverage: "xdebug" - - - name: "Checkout code" - uses: "actions/checkout@v3" - - - name: "Fetch Git base reference" - run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" - - - name: "Install dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "highest" - composer-options: "--optimize-autoloader" - - - name: "Execute Infection" - run: "make ci-mu" + run: "castor deptrac" rector_checkstyle: name: "6️⃣ Rector Checkstyle" @@ -208,7 +177,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Rector" - run: "make rector" + run: "castor rector" exported_files: name: "7️⃣ Exported files" diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml new file mode 100644 index 0000000..fedb91d --- /dev/null +++ b/.github/workflows/lock-closed-issues.yml @@ -0,0 +1,23 @@ +name: 'Lock Issues' + +on: + schedule: + - cron: '12 6 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-inactive-days: '31' + exclude-issue-created-before: '' + exclude-any-issue-labels: '' + add-issue-labels: '' + issue-comment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + issue-lock-reason: 'resolved' + process-only: 'issues' diff --git a/.github/workflows/merge-me.yml b/.github/workflows/merge-me.yml deleted file mode 100644 index 1796365..0000000 --- a/.github/workflows/merge-me.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Merge me! - -on: - check_suite: - types: - - completed - -jobs: - merge-me: - name: Merge me! - runs-on: ubuntu-latest - steps: - - name: Merge me! - uses: ridedott/merge-me-action@v2.10.35 - with: - # Depending on branch protection rules, a manually populated - # `GITHUB_TOKEN_WORKAROUND` environment variable with permissions to - # push to a protected branch must be used. This variable can have an - # arbitrary name, as an example, this repository uses - # `GITHUB_TOKEN_DOTTBOTT`. - # - # When using a custom token, it is recommended to leave the following - # comment for other developers to be aware of the reasoning behind it: - # - # This must be used as GitHub Actions token does not support - # pushing to protected branches. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MERGE_METHOD: MERGE diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index b9986a8..00f7347 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -1,5 +1,3 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - name: "Automatic Releases" on: diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000..a6379ab --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,62 @@ +name: Scorecards supply-chain security + +on: + schedule: + - cron: '34 4 * * 6' + push: + branches: + - "*.*.x" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Used to receive a badge. (Upcoming feature) + id-token: write + # Needs for private repositories. + contents: read + actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@v2.3.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + + # Publish the results for public repositories to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@v4.3.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/Makefile b/Makefile deleted file mode 100644 index aa9acad..0000000 --- a/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -mu: vendor ## Mutation tests - vendor/bin/infection -s --threads=$$(nproc) --min-msi=3 --min-covered-msi=58 - -tests: vendor ## Run all tests - vendor/bin/phpunit --color - -cc: vendor ## Show test coverage rates (HTML) - vendor/bin/phpunit --coverage-html ./build - -cs: vendor ## Fix all files using defined ECS rules - vendor/bin/ecs check --fix - -tu: vendor ## Run only unit tests - vendor/bin/phpunit --color --group Unit - -ti: vendor ## Run only integration tests - vendor/bin/phpunit --color --group Integration - -tf: vendor ## Run only functional tests - vendor/bin/phpunit --color --group Functional - -st: vendor ## Run static analyse - vendor/bin/phpstan analyse - - -################################################ - -ci-mu: vendor ## Mutation tests (for Github only) - vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=3 --min-covered-msi=58 - -ci-cc: vendor ## Show test coverage rates (console) - vendor/bin/phpunit --coverage-text - -ci-cs: vendor ## Check all files using defined ECS rules - vendor/bin/ecs check - -################################################ - - -vendor: composer.json composer.lock - composer validate - composer install - -rector: vendor ## Check all files using Rector - vendor/bin/rector process --ansi --dry-run --xdebug - -.DEFAULT_GOAL := help -help: - @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' -.PHONY: help diff --git a/deptrac.yaml b/deptrac.yaml index 7573742..45f331c 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -4,11 +4,11 @@ parameters: layers: - name: 'CBOR' collectors: - - type: 'className' - regex: '^CBO\\' + - type: 'classLike' + value: '^CBO\\' - name: 'Vendors' collectors: - - { type: className, regex: '^Brick\\' } + - { type: 'classLike', value: '^Brick\\' } ruleset: CBOR: - Vendors diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ac344c7..2c44fb3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,21 +5,41 @@ parameters: count: 1 path: src/NegativeIntegerObject.php + - + message: "#^Cannot access offset 1 on array\\|false\\.$#" + count: 1 + path: src/OtherObject/DoublePrecisionFloatObject.php + - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" count: 3 path: src/OtherObject/DoublePrecisionFloatObject.php + - + message: "#^Parameter \\#2 \\$data of class CBOR\\\\OtherObject\\\\DoublePrecisionFloatObject constructor expects string\\|null, string\\|false given\\.$#" + count: 1 + path: src/OtherObject/DoublePrecisionFloatObject.php + - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" count: 3 path: src/OtherObject/HalfPrecisionFloatObject.php + - + message: "#^Cannot access offset 1 on array\\|false\\.$#" + count: 1 + path: src/OtherObject/SinglePrecisionFloatObject.php + - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" count: 3 path: src/OtherObject/SinglePrecisionFloatObject.php + - + message: "#^Parameter \\#2 \\$data of class CBOR\\\\OtherObject\\\\SinglePrecisionFloatObject constructor expects string\\|null, string\\|false given\\.$#" + count: 1 + path: src/OtherObject/SinglePrecisionFloatObject.php + - message: "#^Parameter \\#1 \\$num1 of function bcmul expects numeric\\-string, string given\\.$#" count: 1 From 1e4a86fb0e6b34be1ab7a453c15a28d0f4b396dd Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 31 Oct 2024 18:28:35 +0100 Subject: [PATCH 5/5] Add Castor tool setup to CI workflow Integrated the Castor tool into various jobs in the GitHub Actions workflow file. This enhances the linting process and ensures compatibility across multiple PHP versions defined in the CI configuration. Updated commands to utilize Castor for improved syntax checking. --- .github/workflows/integrate.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 91effc2..e2bb67d 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -32,6 +32,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" coverage: "none" - name: "Checkout code" @@ -43,7 +44,7 @@ jobs: dependency-versions: "highest" - name: "Check source code for syntax errors" - run: "composer exec -- parallel-lint src/ tests/" + run: "castor lint" unit_tests: name: "2️⃣ Unit and functional tests" @@ -66,6 +67,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "xdebug" @@ -100,6 +102,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "none" @@ -132,6 +135,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "none" @@ -161,6 +165,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "xdebug"