From c27ad9b868fef0a5e0e017806ba043199ab17c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Wed, 10 Jun 2026 10:53:50 +0200 Subject: [PATCH 1/2] Require PHP 8.4, replace Psalm with PHPStan, upgrade Valinor to 2.x, remove PSL - Require PHP >= 8.4 and trim symfony/options-resolver to ^6.4 || ^7.0 || ^8.0 - Replace Psalm with PHPStan (level max) via setono/code-quality-pack ^3.4 - Upgrade cuyz/valinor to ^2.0 and let it handle the top-level JSON shape validation previously done with azjezz/psl - Migrate test suite to PHPUnit 11 (attributes, static data providers) - Modernize rector.php for Rector 2.x with PHP 8.4 + PHPUnit sets - Update CI matrices to PHP 8.4 and 8.5 --- .github/workflows/build.yaml | 14 +- composer-dependency-analyser.php | 11 + composer.json | 19 +- phpstan.dist.neon | 9 + phpunit.xml.dist | 16 +- psalm.xml | 31 -- rector.php | 21 +- src/Block/Image/File.php | 4 +- src/Block/ImageBlock.php | 2 +- src/Block/ListBlock.php | 4 +- src/BlockRenderer/DelimiterBlockRenderer.php | 7 +- src/BlockRenderer/EmbedBlockRenderer.php | 3 +- src/BlockRenderer/GenericBlockRenderer.php | 16 +- src/BlockRenderer/HeaderBlockRenderer.php | 4 +- src/BlockRenderer/ImageBlockRenderer.php | 3 +- src/BlockRenderer/ListBlockRenderer.php | 7 +- src/BlockRenderer/ParagraphBlockRenderer.php | 2 +- src/BlockRenderer/QuoteBlockRenderer.php | 2 +- src/BlockRenderer/RawBlockRenderer.php | 2 +- src/Exception/InvalidDataException.php | 11 +- src/Exception/MappingErrorException.php | 6 +- src/Exception/ReservedKeyException.php | 3 + src/Exception/UnsupportedBlockException.php | 4 +- src/Parser/Parser.php | 28 +- src/Parser/ParserResult.php | 8 +- tests/Block/BlockTestCase.php | 6 +- tests/Block/EmbedBlockTest.php | 4 +- tests/Block/HeaderBlockTest.php | 4 +- tests/Block/ListBlockTest.php | 4 +- tests/BlockRenderer/BlockRendererTestCase.php | 15 +- .../DelimiterBlockRendererTest.php | 2 +- .../BlockRenderer/EmbedBlockRendererTest.php | 2 +- .../GenericBlockRendererTest.php | 16 +- .../BlockRenderer/HeaderBlockRendererTest.php | 2 +- .../BlockRenderer/ImageBlockRendererTest.php | 2 +- tests/BlockRenderer/ListBlockRendererTest.php | 2 +- .../ParagraphBlockRendererTest.php | 2 +- .../BlockRenderer/QuoteBlockRendererTest.php | 2 +- tests/BlockRenderer/RawBlockRendererTest.php | 2 +- tests/IntegrationTest.php | 254 ++++++++-------- tests/Parser/ParserTest.php | 276 ++++++++---------- tests/Renderer/RendererTest.php | 22 +- 42 files changed, 396 insertions(+), 458 deletions(-) create mode 100644 composer-dependency-analyser.php create mode 100644 phpstan.dist.neon delete mode 100644 psalm.xml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5d45097..92298b9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: php-version: - - "8.1" + - "8.4" dependencies: - "highest" @@ -53,10 +53,8 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" dependencies: - "lowest" @@ -89,10 +87,8 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" dependencies: - "lowest" @@ -125,10 +121,8 @@ jobs: strategy: matrix: php-version: - - "8.1" - - "8.2" - - "8.3" - "8.4" + - "8.5" dependencies: - "lowest" diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php new file mode 100644 index 0000000..b6fb20a --- /dev/null +++ b/composer-dependency-analyser.php @@ -0,0 +1,11 @@ +ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::SHADOW_DEPENDENCY]) +; diff --git a/composer.json b/composer.json index 63f28b3..7f3118c 100644 --- a/composer.json +++ b/composer.json @@ -10,20 +10,14 @@ } ], "require": { - "php": ">=8.1", - "azjezz/psl": "^2.9 || ^3.2", - "cuyz/valinor": "^1.9", + "php": ">=8.4", + "cuyz/valinor": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "setono/html-element": "^1.0", - "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" + "symfony/options-resolver": "^6.4 || ^7.0 || ^8.0" }, "require-dev": { - "infection/infection": "^0.28.1", - "php-standard-library/psalm-plugin": "^2.3", - "phpunit/phpunit": "^9.6", - "psalm/plugin-phpunit": "^0.19.5", - "setono/code-quality-pack": "^2.6", - "shipmonk/composer-dependency-analyser": "^1.8.2" + "setono/code-quality-pack": "^3.4" }, "prefer-stable": true, "autoload": { @@ -40,12 +34,13 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": false, "ergebnis/composer-normalize": true, - "infection/extension-installer": true + "infection/extension-installer": true, + "phpstan/extension-installer": true }, "sort-packages": true }, "scripts": { - "analyse": "psalm", + "analyse": "phpstan", "check-style": "ecs check", "fix-style": "ecs check --fix", "phpunit": "phpunit", diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..3923128 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,9 @@ +includes: + - vendor/cuyz/valinor/qa/PHPStan/valinor-phpstan-configuration.php + +parameters: + level: max + paths: + - src + - tests + tmpDir: .build/phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 084f0b9..35b7a8c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,17 @@ - - - src/ - - + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" + bootstrap="vendor/autoload.php" + cacheDirectory=".build/phpunit" + colors="true"> tests + + + src/ + + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 41b5993..0000000 --- a/psalm.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/rector.php b/rector.php index ed2056c..9ee4fe1 100644 --- a/rector.php +++ b/rector.php @@ -4,18 +4,17 @@ use Rector\Caching\ValueObject\Storage\FileCacheStorage; use Rector\Config\RectorConfig; -use Rector\Set\ValueObject\LevelSetList; +use Rector\PHPUnit\Set\PHPUnitSetList; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->cacheClass(FileCacheStorage::class); - $rectorConfig->cacheDirectory('./.build/rector'); - - $rectorConfig->paths([ +return RectorConfig::configure() + ->withCache('./.build/rector', FileCacheStorage::class) + ->withPaths([ __DIR__ . '/src', __DIR__ . '/tests', + ]) + ->withPhpSets(php84: true) + ->withAttributesSets(phpunit: true) + ->withSets([ + PHPUnitSetList::PHPUNIT_100, + PHPUnitSetList::PHPUNIT_110, ]); - - $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_81 - ]); -}; diff --git a/src/Block/Image/File.php b/src/Block/Image/File.php index d89d419..d798cd4 100644 --- a/src/Block/Image/File.php +++ b/src/Block/Image/File.php @@ -5,9 +5,9 @@ namespace Setono\EditorJS\Block\Image; // todo right now it doesn't support the extra data you can set on the File object (see https://github.com/editor-js/image#providing-custom-uploading-methods) -final class File +final readonly class File { - public function __construct(public readonly string $url) + public function __construct(public string $url) { } } diff --git a/src/Block/ImageBlock.php b/src/Block/ImageBlock.php index cf0e28a..c0eabf6 100644 --- a/src/Block/ImageBlock.php +++ b/src/Block/ImageBlock.php @@ -22,7 +22,7 @@ public function __construct( /** * Returns true if the caption is not empty, i.e. $this->caption !== '' * - * @psalm-assert-if-true non-empty-string $this->caption + * @phpstan-assert-if-true non-empty-string $this->caption */ public function hasCaption(): bool { diff --git a/src/Block/ListBlock.php b/src/Block/ListBlock.php index 3c6c23a..3b674c4 100644 --- a/src/Block/ListBlock.php +++ b/src/Block/ListBlock.php @@ -6,9 +6,9 @@ final class ListBlock extends Block { - public const STYLE_ORDERED = 'ordered'; + public const string STYLE_ORDERED = 'ordered'; - public const STYLE_UNORDERED = 'unordered'; + public const string STYLE_UNORDERED = 'unordered'; /** * This is a helper property containing the html tag for the list (i.e. ol/ul) diff --git a/src/BlockRenderer/DelimiterBlockRenderer.php b/src/BlockRenderer/DelimiterBlockRenderer.php index c0f51da..6b0ce85 100644 --- a/src/BlockRenderer/DelimiterBlockRenderer.php +++ b/src/BlockRenderer/DelimiterBlockRenderer.php @@ -16,17 +16,20 @@ public function render(Block $block): HtmlElement { UnsupportedBlockException::assert($this->supports($block), $block, $this); - return (new HtmlElement($this->getOption('tag')))->withClass($this->getClassOption('class')); + $tag = $this->getOption('tag'); + + return new HtmlElement(is_string($tag) ? $tag : 'hr')->withClass($this->getClassOption('class')); } /** - * @psalm-assert-if-true DelimiterBlock $block + * @phpstan-assert-if-true DelimiterBlock $block */ public function supports(Block $block): bool { return $block instanceof DelimiterBlock; } + #[\Override] protected function configureOptions(OptionsResolver $optionsResolver): void { parent::configureOptions($optionsResolver); diff --git a/src/BlockRenderer/EmbedBlockRenderer.php b/src/BlockRenderer/EmbedBlockRenderer.php index c0e67ca..59cff9b 100644 --- a/src/BlockRenderer/EmbedBlockRenderer.php +++ b/src/BlockRenderer/EmbedBlockRenderer.php @@ -31,6 +31,7 @@ public function render(Block $block): HtmlElement )->withClass($this->getClassOption('containerClass')); } + #[\Override] protected function configureOptions(OptionsResolver $optionsResolver): void { parent::configureOptions($optionsResolver); @@ -41,7 +42,7 @@ protected function configureOptions(OptionsResolver $optionsResolver): void } /** - * @psalm-assert-if-true EmbedBlock $block + * @phpstan-assert-if-true EmbedBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/GenericBlockRenderer.php b/src/BlockRenderer/GenericBlockRenderer.php index 74c4b6d..536a755 100644 --- a/src/BlockRenderer/GenericBlockRenderer.php +++ b/src/BlockRenderer/GenericBlockRenderer.php @@ -11,15 +11,21 @@ abstract class GenericBlockRenderer implements BlockRendererInterface { + /** @var array */ private array $options; + /** + * @param array $options + */ public function __construct(array $options = []) { $resolver = new OptionsResolver(); $this->configureOptions($resolver); try { - $this->options = $resolver->resolve($options); + /** @var array $resolvedOptions */ + $resolvedOptions = $resolver->resolve($options); + $this->options = $resolvedOptions; } catch (ExceptionInterface $e) { throw new OptionsResolverException($e, $this); } @@ -34,9 +40,6 @@ protected function configureOptions(OptionsResolver $optionsResolver): void ; } - /** - * @psalm-assert-if-true mixed $this->options[$option] - */ protected function hasOption(string $option): bool { return isset($this->options[$option]); @@ -63,7 +66,8 @@ protected function getClassOption(string $option): string return ''; } - /** @psalm-suppress MixedArgument */ - return sprintf('%s%s', $this->getOption('classPrefix'), $option); + $prefix = $this->getOption('classPrefix'); + + return sprintf('%s%s', is_string($prefix) ? $prefix : '', $option); } } diff --git a/src/BlockRenderer/HeaderBlockRenderer.php b/src/BlockRenderer/HeaderBlockRenderer.php index 0c8de8e..33810ba 100644 --- a/src/BlockRenderer/HeaderBlockRenderer.php +++ b/src/BlockRenderer/HeaderBlockRenderer.php @@ -18,13 +18,13 @@ public function render(Block $block): HtmlElement { UnsupportedBlockException::assert($this->supports($block), $block, $this); - return (new HtmlElement(sprintf('h%d', $block->level), $block->text)) + return new HtmlElement(sprintf('h%d', $block->level), $block->text) ->withClass($this->getClassOption('class')) ; } /** - * @psalm-assert-if-true HeaderBlock $block + * @phpstan-assert-if-true HeaderBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/ImageBlockRenderer.php b/src/BlockRenderer/ImageBlockRenderer.php index 35b5ddd..0549c52 100644 --- a/src/BlockRenderer/ImageBlockRenderer.php +++ b/src/BlockRenderer/ImageBlockRenderer.php @@ -48,6 +48,7 @@ public function render(Block $block): HtmlElement return $container; } + #[\Override] protected function configureOptions(OptionsResolver $optionsResolver): void { parent::configureOptions($optionsResolver); @@ -70,7 +71,7 @@ protected function configureOptions(OptionsResolver $optionsResolver): void } /** - * @psalm-assert-if-true ImageBlock $block + * @phpstan-assert-if-true ImageBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/ListBlockRenderer.php b/src/BlockRenderer/ListBlockRenderer.php index c2fd12b..3f5ba20 100644 --- a/src/BlockRenderer/ListBlockRenderer.php +++ b/src/BlockRenderer/ListBlockRenderer.php @@ -19,14 +19,15 @@ public function render(Block $block): HtmlElement { UnsupportedBlockException::assert($this->supports($block), $block, $this); - return (new HtmlElement($block->tag, ...array_map( + return new HtmlElement($block->tag, ...array_map( fn (string $item) => HtmlElement::li($item)->withClass($this->getClassOption('itemClass')), $block->items, - ))) + )) ->withClass($this->getClassOption('class')) ; } + #[\Override] protected function configureOptions(OptionsResolver $optionsResolver): void { parent::configureOptions($optionsResolver); @@ -37,7 +38,7 @@ protected function configureOptions(OptionsResolver $optionsResolver): void } /** - * @psalm-assert-if-true ListBlock $block + * @phpstan-assert-if-true ListBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/ParagraphBlockRenderer.php b/src/BlockRenderer/ParagraphBlockRenderer.php index 25f9d69..258f6cc 100644 --- a/src/BlockRenderer/ParagraphBlockRenderer.php +++ b/src/BlockRenderer/ParagraphBlockRenderer.php @@ -22,7 +22,7 @@ public function render(Block $block): HtmlElement } /** - * @psalm-assert-if-true ParagraphBlock $block + * @phpstan-assert-if-true ParagraphBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/QuoteBlockRenderer.php b/src/BlockRenderer/QuoteBlockRenderer.php index 90e58a0..250c78b 100644 --- a/src/BlockRenderer/QuoteBlockRenderer.php +++ b/src/BlockRenderer/QuoteBlockRenderer.php @@ -30,7 +30,7 @@ public function render(Block $block): HtmlElement } /** - * @psalm-assert-if-true QuoteBlock $block + * @phpstan-assert-if-true QuoteBlock $block */ public function supports(Block $block): bool { diff --git a/src/BlockRenderer/RawBlockRenderer.php b/src/BlockRenderer/RawBlockRenderer.php index e319812..726e192 100644 --- a/src/BlockRenderer/RawBlockRenderer.php +++ b/src/BlockRenderer/RawBlockRenderer.php @@ -22,7 +22,7 @@ public function render(Block $block): HtmlElement } /** - * @psalm-assert-if-true RawBlock $block + * @phpstan-assert-if-true RawBlock $block */ public function supports(Block $block): bool { diff --git a/src/Exception/InvalidDataException.php b/src/Exception/InvalidDataException.php index 2a02ddd..3a53b6d 100644 --- a/src/Exception/InvalidDataException.php +++ b/src/Exception/InvalidDataException.php @@ -4,15 +4,20 @@ namespace Setono\EditorJS\Exception; -use Psl\Type\Exception\AssertException; +use CuyZ\Valinor\Mapper\MappingError; final class InvalidDataException extends \RuntimeException implements ParserExceptionInterface { - public function __construct(public readonly string $json, AssertException $e) + public function __construct(public readonly string $json, MappingError $e) { + $errors = []; + foreach ($e->messages()->errors() as $message) { + $errors[] = sprintf('%s: %s', $message->path(), $message->toString()); + } + parent::__construct(sprintf( 'You have an error in the supplied data. The error was: %s. You can access the supplied JSON in the %s property', - $e->getMessage(), + implode('; ', $errors), self::class . '::$json', ), 0, $e); } diff --git a/src/Exception/MappingErrorException.php b/src/Exception/MappingErrorException.php index 4cc1b8c..05e22de 100644 --- a/src/Exception/MappingErrorException.php +++ b/src/Exception/MappingErrorException.php @@ -5,7 +5,6 @@ namespace Setono\EditorJS\Exception; use CuyZ\Valinor\Mapper\MappingError; -use CuyZ\Valinor\Mapper\Tree\Message\Messages; use Setono\EditorJS\Block\Block; final class MappingErrorException extends \InvalidArgumentException implements ParserExceptionInterface @@ -17,9 +16,8 @@ public function __construct(MappingError $e, string $type, string $mapping) { $errorMessage = $e->getMessage() . "\n\n"; - $messages = Messages::flattenFromNode($e->node())->errors(); - foreach ($messages as $message) { - $errorMessage .= (string) $message . "\n"; + foreach ($e->messages()->errors() as $message) { + $errorMessage .= sprintf("%s: %s\n", $message->path(), $message->toString()); } parent::__construct(sprintf( diff --git a/src/Exception/ReservedKeyException.php b/src/Exception/ReservedKeyException.php index 9c627c2..377a6c2 100644 --- a/src/Exception/ReservedKeyException.php +++ b/src/Exception/ReservedKeyException.php @@ -6,6 +6,9 @@ final class ReservedKeyException extends \RuntimeException implements ParserExceptionInterface { + /** + * @param array $block + */ public function __construct(string $key, array $block) { $json = null; diff --git a/src/Exception/UnsupportedBlockException.php b/src/Exception/UnsupportedBlockException.php index a1a3e6d..609da8b 100644 --- a/src/Exception/UnsupportedBlockException.php +++ b/src/Exception/UnsupportedBlockException.php @@ -9,7 +9,7 @@ final class UnsupportedBlockException extends \RuntimeException implements RendererExceptionInterface { - public function __construct(Block $block, BlockRendererInterface $blockRenderer = null) + public function __construct(Block $block, ?BlockRendererInterface $blockRenderer = null) { $message = sprintf( 'Could not render block "%s" (id: %s). No block renderer supports this block', @@ -30,7 +30,7 @@ public function __construct(Block $block, BlockRendererInterface $blockRenderer } /** - * @psalm-assert true $test + * @phpstan-assert true $test */ public static function assert(bool $test, Block $block, BlockRendererInterface $blockRenderer): void { diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 5356316..b6a41e8 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -6,7 +6,6 @@ use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\MapperBuilder; -use Psl\Type; use Setono\EditorJS\Block\Block; use Setono\EditorJS\Block\DelimiterBlock; use Setono\EditorJS\Block\EmbedBlock; @@ -46,19 +45,14 @@ public function parse(string $json): ParserResult throw new InvalidJsonException($json, $e); } - $specification = Type\shape([ - 'time' => Type\int(), - 'version' => Type\string(), - 'blocks' => Type\vec(Type\shape([ - 'id' => Type\string(), - 'type' => Type\string(), - 'data' => Type\mixed_dict(), - ])), - ]); - try { - $data = $specification->assert($data); - } catch (Type\Exception\AssertException $e) { + $data = $this->getMapperBuilder() + ->mapper() + ->map( + 'array{time: int, version: string, blocks: list}>}', + $data, + ); + } catch (MappingError $e) { throw new InvalidDataException($json, $e); } @@ -94,15 +88,15 @@ public function parse(string $json): ParserResult public function getMapperBuilder(): MapperBuilder { if (null === $this->mapperBuilder) { - $this->mapperBuilder = (new MapperBuilder())->allowSuperfluousKeys(); + $this->mapperBuilder = new MapperBuilder() + ->allowSuperfluousKeys() + ->allowPermissiveTypes() + ; } return $this->mapperBuilder; } - /** - * @psalm-assert-if-true class-string $this->mapping[$type] - */ public function hasMapping(string $type): bool { return isset($this->mapping[$type]); diff --git a/src/Parser/ParserResult.php b/src/Parser/ParserResult.php index 2a80058..120a064 100644 --- a/src/Parser/ParserResult.php +++ b/src/Parser/ParserResult.php @@ -6,13 +6,13 @@ use Setono\EditorJS\Block\Block; -final class ParserResult +final readonly class ParserResult { public function __construct( - public readonly \DateTimeImmutable $time, - public readonly string $version, + public \DateTimeImmutable $time, + public string $version, /** @var list $blocks */ - public readonly array $blocks, + public array $blocks, ) { } } diff --git a/tests/Block/BlockTestCase.php b/tests/Block/BlockTestCase.php index 04bbc0b..c17b48c 100644 --- a/tests/Block/BlockTestCase.php +++ b/tests/Block/BlockTestCase.php @@ -8,12 +8,10 @@ abstract class BlockTestCase extends TestCase { - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_sets_the_id(): void { - self::assertIsString($this->getBlock()->id); + self::assertSame('id', $this->getBlock()->id); } abstract protected function getBlock(): Block; diff --git a/tests/Block/EmbedBlockTest.php b/tests/Block/EmbedBlockTest.php index a4d8b08..1f5e024 100644 --- a/tests/Block/EmbedBlockTest.php +++ b/tests/Block/EmbedBlockTest.php @@ -11,9 +11,7 @@ protected function getBlock(): EmbedBlock return new EmbedBlock('id', 'youtube', 'https://youtube.com/sadfas', 'https://youtube.com/embed/sadfas', 200, 150); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_returns_aspect_ratio(): void { self::assertSame('200 / 150', $this->getBlock()->getAspectRatio()); diff --git a/tests/Block/HeaderBlockTest.php b/tests/Block/HeaderBlockTest.php index 71b103f..b915ac2 100644 --- a/tests/Block/HeaderBlockTest.php +++ b/tests/Block/HeaderBlockTest.php @@ -11,9 +11,7 @@ protected function getBlock(): HeaderBlock return new HeaderBlock('id', 'Header', 1); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_returns_tag(): void { self::assertSame('h1', $this->getBlock()->getTag()); diff --git a/tests/Block/ListBlockTest.php b/tests/Block/ListBlockTest.php index ec2d269..82ec64f 100644 --- a/tests/Block/ListBlockTest.php +++ b/tests/Block/ListBlockTest.php @@ -11,9 +11,7 @@ protected function getBlock(): Block return new ListBlock('id', ListBlock::STYLE_ORDERED, ['Item 1', 'Item 2']); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_returns_tag(): void { $block = new ListBlock('id', ListBlock::STYLE_UNORDERED, ['Item 1']); diff --git a/tests/BlockRenderer/BlockRendererTestCase.php b/tests/BlockRenderer/BlockRendererTestCase.php index 5784907..c158b2e 100644 --- a/tests/BlockRenderer/BlockRendererTestCase.php +++ b/tests/BlockRenderer/BlockRendererTestCase.php @@ -10,12 +10,9 @@ abstract class BlockRendererTestCase extends TestCase { - /** - * @test - * - * @dataProvider getData - */ - public function it_renders(Block $block, string $html, BlockRendererInterface $blockRenderer = null): void + #[\PHPUnit\Framework\Attributes\DataProvider('getData')] + #[\PHPUnit\Framework\Attributes\Test] + public function it_renders(Block $block, string $html, ?BlockRendererInterface $blockRenderer = null): void { // this could be done much more beautifully, but it works :D $html = str_replace("\n", ' ', $html); @@ -28,9 +25,7 @@ public function it_renders(Block $block, string $html, BlockRendererInterface $b self::assertSame($html, (string) $blockRenderer->render($block)); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_block_is_not_supported(): void { $this->expectException(UnsupportedBlockException::class); @@ -40,7 +35,7 @@ public function it_throws_exception_if_block_is_not_supported(): void /** * @return iterable */ - abstract protected function getData(): iterable; + abstract public static function getData(): iterable; abstract protected function getBlockRenderer(): BlockRendererInterface; } diff --git a/tests/BlockRenderer/DelimiterBlockRendererTest.php b/tests/BlockRenderer/DelimiterBlockRendererTest.php index c6feb0c..43d0246 100644 --- a/tests/BlockRenderer/DelimiterBlockRendererTest.php +++ b/tests/BlockRenderer/DelimiterBlockRendererTest.php @@ -8,7 +8,7 @@ final class DelimiterBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new DelimiterBlock('PqqMsdfbm'), diff --git a/tests/BlockRenderer/EmbedBlockRendererTest.php b/tests/BlockRenderer/EmbedBlockRendererTest.php index aceb333..0c73869 100644 --- a/tests/BlockRenderer/EmbedBlockRendererTest.php +++ b/tests/BlockRenderer/EmbedBlockRendererTest.php @@ -8,7 +8,7 @@ final class EmbedBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new EmbedBlock( diff --git a/tests/BlockRenderer/GenericBlockRendererTest.php b/tests/BlockRenderer/GenericBlockRendererTest.php index 977432e..05b5d73 100644 --- a/tests/BlockRenderer/GenericBlockRendererTest.php +++ b/tests/BlockRenderer/GenericBlockRendererTest.php @@ -10,14 +10,10 @@ use Setono\EditorJS\Exception\UndefinedOptionException; use Setono\HtmlElement\HtmlElement; -/** - * @covers \Setono\EditorJS\BlockRenderer\GenericBlockRenderer - */ +#[\PHPUnit\Framework\Attributes\CoversClass(\Setono\EditorJS\BlockRenderer\GenericBlockRenderer::class)] final class GenericBlockRendererTest extends TestCase { - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_option_is_not_set(): void { $blockRenderer = new class() extends GenericBlockRenderer { @@ -40,9 +36,7 @@ public function supports(Block $block): bool $blockRenderer->render(new GenericBlock()); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_you_try_to_set_an_invalid_option(): void { $this->expectException(OptionsResolverException::class); @@ -67,9 +61,7 @@ public function supports(Block $block): bool }; } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_returns_class_option(): void { $blockRenderer = new class() extends GenericBlockRenderer { diff --git a/tests/BlockRenderer/HeaderBlockRendererTest.php b/tests/BlockRenderer/HeaderBlockRendererTest.php index ab8efac..af2c5b6 100644 --- a/tests/BlockRenderer/HeaderBlockRendererTest.php +++ b/tests/BlockRenderer/HeaderBlockRendererTest.php @@ -8,7 +8,7 @@ final class HeaderBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new HeaderBlock('PqqMsdfbm', 'Header', 1), diff --git a/tests/BlockRenderer/ImageBlockRendererTest.php b/tests/BlockRenderer/ImageBlockRendererTest.php index 9a63aec..3472ede 100644 --- a/tests/BlockRenderer/ImageBlockRendererTest.php +++ b/tests/BlockRenderer/ImageBlockRendererTest.php @@ -9,7 +9,7 @@ final class ImageBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new ImageBlock( diff --git a/tests/BlockRenderer/ListBlockRendererTest.php b/tests/BlockRenderer/ListBlockRendererTest.php index 2f45365..115568d 100644 --- a/tests/BlockRenderer/ListBlockRendererTest.php +++ b/tests/BlockRenderer/ListBlockRendererTest.php @@ -8,7 +8,7 @@ final class ListBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new ListBlock( diff --git a/tests/BlockRenderer/ParagraphBlockRendererTest.php b/tests/BlockRenderer/ParagraphBlockRendererTest.php index efcc365..ca60336 100644 --- a/tests/BlockRenderer/ParagraphBlockRendererTest.php +++ b/tests/BlockRenderer/ParagraphBlockRendererTest.php @@ -8,7 +8,7 @@ final class ParagraphBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new ParagraphBlock('PqqMsdfbm', 'Paragraph'), diff --git a/tests/BlockRenderer/QuoteBlockRendererTest.php b/tests/BlockRenderer/QuoteBlockRendererTest.php index 0052233..884c6b4 100644 --- a/tests/BlockRenderer/QuoteBlockRendererTest.php +++ b/tests/BlockRenderer/QuoteBlockRendererTest.php @@ -8,7 +8,7 @@ final class QuoteBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new QuoteBlock('KmfRaA', 'We are the champions', 'Queen', 'left'), diff --git a/tests/BlockRenderer/RawBlockRendererTest.php b/tests/BlockRenderer/RawBlockRendererTest.php index 5360b23..589883c 100644 --- a/tests/BlockRenderer/RawBlockRendererTest.php +++ b/tests/BlockRenderer/RawBlockRendererTest.php @@ -8,7 +8,7 @@ final class RawBlockRendererTest extends BlockRendererTestCase { - protected function getData(): iterable + public static function getData(): iterable { yield [ new RawBlock('PqqMsdfbm', 'Beautiful image'), diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index e0000cf..ca874ed 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -17,9 +17,7 @@ final class IntegrationTest extends TestCase { - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_parses_and_renders(): void { $parser = new Parser(); @@ -43,133 +41,133 @@ public function it_parses_and_renders(): void private function getInput(): string { - return <<workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core." - } - }, - { - "id" : "gz9NmNc07B", - "type" : "paragraph", - "data" : { - "text" : "There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games." - } - }, + return <<Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on." - } - }, - { - "id" : "36yInXCuYz", - "type" : "paragraph", - "data" : { - "text" : "Clean data is useful to sanitize, validate and process on the backend." - } - }, - { - "id" : "60bwNzOlDg", - "type" : "delimiter", - "data" : {} - }, - { - "id" : "jr5I6hVhs8", - "type" : "paragraph", - "data" : { - "text" : "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏" - } - }, - { - "id" : "rz-kI4Kemj", - "type" : "image", - "data" : { - "file" : { - "url" : "https://codex.so/public/app/img/external/codex2x.png" + "time" : 1648714636619, + "blocks" : [ + { + "id" : "ddqzqrksLS", + "type" : "header", + "data" : { + "text" : "Editor.js", + "level" : 2 + } }, - "caption" : "", - "withBorder" : false, - "stretched" : false, - "withBackground" : false - } - }, - { - "id": "g-y5teN5qG", - "type": "quote", - "data": { - "text": "We are the champions", - "caption": "Queen", - "alignment": "left" - } + { + "id" : "y-xD62aVSs", + "type" : "paragraph", + "data" : { + "text" : "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text." + } + }, + { + "id" : "JsFDw3oujK", + "type" : "header", + "data" : { + "text" : "Key features", + "level" : 3 + } + }, + { + "id" : "W7cxS38p72", + "type" : "list", + "data" : { + "style" : "unordered", + "items" : [ + "It is a block-styled editor", + "It returns clean data output in JSON", + "Designed to be extendable and pluggable with a simple API" + ] + } + }, + { + "id" : "59z0qpoRto", + "type" : "header", + "data" : { + "text" : "What does it mean «block-styled editor»", + "level" : 3 + } + }, + { + "id" : "KwD6DL5mwr", + "type" : "paragraph", + "data" : { + "text" : "Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core." + } + }, + { + "id" : "gz9NmNc07B", + "type" : "paragraph", + "data" : { + "text" : "There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games." + } + }, + { + "id" : "PRFZV4qY6Q", + "type" : "header", + "data" : { + "text" : "What does it mean clean data output", + "level" : 3 + } + }, + { + "id" : "4Ps-zHrERz", + "type" : "paragraph", + "data" : { + "text" : "Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below" + } + }, + { + "id" : "tO01RYnEjt", + "type" : "paragraph", + "data" : { + "text" : "Given data can be used as you want: render with HTML for Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on." + } + }, + { + "id" : "36yInXCuYz", + "type" : "paragraph", + "data" : { + "text" : "Clean data is useful to sanitize, validate and process on the backend." + } + }, + { + "id" : "60bwNzOlDg", + "type" : "delimiter", + "data" : {} + }, + { + "id" : "jr5I6hVhs8", + "type" : "paragraph", + "data" : { + "text" : "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏" + } + }, + { + "id" : "rz-kI4Kemj", + "type" : "image", + "data" : { + "file" : { + "url" : "https://codex.so/public/app/img/external/codex2x.png" + }, + "caption" : "", + "withBorder" : false, + "stretched" : false, + "withBackground" : false + } + }, + { + "id": "g-y5teN5qG", + "type": "quote", + "data": { + "text": "We are the champions", + "caption": "Queen", + "alignment": "left" + } + } + ], + "version" : "2.23.1" } - ], - "version" : "2.23.1" -} -JSON; + JSON_WRAP; } } diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index f699a62..1fcaf73 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -12,14 +12,10 @@ use Setono\EditorJS\Exception\ReservedKeyException; use Setono\EditorJS\Exception\UnmappedTypeException; -/** - * @covers \Setono\EditorJS\Parser\Parser - */ +#[\PHPUnit\Framework\Attributes\CoversClass(\Setono\EditorJS\Parser\Parser::class)] final class ParserTest extends TestCase { - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_json_is_invalid(): void { $this->expectException(InvalidJsonException::class); @@ -28,9 +24,7 @@ public function it_throws_exception_if_json_is_invalid(): void $parser->parse('invalid json'); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_time_is_missing_from_data(): void { $this->expectException(InvalidDataException::class); @@ -39,9 +33,7 @@ public function it_throws_exception_if_time_is_missing_from_data(): void $parser->parse('{"blocks" : [],"version" : "2.23.1"}'); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_version_is_missing_from_data(): void { $this->expectException(InvalidDataException::class); @@ -50,9 +42,7 @@ public function it_throws_exception_if_version_is_missing_from_data(): void $parser->parse('{"time" : 1648714636619,"blocks" : []}'); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_blocks_are_missing_from_data(): void { $this->expectException(InvalidDataException::class); @@ -61,9 +51,7 @@ public function it_throws_exception_if_blocks_are_missing_from_data(): void $parser->parse('{"time" : 1648714636619,"version" : "2.23.1"}'); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_a_block_does_not_have_a_mapping(): void { $this->expectException(UnmappedTypeException::class); @@ -89,9 +77,7 @@ public function it_throws_exception_if_a_block_does_not_have_a_mapping(): void $parser->parse($json); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_data_has_reserved_key(): void { $this->expectException(ReservedKeyException::class); @@ -116,9 +102,7 @@ public function it_throws_exception_if_data_has_reserved_key(): void $parser->parse($json); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_data_cannot_be_mapped(): void { $this->expectException(MappingErrorException::class); @@ -143,9 +127,7 @@ public function it_throws_exception_if_data_cannot_be_mapped(): void $parser->parse($json); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_exposes_the_mapper_builder(): void { $parser = new Parser(); @@ -154,9 +136,7 @@ public function it_exposes_the_mapper_builder(): void self::assertSame($mapperBuilder, $parser->getMapperBuilder()); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_has_mapping_capabilities(): void { $parser = new Parser(); @@ -165,130 +145,128 @@ public function it_has_mapping_capabilities(): void self::assertSame(TestBlock::class, $parser->getMapping('test')); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_parses(): void { - $json = <<workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core." - } - }, - { - "id" : "gz9NmNc07B", - "type" : "paragraph", - "data" : { - "text" : "There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games." - } - }, - { - "id" : "PRFZV4qY6Q", - "type" : "header", - "data" : { - "text" : "What does it mean clean data output", - "level" : 3 - } - }, - { - "id" : "4Ps-zHrERz", - "type" : "paragraph", - "data" : { - "text" : "Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below" - } - }, + $json = <<Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on." - } - }, - { - "id" : "36yInXCuYz", - "type" : "paragraph", - "data" : { - "text" : "Clean data is useful to sanitize, validate and process on the backend." - } - }, - { - "id" : "60bwNzOlDg", - "type" : "delimiter", - "data" : {} - }, - { - "id" : "jr5I6hVhs8", - "type" : "paragraph", - "data" : { - "text" : "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏" - } - }, - { - "id" : "rz-kI4Kemj", - "type" : "image", - "data" : { - "file" : { - "url" : "https://codex.so/public/app/img/external/codex2x.png" + "time" : 1648714636619, + "blocks" : [ + { + "id" : "ddqzqrksLS", + "type" : "header", + "data" : { + "text" : "Editor.js", + "level" : 2 + } }, - "caption" : "", - "withBorder" : false, - "stretched" : false, - "withBackground" : false - } + { + "id" : "y-xD62aVSs", + "type" : "paragraph", + "data" : { + "text" : "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text." + } + }, + { + "id" : "JsFDw3oujK", + "type" : "header", + "data" : { + "text" : "Key features", + "level" : 3 + } + }, + { + "id" : "W7cxS38p72", + "type" : "list", + "data" : { + "style" : "unordered", + "items" : [ + "It is a block-styled editor", + "It returns clean data output in JSON", + "Designed to be extendable and pluggable with a simple API" + ] + } + }, + { + "id" : "59z0qpoRto", + "type" : "header", + "data" : { + "text" : "What does it mean «block-styled editor»", + "level" : 3 + } + }, + { + "id" : "KwD6DL5mwr", + "type" : "paragraph", + "data" : { + "text" : "Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core." + } + }, + { + "id" : "gz9NmNc07B", + "type" : "paragraph", + "data" : { + "text" : "There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games." + } + }, + { + "id" : "PRFZV4qY6Q", + "type" : "header", + "data" : { + "text" : "What does it mean clean data output", + "level" : 3 + } + }, + { + "id" : "4Ps-zHrERz", + "type" : "paragraph", + "data" : { + "text" : "Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below" + } + }, + { + "id" : "tO01RYnEjt", + "type" : "paragraph", + "data" : { + "text" : "Given data can be used as you want: render with HTML for Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on." + } + }, + { + "id" : "36yInXCuYz", + "type" : "paragraph", + "data" : { + "text" : "Clean data is useful to sanitize, validate and process on the backend." + } + }, + { + "id" : "60bwNzOlDg", + "type" : "delimiter", + "data" : {} + }, + { + "id" : "jr5I6hVhs8", + "type" : "paragraph", + "data" : { + "text" : "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏" + } + }, + { + "id" : "rz-kI4Kemj", + "type" : "image", + "data" : { + "file" : { + "url" : "https://codex.so/public/app/img/external/codex2x.png" + }, + "caption" : "", + "withBorder" : false, + "stretched" : false, + "withBackground" : false + } + } + ], + "version" : "2.23.1" } - ], - "version" : "2.23.1" -} -JSON; + JSON_WRAP; $parser = new Parser(); $parserResult = $parser->parse($json); diff --git a/tests/Renderer/RendererTest.php b/tests/Renderer/RendererTest.php index 6804690..a2286b3 100644 --- a/tests/Renderer/RendererTest.php +++ b/tests/Renderer/RendererTest.php @@ -13,14 +13,10 @@ use Setono\EditorJS\Exception\UnsupportedBlockException; use Setono\EditorJS\Parser\ParserResult; -/** - * @covers \Setono\EditorJS\Renderer\Renderer - */ +#[\PHPUnit\Framework\Attributes\CoversClass(\Setono\EditorJS\Renderer\Renderer::class)] final class RendererTest extends TestCase { - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_renders(): void { $parserResult = new ParserResult(new \DateTimeImmutable(), '2.3.4', [ @@ -34,9 +30,7 @@ public function it_renders(): void self::assertSame('

Header

Lorem ipsum

', $renderer->render($parserResult)); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_throws_exception_if_block_is_not_supported(): void { $this->expectException(UnsupportedBlockException::class); @@ -45,12 +39,10 @@ public function it_throws_exception_if_block_is_not_supported(): void new HeaderBlock('id', 'Header', 1), ]); - self::assertSame('

Header

Lorem ipsum

', (new Renderer())->render($parserResult)); + self::assertSame('

Header

Lorem ipsum

', new Renderer()->render($parserResult)); } - /** - * @test - */ + #[\PHPUnit\Framework\Attributes\Test] public function it_does_not_throw_on_unsupported_block_if_throwing_is_disabled(): void { $parserResult = new ParserResult(new \DateTimeImmutable(), '2.3.4', [ @@ -58,11 +50,13 @@ public function it_does_not_throw_on_unsupported_block_if_throwing_is_disabled() ]); $logger = new class() extends AbstractLogger { + /** @var list */ public array $messages = []; /** * @param mixed $level - * @param string $message + * @param string|\Stringable $message + * @param array $context */ public function log($level, $message, array $context = []): void { From 55782c0990ef812982cd66a89efe0ea102a7479d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Wed, 10 Jun 2026 11:05:21 +0200 Subject: [PATCH 2/2] Exclude tests dir from dependency analysis instead of ignoring phpunit shadow dep --- composer-dependency-analyser.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index b6fb20a..8fccf73 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -3,9 +3,7 @@ declare(strict_types=1); use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; -use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; return (new Configuration()) - // PHPUnit is provided by the setono/code-quality-pack - ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::SHADOW_DEPENDENCY]) + ->addPathToExclude(__DIR__ . '/tests') ;