From 7dc8a2cb9bdc4547ff043b4e27938752e95d7976 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Mon, 13 Oct 2025 11:41:09 +0200 Subject: [PATCH 1/3] feat: add ComposerService for handling composer.json data --- .php-cs-fixer.php | 63 ++--- README.md | 16 ++ composer.json | 7 +- composer.lock | 153 ++++++------ rector.php | 18 +- src/Enum/Separate.php | 24 +- src/Generators/DocBlockHeader.php | 99 ++++++-- src/Generators/Generator.php | 24 +- src/Rules/DocBlockHeaderFixer.php | 73 +++--- src/Service/ComposerService.php | 107 ++++++++ tests/src/Enum/SeparateTest.php | 24 +- tests/src/Generators/DocBlockHeaderTest.php | 207 ++++++++++++++-- tests/src/Rules/DocBlockHeaderFixerTest.php | 84 +++++-- tests/src/Service/ComposerServiceTest.php | 255 ++++++++++++++++++++ 14 files changed, 875 insertions(+), 279 deletions(-) create mode 100644 src/Service/ComposerService.php create mode 100644 tests/src/Service/ComposerServiceTest.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index bf26021..9f24f21 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -3,53 +3,32 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -use EliasHaeussler\PhpCsFixerConfig\Config; -use EliasHaeussler\PhpCsFixerConfig\Package; -use EliasHaeussler\PhpCsFixerConfig\Rules; -use Symfony\Component\Finder; - -$header = Rules\Header::create( - 'php-doc-block-header-fixer', - Package\Type::ComposerPackage, - Package\Author::create('Konrad Michalik', 'hej@konradmichalik.dev'), - Package\CopyrightRange::from(2025), - Package\License::GPL3OrLater, -); +use KonradMichalik\PhpCsFixerPreset\Config; +use KonradMichalik\PhpCsFixerPreset\Rules\Header; +use KonradMichalik\PhpCsFixerPreset\Rules\Set\Set; +use KonradMichalik\PhpDocBlockHeaderFixer\Generators\DocBlockHeader; +use KonradMichalik\PhpDocBlockHeaderFixer\Rules\DocBlockHeaderFixer; +use Symfony\Component\Finder\Finder; return Config::create() - ->withRule($header) -// ->withRule( -// RuleSet::fromArray( -// DocBlockHeader::create( -// [ -// 'author' => 'Konrad Michalik ', -// 'license' => 'GPL-3.0-or-later', -// 'package' => 'PhpDocBlockHeaderFixer', -// ] -// )->__toArray() -// ) -// ) -// ->registerCustomFixers([new KonradMichalik\PhpDocBlockHeaderFixer\Rules\DocBlockHeaderFixer()]) // Temporarily disabled - ->withFinder(static fn (Finder\Finder $finder) => $finder - ->in(__DIR__) - ->exclude('vendor'), + ->registerCustomFixers([ + new DocBlockHeaderFixer(), + ]) + ->withRule( + Header::fromComposer(), + ) + ->withRule( + Set::fromArray( + DocBlockHeader::fromComposer()->__toArray(), + ), ) + ->withFinder(static fn (Finder $finder) => $finder->in(__DIR__)) ; diff --git a/README.md b/README.md index 25bd2cc..9911605 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,22 @@ return (new PhpCsFixer\Config()) ; ``` +Or even simpler, automatically read all authors and license from your `composer.json`: + +```php +registerCustomFixers([ + new KonradMichalik\PhpDocBlockHeaderFixer\Rules\DocBlockHeaderFixer() + ]) + ->setRules([ + KonradMichalik\PhpDocBlockHeaderFixer\Generators\DocBlockHeader::fromComposer()->__toArray() + ]) +; +``` + ## ⚙️ Configuration - `annotations` (array): DocBlock annotations to add to classes diff --git a/composer.json b/composer.json index 4733dd3..a78034e 100644 --- a/composer.json +++ b/composer.json @@ -27,9 +27,9 @@ }, "require-dev": { "armin/editorconfig-cli": "^1.0 || ^2.0", - "eliashaeussler/php-cs-fixer-config": "2.3.0", "eliashaeussler/rector-config": "^3.0", "ergebnis/composer-normalize": "^2.44", + "konradmichalik/php-cs-fixer-preset": "^0.1.0", "phpstan/phpstan": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-symfony": "^2.0", @@ -40,6 +40,11 @@ "KonradMichalik\\PhpDocBlockHeaderFixer\\": "src" } }, + "autoload-dev": { + "psr-4": { + "KonradMichalik\\PhpDocBlockHeaderFixer\\Tests\\": "tests/src" + } + }, "config": { "allow-plugins": { "ergebnis/composer-normalize": true diff --git a/composer.lock b/composer.lock index 57b4367..a08736a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "76cd98ad45908392d483ffe695b2856d", + "content-hash": "a98357b949b4869d0fe4375845eedbae", "packages": [ { "name": "clue/ndjson-react", @@ -2723,59 +2723,6 @@ }, "time": "2025-06-02T17:23:31+00:00" }, - { - "name": "eliashaeussler/php-cs-fixer-config", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/eliashaeussler/php-cs-fixer-config.git", - "reference": "130111f9aef350910bd2224cd244e6e1b8b5ba82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/eliashaeussler/php-cs-fixer-config/zipball/130111f9aef350910bd2224cd244e6e1b8b5ba82", - "reference": "130111f9aef350910bd2224cd244e6e1b8b5ba82", - "shasum": "" - }, - "require": { - "friendsofphp/php-cs-fixer": "^3.14", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0" - }, - "require-dev": { - "armin/editorconfig-cli": "^1.5 || ^2.0", - "eliashaeussler/phpstan-config": "^2.0.0", - "eliashaeussler/phpunit-attributes": "^1.1", - "eliashaeussler/rector-config": "^3.0", - "ergebnis/composer-normalize": "^2.29", - "phpstan/extension-installer": "^1.2", - "phpunit/phpunit": "^10.4 || ^11.0 || ^12.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "EliasHaeussler\\PhpCsFixerConfig\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "Elias Häußler", - "email": "elias@haeussler.dev", - "homepage": "https://haeussler.dev", - "role": "Maintainer" - } - ], - "description": "My personal configuration for PHP-CS-Fixer", - "support": { - "issues": "https://github.com/eliashaeussler/php-cs-fixer-config/issues", - "source": "https://github.com/eliashaeussler/php-cs-fixer-config/tree/2.3.0" - }, - "time": "2025-05-18T14:45:22+00:00" - }, { "name": "eliashaeussler/rector-config", "version": "3.1.1", @@ -3352,16 +3299,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.5.2", + "version": "6.6.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8" + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ac0d369c09653cf7af561f6d91a705bc617a87b8", - "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3", + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3", "shasum": "" }, "require": { @@ -3421,9 +3368,67 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0" + }, + "time": "2025-10-10T11:34:09+00:00" + }, + { + "name": "konradmichalik/php-cs-fixer-preset", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/jackd248/php-cs-fixer-preset.git", + "reference": "480a70abaeef501a839bbce3c99696024c205c2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jackd248/php-cs-fixer-preset/zipball/480a70abaeef501a839bbce3c99696024c205c2c", + "reference": "480a70abaeef501a839bbce3c99696024c205c2c", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^3.14", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "armin/editorconfig-cli": "^1.0 || ^2.0", + "eliashaeussler/rector-config": "^3.0", + "ergebnis/composer-normalize": "^2.44", + "konradmichalik/php-doc-block-header-fixer": "^0.2.2", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "^10.2 || ^11.0 || ^12.0" + }, + "type": "library", + "extra": { + "konradmichalik/php-cs-fixer-preset": { + "copyright": 2025 + } + }, + "autoload": { + "psr-4": { + "KonradMichalik\\PhpCsFixerPreset\\": "src" + } }, - "time": "2025-09-09T09:42:27+00:00" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Konrad Michalik", + "email": "hej@konradmichalik.dev", + "role": "Maintainer" + } + ], + "description": "Preset configuration for PHP-CS-Fixer", + "support": { + "issues": "https://github.com/jackd248/php-cs-fixer-preset/issues", + "source": "https://github.com/jackd248/php-cs-fixer-preset/tree/0.1.0" + }, + "time": "2025-10-08T05:44:53+00:00" }, { "name": "localheinz/diff", @@ -4302,16 +4307,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.3.15", + "version": "12.4.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b035ee2cd8ecad4091885b61017ebb1d80eb0e57" + "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b035ee2cd8ecad4091885b61017ebb1d80eb0e57", - "reference": "b035ee2cd8ecad4091885b61017ebb1d80eb0e57", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", + "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", "shasum": "" }, "require": { @@ -4347,7 +4352,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.3-dev" + "dev-main": "12.4-dev" } }, "autoload": { @@ -4379,7 +4384,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.1" }, "funding": [ { @@ -4403,25 +4408,25 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:10:54+00:00" + "time": "2025-10-09T14:08:29+00:00" }, { "name": "rector/rector", - "version": "2.1.7", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce" + "reference": "d27f976a332a87b5d03553c2e6f04adbe5da034f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/c34cc07c4698f007a20dc5c99ff820089ae413ce", - "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/d27f976a332a87b5d03553c2e6f04adbe5da034f", + "reference": "d27f976a332a87b5d03553c2e6f04adbe5da034f", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.18" + "phpstan/phpstan": "^2.1.26" }, "conflict": { "rector/rector-doctrine": "*", @@ -4455,7 +4460,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.1.7" + "source": "https://github.com/rectorphp/rector/tree/2.2.3" }, "funding": [ { @@ -4463,7 +4468,7 @@ "type": "github" } ], - "time": "2025-09-10T11:13:58+00:00" + "time": "2025-10-11T21:50:23+00:00" }, { "name": "sebastian/cli-parser", diff --git a/rector.php b/rector.php index 3f8efb1..2f07642 100644 --- a/rector.php +++ b/rector.php @@ -3,22 +3,12 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ use EliasHaeussler\RectorConfig\Config\Config; diff --git a/src/Enum/Separate.php b/src/Enum/Separate.php index 70c20f6..f8d1e84 100644 --- a/src/Enum/Separate.php +++ b/src/Enum/Separate.php @@ -3,26 +3,22 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Enum; +/** + * Separate. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ enum Separate: string { case Top = 'top'; diff --git a/src/Generators/DocBlockHeader.php b/src/Generators/DocBlockHeader.php index caee8c4..0b4fb4c 100644 --- a/src/Generators/DocBlockHeader.php +++ b/src/Generators/DocBlockHeader.php @@ -3,31 +3,32 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Generators; use InvalidArgumentException; use KonradMichalik\PhpDocBlockHeaderFixer\Enum\Separate; +use KonradMichalik\PhpDocBlockHeaderFixer\Service\ComposerService; +use function count; +use function gettype; +use function in_array; use function is_string; +use function sprintf; +/** + * DocBlockHeader. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ final class DocBlockHeader implements Generator { private function __construct( @@ -38,6 +39,21 @@ private function __construct( public readonly bool $addStructureName, ) {} + /** + * @return array> + */ + public function __toArray(): array + { + return [ + 'KonradMichalik/docblock_header_comment' => [ + 'annotations' => $this->annotations, + 'preserve_existing' => $this->preserveExisting, + 'separate' => $this->separate->value, + 'add_structure_name' => $this->addStructureName, + ], + ]; + } + /** * @param array> $annotations */ @@ -45,7 +61,7 @@ public static function create( array $annotations, bool $preserveExisting = true, Separate $separate = Separate::Both, - bool $addStructureName = false, + bool $addStructureName = true, ): self { self::validateAnnotations($annotations); @@ -53,18 +69,49 @@ public static function create( } /** - * @return array> + * @param array> $additionalAnnotations */ - public function __toArray(): array - { - return [ - 'KonradMichalik/docblock_header_comment' => [ - 'annotations' => $this->annotations, - 'preserve_existing' => $this->preserveExisting, - 'separate' => $this->separate->value, - 'add_structure_name' => $this->addStructureName, - ], - ]; + public static function fromComposer( + string $composerJsonPath = 'composer.json', + array $additionalAnnotations = [], + bool $preserveExisting = true, + Separate $separate = Separate::Both, + bool $addStructureName = true, + ): self { + $composerData = ComposerService::readComposerJson($composerJsonPath); + + $annotations = []; + + $authors = ComposerService::extractAuthors($composerData); + if (!empty($authors)) { + if (count($authors) > 1) { + $authorStrings = []; + foreach ($authors as $author) { + $authorString = $author['name']; + if (isset($author['email'])) { + $authorString .= sprintf(' <%s>', $author['email']); + } + $authorStrings[] = $authorString; + } + $annotations['author'] = $authorStrings; + } else { + $primaryAuthor = $authors[0]; + $authorString = $primaryAuthor['name']; + if (isset($primaryAuthor['email'])) { + $authorString .= sprintf(' <%s>', $primaryAuthor['email']); + } + $annotations['author'] = $authorString; + } + } + + $license = ComposerService::extractLicense($composerData); + if (null !== $license) { + $annotations['license'] = $license; + } + + $annotations = [...$annotations, ...$additionalAnnotations]; + + return self::create($annotations, $preserveExisting, $separate, $addStructureName); } /** diff --git a/src/Generators/Generator.php b/src/Generators/Generator.php index d80f890..5d07619 100644 --- a/src/Generators/Generator.php +++ b/src/Generators/Generator.php @@ -3,26 +3,22 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Generators; +/** + * Generator. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ interface Generator { /** diff --git a/src/Rules/DocBlockHeaderFixer.php b/src/Rules/DocBlockHeaderFixer.php index 5f3fcdd..5fb6545 100644 --- a/src/Rules/DocBlockHeaderFixer.php +++ b/src/Rules/DocBlockHeaderFixer.php @@ -3,22 +3,12 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Rules; @@ -26,16 +16,17 @@ use KonradMichalik\PhpDocBlockHeaderFixer\Enum\Separate; use PhpCsFixer\AbstractFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; -use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; -use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; -use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; -use PhpCsFixer\FixerDefinition\FixerDefinition; -use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; -use PhpCsFixer\Tokenizer\Token; -use PhpCsFixer\Tokenizer\Tokens; +use PhpCsFixer\FixerConfiguration\{FixerConfigurationResolver, FixerConfigurationResolverInterface, FixerOptionBuilder}; +use PhpCsFixer\FixerDefinition\{FixerDefinition, FixerDefinitionInterface}; +use PhpCsFixer\Tokenizer\{Token, Tokens}; use SplFileInfo; +use function in_array; +use function is_array; + /** + * DocBlockHeaderFixer. + * * @author Konrad Michalik * @license GPL-3.0-or-later * @@ -70,10 +61,10 @@ public function getPriority(): int public function isCandidate(Tokens $tokens): bool { - return $tokens->isTokenKindFound(T_CLASS) - || $tokens->isTokenKindFound(T_INTERFACE) - || $tokens->isTokenKindFound(T_TRAIT) - || $tokens->isTokenKindFound(T_ENUM); + return $tokens->isTokenKindFound(\T_CLASS) + || $tokens->isTokenKindFound(\T_INTERFACE) + || $tokens->isTokenKindFound(\T_TRAIT) + || $tokens->isTokenKindFound(\T_ENUM); } public function getConfigurationDefinition(): FixerConfigurationResolverInterface @@ -117,12 +108,12 @@ protected function applyFix(SplFileInfo $file, Tokens $tokens): void for ($index = 0, $limit = $tokens->count(); $index < $limit; ++$index) { $token = $tokens[$index]; - if (!$token->isGivenKind([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM])) { + if (!$token->isGivenKind([\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_ENUM])) { continue; } // Skip anonymous classes (preceded by 'new' keyword) - if ($token->isGivenKind(T_CLASS) && $this->isAnonymousClass($tokens, $index)) { + if ($token->isGivenKind(\T_CLASS) && $this->isAnonymousClass($tokens, $index)) { continue; } @@ -150,7 +141,7 @@ private function isAnonymousClass(Tokens $tokens, int $classIndex): bool } // T_ATTRIBUTE '#[' marks the start of an attribute (we exit it when going backwards) - if ($token->isGivenKind(T_ATTRIBUTE)) { + if ($token->isGivenKind(\T_ATTRIBUTE)) { $insideAttribute = false; continue; } @@ -161,12 +152,12 @@ private function isAnonymousClass(Tokens $tokens, int $classIndex): bool } // Skip modifiers that can appear between 'new' and 'class' - if ($token->isGivenKind([T_FINAL, T_READONLY])) { + if ($token->isGivenKind([\T_FINAL, \T_READONLY])) { continue; } // If we find 'new', it's an anonymous class - if ($token->isGivenKind(T_NEW)) { + if ($token->isGivenKind(\T_NEW)) { return true; } @@ -207,7 +198,7 @@ private function getStructureName(Tokens $tokens, int $structureIndex): string } // The first non-whitespace token after the keyword should be the structure name - if ($token->isGivenKind(T_STRING)) { + if ($token->isGivenKind(\T_STRING)) { return $token->getContent(); } @@ -227,12 +218,12 @@ private function findExistingDocBlock(Tokens $tokens, int $structureIndex): ?int continue; } - if ($token->isGivenKind(T_DOC_COMMENT)) { + if ($token->isGivenKind(\T_DOC_COMMENT)) { return $i; } // If we hit any other meaningful token (except modifiers), stop looking - if (!$token->isGivenKind([T_FINAL, T_ABSTRACT, T_ATTRIBUTE])) { + if (!$token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE])) { break; } } @@ -250,7 +241,7 @@ private function mergeWithExistingDocBlock(Tokens $tokens, int $docBlockIndex, a $mergedAnnotations = $this->mergeAnnotations($existingAnnotations, $annotations); $newDocBlock = $this->buildDocBlock($mergedAnnotations, $structureName); - $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $newDocBlock]); + $tokens[$docBlockIndex] = new Token([\T_DOC_COMMENT, $newDocBlock]); // Ensure there's proper spacing after existing DocBlock $ensureSpacing = $this->resolvedConfiguration['ensure_spacing'] ?? true; @@ -265,7 +256,7 @@ private function mergeWithExistingDocBlock(Tokens $tokens, int $docBlockIndex, a private function replaceDocBlock(Tokens $tokens, int $docBlockIndex, array $annotations, string $structureName): void { $newDocBlock = $this->buildDocBlock($annotations, $structureName); - $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $newDocBlock]); + $tokens[$docBlockIndex] = new Token([\T_DOC_COMMENT, $newDocBlock]); // Ensure there's proper spacing after replaced DocBlock $ensureSpacing = $this->resolvedConfiguration['ensure_spacing'] ?? true; @@ -286,18 +277,18 @@ private function insertNewDocBlock(Tokens $tokens, int $structureIndex, array $a // Add separation before comment if needed if (in_array($separate, ['top', 'both'], true)) { - $tokensToInsert[] = new Token([T_WHITESPACE, "\n"]); + $tokensToInsert[] = new Token([\T_WHITESPACE, "\n"]); } // Add the DocBlock $docBlock = $this->buildDocBlock($annotations, $structureName); - $tokensToInsert[] = new Token([T_DOC_COMMENT, $docBlock]); + $tokensToInsert[] = new Token([\T_DOC_COMMENT, $docBlock]); // Add a newline after the DocBlock if ensure_spacing is enabled (default) // This prevents conflicts with single_line_after_imports and no_extra_blank_lines rules $ensureSpacing = $this->resolvedConfiguration['ensure_spacing'] ?? true; if ($ensureSpacing) { - $tokensToInsert[] = new Token([T_WHITESPACE, "\n"]); + $tokensToInsert[] = new Token([\T_WHITESPACE, "\n"]); } // Add additional separation if configured @@ -305,7 +296,7 @@ private function insertNewDocBlock(Tokens $tokens, int $structureIndex, array $a // Check if there's already whitespace after the structure declaration $nextToken = $tokens[$structureIndex] ?? null; if (null !== $nextToken && !$nextToken->isWhitespace()) { - $tokensToInsert[] = new Token([T_WHITESPACE, "\n"]); + $tokensToInsert[] = new Token([\T_WHITESPACE, "\n"]); } } @@ -324,7 +315,7 @@ private function findInsertPosition(Tokens $tokens, int $structureIndex): int continue; } - if ($token->isGivenKind([T_FINAL, T_ABSTRACT, T_ATTRIBUTE])) { + if ($token->isGivenKind([\T_FINAL, \T_ABSTRACT, \T_ATTRIBUTE])) { $insertIndex = $i; continue; } @@ -422,7 +413,7 @@ private function ensureProperSpacingAfterDocBlock(Tokens $tokens, int $docBlockI // If the next token is not whitespace or doesn't contain a newline, add one if (!$nextToken->isWhitespace() || !str_contains($nextToken->getContent(), "\n")) { // Insert a newline token after the DocBlock - $tokens->insertAt($nextIndex, [new Token([T_WHITESPACE, "\n"])]); + $tokens->insertAt($nextIndex, [new Token([\T_WHITESPACE, "\n"])]); } } } diff --git a/src/Service/ComposerService.php b/src/Service/ComposerService.php new file mode 100644 index 0000000..34ba72c --- /dev/null +++ b/src/Service/ComposerService.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace KonradMichalik\PhpDocBlockHeaderFixer\Service; + +use RuntimeException; + +use function file_exists; +use function file_get_contents; +use function is_array; +use function json_decode; +use function sprintf; + +/** + * ComposerService. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ +final class ComposerService +{ + /** + * @return array + * + * @throws RuntimeException + */ + public static function readComposerJson(string $composerJsonPath = 'composer.json'): array + { + if (!file_exists($composerJsonPath)) { + throw new RuntimeException(sprintf('The file "%s" does not exist.', $composerJsonPath)); + } + + $contents = file_get_contents($composerJsonPath); + + if (false === $contents) { + throw new RuntimeException(sprintf('Unable to read file "%s".', $composerJsonPath)); + } + + $data = json_decode($contents, true); + + if (!is_array($data)) { + throw new RuntimeException(sprintf('Unable to decode JSON from file "%s".', $composerJsonPath)); + } + + return $data; + } + + /** + * @param array $composerData + */ + public static function extractLicense(array $composerData): ?string + { + if (!isset($composerData['license'])) { + return null; + } + + return is_array($composerData['license']) + ? $composerData['license'][0] ?? null + : $composerData['license']; + } + + /** + * @param array $composerData + * + * @return list + */ + public static function extractAuthors(array $composerData): array + { + if (!isset($composerData['authors']) || !is_array($composerData['authors'])) { + return []; + } + + $authors = []; + + foreach ($composerData['authors'] as $author) { + if (!is_array($author) || !isset($author['name'])) { + continue; + } + + $authors[] = $author; + } + + return $authors; + } + + /** + * @param array $composerData + * + * @return array{name: string, email?: string, role?: string}|null + */ + public static function getPrimaryAuthor(array $composerData): ?array + { + $authors = self::extractAuthors($composerData); + + return $authors[0] ?? null; + } +} diff --git a/tests/src/Enum/SeparateTest.php b/tests/src/Enum/SeparateTest.php index 5d18453..0249c45 100644 --- a/tests/src/Enum/SeparateTest.php +++ b/tests/src/Enum/SeparateTest.php @@ -3,22 +3,12 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Tests\Enum; @@ -31,6 +21,12 @@ * @internal */ #[\PHPUnit\Framework\Attributes\CoversClass(Separate::class)] +/** + * SeparateTest. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ final class SeparateTest extends TestCase { public function testEnumCases(): void diff --git a/tests/src/Generators/DocBlockHeaderTest.php b/tests/src/Generators/DocBlockHeaderTest.php index 333964e..393828d 100644 --- a/tests/src/Generators/DocBlockHeaderTest.php +++ b/tests/src/Generators/DocBlockHeaderTest.php @@ -3,30 +3,19 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Tests\Generators; use InvalidArgumentException; use KonradMichalik\PhpDocBlockHeaderFixer\Enum\Separate; -use KonradMichalik\PhpDocBlockHeaderFixer\Generators\DocBlockHeader; -use KonradMichalik\PhpDocBlockHeaderFixer\Generators\Generator; +use KonradMichalik\PhpDocBlockHeaderFixer\Generators\{DocBlockHeader, Generator}; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -34,6 +23,12 @@ * @internal */ #[\PHPUnit\Framework\Attributes\CoversClass(DocBlockHeader::class)] +/** + * DocBlockHeaderTest. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ final class DocBlockHeaderTest extends TestCase { public function testImplementsGeneratorInterface(): void @@ -91,7 +86,7 @@ public function testToArrayReturnsCorrectStructure(): void 'annotations' => $annotations, 'preserve_existing' => false, 'separate' => 'top', - 'add_structure_name' => false, + 'add_structure_name' => true, ], ]; @@ -110,7 +105,7 @@ public function testToArrayWithDefaultParameters(): void 'annotations' => $annotations, 'preserve_existing' => true, 'separate' => 'both', - 'add_structure_name' => false, + 'add_structure_name' => true, ], ]; @@ -309,4 +304,180 @@ public function testToArrayWithAddStructureName(): void self::assertSame($expected, $result); } + + public function testFromComposerWithSingleAuthor(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ], + 'license' => 'MIT', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer($testComposerPath); + + self::assertSame('John Doe ', $docBlockHeader->annotations['author']); + self::assertSame('MIT', $docBlockHeader->annotations['license']); + self::assertTrue($docBlockHeader->preserveExisting); + self::assertSame(Separate::Both, $docBlockHeader->separate); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithMultipleAuthors(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ['name' => 'Jane Smith', 'email' => 'jane@example.com'], + ], + 'license' => 'GPL-3.0-or-later', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer($testComposerPath); + + self::assertIsArray($docBlockHeader->annotations['author']); + self::assertCount(2, $docBlockHeader->annotations['author']); + self::assertSame('John Doe ', $docBlockHeader->annotations['author'][0]); + self::assertSame('Jane Smith ', $docBlockHeader->annotations['author'][1]); + self::assertSame('GPL-3.0-or-later', $docBlockHeader->annotations['license']); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithAuthorWithoutEmail(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe'], + ], + 'license' => 'MIT', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer($testComposerPath); + + self::assertSame('John Doe', $docBlockHeader->annotations['author']); + self::assertSame('MIT', $docBlockHeader->annotations['license']); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithNoAuthors(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'license' => 'MIT', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer($testComposerPath); + + self::assertArrayNotHasKey('author', $docBlockHeader->annotations); + self::assertSame('MIT', $docBlockHeader->annotations['license']); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithNoLicense(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ], + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer($testComposerPath); + + self::assertSame('John Doe ', $docBlockHeader->annotations['author']); + self::assertArrayNotHasKey('license', $docBlockHeader->annotations); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithAdditionalAnnotations(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ], + 'license' => 'MIT', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer( + $testComposerPath, + ['copyright' => '2025', 'version' => '1.0.0'], + ); + + self::assertSame('John Doe ', $docBlockHeader->annotations['author']); + self::assertSame('MIT', $docBlockHeader->annotations['license']); + self::assertSame('2025', $docBlockHeader->annotations['copyright']); + self::assertSame('1.0.0', $docBlockHeader->annotations['version']); + } finally { + unlink($testComposerPath); + } + } + + public function testFromComposerWithCustomParameters(): void + { + $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ], + 'license' => 'MIT', + ]; + + file_put_contents($testComposerPath, json_encode($composerData)); + + try { + $docBlockHeader = DocBlockHeader::fromComposer( + $testComposerPath, + [], + false, + Separate::None, + false, + ); + + self::assertSame('John Doe ', $docBlockHeader->annotations['author']); + self::assertFalse($docBlockHeader->preserveExisting); + self::assertSame(Separate::None, $docBlockHeader->separate); + self::assertFalse($docBlockHeader->addStructureName); + } finally { + unlink($testComposerPath); + } + } } diff --git a/tests/src/Rules/DocBlockHeaderFixerTest.php b/tests/src/Rules/DocBlockHeaderFixerTest.php index c73fbf5..8c94e3f 100644 --- a/tests/src/Rules/DocBlockHeaderFixerTest.php +++ b/tests/src/Rules/DocBlockHeaderFixerTest.php @@ -3,22 +3,12 @@ declare(strict_types=1); /* - * This file is part of the Composer package "php-doc-block-header-fixer". + * This file is part of the "php-doc-block-header-fixer" Composer package. * - * Copyright (C) 2025 Konrad Michalik + * (c) Konrad Michalik * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace KonradMichalik\PhpDocBlockHeaderFixer\Tests\Rules; @@ -33,6 +23,12 @@ * @internal */ #[\PHPUnit\Framework\Attributes\CoversClass(DocBlockHeaderFixer::class)] +/** + * DocBlockHeaderFixerTest. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ final class DocBlockHeaderFixerTest extends TestCase { private DocBlockHeaderFixer $fixer; @@ -54,6 +50,11 @@ public function testGetName(): void self::assertSame('KonradMichalik/docblock_header_comment', $this->fixer->getName()); } + public function testGetPriority(): void + { + self::assertSame(1, $this->fixer->getPriority()); + } + public function testIsCandidate(): void { $tokens = Tokens::fromCode('count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_CLASS)) { + if ($tokens[$i]->isGivenKind(\T_CLASS)) { $classIndex = $i; break; } @@ -597,7 +598,7 @@ public function testGetStructureNameInterface(): void // Find the interface token index $interfaceIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_INTERFACE)) { + if ($tokens[$i]->isGivenKind(\T_INTERFACE)) { $interfaceIndex = $i; break; } @@ -618,7 +619,7 @@ public function testGetStructureNameTrait(): void // Find the trait token index $traitIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_TRAIT)) { + if ($tokens[$i]->isGivenKind(\T_TRAIT)) { $traitIndex = $i; break; } @@ -639,7 +640,7 @@ public function testGetStructureNameEnum(): void // Find the enum token index $enumIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_ENUM)) { + if ($tokens[$i]->isGivenKind(\T_ENUM)) { $enumIndex = $i; break; } @@ -898,7 +899,7 @@ public function testIsAnonymousClassDetectsAnonymousClass(): void // Find the class token index $classIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_CLASS)) { + if ($tokens[$i]->isGivenKind(\T_CLASS)) { $classIndex = $i; break; } @@ -919,7 +920,7 @@ public function testIsAnonymousClassReturnsFalseForRegularClass(): void // Find the class token index $classIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_CLASS)) { + if ($tokens[$i]->isGivenKind(\T_CLASS)) { $classIndex = $i; break; } @@ -940,7 +941,28 @@ public function testIsAnonymousClassWithAttribute(): void // Find the class token index $classIndex = null; for ($i = 0; $i < $tokens->count(); ++$i) { - if ($tokens[$i]->isGivenKind(T_CLASS)) { + if ($tokens[$i]->isGivenKind(\T_CLASS)) { + $classIndex = $i; + break; + } + } + + $result = $method->invoke($this->fixer, $tokens, $classIndex); + + self::assertTrue($result); + } + + public function testIsAnonymousClassWithReadonlyModifier(): void + { + $code = 'fixer, 'isAnonymousClass'); + + // Find the class token index + $classIndex = null; + for ($i = 0; $i < $tokens->count(); ++$i) { + if ($tokens[$i]->isGivenKind(\T_CLASS)) { $classIndex = $i; break; } @@ -948,6 +970,26 @@ public function testIsAnonymousClassWithAttribute(): void $result = $method->invoke($this->fixer, $tokens, $classIndex); + // This tests line 156: continue when token is T_READONLY or T_FINAL self::assertTrue($result); } + + public function testSkipsAnonymousClassWithReadonlyModifier(): void + { + $code = 'fixer, 'applyFix'); + + $this->fixer->configure([ + 'annotations' => ['author' => 'John Doe'], + 'separate' => 'none', + 'ensure_spacing' => false, + ]); + $method->invoke($this->fixer, $file, $tokens); + + // Anonymous class with readonly modifier should NOT have DocBlock added + self::assertSame($code, $tokens->generateCode()); + } } diff --git a/tests/src/Service/ComposerServiceTest.php b/tests/src/Service/ComposerServiceTest.php new file mode 100644 index 0000000..c52226f --- /dev/null +++ b/tests/src/Service/ComposerServiceTest.php @@ -0,0 +1,255 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace KonradMichalik\PhpDocBlockHeaderFixer\Tests\Service; + +use KonradMichalik\PhpDocBlockHeaderFixer\Service\ComposerService; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +/** + * @internal + */ +#[\PHPUnit\Framework\Attributes\CoversClass(ComposerService::class)] +/** + * ComposerServiceTest. + * + * @author Konrad Michalik + * @license GPL-3.0-or-later + */ +final class ComposerServiceTest extends TestCase +{ + private string $testComposerJsonPath; + + protected function setUp(): void + { + $this->testComposerJsonPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + } + + protected function tearDown(): void + { + if (file_exists($this->testComposerJsonPath)) { + unlink($this->testComposerJsonPath); + } + } + + public function testReadComposerJsonSuccess(): void + { + $composerData = [ + 'name' => 'test/package', + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ], + 'license' => 'MIT', + ]; + + file_put_contents($this->testComposerJsonPath, json_encode($composerData)); + + $result = ComposerService::readComposerJson($this->testComposerJsonPath); + + self::assertSame($composerData, $result); + } + + public function testReadComposerJsonThrowsExceptionWhenFileDoesNotExist(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The file "non-existent.json" does not exist.'); + + ComposerService::readComposerJson('non-existent.json'); + } + + public function testReadComposerJsonThrowsExceptionWhenFileCannotBeRead(): void + { + // Create an empty directory to simulate unreadable file + $dirPath = sys_get_temp_dir() . '/test-dir-' . uniqid(); + mkdir($dirPath); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/Unable to (read file|decode JSON)/'); + + try { + // Suppress the expected PHP notice when reading a directory + @ComposerService::readComposerJson($dirPath); + } finally { + rmdir($dirPath); + } + } + + public function testReadComposerJsonThrowsExceptionOnReadFailure(): void + { + // Create a file with no read permissions to force file_get_contents to return false + $invalidPath = sys_get_temp_dir() . '/test-unreadable-' . uniqid() . '.json'; + file_put_contents($invalidPath, '{}'); + + // Make file unreadable (this may not work on all systems, especially Windows) + chmod($invalidPath, 0000); + + try { + // Only run this test if we can actually make the file unreadable + if (!is_readable($invalidPath)) { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to read file'); + // Suppress the expected PHP warning when reading an unreadable file + @ComposerService::readComposerJson($invalidPath); + } else { + // If we can't make the file unreadable, mark test as skipped + $this->markTestSkipped('Unable to create unreadable file on this platform'); + } + } finally { + // Restore permissions before deletion + @chmod($invalidPath, 0644); + @unlink($invalidPath); + } + } + + public function testReadComposerJsonThrowsExceptionWhenJsonIsInvalid(): void + { + file_put_contents($this->testComposerJsonPath, 'invalid json'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to decode JSON'); + + ComposerService::readComposerJson($this->testComposerJsonPath); + } + + public function testExtractLicenseWithStringLicense(): void + { + $composerData = ['license' => 'MIT']; + + $result = ComposerService::extractLicense($composerData); + + self::assertSame('MIT', $result); + } + + public function testExtractLicenseWithArrayLicense(): void + { + $composerData = ['license' => ['MIT', 'Apache-2.0']]; + + $result = ComposerService::extractLicense($composerData); + + self::assertSame('MIT', $result); + } + + public function testExtractLicenseWithEmptyArray(): void + { + $composerData = ['license' => []]; + + $result = ComposerService::extractLicense($composerData); + + self::assertNull($result); + } + + public function testExtractLicenseWhenNotSet(): void + { + $composerData = ['name' => 'test/package']; + + $result = ComposerService::extractLicense($composerData); + + self::assertNull($result); + } + + public function testExtractAuthorsWithValidAuthors(): void + { + $composerData = [ + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com', 'role' => 'Developer'], + ['name' => 'Jane Smith', 'email' => 'jane@example.com'], + ], + ]; + + $result = ComposerService::extractAuthors($composerData); + + self::assertCount(2, $result); + self::assertSame('John Doe', $result[0]['name']); + self::assertSame('john@example.com', $result[0]['email']); + self::assertSame('Developer', $result[0]['role']); + self::assertSame('Jane Smith', $result[1]['name']); + self::assertSame('jane@example.com', $result[1]['email']); + } + + public function testExtractAuthorsWithNameOnly(): void + { + $composerData = [ + 'authors' => [ + ['name' => 'John Doe'], + ], + ]; + + $result = ComposerService::extractAuthors($composerData); + + self::assertCount(1, $result); + self::assertSame('John Doe', $result[0]['name']); + self::assertArrayNotHasKey('email', $result[0]); + } + + public function testExtractAuthorsFiltersOutInvalidEntries(): void + { + $composerData = [ + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ['email' => 'no-name@example.com'], // Missing name + 'invalid', // Not an array + ['name' => 'Jane Smith'], + ], + ]; + + $result = ComposerService::extractAuthors($composerData); + + self::assertCount(2, $result); + self::assertSame('John Doe', $result[0]['name']); + self::assertSame('Jane Smith', $result[1]['name']); + } + + public function testExtractAuthorsWhenNotSet(): void + { + $composerData = ['name' => 'test/package']; + + $result = ComposerService::extractAuthors($composerData); + + self::assertSame([], $result); + } + + public function testExtractAuthorsWhenNotArray(): void + { + $composerData = ['authors' => 'not an array']; + + $result = ComposerService::extractAuthors($composerData); + + self::assertSame([], $result); + } + + public function testGetPrimaryAuthor(): void + { + $composerData = [ + 'authors' => [ + ['name' => 'John Doe', 'email' => 'john@example.com'], + ['name' => 'Jane Smith', 'email' => 'jane@example.com'], + ], + ]; + + $result = ComposerService::getPrimaryAuthor($composerData); + + self::assertNotNull($result); + self::assertSame('John Doe', $result['name']); + self::assertSame('john@example.com', $result['email']); + } + + public function testGetPrimaryAuthorWhenNoAuthors(): void + { + $composerData = ['name' => 'test/package']; + + $result = ComposerService::getPrimaryAuthor($composerData); + + self::assertNull($result); + } +} From 063b6dcfe0caccf32a354ff8efc0cad8b0f61208 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Mon, 13 Oct 2025 11:46:38 +0200 Subject: [PATCH 2/3] fix: streamline temporary file path generation in tests --- tests/src/Generators/DocBlockHeaderTest.php | 14 +++++++------- tests/src/Rules/DocBlockHeaderFixerTest.php | 2 ++ tests/src/Service/ComposerServiceTest.php | 10 +++++++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/src/Generators/DocBlockHeaderTest.php b/tests/src/Generators/DocBlockHeaderTest.php index 393828d..8a76a3f 100644 --- a/tests/src/Generators/DocBlockHeaderTest.php +++ b/tests/src/Generators/DocBlockHeaderTest.php @@ -307,7 +307,7 @@ public function testToArrayWithAddStructureName(): void public function testFromComposerWithSingleAuthor(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ @@ -332,7 +332,7 @@ public function testFromComposerWithSingleAuthor(): void public function testFromComposerWithMultipleAuthors(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ @@ -359,7 +359,7 @@ public function testFromComposerWithMultipleAuthors(): void public function testFromComposerWithAuthorWithoutEmail(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ @@ -382,7 +382,7 @@ public function testFromComposerWithAuthorWithoutEmail(): void public function testFromComposerWithNoAuthors(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'license' => 'MIT', @@ -402,7 +402,7 @@ public function testFromComposerWithNoAuthors(): void public function testFromComposerWithNoLicense(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ @@ -424,7 +424,7 @@ public function testFromComposerWithNoLicense(): void public function testFromComposerWithAdditionalAnnotations(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ @@ -452,7 +452,7 @@ public function testFromComposerWithAdditionalAnnotations(): void public function testFromComposerWithCustomParameters(): void { - $testComposerPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $testComposerPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; $composerData = [ 'name' => 'test/package', 'authors' => [ diff --git a/tests/src/Rules/DocBlockHeaderFixerTest.php b/tests/src/Rules/DocBlockHeaderFixerTest.php index 8c94e3f..c048d22 100644 --- a/tests/src/Rules/DocBlockHeaderFixerTest.php +++ b/tests/src/Rules/DocBlockHeaderFixerTest.php @@ -952,6 +952,7 @@ public function testIsAnonymousClassWithAttribute(): void self::assertTrue($result); } + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')] public function testIsAnonymousClassWithReadonlyModifier(): void { $code = '= 8.2')] public function testSkipsAnonymousClassWithReadonlyModifier(): void { $code = 'testComposerJsonPath = sys_get_temp_dir() . '/test-composer-' . uniqid() . '.json'; + $this->testComposerJsonPath = sys_get_temp_dir().'/test-composer-'.uniqid().'.json'; } protected function tearDown(): void @@ -71,7 +71,7 @@ public function testReadComposerJsonThrowsExceptionWhenFileDoesNotExist(): void public function testReadComposerJsonThrowsExceptionWhenFileCannotBeRead(): void { // Create an empty directory to simulate unreadable file - $dirPath = sys_get_temp_dir() . '/test-dir-' . uniqid(); + $dirPath = sys_get_temp_dir().'/test-dir-'.uniqid(); mkdir($dirPath); $this->expectException(RuntimeException::class); @@ -88,7 +88,7 @@ public function testReadComposerJsonThrowsExceptionWhenFileCannotBeRead(): void public function testReadComposerJsonThrowsExceptionOnReadFailure(): void { // Create a file with no read permissions to force file_get_contents to return false - $invalidPath = sys_get_temp_dir() . '/test-unreadable-' . uniqid() . '.json'; + $invalidPath = sys_get_temp_dir().'/test-unreadable-'.uniqid().'.json'; file_put_contents($invalidPath, '{}'); // Make file unreadable (this may not work on all systems, especially Windows) @@ -171,9 +171,12 @@ public function testExtractAuthorsWithValidAuthors(): void self::assertCount(2, $result); self::assertSame('John Doe', $result[0]['name']); + /* @phpstan-ignore-next-line offsetAccess.notFound */ self::assertSame('john@example.com', $result[0]['email']); + /* @phpstan-ignore-next-line offsetAccess.notFound */ self::assertSame('Developer', $result[0]['role']); self::assertSame('Jane Smith', $result[1]['name']); + /* @phpstan-ignore-next-line offsetAccess.notFound */ self::assertSame('jane@example.com', $result[1]['email']); } @@ -241,6 +244,7 @@ public function testGetPrimaryAuthor(): void self::assertNotNull($result); self::assertSame('John Doe', $result['name']); + /* @phpstan-ignore-next-line offsetAccess.notFound */ self::assertSame('john@example.com', $result['email']); } From d8a5356a55129d2a82f65419d1590b2cc6e47454 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Mon, 13 Oct 2025 11:53:13 +0200 Subject: [PATCH 3/3] fix: update PHPUnit attribute requirements to PHP 8.3 --- tests/src/Rules/DocBlockHeaderFixerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/Rules/DocBlockHeaderFixerTest.php b/tests/src/Rules/DocBlockHeaderFixerTest.php index c048d22..ca57d44 100644 --- a/tests/src/Rules/DocBlockHeaderFixerTest.php +++ b/tests/src/Rules/DocBlockHeaderFixerTest.php @@ -952,7 +952,7 @@ public function testIsAnonymousClassWithAttribute(): void self::assertTrue($result); } - #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.2')] + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.3')] public function testIsAnonymousClassWithReadonlyModifier(): void { $code = '= 8.2')] + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 8.3')] public function testSkipsAnonymousClassWithReadonlyModifier(): void { $code = '