From 08eb9272d9c1ee04d2d930f52a80c1b18b21053a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Thu, 9 Apr 2026 23:27:13 -0300 Subject: [PATCH 1/4] feat(sync): add license file generation to dev-tools:sync Implements issue #11 - auto-create LICENSE file from composer.json - Added new License namespace with isolated classes: - Reader: reads license metadata from composer.json - Resolver: resolves SPDX license identifiers to templates - TemplateLoader: loads license templates from resources - PlaceholderResolver: resolves template placeholders (year, organization, author, project) - Generator: orchestrates license file generation - Added license templates for: MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, ISC, GPL-3.0-or-later, LGPL-3.0-or-later, MPL-2.0, Unlicense - Updated SyncCommand to call generateLicense() method - Added unit tests for all License classes Behavior: - If LICENSE already exists, skip generation - If no license in composer.json, warn and skip - If unsupported license, warn and skip - Only generates for single supported SPDX license --- src/Command/SyncCommand.php | 45 +++++ src/License/Generator.php | 99 ++++++++++ src/License/PlaceholderResolver.php | 51 +++++ src/License/Reader.php | 123 ++++++++++++ src/License/Resolver.php | 75 +++++++ src/License/TemplateLoader.php | 58 ++++++ .../resources/templates/apache-2.0.txt | 39 ++++ .../resources/templates/bsd-2-clause.txt | 24 +++ .../resources/templates/bsd-3-clause.txt | 28 +++ .../resources/templates/gpl-3.0-or-later.txt | 17 ++ src/License/resources/templates/isc.txt | 15 ++ .../resources/templates/lgpl-3.0-or-later.txt | 17 ++ src/License/resources/templates/mit.txt | 21 ++ src/License/resources/templates/mpl-2.0.txt | 185 ++++++++++++++++++ src/License/resources/templates/unlicense.txt | 24 +++ tests/License/PlaceholderResolverTest.php | 119 +++++++++++ tests/License/ReaderTest.php | 174 ++++++++++++++++ tests/License/ResolverTest.php | 130 ++++++++++++ tests/License/TemplateLoaderTest.php | 64 ++++++ 19 files changed, 1308 insertions(+) create mode 100644 src/License/Generator.php create mode 100644 src/License/PlaceholderResolver.php create mode 100644 src/License/Reader.php create mode 100644 src/License/Resolver.php create mode 100644 src/License/TemplateLoader.php create mode 100644 src/License/resources/templates/apache-2.0.txt create mode 100644 src/License/resources/templates/bsd-2-clause.txt create mode 100644 src/License/resources/templates/bsd-3-clause.txt create mode 100644 src/License/resources/templates/gpl-3.0-or-later.txt create mode 100644 src/License/resources/templates/isc.txt create mode 100644 src/License/resources/templates/lgpl-3.0-or-later.txt create mode 100644 src/License/resources/templates/mit.txt create mode 100644 src/License/resources/templates/mpl-2.0.txt create mode 100644 src/License/resources/templates/unlicense.txt create mode 100644 tests/License/PlaceholderResolverTest.php create mode 100644 tests/License/ReaderTest.php create mode 100644 tests/License/ResolverTest.php create mode 100644 tests/License/TemplateLoaderTest.php diff --git a/src/Command/SyncCommand.php b/src/Command/SyncCommand.php index 5911015..3674ead 100644 --- a/src/Command/SyncCommand.php +++ b/src/Command/SyncCommand.php @@ -19,6 +19,11 @@ namespace FastForward\DevTools\Command; use Composer\Factory; +use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\Reader; +use FastForward\DevTools\License\Resolver; +use FastForward\DevTools\License\TemplateLoader; +use FastForward\DevTools\License\PlaceholderResolver; use Composer\Json\JsonManipulator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -76,6 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->addRepositoryWikiGitSubmodule(); $this->runCommand('gitignore', $output); $this->runCommand('skills', $output); + $this->generateLicense($output); return self::SUCCESS; } @@ -235,4 +241,43 @@ private function getGitRepositoryUrl(): string return trim($process->getOutput()); } + + /** + * Generates a LICENSE file if one does not exist and a supported license is declared in composer.json. + * + * @param OutputInterface $output the console output stream + * + * @return void + */ + private function generateLicense(OutputInterface $output): void + { + $targetPath = $this->getConfigFile('LICENSE', true); + + if ($this->filesystem->exists($targetPath)) { + $output->writeln('LICENSE file already exists. Skipping generation.'); + + return; + } + + $composer = $this->requireComposer(); + + $reader = new Reader($composer); + $resolver = new Resolver(); + $templateLoader = new TemplateLoader(); + $placeholderResolver = new PlaceholderResolver(); + + $generator = new Generator($reader, $resolver, $templateLoader, $placeholderResolver, $this->filesystem); + + $license = $generator->generate($targetPath); + + if (null === $license) { + $output->writeln( + 'No supported license found in composer.json or license is unsupported. Skipping LICENSE generation.' + ); + + return; + } + + $output->writeln('LICENSE file generated successfully.'); + } } diff --git a/src/License/Generator.php b/src/License/Generator.php new file mode 100644 index 0000000..9fc3257 --- /dev/null +++ b/src/License/Generator.php @@ -0,0 +1,99 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +use Symfony\Component\Filesystem\Filesystem; + +final readonly class Generator +{ + /** + * @param Reader $reader + * @param Resolver $resolver + * @param TemplateLoader $templateLoader + * @param PlaceholderResolver $placeholderResolver + * @param Filesystem $filesystem + */ + public function __construct( + private Reader $reader, + private Resolver $resolver, + private TemplateLoader $templateLoader, + private PlaceholderResolver $placeholderResolver, + private Filesystem $filesystem = new Filesystem() + ) {} + + /** + * @param string $targetPath + * + * @return string|null + */ + public function generate(string $targetPath): ?string + { + $license = $this->reader->getLicense(); + + if (null === $license) { + return null; + } + + if (! $this->resolver->isSupported($license)) { + return null; + } + + if ($this->filesystem->exists($targetPath)) { + return null; + } + + $templateFilename = $this->resolver->resolve($license); + + if (null === $templateFilename) { + return null; + } + + $template = $this->templateLoader->load($templateFilename); + + $authors = $this->reader->getAuthors(); + $firstAuthor = $authors[0] ?? null; + + $metadata = [ + 'year' => $this->reader->getYear(), + 'organization' => $this->reader->getVendor(), + 'author' => null !== $firstAuthor ? ($firstAuthor['name'] ?: ($firstAuthor['email'] ?? '')) : '', + 'project' => $this->reader->getPackageName(), + ]; + + $content = $this->placeholderResolver->resolve($template, $metadata); + + $this->filesystem->dumpFile($targetPath, $content); + + return $content; + } + + /** + * @return bool + */ + public function hasLicense(): bool + { + $license = $this->reader->getLicense(); + + if (null === $license) { + return false; + } + + return $this->resolver->isSupported($license); + } +} diff --git a/src/License/PlaceholderResolver.php b/src/License/PlaceholderResolver.php new file mode 100644 index 0000000..139e78a --- /dev/null +++ b/src/License/PlaceholderResolver.php @@ -0,0 +1,51 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +use function Safe\preg_replace; + +final class PlaceholderResolver +{ + /** + * @param array{year?: int, organization?: string, author?: string, project?: string} $metadata + * @param string $template + */ + public function resolve(string $template, array $metadata): string + { + $replacements = [ + '{{ year }}' => (string) ($metadata['year'] ?? date('Y')), + '{{ organization }}' => $metadata['organization'] ?? '', + '{{ author }}' => $metadata['author'] ?? '', + '{{ project }}' => $metadata['project'] ?? '', + '{{ copyright_holder }}' => $metadata['organization'] ?? $metadata['author'] ?? '', + ]; + + $result = $template; + + foreach ($replacements as $placeholder => $value) { + $result = str_replace($placeholder, $value, $result); + } + + $result = preg_replace('/\{\{\s*\w+\s*\}\}/', '', $result); + + $result = preg_replace('/\n{3,}/', "\n\n", $result); + + return trim((string) $result); + } +} diff --git a/src/License/Reader.php b/src/License/Reader.php new file mode 100644 index 0000000..6398460 --- /dev/null +++ b/src/License/Reader.php @@ -0,0 +1,123 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +use Composer\Composer; +use Composer\Package\RootPackageInterface; + +final readonly class Reader +{ + /** + * @param Composer $composer + */ + public function __construct( + private Composer $composer + ) {} + + /** + * @return string|null + */ + public function getLicense(): ?string + { + $package = $this->composer->getPackage(); + + return $this->extractLicense($package); + } + + /** + * @return string + */ + public function getPackageName(): string + { + $package = $this->composer->getPackage(); + + return $package->getName(); + } + + /** + * @return array + */ + public function getAuthors(): array + { + $package = $this->composer->getPackage(); + $authors = $package->getAuthors(); + + if ([] === $authors) { + return []; + } + + return array_map( + static fn(array $author): array => [ + 'name' => $author['name'] ?? '', + 'email' => $author['email'] ?? '', + 'homepage' => $author['homepage'] ?? '', + 'role' => $author['role'] ?? '', + ], + $authors + ); + } + + /** + * @return string|null + */ + public function getVendor(): ?string + { + $packageName = $this->getPackageName(); + + if (null === $packageName) { + return null; + } + + $parts = explode('/', $packageName, 2); + + if (! isset($parts[1])) { + return null; + } + + return $parts[0]; + } + + /** + * @return int + */ + public function getYear(): int + { + return (int) date('Y'); + } + + /** + * @param RootPackageInterface $package + * + * @return string|null + */ + private function extractLicense(RootPackageInterface $package): ?string + { + $license = $package->getLicense(); + + if ([] === $license) { + return null; + } + + if (1 === \count($license)) { + return $license[0]; + } + + return null; + } +} diff --git a/src/License/Resolver.php b/src/License/Resolver.php new file mode 100644 index 0000000..2e65d3c --- /dev/null +++ b/src/License/Resolver.php @@ -0,0 +1,75 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +final class Resolver +{ + private const array SUPPORTED_LICENSES = [ + 'MIT' => 'mit.txt', + 'BSD-2-Clause' => 'bsd-2-clause.txt', + 'BSD-3-Clause' => 'bsd-3-clause.txt', + 'Apache-2.0' => 'apache-2.0.txt', + 'Apache-2' => 'apache-2.0.txt', + 'GPL-3.0-or-later' => 'gpl-3.0-or-later.txt', + 'GPL-3.0' => 'gpl-3.0-or-later.txt', + 'GPL-3+' => 'gpl-3.0-or-later.txt', + 'LGPL-3.0-or-later' => 'lgpl-3.0-or-later.txt', + 'LGPL-3.0' => 'lgpl-3.0-or-later.txt', + 'LGPL-3+' => 'lgpl-3.0-or-later.txt', + 'MPL-2.0' => 'mpl-2.0.txt', + 'ISC' => 'isc.txt', + 'Unlicense' => 'unlicense.txt', + ]; + + /** + * @param string $license + * + * @return bool + */ + public function isSupported(string $license): bool + { + return isset(self::SUPPORTED_LICENSES[$this->normalize($license)]); + } + + /** + * @param string $license + * + * @return string|null + */ + public function resolve(string $license): ?string + { + $normalized = $this->normalize($license); + + if (! isset(self::SUPPORTED_LICENSES[$normalized])) { + return null; + } + + return self::SUPPORTED_LICENSES[$normalized]; + } + + /** + * @param string $license + * + * @return string + */ + private function normalize(string $license): string + { + return trim($license); + } +} diff --git a/src/License/TemplateLoader.php b/src/License/TemplateLoader.php new file mode 100644 index 0000000..8f0e6de --- /dev/null +++ b/src/License/TemplateLoader.php @@ -0,0 +1,58 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +use RuntimeException; + +use function Safe\file_get_contents; + +final readonly class TemplateLoader +{ + private string $templatesPath; + + /** + * @param string|null $templatesPath + */ + public function __construct(?string $templatesPath = null) + { + $this->templatesPath = $templatesPath ?? __DIR__ . '/resources/templates'; + } + + /** + * @param string $templateFilename + * + * @return string + * + * @throws RuntimeException + */ + public function load(string $templateFilename): string + { + $templatePath = $this->templatesPath . '/' . $templateFilename; + + if (! file_exists($templatePath)) { + throw new RuntimeException(\sprintf( + 'License template "%s" not found in "%s"', + $templateFilename, + $this->templatesPath + )); + } + + return file_get_contents($templatePath); + } +} diff --git a/src/License/resources/templates/apache-2.0.txt b/src/License/resources/templates/apache-2.0.txt new file mode 100644 index 0000000..f5020b0 --- /dev/null +++ b/src/License/resources/templates/apache-2.0.txt @@ -0,0 +1,39 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + 2. Grant of Copyright License. + + 3. Grant of Patent License. + + 4. Redistribution. + + 5. Submission of Contributions. + + 6. Trademarks. + + 7. Disclaimer of Warranty. + + 8. Limitation of Liability. + + 9. Accepting Warranty or Additional Liability. + + END OF TERMS AND CONDITIONS + + Copyright {{ year }} {{ organization }}{{ author }} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/License/resources/templates/bsd-2-clause.txt b/src/License/resources/templates/bsd-2-clause.txt new file mode 100644 index 0000000..14fbac7 --- /dev/null +++ b/src/License/resources/templates/bsd-2-clause.txt @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) {{ year }} {{ organization }}{{ author }} + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/License/resources/templates/bsd-3-clause.txt b/src/License/resources/templates/bsd-3-clause.txt new file mode 100644 index 0000000..fd29ef5 --- /dev/null +++ b/src/License/resources/templates/bsd-3-clause.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) {{ year }} {{ organization }}{{ author }} + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/License/resources/templates/gpl-3.0-or-later.txt b/src/License/resources/templates/gpl-3.0-or-later.txt new file mode 100644 index 0000000..4e78540 --- /dev/null +++ b/src/License/resources/templates/gpl-3.0-or-later.txt @@ -0,0 +1,17 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) {{ year }} {{ organization }}{{ author }} + +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 . \ No newline at end of file diff --git a/src/License/resources/templates/isc.txt b/src/License/resources/templates/isc.txt new file mode 100644 index 0000000..788ed02 --- /dev/null +++ b/src/License/resources/templates/isc.txt @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) {{ year }} {{ organization }}{{ author }} + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/src/License/resources/templates/lgpl-3.0-or-later.txt b/src/License/resources/templates/lgpl-3.0-or-later.txt new file mode 100644 index 0000000..9ca20a3 --- /dev/null +++ b/src/License/resources/templates/lgpl-3.0-or-later.txt @@ -0,0 +1,17 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) {{ year }} {{ organization }}{{ author }} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/src/License/resources/templates/mit.txt b/src/License/resources/templates/mit.txt new file mode 100644 index 0000000..8403eef --- /dev/null +++ b/src/License/resources/templates/mit.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {{ year }} {{ organization }}{{ author }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/License/resources/templates/mpl-2.0.txt b/src/License/resources/templates/mpl-2.0.txt new file mode 100644 index 0000000..7869ab3 --- /dev/null +++ b/src/License/resources/templates/mpl-2.0.txt @@ -0,0 +1,185 @@ +Mozilla Public License Version 2.0 + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of all Contributors + together with the information to which such Contributions relate and + which such Contributors would have been required to provide to the + Initial Developer or the Initial Developer's designated agent. + +1.3. "Covered Software" + means Source Code Form to which the Initial Developer has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.4. "Executable" + means Covered Software in any form other than Source Code Form. + +1.5. "Initial Developer" + means the individual or entity that first makes Covered Software + available under this License. + +1.6. "Larger Work" + means a work which combines Covered Software or portions thereof + with code not governed by the terms of this License. + +1.7. "License" + means this document. + +1.8. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently acquired, + any rights expressed in this License. + +1.9. "Modifications" + means any of the following: (a) any file in Source Code Form that + results from an addition to, deletion from, or modification of the + contents of Covered Software; or (b) any new file in Source Code + Form that contains any Covered Software. + +1.10. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by making, using, or selling its Contributions, alone + or by combination with its Contributor Version (or portions of + such combination), to make, use, sell, offer for sale, have made, + import, or otherwise transfer either solely on its own or + together with its Contributor Version (or portions of such + combination), the Contributor Version where such Contributor + claims to be the source code of its Contributor Version, together + with the patent claims that are readable by the Contributor + Version that are necessarily infringed by its Contributor Version + alone (not together with any other Contributor Versions). + +1.11. "Source Code Form" + means the form of a work preferred for making modifications. + +1.12. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. + For purposes of this definition, "control" means (a) the power, + direct or indirect, to cause the direction or management of such + entity, whether by contract or otherwise, or (b) ownership of + fifty percent (50%) or more of the outstanding shares or + beneficial ownership of such entity. + +2. License Grants and Conditions +--------------------------------- + +2.1. Grants + +Each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable (except as stated +in this section) patent license to make, use, sell, offer for sale, +have made, import, and otherwise transfer its Contributions, alone +or by combination with its Contributor Version (or portions of such +combination), to make, use, sell, offer for sale, have made, import, +or otherwise transfer the Contributor Version (or portions thereof). + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for the Contribution on the date the Contributor +first places such Contribution in Source Code Form. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted +under this License. No additional rights or licenses will be implied +from the distribution or licensing of Covered Software under this +License. Notwithstanding Section 2.1(b), nothing in this License is +intended to affect any license to use code that is a part of the +library definition (the "Library") while the Library is in both +Source Code and Executable form, provided that such license extends +only to the patent claims licensable by Contributors that are +necessarily infringed by their Contributions alone or by combination +of their Contributions with the Library. + +2.4. Third Party Patent Claims + +Should the Library be modified by a Contributor to cause the Library +to fail to satisfy the conditions of this License, then You must +terminate this License. In the event a Contributor causes the +Library to fail to satisfy this License, then this License terminates +immediately. Should a Contributor bring a patent claim against any +Contributor alleging that the Library infringes such Contributor's +patent claim, then the Contributor must terminate this License. + +2.5. Disclaimer of Warranty + +COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, +WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF +DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. +THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED +SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN +ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) +ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS +DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. +NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER +THIS DISCLAIMER. + +2.6. Termination + +This License and the rights granted hereunder will terminate +automatically if You fail to comply with any term(s) of this License +and fail to cure such breach within 30 days of becoming aware of the +breach. Provisions which, by their nature, must remain in effect +beyond the termination of this License shall survive. + +2.7. Limitation of Liability + +UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT +(INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL ANY INITIAL +DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED +SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY +PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR +LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR +ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY +SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS +LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR +PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT +APPLICABLE LAW PROHIBITS SUCH LIMITATION. FURTHERMORE, NO CONTRIBUTOR +SHALL BE LIABLE UNDER THIS LICENSE FOR ANY REVENUE LOSS, PROFIT LOSS +OR DATA, OR FOR COSTS OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, +OR FOR ANY INDIRECT, CONSEQUENTIAL, PUNITIVE, OR SPECIAL DAMAGES +WHATSOEVER ARISING OUT OF OR IN CONNECTION WITH THIS LICENSE, WHETHER +SUCH PARTY IS ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +2.8. Acceptance + +This License is not intended to grant any rights to any person or +entity other than the original licensees of the Covered Software, +each of which has voluntarily agreed to the terms of this License. + +3. If You distribute or make the Covered Software available under +this License in Source Code Form: + + a) You must cause the Source Code Form to be prominently marked + with a simple text file (not a binary) containing the text: + "This file is part of the Covered Software. For copyright and + licensing information, see the file named COPYING in the main + directory of the Covered Software." + + b) You must cause any files that contain modifications to the + Source Code Form to be clearly marked as such in a reasonable + manner (for example, by including them in a file named "NOTICE"). + + c) You must include in each file of Source Code Form at least the + following notice: "This file contains modifications made by You" + and a reference to this License. + +This License applies to all files in the Source Code Form that contain +modifications made by You. + +Copyright {{ year }} {{ organization }}{{ author }} \ No newline at end of file diff --git a/src/License/resources/templates/unlicense.txt b/src/License/resources/templates/unlicense.txt new file mode 100644 index 0000000..6bb8a29 --- /dev/null +++ b/src/License/resources/templates/unlicense.txt @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/tests/License/PlaceholderResolverTest.php b/tests/License/PlaceholderResolverTest.php new file mode 100644 index 0000000..6c5b726 --- /dev/null +++ b/tests/License/PlaceholderResolverTest.php @@ -0,0 +1,119 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\License; + +use FastForward\DevTools\License\PlaceholderResolver; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(PlaceholderResolver::class)] +final class PlaceholderResolverTest extends TestCase +{ + private PlaceholderResolver $resolver; + + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + + $this->resolver = new PlaceholderResolver(); + } + + /** + * @return void + */ + #[Test] + public function resolveWithAllPlaceholdersWillReplaceAll(): void + { + $template = 'Copyright {{ year }} {{ organization }} {{ author }} {{ project }}'; + $metadata = [ + 'year' => 2026, + 'organization' => 'FastForward', + 'author' => 'Felipe', + 'project' => 'dev-tools', + ]; + + $result = $this->resolver->resolve($template, $metadata); + + self::assertSame('Copyright 2026 FastForward Felipe dev-tools', $result); + } + + /** + * @return void + */ + #[Test] + public function resolveWithPartialMetadataWillReplaceAvailableAndRemoveOthers(): void + { + $template = 'Copyright {{ year }} {{ organization }}{{ author }}'; + $metadata = [ + 'year' => 2026, + ]; + + $result = $this->resolver->resolve($template, $metadata); + + self::assertSame('Copyright 2026', $result); + } + + /** + * @return void + */ + #[Test] + public function resolveWithNoMetadataWillRemovePlaceholders(): void + { + $template = 'Copyright {{ year }} {{ organization }}{{ author }} {{ project }}'; + $metadata = []; + + $result = $this->resolver->resolve($template, $metadata); + + self::assertStringNotContainsString('{{', $result); + } + + /** + * @return void + */ + #[Test] + public function resolveWillCollapseMultipleBlankLines(): void + { + $template = "Line 1\n\n\n\nLine 2"; + $metadata = []; + + $result = $this->resolver->resolve($template, $metadata); + + self::assertSame("Line 1\n\nLine 2", $result); + } + + /** + * @return void + */ + #[Test] + public function resolveWillTrimResult(): void + { + $template = ' {{ year }} '; + $metadata = [ + 'year' => 2026, + ]; + + $result = $this->resolver->resolve($template, $metadata); + + self::assertSame('2026', $result); + } +} diff --git a/tests/License/ReaderTest.php b/tests/License/ReaderTest.php new file mode 100644 index 0000000..01e0eef --- /dev/null +++ b/tests/License/ReaderTest.php @@ -0,0 +1,174 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\License; + +use PHPUnit\Framework\MockObject\MockObject; +use FastForward\DevTools\License\Reader; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Composer\Composer; +use Composer\Package\RootPackageInterface; +use Prophecy\PhpUnit\ProphecyTrait; + +#[CoversClass(Reader::class)] +final class ReaderTest extends TestCase +{ + use ProphecyTrait; + + private Reader $reader; + + private MockObject $composer; + + private MockObject $package; + + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + + $this->composer = $this->createMock(Composer::class); + $this->package = $this->createMock(RootPackageInterface::class); + + $this->composer->method('getPackage') + ->willReturn($this->package); + + $this->reader = new Reader($this->composer); + } + + /** + * @return void + */ + #[Test] + public function getLicenseWithSingleLicenseWillReturnLicenseString(): void + { + $this->package->method('getLicense') + ->willReturn(['MIT']); + + self::assertSame('MIT', $this->reader->getLicense()); + } + + /** + * @return void + */ + #[Test] + public function getLicenseWithNoLicenseWillReturnNull(): void + { + $this->package->method('getLicense') + ->willReturn([]); + + self::assertNull($this->reader->getLicense()); + } + + /** + * @return void + */ + #[Test] + public function getLicenseWithMultipleLicensesWillReturnNull(): void + { + $this->package->method('getLicense') + ->willReturn(['MIT', 'Apache-2.0']); + + self::assertNull($this->reader->getLicense()); + } + + /** + * @return void + */ + #[Test] + public function getPackageNameWillReturnPackageName(): void + { + $this->package->method('getName') + ->willReturn('fast-forward/dev-tools'); + + self::assertSame('fast-forward/dev-tools', $this->reader->getPackageName()); + } + + /** + * @return void + */ + #[Test] + public function getVendorWillExtractVendorFromPackageName(): void + { + $this->package->method('getName') + ->willReturn('fast-forward/dev-tools'); + $this->package->method('getLicense') + ->willReturn(['MIT']); + + self::assertSame('fast-forward', $this->reader->getVendor()); + } + + /** + * @return void + */ + #[Test] + public function getVendorWithSingleNamePackageWillReturnNull(): void + { + $this->package->method('getName') + ->willReturn('dev-tools'); + $this->package->method('getLicense') + ->willReturn(['MIT']); + + self::assertNull($this->reader->getVendor()); + } + + /** + * @return void + */ + #[Test] + public function getAuthorsWillReturnAuthorsArray(): void + { + $authors = [ + [ + 'name' => 'Felipe Abreu', + 'email' => 'test@example.com', + 'homepage' => 'https://example.com', + 'role' => 'Developer', + ], + ]; + + $this->package->method('getAuthors') + ->willReturn($authors); + + self::assertSame($authors, $this->reader->getAuthors()); + } + + /** + * @return void + */ + #[Test] + public function getAuthorsWithNoAuthorsWillReturnEmptyArray(): void + { + $this->package->method('getAuthors') + ->willReturn([]); + + self::assertSame([], $this->reader->getAuthors()); + } + + /** + * @return void + */ + #[Test] + public function getYearWillReturnCurrentYear(): void + { + self::assertSame((int) date('Y'), $this->reader->getYear()); + } +} diff --git a/tests/License/ResolverTest.php b/tests/License/ResolverTest.php new file mode 100644 index 0000000..a1f5e9a --- /dev/null +++ b/tests/License/ResolverTest.php @@ -0,0 +1,130 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\License; + +use FastForward\DevTools\License\Resolver; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(Resolver::class)] +final class ResolverTest extends TestCase +{ + private Resolver $resolver; + + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + + $this->resolver = new Resolver(); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithMITWillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('MIT')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithBSD3ClauseWillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('BSD-3-Clause')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithApache20WillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('Apache-2.0')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithApache2WillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('Apache-2')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithGPL3WillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('GPL-3.0')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithGPL3PlusWillReturnTrue(): void + { + self::assertTrue($this->resolver->isSupported('GPL-3+')); + } + + /** + * @return void + */ + #[Test] + public function isSupportedWithUnknownLicenseWillReturnFalse(): void + { + self::assertFalse($this->resolver->isSupported('Unknown-License')); + } + + /** + * @return void + */ + #[Test] + public function resolveWithMITWillReturnTemplateFilename(): void + { + self::assertSame('mit.txt', $this->resolver->resolve('MIT')); + } + + /** + * @return void + */ + #[Test] + public function resolveWithApache2WillReturnApache20Template(): void + { + self::assertSame('apache-2.0.txt', $this->resolver->resolve('Apache-2')); + } + + /** + * @return void + */ + #[Test] + public function resolveWithUnknownLicenseWillReturnNull(): void + { + self::assertNull($this->resolver->resolve('Unknown-License')); + } +} diff --git a/tests/License/TemplateLoaderTest.php b/tests/License/TemplateLoaderTest.php new file mode 100644 index 0000000..c47f1d6 --- /dev/null +++ b/tests/License/TemplateLoaderTest.php @@ -0,0 +1,64 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\License; + +use FastForward\DevTools\License\TemplateLoader; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +#[CoversClass(TemplateLoader::class)] +final class TemplateLoaderTest extends TestCase +{ + private TemplateLoader $loader; + + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + + $this->loader = new TemplateLoader(); + } + + /** + * @return void + */ + #[Test] + public function loadWithExistingTemplateWillReturnContent(): void + { + $content = $this->loader->load('mit.txt'); + + self::assertStringContainsString('MIT', $content); + } + + /** + * @return void + */ + #[Test] + public function loadWithNonExistentTemplateWillThrow(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('not found'); + + $this->loader->load('nonexistent.txt'); + } +} From 35bcf2e24f91186f5c768587833c76866220f9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Thu, 9 Apr 2026 23:41:17 -0300 Subject: [PATCH 2/4] feat(license): add various license files and update template loader path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- .../resources/templates => resources/licenses}/apache-2.0.txt | 0 .../resources/templates => resources/licenses}/bsd-2-clause.txt | 0 .../resources/templates => resources/licenses}/bsd-3-clause.txt | 0 .../templates => resources/licenses}/gpl-3.0-or-later.txt | 0 {src/License/resources/templates => resources/licenses}/isc.txt | 0 .../templates => resources/licenses}/lgpl-3.0-or-later.txt | 0 {src/License/resources/templates => resources/licenses}/mit.txt | 0 .../resources/templates => resources/licenses}/mpl-2.0.txt | 0 .../resources/templates => resources/licenses}/unlicense.txt | 0 src/License/TemplateLoader.php | 2 +- 10 files changed, 1 insertion(+), 1 deletion(-) rename {src/License/resources/templates => resources/licenses}/apache-2.0.txt (100%) rename {src/License/resources/templates => resources/licenses}/bsd-2-clause.txt (100%) rename {src/License/resources/templates => resources/licenses}/bsd-3-clause.txt (100%) rename {src/License/resources/templates => resources/licenses}/gpl-3.0-or-later.txt (100%) rename {src/License/resources/templates => resources/licenses}/isc.txt (100%) rename {src/License/resources/templates => resources/licenses}/lgpl-3.0-or-later.txt (100%) rename {src/License/resources/templates => resources/licenses}/mit.txt (100%) rename {src/License/resources/templates => resources/licenses}/mpl-2.0.txt (100%) rename {src/License/resources/templates => resources/licenses}/unlicense.txt (100%) diff --git a/src/License/resources/templates/apache-2.0.txt b/resources/licenses/apache-2.0.txt similarity index 100% rename from src/License/resources/templates/apache-2.0.txt rename to resources/licenses/apache-2.0.txt diff --git a/src/License/resources/templates/bsd-2-clause.txt b/resources/licenses/bsd-2-clause.txt similarity index 100% rename from src/License/resources/templates/bsd-2-clause.txt rename to resources/licenses/bsd-2-clause.txt diff --git a/src/License/resources/templates/bsd-3-clause.txt b/resources/licenses/bsd-3-clause.txt similarity index 100% rename from src/License/resources/templates/bsd-3-clause.txt rename to resources/licenses/bsd-3-clause.txt diff --git a/src/License/resources/templates/gpl-3.0-or-later.txt b/resources/licenses/gpl-3.0-or-later.txt similarity index 100% rename from src/License/resources/templates/gpl-3.0-or-later.txt rename to resources/licenses/gpl-3.0-or-later.txt diff --git a/src/License/resources/templates/isc.txt b/resources/licenses/isc.txt similarity index 100% rename from src/License/resources/templates/isc.txt rename to resources/licenses/isc.txt diff --git a/src/License/resources/templates/lgpl-3.0-or-later.txt b/resources/licenses/lgpl-3.0-or-later.txt similarity index 100% rename from src/License/resources/templates/lgpl-3.0-or-later.txt rename to resources/licenses/lgpl-3.0-or-later.txt diff --git a/src/License/resources/templates/mit.txt b/resources/licenses/mit.txt similarity index 100% rename from src/License/resources/templates/mit.txt rename to resources/licenses/mit.txt diff --git a/src/License/resources/templates/mpl-2.0.txt b/resources/licenses/mpl-2.0.txt similarity index 100% rename from src/License/resources/templates/mpl-2.0.txt rename to resources/licenses/mpl-2.0.txt diff --git a/src/License/resources/templates/unlicense.txt b/resources/licenses/unlicense.txt similarity index 100% rename from src/License/resources/templates/unlicense.txt rename to resources/licenses/unlicense.txt diff --git a/src/License/TemplateLoader.php b/src/License/TemplateLoader.php index 8f0e6de..06562b8 100644 --- a/src/License/TemplateLoader.php +++ b/src/License/TemplateLoader.php @@ -31,7 +31,7 @@ */ public function __construct(?string $templatesPath = null) { - $this->templatesPath = $templatesPath ?? __DIR__ . '/resources/templates'; + $this->templatesPath = $templatesPath ?? \dirname(__DIR__, 2) . '/resources/licenses'; } /** From fad9aff3ea3d71d998d78def295973ae659b29a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 10 Apr 2026 00:50:08 -0300 Subject: [PATCH 3/4] feat(license): implement CopyLicenseCommand for generating LICENSE files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Command/CopyLicenseCommand.php | 118 ++++++++++++++++++ src/Command/SyncCommand.php | 46 +------ .../Capability/DevToolsCommandProvider.php | 2 + src/License/Generator.php | 2 +- src/License/GeneratorInterface.php | 34 +++++ src/License/PlaceholderResolver.php | 2 +- src/License/PlaceholderResolverInterface.php | 28 +++++ src/License/Reader.php | 56 ++++++--- src/License/ReaderInterface.php | 47 +++++++ src/License/Resolver.php | 2 +- src/License/ResolverInterface.php | 36 ++++++ src/License/TemplateLoader.php | 2 +- src/License/TemplateLoaderInterface.php | 29 +++++ tests/Command/CopyLicenseCommandTest.php | 103 +++++++++++++++ tests/Command/DependenciesCommandTest.php | 18 +-- .../DevToolsCommandProviderTest.php | 3 + tests/License/ReaderTest.php | 113 ++++++++++------- 17 files changed, 512 insertions(+), 129 deletions(-) create mode 100644 src/Command/CopyLicenseCommand.php create mode 100644 src/License/GeneratorInterface.php create mode 100644 src/License/PlaceholderResolverInterface.php create mode 100644 src/License/ReaderInterface.php create mode 100644 src/License/ResolverInterface.php create mode 100644 src/License/TemplateLoaderInterface.php create mode 100644 tests/Command/CopyLicenseCommandTest.php diff --git a/src/Command/CopyLicenseCommand.php b/src/Command/CopyLicenseCommand.php new file mode 100644 index 0000000..b09f6e4 --- /dev/null +++ b/src/Command/CopyLicenseCommand.php @@ -0,0 +1,118 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Command; + +use Composer\Factory; +use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\GeneratorInterface; +use FastForward\DevTools\License\PlaceholderResolver; +use FastForward\DevTools\License\Reader; +use FastForward\DevTools\License\Resolver; +use FastForward\DevTools\License\TemplateLoader; +use SplFileObject; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Generates and copies LICENSE files to projects. + * + * This command generates a LICENSE file if one does not exist and a supported + * license is declared in composer.json. + */ +final class CopyLicenseCommand extends AbstractCommand +{ + /** + * Creates a new CopyLicenseCommand instance. + * + * @param Filesystem|null $filesystem the filesystem component + * @param GeneratorInterface|null $generator the generator component + */ + public function __construct( + ?Filesystem $filesystem = null, + private readonly ?GeneratorInterface $generator = null, + ) { + parent::__construct($filesystem); + } + + /** + * @return GeneratorInterface + */ + private function getGenerator(): GeneratorInterface + { + return $this->generator ?? new Generator( + new Reader(new SplFileObject(Factory::getComposerFile())), + new Resolver(), + new TemplateLoader(), + new PlaceholderResolver(), + $this->filesystem, + ); + } + + /** + * Configures the current command. + * + * This method MUST define the name, description, and help text for the command. + */ + protected function configure(): void + { + $this + ->setName('license') + ->setDescription('Generates a LICENSE file from composer.json license information.') + ->setHelp( + 'This command generates a LICENSE file if one does not exist and a supported license is declared in composer.json.' + ); + } + + /** + * Executes the license generation process. + * + * Generates a LICENSE file if one does not exist and a supported license is declared in composer.json. + * + * @param InputInterface $input the input interface + * @param OutputInterface $output the output interface + * + * @return int the status code + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $targetPath = $this->getConfigFile('LICENSE', true); + + if ($this->filesystem->exists($targetPath)) { + $output->writeln('LICENSE file already exists. Skipping generation.'); + + return self::SUCCESS; + } + + $license = $this->getGenerator() + ->generate($targetPath); + + if (null === $license) { + $output->writeln( + 'No supported license found in composer.json or license is unsupported. Skipping LICENSE generation.' + ); + + return self::SUCCESS; + } + + $output->writeln('LICENSE file generated successfully.'); + + return self::SUCCESS; + } +} diff --git a/src/Command/SyncCommand.php b/src/Command/SyncCommand.php index 3674ead..2384a43 100644 --- a/src/Command/SyncCommand.php +++ b/src/Command/SyncCommand.php @@ -19,11 +19,6 @@ namespace FastForward\DevTools\Command; use Composer\Factory; -use FastForward\DevTools\License\Generator; -use FastForward\DevTools\License\Reader; -use FastForward\DevTools\License\Resolver; -use FastForward\DevTools\License\TemplateLoader; -use FastForward\DevTools\License\PlaceholderResolver; use Composer\Json\JsonManipulator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -81,7 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->addRepositoryWikiGitSubmodule(); $this->runCommand('gitignore', $output); $this->runCommand('skills', $output); - $this->generateLicense($output); + $this->runCommand('license', $output); return self::SUCCESS; } @@ -241,43 +236,4 @@ private function getGitRepositoryUrl(): string return trim($process->getOutput()); } - - /** - * Generates a LICENSE file if one does not exist and a supported license is declared in composer.json. - * - * @param OutputInterface $output the console output stream - * - * @return void - */ - private function generateLicense(OutputInterface $output): void - { - $targetPath = $this->getConfigFile('LICENSE', true); - - if ($this->filesystem->exists($targetPath)) { - $output->writeln('LICENSE file already exists. Skipping generation.'); - - return; - } - - $composer = $this->requireComposer(); - - $reader = new Reader($composer); - $resolver = new Resolver(); - $templateLoader = new TemplateLoader(); - $placeholderResolver = new PlaceholderResolver(); - - $generator = new Generator($reader, $resolver, $templateLoader, $placeholderResolver, $this->filesystem); - - $license = $generator->generate($targetPath); - - if (null === $license) { - $output->writeln( - 'No supported license found in composer.json or license is unsupported. Skipping LICENSE generation.' - ); - - return; - } - - $output->writeln('LICENSE file generated successfully.'); - } } diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index c031c1e..515f3e6 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -21,6 +21,7 @@ use FastForward\DevTools\Command\AbstractCommand; use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability; use FastForward\DevTools\Command\CodeStyleCommand; +use FastForward\DevTools\Command\CopyLicenseCommand; use FastForward\DevTools\Command\DependenciesCommand; use FastForward\DevTools\Command\DocsCommand; use FastForward\DevTools\Command\GitIgnoreCommand; @@ -62,6 +63,7 @@ public function getCommands() new SyncCommand(), new GitIgnoreCommand(), new SkillsCommand(), + new CopyLicenseCommand(), ]; } } diff --git a/src/License/Generator.php b/src/License/Generator.php index 9fc3257..45c99aa 100644 --- a/src/License/Generator.php +++ b/src/License/Generator.php @@ -20,7 +20,7 @@ use Symfony\Component\Filesystem\Filesystem; -final readonly class Generator +final readonly class Generator implements GeneratorInterface { /** * @param Reader $reader diff --git a/src/License/GeneratorInterface.php b/src/License/GeneratorInterface.php new file mode 100644 index 0000000..21425e1 --- /dev/null +++ b/src/License/GeneratorInterface.php @@ -0,0 +1,34 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +interface GeneratorInterface +{ + /** + * @param string $targetPath + * + * @return string|null + */ + public function generate(string $targetPath): ?string; + + /** + * @return bool + */ + public function hasLicense(): bool; +} diff --git a/src/License/PlaceholderResolver.php b/src/License/PlaceholderResolver.php index 139e78a..d28de3d 100644 --- a/src/License/PlaceholderResolver.php +++ b/src/License/PlaceholderResolver.php @@ -20,7 +20,7 @@ use function Safe\preg_replace; -final class PlaceholderResolver +final class PlaceholderResolver implements PlaceholderResolverInterface { /** * @param array{year?: int, organization?: string, author?: string, project?: string} $metadata diff --git a/src/License/PlaceholderResolverInterface.php b/src/License/PlaceholderResolverInterface.php new file mode 100644 index 0000000..564e90b --- /dev/null +++ b/src/License/PlaceholderResolverInterface.php @@ -0,0 +1,28 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +interface PlaceholderResolverInterface +{ + /** + * @param string $template + * @param array{year?: int, organization?: string, author?: string, project?: string} $metadata + */ + public function resolve(string $template, array $metadata): string; +} diff --git a/src/License/Reader.php b/src/License/Reader.php index 6398460..871d3b8 100644 --- a/src/License/Reader.php +++ b/src/License/Reader.php @@ -18,26 +18,49 @@ namespace FastForward\DevTools\License; -use Composer\Composer; -use Composer\Package\RootPackageInterface; +use Safe\Exceptions\JsonException; +use SplFileObject; -final readonly class Reader +use function Safe\json_decode; + +final readonly class Reader implements ReaderInterface { + private array $data; + /** - * @param Composer $composer + * @param SplFileObject $source The source file to read from, typically composer.json */ - public function __construct( - private Composer $composer - ) {} + public function __construct(SplFileObject $source) + { + $this->data = $this->readData($source); + } + + /** + * @param SplFileObject $source The source file to read from, typically composer.json + * + * @return array + * + * @throws JsonException if the JSON is invalid + */ + private function readData(SplFileObject $source): array + { + $content = $source->fread($source->getSize()); + + return json_decode($content, true); + } /** * @return string|null */ public function getLicense(): ?string { - $package = $this->composer->getPackage(); + $license = $this->data['license'] ?? []; - return $this->extractLicense($package); + if (\is_string($license)) { + return $license; + } + + return $this->extractLicense($license); } /** @@ -45,9 +68,7 @@ public function getLicense(): ?string */ public function getPackageName(): string { - $package = $this->composer->getPackage(); - - return $package->getName(); + return $this->data['name'] ?? ''; } /** @@ -55,8 +76,7 @@ public function getPackageName(): string */ public function getAuthors(): array { - $package = $this->composer->getPackage(); - $authors = $package->getAuthors(); + $authors = $this->data['authors'] ?? []; if ([] === $authors) { return []; @@ -80,7 +100,7 @@ public function getVendor(): ?string { $packageName = $this->getPackageName(); - if (null === $packageName) { + if ('' === $packageName) { return null; } @@ -102,14 +122,12 @@ public function getYear(): int } /** - * @param RootPackageInterface $package + * @param array $license * * @return string|null */ - private function extractLicense(RootPackageInterface $package): ?string + private function extractLicense(array $license): ?string { - $license = $package->getLicense(); - if ([] === $license) { return null; } diff --git a/src/License/ReaderInterface.php b/src/License/ReaderInterface.php new file mode 100644 index 0000000..06be9f1 --- /dev/null +++ b/src/License/ReaderInterface.php @@ -0,0 +1,47 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +interface ReaderInterface +{ + /** + * @return string|null + */ + public function getLicense(): ?string; + + /** + * @return string + */ + public function getPackageName(): string; + + /** + * @return array + */ + public function getAuthors(): array; + + /** + * @return string|null + */ + public function getVendor(): ?string; + + /** + * @return int + */ + public function getYear(): int; +} diff --git a/src/License/Resolver.php b/src/License/Resolver.php index 2e65d3c..3de3ab1 100644 --- a/src/License/Resolver.php +++ b/src/License/Resolver.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\License; -final class Resolver +final class Resolver implements ResolverInterface { private const array SUPPORTED_LICENSES = [ 'MIT' => 'mit.txt', diff --git a/src/License/ResolverInterface.php b/src/License/ResolverInterface.php new file mode 100644 index 0000000..3e018a1 --- /dev/null +++ b/src/License/ResolverInterface.php @@ -0,0 +1,36 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +interface ResolverInterface +{ + /** + * @param string $license + * + * @return bool + */ + public function isSupported(string $license): bool; + + /** + * @param string $license + * + * @return string|null + */ + public function resolve(string $license): ?string; +} diff --git a/src/License/TemplateLoader.php b/src/License/TemplateLoader.php index 06562b8..6c72881 100644 --- a/src/License/TemplateLoader.php +++ b/src/License/TemplateLoader.php @@ -22,7 +22,7 @@ use function Safe\file_get_contents; -final readonly class TemplateLoader +final readonly class TemplateLoader implements TemplateLoaderInterface { private string $templatesPath; diff --git a/src/License/TemplateLoaderInterface.php b/src/License/TemplateLoaderInterface.php new file mode 100644 index 0000000..4b8f797 --- /dev/null +++ b/src/License/TemplateLoaderInterface.php @@ -0,0 +1,29 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\License; + +interface TemplateLoaderInterface +{ + /** + * @param string $templateFilename + * + * @return string + */ + public function load(string $templateFilename): string; +} diff --git a/tests/Command/CopyLicenseCommandTest.php b/tests/Command/CopyLicenseCommandTest.php new file mode 100644 index 0000000..1cf0a41 --- /dev/null +++ b/tests/Command/CopyLicenseCommandTest.php @@ -0,0 +1,103 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Command; + +use FastForward\DevTools\Command\CopyLicenseCommand; +use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\PlaceholderResolver; +use FastForward\DevTools\License\Reader; +use FastForward\DevTools\License\Resolver; +use FastForward\DevTools\License\TemplateLoader; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; + +#[CoversClass(CopyLicenseCommand::class)] +#[UsesClass(Reader::class)] +#[UsesClass(Resolver::class)] +#[UsesClass(TemplateLoader::class)] +#[UsesClass(PlaceholderResolver::class)] +#[UsesClass(Generator::class)] +final class CopyLicenseCommandTest extends AbstractCommandTestCase +{ + use ProphecyTrait; + + /** + * @return string + */ + protected function getCommandClass(): string + { + return CopyLicenseCommand::class; + } + + /** + * @return string + */ + protected function getCommandName(): string + { + return 'license'; + } + + /** + * @return string + */ + protected function getCommandDescription(): string + { + return 'Generates a LICENSE file from composer.json license information.'; + } + + /** + * @return string + */ + protected function getCommandHelp(): string + { + return 'This command generates a LICENSE file if one does not exist and a supported license is declared in composer.json.'; + } + + /** + * @return void + */ + #[Test] + public function executeWillReturnSuccessAndWriteInfo(): void + { + $this->filesystem->exists(Argument::type('string'))->willReturn(false); + $this->filesystem->dumpFile(Argument::cetera())->shouldBeCalled(); + + $this->output->writeln(Argument::type('string')) + ->shouldBeCalled(); + + self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); + } + + /** + * @return void + */ + #[Test] + public function executeWillSkipWhenLicenseFileExists(): void + { + $this->filesystem->exists(Argument::type('string'))->willReturn(true); + + $this->output->writeln(Argument::type('string')) + ->shouldBeCalled(); + + self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); + } +} diff --git a/tests/Command/DependenciesCommandTest.php b/tests/Command/DependenciesCommandTest.php index 57976bd..6a8ba4b 100644 --- a/tests/Command/DependenciesCommandTest.php +++ b/tests/Command/DependenciesCommandTest.php @@ -70,6 +70,8 @@ protected function setUp(): void { parent::setUp(); + $this->output->writeln(Argument::type('string')); + $cwd = getcwd(); $this->filesystem->exists($cwd . '/composer.json')->willReturn(true); } @@ -104,10 +106,6 @@ public function executeWillReturnSuccessWhenBothToolsSucceed(): void $this->output->writeln('Running dependency analysis...') ->shouldBeCalled(); - $this->output->writeln( - 'Warning: TTY is not supported. The command may not display output as expected.' - ) - ->shouldBeCalled(); self::assertSame(DependenciesCommand::SUCCESS, $this->invokeExecute()); } @@ -142,10 +140,6 @@ public function executeWillReturnFailureWhenFirstToolFails(): void $this->output->writeln('Running dependency analysis...') ->shouldBeCalled(); - $this->output->writeln( - 'Warning: TTY is not supported. The command may not display output as expected.' - ) - ->shouldBeCalled(); self::assertSame(DependenciesCommand::FAILURE, $this->invokeExecute()); } @@ -180,10 +174,6 @@ public function executeWillReturnFailureWhenSecondToolFails(): void $this->output->writeln('Running dependency analysis...') ->shouldBeCalled(); - $this->output->writeln( - 'Warning: TTY is not supported. The command may not display output as expected.' - ) - ->shouldBeCalled(); self::assertSame(DependenciesCommand::FAILURE, $this->invokeExecute()); } @@ -230,10 +220,6 @@ public function executeWillCallBothDependencyToolsWithComposerJson(): void $this->output->writeln('Running dependency analysis...') ->shouldBeCalled(); - $this->output->writeln( - 'Warning: TTY is not supported. The command may not display output as expected.' - ) - ->shouldBeCalled(); self::assertSame(DependenciesCommand::SUCCESS, $this->invokeExecute()); } diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index b867d01..9067fdd 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -20,6 +20,7 @@ use FastForward\DevTools\Command\AbstractCommand; use FastForward\DevTools\Command\CodeStyleCommand; +use FastForward\DevTools\Command\CopyLicenseCommand; use FastForward\DevTools\Command\DependenciesCommand; use FastForward\DevTools\Command\DocsCommand; use FastForward\DevTools\Command\GitIgnoreCommand; @@ -53,6 +54,7 @@ #[UsesClass(SyncCommand::class)] #[UsesClass(GitIgnoreCommand::class)] #[UsesClass(SkillsCommand::class)] +#[UsesClass(CopyLicenseCommand::class)] #[UsesClass(SkillsSynchronizer::class)] #[UsesClass(Merger::class)] #[UsesClass(Writer::class)] @@ -88,6 +90,7 @@ public function getCommandsWillReturnAllSupportedCommandsInExpectedOrder(): void new SyncCommand(), new GitIgnoreCommand(), new SkillsCommand(), + new CopyLicenseCommand(), ], $this->commandProvider->getCommands(), ); diff --git a/tests/License/ReaderTest.php b/tests/License/ReaderTest.php index 01e0eef..a4907ab 100644 --- a/tests/License/ReaderTest.php +++ b/tests/License/ReaderTest.php @@ -18,52 +18,66 @@ namespace FastForward\DevTools\Tests\License; -use PHPUnit\Framework\MockObject\MockObject; use FastForward\DevTools\License\Reader; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Composer\Composer; -use Composer\Package\RootPackageInterface; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use SplFileObject; + +use function Safe\json_encode; #[CoversClass(Reader::class)] final class ReaderTest extends TestCase { use ProphecyTrait; - private Reader $reader; + /** + * @param array $data + * + * @return void + */ + private function createReader(array $data): Reader + { + $json = json_encode($data, \JSON_PRETTY_PRINT); - private MockObject $composer; + /** @var ObjectProphecy $file */ + $file = $this->prophesize(SplFileObject::class); + $file->getSize() + ->willReturn(\strlen($json)); + $file->fread(\strlen($json)) + ->willReturn($json); - private MockObject $package; + return new Reader($file->reveal()); + } /** * @return void */ - protected function setUp(): void + #[Test] + public function getLicenseWithSingleLicenseWillReturnLicenseString(): void { - parent::setUp(); - - $this->composer = $this->createMock(Composer::class); - $this->package = $this->createMock(RootPackageInterface::class); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + 'license' => ['MIT'], + ]); - $this->composer->method('getPackage') - ->willReturn($this->package); - - $this->reader = new Reader($this->composer); + self::assertSame('MIT', $reader->getLicense()); } /** * @return void */ #[Test] - public function getLicenseWithSingleLicenseWillReturnLicenseString(): void + public function getLicenseWithStringLicenseWillReturnLicenseString(): void { - $this->package->method('getLicense') - ->willReturn(['MIT']); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + 'license' => 'MIT', + ]); - self::assertSame('MIT', $this->reader->getLicense()); + self::assertSame('MIT', $reader->getLicense()); } /** @@ -72,10 +86,11 @@ public function getLicenseWithSingleLicenseWillReturnLicenseString(): void #[Test] public function getLicenseWithNoLicenseWillReturnNull(): void { - $this->package->method('getLicense') - ->willReturn([]); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + ]); - self::assertNull($this->reader->getLicense()); + self::assertNull($reader->getLicense()); } /** @@ -84,10 +99,12 @@ public function getLicenseWithNoLicenseWillReturnNull(): void #[Test] public function getLicenseWithMultipleLicensesWillReturnNull(): void { - $this->package->method('getLicense') - ->willReturn(['MIT', 'Apache-2.0']); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + 'license' => ['MIT', 'Apache-2.0'], + ]); - self::assertNull($this->reader->getLicense()); + self::assertNull($reader->getLicense()); } /** @@ -96,10 +113,11 @@ public function getLicenseWithMultipleLicensesWillReturnNull(): void #[Test] public function getPackageNameWillReturnPackageName(): void { - $this->package->method('getName') - ->willReturn('fast-forward/dev-tools'); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + ]); - self::assertSame('fast-forward/dev-tools', $this->reader->getPackageName()); + self::assertSame('fast-forward/dev-tools', $reader->getPackageName()); } /** @@ -108,12 +126,11 @@ public function getPackageNameWillReturnPackageName(): void #[Test] public function getVendorWillExtractVendorFromPackageName(): void { - $this->package->method('getName') - ->willReturn('fast-forward/dev-tools'); - $this->package->method('getLicense') - ->willReturn(['MIT']); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + ]); - self::assertSame('fast-forward', $this->reader->getVendor()); + self::assertSame('fast-forward', $reader->getVendor()); } /** @@ -122,12 +139,11 @@ public function getVendorWillExtractVendorFromPackageName(): void #[Test] public function getVendorWithSingleNamePackageWillReturnNull(): void { - $this->package->method('getName') - ->willReturn('dev-tools'); - $this->package->method('getLicense') - ->willReturn(['MIT']); + $reader = $this->createReader([ + 'name' => 'dev-tools', + ]); - self::assertNull($this->reader->getVendor()); + self::assertNull($reader->getVendor()); } /** @@ -145,10 +161,12 @@ public function getAuthorsWillReturnAuthorsArray(): void ], ]; - $this->package->method('getAuthors') - ->willReturn($authors); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + 'authors' => $authors, + ]); - self::assertSame($authors, $this->reader->getAuthors()); + self::assertSame($authors, $reader->getAuthors()); } /** @@ -157,10 +175,11 @@ public function getAuthorsWillReturnAuthorsArray(): void #[Test] public function getAuthorsWithNoAuthorsWillReturnEmptyArray(): void { - $this->package->method('getAuthors') - ->willReturn([]); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + ]); - self::assertSame([], $this->reader->getAuthors()); + self::assertSame([], $reader->getAuthors()); } /** @@ -169,6 +188,10 @@ public function getAuthorsWithNoAuthorsWillReturnEmptyArray(): void #[Test] public function getYearWillReturnCurrentYear(): void { - self::assertSame((int) date('Y'), $this->reader->getYear()); + $reader = $this->createReader([ + 'name' => 'fast-forward/dev-tools', + ]); + + self::assertSame((int) date('Y'), $reader->getYear()); } } From df8564e9c9e32508bd241345546304616c396b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 10 Apr 2026 00:50:46 -0300 Subject: [PATCH 4/4] feat(license): add command and documentation for LICENSE file generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- README.md | 3 +++ docs/api/commands.rst | 9 ++++++--- docs/running/specialized-commands.rst | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f37de0..300fc69 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ composer dev-tools skills # Merges and synchronizes .gitignore files composer dev-tools gitignore +# Generates a LICENSE file from composer.json license information +composer dev-tools license + # Installs and synchronizes dev-tools scripts, GitHub Actions workflows, # .editorconfig, .gitignore rules, packaged skills, and the repository wiki # submodule in .github/wiki diff --git a/docs/api/commands.rst b/docs/api/commands.rst index 9226d2a..07628a7 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -49,6 +49,9 @@ resolution, configuration fallback, PSR-4 lookup, and child-command dispatch. - ``dev-tools:sync`` - Synchronizes consumer-facing scripts, automation assets, and packaged skills. - * - ``FastForward\DevTools\Command\GitIgnoreCommand`` - - ``gitignore`` - - Merges and synchronizes .gitignore files. + * - ``FastForward\DevTools\Command\GitIgnoreCommand`` + - ``gitignore`` + - Merges and synchronizes .gitignore files. + * - ``FastForward\DevTools\Command\CopyLicenseCommand`` + - ``license`` + - Generates a LICENSE file from composer.json license information. diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index c2f53a3..0ba25d7 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -199,3 +199,22 @@ Important details: - duplicates are removed and entries are sorted alphabetically; - it uses the Reader, Merger, and Writer components from the GitIgnore namespace. + +``license`` +---------- + +Generates a LICENSE file from composer.json license information. + +.. code-block:: bash + + composer dev-tools license + +Important details: + +- it reads the ``license`` field from ``composer.json``; +- it supports common open-source licenses (MIT, Apache-2.0, BSD-2-Clause, + BSD-3-Clause, GPL-3.0, LGPL-3.0, and MPL-2.0); +- it resolves placeholders such as ``[year]``, ``[author]``, and + ``[project]`` using information from ``composer.json``; +- it uses template files from ``resources/license-templates/``; +- it skips generation if a LICENSE file already exists.