├── .php-version ├── .dockerignore ├── dummy ├── docs │ ├── _static │ │ └── .gitkeep │ ├── requirements.txt │ ├── index.rst │ └── tutorial │ │ └── introduction_one.rst └── .doctor-rst.yaml ├── tests ├── Fixtures │ ├── max_blanklines.rst │ └── Rule │ │ ├── DummyRule.php │ │ ├── DummyFileInfoRule.php │ │ ├── DummyFileContentRule.php │ │ └── DummyLineContentRule.php ├── Util │ ├── ListItemTraitWrapper.php │ └── DirectiveTraitWrapper.php ├── RstSample.php ├── Analyzer │ └── RstAnalyzerTest.php ├── Helper │ ├── YamlHelperTest.php │ ├── TwigHelperTest.php │ └── XmlHelperTest.php ├── Formatter │ └── RegistryTest.php ├── Rule │ ├── NoEmptyLiteralsTest.php │ ├── AbstractLineContentRuleTestCase.php │ ├── NoComposerReqTest.php │ ├── NoFootnotesTest.php │ ├── NoPhpPrefixBeforeComposerTest.php │ ├── YarnDevOptionAtTheEndTest.php │ ├── YarnDevOptionNotAtTheEndTest.php │ ├── LineLengthTest.php │ ├── NoPhpPrefixBeforeBinConsoleTest.php │ ├── ComposerDevOptionAtTheEndTest.php │ ├── ComposerDevOptionNotAtTheEndTest.php │ ├── ExtensionXlfInsteadOfXliffTest.php │ ├── UseHttpsXsdUrlsTest.php │ ├── SpaceBetweenLabelAndLinkInDocTest.php │ ├── NoBracketsInMethodDirectiveTest.php │ ├── SpaceBetweenLabelAndLinkInRefTest.php │ ├── NoInheritdocInCodeExamplesTest.php │ ├── RemoveTrailingWhitespaceTest.php │ ├── DeprecatedDirectiveMinVersionTest.php │ └── ValidUseStatementsTests.php ├── UnitTestCase.php ├── Rst │ └── Value │ │ ├── LinkUsageTest.php │ │ ├── DirectiveContentTest.php │ │ └── LinkDefinitionTest.php └── Value │ ├── ExcludedViolationListTest.php │ └── LinesTest.php ├── entrypoint.sh ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build-docker.yaml │ └── documentation.yaml ├── bin ├── doctor-rst └── phpunit ├── box.json.dist ├── .gitignore ├── phpstan.neon.dist ├── action.yml ├── composer-require-checker.json ├── src ├── Attribute │ └── Rule │ │ ├── Description.php │ │ ├── ValidExample.php │ │ └── InvalidExample.php ├── Helper │ ├── YamlHelper.php │ ├── Helper.php │ ├── TwigHelper.php │ └── XmlHelper.php ├── Rule │ ├── Configurable.php │ ├── FileInfoRule.php │ ├── LineContentRule.php │ ├── Rule.php │ ├── FileContentRule.php │ ├── CheckListRule.php │ ├── AbstractRule.php │ ├── NoAppBundle.php │ ├── ShortArraySyntax.php │ ├── NoComposerPhar.php │ ├── NoPhpPrefixBeforeComposer.php │ ├── NoComposerReq.php │ ├── FinalAdminClasses.php │ ├── NoSpaceBeforeSelfXmlClosingTag.php │ ├── SpaceBeforeSelfXmlClosingTag.php │ ├── FinalAdminExtensionClasses.php │ ├── NoAppConsole.php │ ├── NoMergeConflict.php │ ├── NoConfigYaml.php │ ├── UseHttpsXsdUrls.php │ ├── YarnDevOptionNotAtTheEnd.php │ ├── EnsureCorrectFormatForPhpfunction.php │ ├── NoInheritdocInCodeExamples.php │ ├── ValidUseStatements.php │ ├── NoFootnotes.php │ ├── RemoveTrailingWhitespace.php │ ├── NoPhpPrefixBeforeBinConsole.php │ ├── NoBrokenRefDirective.php │ ├── KernelInsteadOfAppKernel.php │ ├── NoAttributeRedundantParenthesis.php │ ├── MaxColons.php │ ├── LowercaseAsInUseStatements.php │ ├── NoEmptyLiterals.php │ ├── StringReplacement.php │ ├── ComposerDevOptionAtTheEnd.php │ ├── ComposerDevOptionNotAtTheEnd.php │ ├── YarnDevOptionAtTheEnd.php │ ├── ExtensionXlfInsteadOfXliff.php │ ├── NoAdminYaml.php │ ├── NoBracketsInMethodDirective.php │ ├── EnsureExactlyOneSpaceBeforeDirectiveType.php │ ├── EnsureClassConstant.php │ ├── FilenameUsesUnderscoresOnly.php │ ├── UseNamedConstructorWithoutNewKeywordRule.php │ ├── ExtendAbstractAdmin.php │ ├── NoPhpOpenTagInCodeBlockPhpDirective.php │ ├── LineLength.php │ ├── UseDeprecatedDirectiveInsteadOfVersionadded.php │ ├── EnsureExactlyOneSpaceBetweenLinkDefinitionAndLink.php │ ├── BlankLineAfterAnchor.php │ ├── ExtendController.php │ ├── NoTypographicQuotes.php │ ├── ExtendAbstractController.php │ ├── NonStaticPhpunitAssertions.php │ ├── NoDirectiveAfterShorthand.php │ ├── TitleUnderlineLengthMustMatchTitleLength.php │ ├── BlankLineAfterColon.php │ ├── BlankLineBeforeDirective.php │ ├── BeKindToNewcomers.php │ ├── PhpOpenTagInCodeBlockPhpDirective.php │ ├── OnlyBackslashesInUseStatementsInPhpCodeBlock.php │ ├── NoBashPrompt.php │ ├── OnlyBackslashesInNamespaceInPhpCodeBlock.php │ ├── EnsureGithubDirectiveStartWithPrefix.php │ ├── SpaceBetweenLabelAndLinkInDoc.php │ └── EnsureAttributeBetweenBackticksInContent.php ├── Analyzer │ ├── Analyzer.php │ ├── RuleFilter.php │ ├── Cache.php │ ├── MemoizingAnalyzer.php │ └── InMemoryCache.php ├── Value │ ├── ViolationInterface.php │ ├── FileResult.php │ ├── RuleName.php │ ├── NullViolation.php │ ├── RuleGroup.php │ ├── Violation.php │ └── RulesConfiguration.php ├── Formatter │ ├── Formatter.php │ ├── Exception │ │ └── FormatterNotFound.php │ ├── Registry.php │ └── GithubFormatter.php └── Rst │ └── Value │ ├── LinkName.php │ ├── LinkUrl.php │ ├── LinkUsage.php │ ├── LinkDefinition.php │ └── DirectiveContent.php ├── phpstan-baseline.neon ├── config ├── cache.php └── services.php ├── composer-unused.php ├── phpunit.xml.dist ├── LICENSE ├── Dockerfile └── Makefile /.php-version: -------------------------------------------------------------------------------- 1 | 8.4 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /dummy 2 | -------------------------------------------------------------------------------- /dummy/docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | gitkeep -------------------------------------------------------------------------------- /tests/Fixtures/max_blanklines.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | 3 | sh -c "php /usr/bin/doctor-rst analyze $DOCS_DIR $*" 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [OskarStark] 4 | -------------------------------------------------------------------------------- /dummy/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx!=1.8.0 2 | git+https://github.com/fabpot/sphinx-php.git 3 | sphinx_rtd_theme 4 | -------------------------------------------------------------------------------- /bin/doctor-rst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 9 | -------------------------------------------------------------------------------- /box.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "config/services.php", 4 | "config/cache.php" 5 | ], 6 | "compression": "GZ", 7 | "main": "bin/doctor-rst", 8 | "git-version": "git-version" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.php-cs-fixer.php 3 | /.php-cs-fixer.cache 4 | .doctor-rst.cache 5 | phpstan.neon 6 | 7 | ###> symfony/phpunit-bridge ### 8 | .phpunit 9 | /phpunit.xml 10 | .phpunit.result.cache 11 | ###< symfony/phpunit-bridge ### 12 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | ignoreErrors: 6 | - 7 | identifier: missingType.iterableValue 8 | - 9 | identifier: missingType.generics 10 | paths: 11 | - src 12 | - tests 13 | level: max 14 | -------------------------------------------------------------------------------- /dummy/docs/index.rst: -------------------------------------------------------------------------------- 1 | Doctrine ORM Admin Bundle 2 | ========================= 3 | 4 | The ``Doctrine ORM Admin`` provides services to work with the ``Admin Bundle`` and the ``Doctrine Project``. 5 | 6 | .. toctree:: 7 | :caption: Tutorials 8 | :name: tutorials 9 | :maxdepth: 1 10 | :numbered: 11 | 12 | tutorial/introduction_one 13 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/metadata-syntax-for-github-actions 2 | 3 | author: 'OskarStark' 4 | 5 | branding: 6 | icon: 'check' 7 | color: 'blue' 8 | 9 | description: 'Check *.rst files against given rules.' 10 | 11 | name: 'DOCtor-RST' 12 | 13 | runs: 14 | using: 'docker' 15 | image: 'docker://oskarstark/doctor-rst:1.27.0' 16 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "array", 4 | "bool", 5 | "callable", 6 | "dump", 7 | "false", 8 | "float", 9 | "int", 10 | "iterable", 11 | "mixed", 12 | "null", 13 | "object", 14 | "parent", 15 | "self", 16 | "static", 17 | "string", 18 | "true", 19 | "void", 20 | "never" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/Rule/DummyRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Fixtures\Rule; 15 | 16 | use App\Rule\AbstractRule; 17 | use App\Rule\Rule; 18 | 19 | final class DummyRule extends AbstractRule implements Rule 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Attribute/Rule/Description.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Attribute\Rule; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS)] 17 | class Description 18 | { 19 | public function __construct( 20 | public readonly string $value, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helper/YamlHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Helper; 15 | 16 | use App\Value\Line; 17 | 18 | final class YamlHelper 19 | { 20 | public static function isComment(Line $line): bool 21 | { 22 | return [] !== $line->clean()->match('/^#(.*)/'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Attribute/Rule/ValidExample.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Attribute\Rule; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] 17 | class ValidExample 18 | { 19 | public function __construct( 20 | public readonly string $value, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Attribute/Rule/InvalidExample.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Attribute\Rule; 15 | 16 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] 17 | class InvalidExample 18 | { 19 | public function __construct( 20 | public readonly string $value, 21 | ) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Call to function method_exists\(\) with ''App\\\\Tests\\\\Util\\\\DirectiveTraitWrapper'' and ''in'' will always evaluate to true\.$#' 5 | identifier: function.alreadyNarrowedType 6 | count: 1 7 | path: tests/Traits/DirectiveTraitTest.php 8 | 9 | - 10 | message: '#^Call to function method_exists\(\) with ''App\\\\Tests\\\\Util\\\\ListItemTraitWrapper'' and ''isPartOfListItem'' will always evaluate to true\.$#' 11 | identifier: function.alreadyNarrowedType 12 | count: 1 13 | path: tests/Traits/ListTraitTest.php 14 | -------------------------------------------------------------------------------- /dummy/docs/tutorial/introduction_one.rst: -------------------------------------------------------------------------------- 1 | .. index:: 2 | single: Tutorial 3 | 4 | Introduction 5 | ============ 6 | 7 | This is a tutorial & how-to for creating your first ``Admin`` classes using the ``AdminBundle``. 8 | In this example, we'll create the backend of a blog application. 9 | 10 | The tutorial will explain how to define `orphanRemoval`_.: 11 | 12 | * Entities, 13 | * The routing, 14 | * CRUD controller, 15 | * The ``Admin`` class. 16 | 17 | 18 | 19 | .. _`orphanRemoval`: https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/working-with-associations.html#orphan-removal 20 | -------------------------------------------------------------------------------- /src/Rule/Configurable.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use Symfony\Component\OptionsResolver\OptionsResolver; 17 | 18 | interface Configurable 19 | { 20 | public function configureOptions(OptionsResolver $resolver): OptionsResolver; 21 | 22 | public function setOptions(array $options): void; 23 | } 24 | -------------------------------------------------------------------------------- /src/Analyzer/Analyzer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Analyzer; 15 | 16 | use App\Rule\Rule; 17 | use App\Value\Violation; 18 | 19 | interface Analyzer 20 | { 21 | /** 22 | * @param Rule[] $rules 23 | * 24 | * @return Violation[] 25 | */ 26 | public function analyze(\SplFileInfo $file, array $rules): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Helper/Helper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Helper; 15 | 16 | class Helper 17 | { 18 | public static function cloneIterator(\ArrayIterator $iterator, int $number): \ArrayIterator 19 | { 20 | $clone = new \ArrayIterator($iterator->getArrayCopy()); 21 | $clone->seek($number); 22 | 23 | return $clone; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Value/ViolationInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Value; 15 | 16 | interface ViolationInterface 17 | { 18 | public function message(): string; 19 | 20 | public function filename(): string; 21 | 22 | public function lineno(): int; 23 | 24 | public function rawLine(): string; 25 | 26 | public function isNull(): bool; 27 | } 28 | -------------------------------------------------------------------------------- /src/Rule/FileInfoRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\ViolationInterface; 17 | 18 | /** 19 | * Rules using this interface are only run once, 20 | * and get a \SplFileInfo containing infos of the file. 21 | */ 22 | interface FileInfoRule extends Rule 23 | { 24 | public function check(\SplFileInfo $file): ViolationInterface; 25 | } 26 | -------------------------------------------------------------------------------- /src/Formatter/Formatter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Formatter; 15 | 16 | use App\Value\AnalyzerResult; 17 | use Symfony\Component\Console\Style\OutputStyle; 18 | 19 | interface Formatter 20 | { 21 | public function format(OutputStyle $style, AnalyzerResult $analyzerResult, string $analyzeDir, bool $showValidFiles): void; 22 | 23 | public function name(): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/LineContentRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\ViolationInterface; 18 | 19 | /** 20 | * Rules using this interface run for every line of the file content. 21 | */ 22 | interface LineContentRule extends Rule 23 | { 24 | public function check(Lines $lines, int $number, string $filename): ViolationInterface; 25 | } 26 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Formatter\Exception; 15 | 16 | final class FormatterNotFound extends \InvalidArgumentException 17 | { 18 | public static function byName(string $name): self 19 | { 20 | return new self(\sprintf( 21 | 'Formatter "%s" not found', 22 | $name, 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Rule/Rule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\RuleGroup; 17 | use App\Value\RuleName; 18 | 19 | interface Rule 20 | { 21 | public static function getName(): RuleName; 22 | 23 | /** 24 | * @return RuleGroup[] 25 | */ 26 | public static function getGroups(): array; 27 | 28 | public static function runOnlyOnBlankline(): bool; 29 | 30 | public static function isExperimental(): bool; 31 | } 32 | -------------------------------------------------------------------------------- /tests/Util/ListItemTraitWrapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Util; 15 | 16 | use App\Traits\ListTrait; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class ListItemTraitWrapper 22 | { 23 | use ListTrait { 24 | ListTrait::isPartOfListItem as public; 25 | ListTrait::isPartOfFootnote as public; 26 | ListTrait::isPartOfRstComment as public; 27 | ListTrait::isPartOfLineNumberAnnotation as public; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/Rule/DummyFileInfoRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Fixtures\Rule; 15 | 16 | use App\Rule\AbstractRule; 17 | use App\Rule\FileInfoRule; 18 | use App\Value\NullViolation; 19 | use App\Value\ViolationInterface; 20 | 21 | final class DummyFileInfoRule extends AbstractRule implements FileInfoRule 22 | { 23 | public function check(\SplFileInfo $file): ViolationInterface 24 | { 25 | return NullViolation::create(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Rule/FileContentRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\ViolationInterface; 18 | 19 | /** 20 | * Rules using this interface are only run once per file, and are 21 | * responsible to check the whole file content on its own, if needed. 22 | * They always start on the first line of the document. 23 | */ 24 | interface FileContentRule extends Rule 25 | { 26 | public function check(Lines $lines, string $filename): ViolationInterface; 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/Rule/DummyFileContentRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Fixtures\Rule; 15 | 16 | use App\Rule\AbstractRule; 17 | use App\Rule\FileContentRule; 18 | use App\Value\Lines; 19 | use App\Value\NullViolation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class DummyFileContentRule extends AbstractRule implements FileContentRule 23 | { 24 | public function check(Lines $lines, string $filename): ViolationInterface 25 | { 26 | return NullViolation::create(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Analyzer/RuleFilter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Analyzer; 15 | 16 | use App\Rule\Rule; 17 | 18 | final class RuleFilter 19 | { 20 | /** 21 | * @template T of Rule 22 | * 23 | * @param Rule[] $rules 24 | * @param class-string $type 25 | * 26 | * @return T[] 27 | */ 28 | public static function byType(array $rules, string $type): array 29 | { 30 | return array_filter($rules, static fn (Rule $rule): bool => $rule instanceof $type); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/Rule/DummyLineContentRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Fixtures\Rule; 15 | 16 | use App\Rule\AbstractRule; 17 | use App\Rule\LineContentRule; 18 | use App\Value\Lines; 19 | use App\Value\NullViolation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class DummyLineContentRule extends AbstractRule implements LineContentRule 23 | { 24 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 25 | { 26 | return NullViolation::create(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Value/FileResult.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Value; 15 | 16 | final readonly class FileResult 17 | { 18 | public function __construct( 19 | private \SplFileInfo $file, 20 | private ExcludedViolationList $violationList, 21 | ) { 22 | } 23 | 24 | public function filename(): string 25 | { 26 | return (string) $this->file->getRealPath(); 27 | } 28 | 29 | public function violationList(): ExcludedViolationList 30 | { 31 | return $this->violationList; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | use App\Analyzer\Cache; 15 | use App\Analyzer\FileCache; 16 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 17 | 18 | return static function (ContainerConfigurator $container): void { 19 | $services = $container->services(); 20 | 21 | $services->defaults() 22 | ->autowire() 23 | ->autoconfigure(); 24 | 25 | $services->set(FileCache::class) 26 | ->arg('$cacheFile', '%cache.file%'); 27 | 28 | $services->alias(Cache::class, FileCache::class); 29 | }; 30 | -------------------------------------------------------------------------------- /tests/RstSample.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests; 15 | 16 | use App\Value\Lines; 17 | 18 | final readonly class RstSample 19 | { 20 | public Lines $lines; 21 | 22 | /** 23 | * @param array|string $content 24 | */ 25 | public function __construct( 26 | array|string $content, 27 | public int $lineNumber = 0, 28 | ) { 29 | if (!\is_array($content)) { 30 | $content = explode(\PHP_EOL, $content); 31 | } 32 | 33 | $this->lines = Lines::fromArray($content); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Util/DirectiveTraitWrapper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Util; 15 | 16 | use App\Traits\DirectiveTrait; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class DirectiveTraitWrapper 22 | { 23 | use DirectiveTrait { 24 | DirectiveTrait::getDirectiveContent as public; 25 | DirectiveTrait::getLineNumberOfDirective as public; 26 | DirectiveTrait::in as public; 27 | DirectiveTrait::inPhpCodeBlock as public; 28 | DirectiveTrait::inShellCodeBlock as public; 29 | DirectiveTrait::previousDirectiveIs as public; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Rst/Value/LinkName.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rst\Value; 15 | 16 | final readonly class LinkName 17 | { 18 | private string $value; 19 | 20 | private function __construct(string $value) 21 | { 22 | $this->value = trim($value); 23 | } 24 | 25 | public static function fromString(string $value): self 26 | { 27 | return new self($value); 28 | } 29 | 30 | public function value(): string 31 | { 32 | return $this->value; 33 | } 34 | 35 | public function toLower(): string 36 | { 37 | return strtolower($this->value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Rst/Value/LinkUrl.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rst\Value; 15 | 16 | use Webmozart\Assert\Assert; 17 | 18 | final readonly class LinkUrl 19 | { 20 | private string $value; 21 | 22 | private function __construct(string $value) 23 | { 24 | $value = trim($value); 25 | 26 | Assert::stringNotEmpty($value); 27 | Assert::notWhitespaceOnly($value); 28 | 29 | $this->value = $value; 30 | } 31 | 32 | public static function fromString(string $value): self 33 | { 34 | return new self($value); 35 | } 36 | 37 | public function value(): string 38 | { 39 | return $this->value; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer-unused.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | use ComposerUnused\ComposerUnused\Configuration\Configuration; 15 | use ComposerUnused\ComposerUnused\Configuration\NamedFilter; 16 | use Webmozart\Glob\Glob; 17 | 18 | return static fn (Configuration $config): Configuration => $config 19 | ->addNamedFilter(NamedFilter::fromString('ext-iconv')) 20 | ->setAdditionalFilesFor('oskarstark/doctor-rst', [ 21 | __FILE__, 22 | ...array_merge( 23 | Glob::glob(__DIR__.'/bin/*.php'), 24 | Glob::glob(__DIR__.'/config/*.php'), 25 | Glob::glob(__DIR__.'/public/*.php'), 26 | Glob::glob(__DIR__.'/templates/*.php'), 27 | ), 28 | ]); 29 | -------------------------------------------------------------------------------- /src/Analyzer/Cache.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Analyzer; 15 | 16 | use App\Rule\Rule; 17 | use App\Value\Violation; 18 | 19 | interface Cache 20 | { 21 | /** 22 | * @param Rule[] $rules 23 | */ 24 | public function has(\SplFileInfo $file, array $rules): bool; 25 | 26 | /** 27 | * @param Rule[] $rules 28 | * 29 | * @return Violation[] 30 | */ 31 | public function get(\SplFileInfo $file, array $rules): array; 32 | 33 | /** 34 | * @param Rule[] $rules 35 | * @param Violation[] $violations 36 | */ 37 | public function set(\SplFileInfo $file, array $rules, array $violations): void; 38 | 39 | public function write(): void; 40 | } 41 | -------------------------------------------------------------------------------- /src/Helper/TwigHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Helper; 15 | 16 | use App\Value\Line; 17 | 18 | final class TwigHelper 19 | { 20 | public static function isComment(Line $line, ?bool $closed = null): bool 21 | { 22 | $string = $line->clean(); 23 | 24 | if ($string->equalsTo(['{#', '#}'])) { 25 | return true; 26 | } 27 | 28 | if (null === $closed && $string->startsWith('{#')) { 29 | return true; 30 | } 31 | 32 | return $string->startsWith('{#') 33 | && ( 34 | ($closed && $string->endsWith('#}')) 35 | || (!$closed && !$string->endsWith('#}')) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 23 | 24 | src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#target-branch 2 | 3 | version: 2 4 | 5 | updates: 6 | - 7 | allow: 8 | - dependency-type: "direct" 9 | commit-message: 10 | include: "scope" 11 | prefix: "composer" 12 | directory: "/" 13 | labels: 14 | - "dependencies" 15 | - "automerge" 16 | open-pull-requests-limit: 5 17 | package-ecosystem: "composer" 18 | schedule: 19 | interval: "daily" 20 | versioning-strategy: "lockfile-only" 21 | 22 | - 23 | commit-message: 24 | include: "scope" 25 | prefix: "github-actions" 26 | directory: "/" 27 | labels: 28 | - "dependencies" 29 | - "automerge" 30 | open-pull-requests-limit: 5 31 | package-ecosystem: "github-actions" 32 | schedule: 33 | interval: "daily" 34 | -------------------------------------------------------------------------------- /dummy/.doctor-rst.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | short_array_syntax: ~ 3 | max_blank_lines: 4 | max: 2 5 | yaml_instead_of_yml_suffix: ~ 6 | composer_dev_option_not_at_the_end: ~ 7 | yarn_dev_option_at_the_end: ~ 8 | versionadded_directive_should_have_version: ~ 9 | unused_links: ~ 10 | filename_uses_underscores_only: ~ 11 | 12 | # do not report as violation 13 | whitelist: 14 | regex: 15 | - '/FOSUserBundle(.*)\.yml/' 16 | - '/``.yml``/' 17 | - '/(.*)\.orm\.yml/' # currently DoctrineBundle only supports .yml 18 | lines: 19 | - 'in config files, so the old ``app/config/config_dev.yml`` goes to' 20 | - '#. The most important config file is ``app/config/services.yml``, which now is' 21 | - 'php "%s/../bin/console" cache:clear --env=%s --no-warmup' 22 | - 'code in production without a proxy, it becomes trivially easy to abuse your' 23 | 24 | exclude_rule_for_file: 25 | - path: docs/tutorial/introduction_one.rst 26 | rule_name: max_blank_lines 27 | -------------------------------------------------------------------------------- /src/Formatter/Registry.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Formatter; 15 | 16 | use App\Formatter\Exception\FormatterNotFound; 17 | 18 | final class Registry 19 | { 20 | /** 21 | * @var Formatter[] 22 | */ 23 | private array $formatters = []; 24 | 25 | public function __construct(Formatter ...$formatters) 26 | { 27 | foreach ($formatters as $formatter) { 28 | $this->formatters[$formatter->name()] = $formatter; 29 | } 30 | } 31 | 32 | public function get(string $name): Formatter 33 | { 34 | if (!\array_key_exists($name, $this->formatters)) { 35 | throw FormatterNotFound::byName($name); 36 | } 37 | 38 | return $this->formatters[$name]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Rule/CheckListRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | abstract class CheckListRule extends AbstractRule 17 | { 18 | public string $search; 19 | public string $message; 20 | 21 | /** 22 | * @return static 23 | */ 24 | public function configure(string $pattern, ?string $message): self 25 | { 26 | $this->search = $pattern; 27 | $this->message = $message ?? static::getDefaultMessage(); 28 | 29 | return $this; 30 | } 31 | 32 | public static function getDefaultMessage(): string 33 | { 34 | return 'Please don\'t use: %s'; 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | abstract public static function getList(): array; 41 | } 42 | -------------------------------------------------------------------------------- /tests/Analyzer/RstAnalyzerTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Analyzer; 15 | 16 | use App\Analyzer\RstAnalyzer; 17 | use App\Rule\MaxBlankLines; 18 | use App\Tests\UnitTestCase; 19 | use PHPUnit\Framework\Attributes\Test; 20 | 21 | final class RstAnalyzerTest extends UnitTestCase 22 | { 23 | #[Test] 24 | public function onlyOneMaxBlankLineViolationMessageOccurs(): void 25 | { 26 | $maxBlankLines = new MaxBlankLines(); 27 | $maxBlankLines->setOptions(['max' => 3]); 28 | 29 | $violations = (new RstAnalyzer())->analyze( 30 | new \SplFileInfo(__DIR__.'/../Fixtures/max_blanklines.rst'), 31 | [$maxBlankLines], 32 | ); 33 | 34 | self::assertCount(1, $violations); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Rst/Value/LinkUsage.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rst\Value; 15 | 16 | use Webmozart\Assert\Assert; 17 | 18 | final readonly class LinkUsage 19 | { 20 | private function __construct( 21 | private LinkName $name, 22 | ) { 23 | } 24 | 25 | public static function fromLine(string $line): self 26 | { 27 | preg_match('/(`[^`]+`|(?:(?!_)\w)+(?:[-._+:](?:(?!_)\w)+)*+)_/', $line, $matches); 28 | Assert::keyExists($matches, 1); 29 | $name = trim($matches[1], '`'); 30 | 31 | return new self(LinkName::fromString($name)); 32 | } 33 | 34 | public static function fromLinkName(LinkName $name): self 35 | { 36 | return new self($name); 37 | } 38 | 39 | public function name(): LinkName 40 | { 41 | return $this->name; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Analyzer/MemoizingAnalyzer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Analyzer; 15 | 16 | final readonly class MemoizingAnalyzer implements Analyzer 17 | { 18 | public function __construct( 19 | private Analyzer $analyzer, 20 | private Cache $cache, 21 | ) { 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function analyze(\SplFileInfo $file, array $rules): array 28 | { 29 | if ($this->cache->has($file, $rules)) { 30 | return $this->cache->get($file, $rules); 31 | } 32 | 33 | $violations = $this->analyzer->analyze($file, $rules); 34 | $this->cache->set($file, $rules, $violations); 35 | 36 | return $violations; 37 | } 38 | 39 | public function write(): void 40 | { 41 | $this->cache->write(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Oskar Stark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Helper/YamlHelperTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Helper; 15 | 16 | use App\Helper\YamlHelper; 17 | use App\Tests\UnitTestCase; 18 | use App\Value\Line; 19 | use PHPUnit\Framework\Attributes\DataProvider; 20 | use PHPUnit\Framework\Attributes\Test; 21 | 22 | final class YamlHelperTest extends UnitTestCase 23 | { 24 | #[Test] 25 | #[DataProvider('isCommentProvider')] 26 | public function isComment(bool $expected, string $line): void 27 | { 28 | self::assertSame( 29 | $expected, 30 | YamlHelper::isComment(new Line($line)), 31 | ); 32 | } 33 | 34 | /** 35 | * @return \Generator 36 | */ 37 | public static function isCommentProvider(): iterable 38 | { 39 | yield [true, '# comment']; 40 | yield [false, 'no comment']; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Rule/AbstractRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\RuleGroup; 17 | use App\Value\RuleName; 18 | 19 | abstract class AbstractRule 20 | { 21 | public static function getName(): RuleName 22 | { 23 | return RuleName::fromClassString(static::class); 24 | } 25 | 26 | /** 27 | * @return RuleGroup[] 28 | */ 29 | public static function getGroups(): array 30 | { 31 | return []; 32 | } 33 | 34 | public static function runOnlyOnBlankline(): bool 35 | { 36 | return false; 37 | } 38 | 39 | public static function isExperimental(): bool 40 | { 41 | foreach (static::getGroups() as $group) { 42 | if ($group->equals(RuleGroup::Experimental())) { 43 | return true; 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Formatter/RegistryTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Formatter; 15 | 16 | use App\Formatter\ConsoleFormatter; 17 | use App\Formatter\Exception\FormatterNotFound; 18 | use App\Formatter\Registry; 19 | use App\Tests\UnitTestCase; 20 | use PHPUnit\Framework\Attributes\Test; 21 | 22 | final class RegistryTest extends UnitTestCase 23 | { 24 | #[Test] 25 | public function invalidNameThrowsException(): void 26 | { 27 | $this->expectException(FormatterNotFound::class); 28 | $this->expectExceptionMessage('Formatter "invalid" not found'); 29 | 30 | (new Registry(new ConsoleFormatter()))->get('invalid'); 31 | } 32 | 33 | #[Test] 34 | public function validName(): void 35 | { 36 | $formatter = new ConsoleFormatter(); 37 | 38 | self::assertSame($formatter, (new Registry($formatter))->get('console')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.4-cli-alpine AS build 2 | 3 | RUN apk add --no-cache git # required for box to detect the version 4 | RUN apk add --no-cache icu-dev && docker-php-ext-install -j$(nproc) intl # related to https://github.com/box-project/box/issues/516 5 | 6 | COPY --from=composer:2.9.2 /usr/bin/composer /usr/bin/composer 7 | 8 | WORKDIR /usr/src/app 9 | ADD . /usr/src/app 10 | 11 | RUN composer install --classmap-authoritative --no-interaction --no-dev --optimize-autoloader 12 | 13 | ADD https://github.com/humbug/box/releases/download/4.6.10/box.phar ./box.phar 14 | RUN php box.phar compile 15 | 16 | FROM php:8.4-cli-alpine 17 | 18 | LABEL "com.github.actions.name"="DOCtor-RST" 19 | LABEL "com.github.actions.description"="check *.rst files against given rules" 20 | LABEL "com.github.actions.icon"="check" 21 | LABEL "com.github.actions.color"="blue" 22 | 23 | LABEL "repository"="http://github.com/oskarstark/doctor-rst" 24 | LABEL "homepage"="http://github.com/actions" 25 | LABEL "maintainer"="Oskar Stark " 26 | 27 | COPY --from=build /usr/src/app/bin/doctor-rst.phar /usr/bin/doctor-rst 28 | COPY entrypoint.sh /entrypoint.sh 29 | 30 | ENTRYPOINT ["/entrypoint.sh"] 31 | -------------------------------------------------------------------------------- /src/Analyzer/InMemoryCache.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Analyzer; 15 | 16 | use App\Value\Violation; 17 | 18 | final class InMemoryCache implements Cache 19 | { 20 | /** 21 | * @var array 22 | */ 23 | private array $cache; 24 | 25 | public function __construct() 26 | { 27 | $this->cache = []; 28 | } 29 | 30 | public function has(\SplFileInfo $file, array $rules): bool 31 | { 32 | return isset($this->cache[$file->getPathname()]); 33 | } 34 | 35 | public function get(\SplFileInfo $file, array $rules): array 36 | { 37 | return $this->cache[$file->getPathname()] ?? []; 38 | } 39 | 40 | public function set(\SplFileInfo $file, array $rules, array $violations): void 41 | { 42 | $this->cache[$file->getPathname()] = $violations; 43 | } 44 | 45 | public function write(): void 46 | { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Rule/NoAppBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoAppBundle extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->raw()->match('/AppBundle/')) { 35 | return Violation::from( 36 | 'Please don\'t use "AppBundle" anymore', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests-changed 2 | tests-changed: export APP_ENV=test 3 | tests-changed: vendor doctrine 4 | symfony php vendor/bin/phpunit --configuration=phpunit.xml.dist $(shell git diff HEAD --name-only | grep Test.php | xargs ) 5 | 6 | .PHONY: tests 7 | tests: export APP_ENV=test 8 | tests: 9 | vendor/bin/phpunit 10 | 11 | .PHONY: cs 12 | cs: vendor 13 | symfony php vendor/bin/php-cs-fixer fix --diff --verbose 14 | 15 | .PHONY: static-code-analysis 16 | static-code-analysis: 17 | vendor/bin/phpstan analyse -c phpstan.neon.dist --memory-limit=-1 18 | 19 | .PHONY: static-code-analysis-baseline 20 | static-code-analysis-baseline: 21 | vendor/bin/phpstan analyse -c phpstan.neon.dist --generate-baseline=phpstan-baseline.neon --memory-limit=1G 22 | 23 | .PHONY: refactoring 24 | refactoring: 25 | vendor/bin/rector process --config rector.php 26 | 27 | .PHONY: dependency-analysis 28 | dependency-analysis: vendor ## Runs a dependency analysis with maglnet/composer-require-checker 29 | symfony php vendor/bin/composer-require-checker check --config-file=$(shell pwd)/composer-require-checker.json 30 | symfony php vendor/bin/composer-unused 31 | symfony composer audit 32 | 33 | .PHONY: docs 34 | docs: vendor 35 | symfony php bin/doctor-rst rules > docs/rules.md 36 | -------------------------------------------------------------------------------- /src/Rule/ShortArraySyntax.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class ShortArraySyntax extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/[\\s|\()]array\(/')) { 35 | return Violation::from( 36 | 'Please use short array syntax', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Rule/NoEmptyLiteralsTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Rule; 15 | 16 | use App\Rule\NoEmptyLiterals; 17 | use App\Tests\RstSample; 18 | use App\Value\NullViolation; 19 | use App\Value\Violation; 20 | 21 | final class NoEmptyLiteralsTest extends AbstractLineContentRuleTestCase 22 | { 23 | public function createRule(): NoEmptyLiterals 24 | { 25 | return new NoEmptyLiterals(); 26 | } 27 | 28 | public static function checkProvider(): iterable 29 | { 30 | $invalid = 'Please use ````...'; 31 | $valid = 'Please use ``foo``...'; 32 | 33 | yield 'valid' => [NullViolation::create(), new RstSample($valid)]; 34 | 35 | yield 'invalid' => [ 36 | Violation::from( 37 | 'Empty literals (````) are not allowed!', 38 | 'filename', 39 | 1, 40 | $invalid, 41 | ), 42 | new RstSample($invalid), 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/NoComposerPhar.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoComposerPhar extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->raw()->match('/composer\.phar/')) { 35 | return Violation::from( 36 | 'Please use "composer" instead of "composer.phar"', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/NoPhpPrefixBeforeComposer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoPhpPrefixBeforeComposer extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->raw()->match('/php composer/')) { 35 | return Violation::from( 36 | 'Please remove "php" prefix', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Rule/AbstractLineContentRuleTestCase.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Rule; 15 | 16 | use App\Rule\LineContentRule; 17 | use App\Tests\RstSample; 18 | use App\Tests\UnitTestCase; 19 | use App\Value\ViolationInterface; 20 | use PHPUnit\Framework\Attributes\DataProvider; 21 | use PHPUnit\Framework\Attributes\Test; 22 | 23 | abstract class AbstractLineContentRuleTestCase extends UnitTestCase 24 | { 25 | abstract public function createRule(): LineContentRule; 26 | 27 | #[Test] 28 | #[DataProvider('checkProvider')] 29 | public function check(ViolationInterface $expected, RstSample $sample): void 30 | { 31 | self::assertEquals( 32 | $expected, 33 | static::createRule()->check($sample->lines, $sample->lineNumber, 'filename'), 34 | ); 35 | } 36 | 37 | /** 38 | * @return \Generator 39 | */ 40 | abstract public static function checkProvider(): iterable; 41 | } 42 | -------------------------------------------------------------------------------- /src/Rule/NoComposerReq.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoComposerReq extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Symfony()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/composer req /')) { 35 | return Violation::from( 36 | 'Please "composer require" instead of "composer req"', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Helper/XmlHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Helper; 15 | 16 | use App\Value\Line; 17 | 18 | final class XmlHelper 19 | { 20 | public static function isComment(Line $line, ?bool $closed = null): bool 21 | { 22 | $string = $line->clean()->toString(); 23 | 24 | if ('' === $string) { 25 | return true; 26 | } 27 | 28 | if (null === $closed) { 29 | if (preg_match('/^$/', $string)) 36 | || ( 37 | !$closed && !preg_match('/(.*)-->$/', $string) 38 | ) 39 | ) 40 | ) { 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Rule/FinalAdminClasses.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class FinalAdminClasses extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/^class(.*)extends AbstractAdmin$/')) { 35 | return Violation::from( 36 | 'Please use "final" for Admin class', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/NoSpaceBeforeSelfXmlClosingTag.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoSpaceBeforeSelfXmlClosingTag extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ('/>' !== $line->clean()->toString() && $line->raw()->match('/\ \/>/')) { 35 | return Violation::from( 36 | 'Please remove space before "/>"', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/SpaceBeforeSelfXmlClosingTag.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Rst\RstParser; 17 | use App\Value\Lines; 18 | use App\Value\NullViolation; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class SpaceBeforeSelfXmlClosingTag extends AbstractRule implements LineContentRule 23 | { 24 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 25 | { 26 | $lines->seek($number); 27 | $line = $lines->current()->raw()->toString(); 28 | 29 | if (!preg_match('/\/>/', $line)) { 30 | return NullViolation::create(); 31 | } 32 | 33 | if (!preg_match('/\ \/>/', $line) && !RstParser::isLinkUsage($line)) { 34 | return Violation::from( 35 | 'Please add space before "/>"', 36 | $filename, 37 | $number + 1, 38 | $line, 39 | ); 40 | } 41 | 42 | return NullViolation::create(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Rule/FinalAdminExtensionClasses.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class FinalAdminExtensionClasses extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [RuleGroup::Sonata()]; 27 | } 28 | 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/^class(.*)extends AbstractAdminExtension$/')) { 35 | return Violation::from( 36 | 'Please use "final" for AdminExtension class', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rst/Value/LinkDefinition.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rst\Value; 15 | 16 | use Webmozart\Assert\Assert; 17 | 18 | final readonly class LinkDefinition 19 | { 20 | private function __construct( 21 | private LinkName $name, 22 | private LinkUrl $url, 23 | ) { 24 | } 25 | 26 | public static function fromLine(string $line): self 27 | { 28 | preg_match('/^\s*\.\. _`?([^`]+)`?: (.*)$/', $line, $matches); 29 | Assert::keyExists($matches, 1); 30 | Assert::keyExists($matches, 2); 31 | 32 | return new self( 33 | LinkName::fromString($matches[1]), 34 | LinkUrl::fromString($matches[2]), 35 | ); 36 | } 37 | 38 | public static function fromValues(LinkName $name, LinkUrl $url): self 39 | { 40 | return new self($name, $url); 41 | } 42 | 43 | public function name(): LinkName 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function url(): LinkUrl 49 | { 50 | return $this->url; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Rule/NoAppConsole.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoAppConsole extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [ 27 | RuleGroup::Sonata(), 28 | RuleGroup::Symfony(), 29 | ]; 30 | } 31 | 32 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 33 | { 34 | $lines->seek($number); 35 | $line = $lines->current(); 36 | 37 | if ($line->raw()->match('/app\/console/')) { 38 | return Violation::from( 39 | 'Please use "bin/console" instead of "app/console"', 40 | $filename, 41 | $number + 1, 42 | $line, 43 | ); 44 | } 45 | 46 | return NullViolation::create(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Value/RuleName.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Value; 15 | 16 | use Webmozart\Assert\Assert; 17 | use function Symfony\Component\String\u; 18 | 19 | final readonly class RuleName 20 | { 21 | private string $name; 22 | 23 | private function __construct(string $value) 24 | { 25 | $value = trim($value); 26 | 27 | Assert::stringNotEmpty($value); 28 | Assert::notWhitespaceOnly($value); 29 | 30 | $this->name = $value; 31 | } 32 | 33 | public static function fromClassString(string $class): self 34 | { 35 | $class = trim($class); 36 | 37 | Assert::stringNotEmpty($class); 38 | Assert::notWhitespaceOnly($class); 39 | 40 | return self::fromString( 41 | u(substr((string) strrchr($class, '\\'), 1))->snake()->toString(), 42 | ); 43 | } 44 | 45 | public static function fromString(string $value): self 46 | { 47 | return new self($value); 48 | } 49 | 50 | public function toString(): string 51 | { 52 | return $this->name; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/UnitTestCase.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests; 15 | 16 | use App\Rst\RstParser; 17 | use Faker\Factory; 18 | use Faker\Generator; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | abstract class UnitTestCase extends TestCase 22 | { 23 | /** 24 | * @return string[] 25 | */ 26 | final public static function phpCodeBlocks(): array 27 | { 28 | $codeBlocks = [ 29 | RstParser::CODE_BLOCK_PHP, 30 | RstParser::CODE_BLOCK_PHP_ANNOTATIONS, 31 | RstParser::CODE_BLOCK_PHP_ATTRIBUTES, 32 | RstParser::CODE_BLOCK_PHP_SYMFONY, 33 | RstParser::CODE_BLOCK_PHP_STANDALONE, 34 | ]; 35 | 36 | $result = []; 37 | 38 | foreach ($codeBlocks as $codeBlock) { 39 | $result[] = '.. code-block:: '.$codeBlock; 40 | } 41 | 42 | $result[] = 'A PHP code block follows::'; 43 | 44 | return $result; 45 | } 46 | final protected static function faker(string $locale = 'de_DE'): Generator 47 | { 48 | return Factory::create($locale); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Rule/NoMergeConflict.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Attribute\Rule\Description; 17 | use App\Value\Lines; 18 | use App\Value\NullViolation; 19 | use App\Value\RuleGroup; 20 | use App\Value\Violation; 21 | use App\Value\ViolationInterface; 22 | 23 | #[Description('Ensure that the files does not contain merge conflicts.')] 24 | final class NoMergeConflict extends AbstractRule implements LineContentRule 25 | { 26 | public static function getGroups(): array 27 | { 28 | return [RuleGroup::Symfony()]; 29 | } 30 | 31 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 32 | { 33 | $lines->seek($number); 34 | $line = $lines->current(); 35 | 36 | if ($line->clean()->equalsTo('<<<<<<< HEAD')) { 37 | return Violation::from( 38 | 'Please get rid of the merge conflict', 39 | $filename, 40 | $number + 1, 41 | $line, 42 | ); 43 | } 44 | 45 | return NullViolation::create(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Value/NullViolation.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Value; 15 | 16 | final readonly class NullViolation implements ViolationInterface 17 | { 18 | private string $message; 19 | private string $filename; 20 | private int $lineno; 21 | private string $rawLine; 22 | 23 | private function __construct() 24 | { 25 | $this->message = ''; 26 | $this->filename = ''; 27 | $this->lineno = 0; 28 | $this->rawLine = ''; 29 | } 30 | 31 | public static function create(): self 32 | { 33 | return new self(); 34 | } 35 | 36 | public function message(): string 37 | { 38 | return $this->message; 39 | } 40 | 41 | public function filename(): string 42 | { 43 | return $this->filename; 44 | } 45 | 46 | public function lineno(): int 47 | { 48 | return $this->lineno; 49 | } 50 | 51 | public function rawLine(): string 52 | { 53 | return $this->rawLine; 54 | } 55 | 56 | public function isNull(): bool 57 | { 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Rule/NoConfigYaml.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class NoConfigYaml extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [ 27 | RuleGroup::Sonata(), 28 | RuleGroup::Symfony(), 29 | ]; 30 | } 31 | 32 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 33 | { 34 | $lines->seek($number); 35 | $line = $lines->current(); 36 | 37 | if ($line->raw()->match('/app\/config\/config\.yml/')) { 38 | return Violation::from( 39 | 'Please use specific config class in "config/packages/..." instead of "app/config/config.yml"', 40 | $filename, 41 | $number + 1, 42 | $line, 43 | ); 44 | } 45 | 46 | return NullViolation::create(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Rule/UseHttpsXsdUrls.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class UseHttpsXsdUrls extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [ 27 | RuleGroup::Sonata(), 28 | RuleGroup::Symfony(), 29 | ]; 30 | } 31 | 32 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 33 | { 34 | $lines->seek($number); 35 | $line = $lines->current(); 36 | 37 | if ($matches = $line->raw()->match('/http\:\/\/([^\s]+)\.xsd/')) { 38 | /** @var string[] $matches */ 39 | return Violation::from( 40 | \sprintf('Please use "https" for %s', $matches[0]), 41 | $filename, 42 | $number + 1, 43 | $line, 44 | ); 45 | } 46 | 47 | return NullViolation::create(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Helper/TwigHelperTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Helper; 15 | 16 | use App\Helper\TwigHelper; 17 | use App\Tests\UnitTestCase; 18 | use App\Value\Line; 19 | use PHPUnit\Framework\Attributes\DataProvider; 20 | use PHPUnit\Framework\Attributes\Test; 21 | 22 | final class TwigHelperTest extends UnitTestCase 23 | { 24 | #[Test] 25 | #[DataProvider('isCommentProvider')] 26 | public function isComment(bool $expected, string $line, ?bool $closed): void 27 | { 28 | self::assertSame( 29 | $expected, 30 | TwigHelper::isComment(new Line($line), $closed), 31 | ); 32 | } 33 | 34 | public static function isCommentProvider(): iterable 35 | { 36 | yield [true, '{#', null]; 37 | yield [true, '#}', null]; 38 | 39 | yield [true, '{# comment #}', true]; 40 | yield [false, '{# comment #}', false]; 41 | yield [true, '{# comment', false]; 42 | yield [false, '{# comment', true]; 43 | 44 | yield [false, 'no comment', null]; 45 | yield [false, 'no comment', true]; 46 | yield [false, 'no comment', false]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Rst/Value/LinkUsageTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Rst\Value; 15 | 16 | use App\Rst\Value\LinkName; 17 | use App\Rst\Value\LinkUsage; 18 | use App\Tests\UnitTestCase; 19 | use PHPUnit\Framework\Attributes\DataProvider; 20 | use PHPUnit\Framework\Attributes\Test; 21 | 22 | final class LinkUsageTest extends UnitTestCase 23 | { 24 | #[Test] 25 | #[DataProvider('fromLineProvider')] 26 | public function fromLine(string $expected, string $line): void 27 | { 28 | $usage = LinkUsage::fromLine($line); 29 | 30 | self::assertSame($expected, $usage->name()->value()); 31 | } 32 | 33 | /** 34 | * @return \Generator 35 | */ 36 | public static function fromLineProvider(): iterable 37 | { 38 | yield ['Link1', '`Link1`_']; 39 | yield ['Link 1', '`Link 1`_']; 40 | } 41 | 42 | #[Test] 43 | public function fromLinkName(): void 44 | { 45 | $name = 'foo'; 46 | 47 | self::assertSame( 48 | $name, 49 | LinkUsage::fromLinkName(LinkName::fromString($name))->name()->value(), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Formatter/GithubFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Formatter; 15 | 16 | use App\Value\AnalyzerResult; 17 | use Symfony\Component\Console\Style\OutputStyle; 18 | 19 | class GithubFormatter implements Formatter 20 | { 21 | public function __construct( 22 | private readonly ConsoleFormatter $consoleFormatter, 23 | ) { 24 | } 25 | 26 | public function format(OutputStyle $style, AnalyzerResult $analyzerResult, string $analyzeDir, bool $showValidFiles): void 27 | { 28 | $this->consoleFormatter->format($style, $analyzerResult, $analyzeDir, $showValidFiles); 29 | 30 | foreach ($analyzerResult->all() as $fileResult) { 31 | foreach ($fileResult->violationList()->violations() as $violation) { 32 | $style->writeln(\sprintf( 33 | '::error file=%s,line=%d::%s', 34 | $fileResult->filename(), 35 | $violation->lineno(), 36 | $violation->message(), 37 | )); 38 | } 39 | } 40 | } 41 | 42 | public function name(): string 43 | { 44 | return 'github'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Rule/YarnDevOptionNotAtTheEnd.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Attribute\Rule\Description; 17 | use App\Attribute\Rule\InvalidExample; 18 | use App\Attribute\Rule\ValidExample; 19 | use App\Value\Lines; 20 | use App\Value\NullViolation; 21 | use App\Value\Violation; 22 | use App\Value\ViolationInterface; 23 | 24 | #[Description('Make sure yarn `--dev` option for `add` command is used at the end.')] 25 | #[ValidExample('yarn add --dev jquery')] 26 | #[InvalidExample('yarn add jquery --dev')] 27 | final class YarnDevOptionNotAtTheEnd extends AbstractRule implements LineContentRule 28 | { 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/yarn add(.*)\-\-dev$/')) { 35 | return Violation::from( 36 | 'Please move "--dev" option before the package', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/EnsureCorrectFormatForPhpfunction.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Attribute\Rule\Description; 17 | use App\Attribute\Rule\InvalidExample; 18 | use App\Attribute\Rule\ValidExample; 19 | use App\Value\Lines; 20 | use App\Value\NullViolation; 21 | use App\Value\Violation; 22 | use App\Value\ViolationInterface; 23 | 24 | #[Description('Ensure phpfunction directive do not end with ().')] 25 | #[InvalidExample(':phpfunction:`mb_detect_encoding()`.')] 26 | #[ValidExample(':phpfunction:`mb_detect_encoding`.')] 27 | final class EnsureCorrectFormatForPhpfunction extends AbstractRule implements LineContentRule 28 | { 29 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 30 | { 31 | $lines->seek($number); 32 | $line = $lines->current(); 33 | 34 | if ($line->clean()->match('/:phpfunction:`.*\(\)`/')) { 35 | return Violation::from( 36 | 'Please do not use () at the end of PHP function', 37 | $filename, 38 | $number + 1, 39 | $line, 40 | ); 41 | } 42 | 43 | return NullViolation::create(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/NoInheritdocInCodeExamples.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Traits\DirectiveTrait; 17 | use App\Value\Lines; 18 | use App\Value\NullViolation; 19 | use App\Value\RuleGroup; 20 | use App\Value\Violation; 21 | use App\Value\ViolationInterface; 22 | 23 | final class NoInheritdocInCodeExamples extends AbstractRule implements LineContentRule 24 | { 25 | use DirectiveTrait; 26 | 27 | public static function getGroups(): array 28 | { 29 | return [ 30 | RuleGroup::Sonata(), 31 | RuleGroup::Symfony(), 32 | ]; 33 | } 34 | 35 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 36 | { 37 | $lines->seek($number); 38 | $line = $lines->current(); 39 | 40 | if ($line->raw()->match('/@inheritdoc/') 41 | && $this->inPhpCodeBlock($lines, $number) 42 | ) { 43 | return Violation::from( 44 | 'Please do not use "@inheritdoc"', 45 | $filename, 46 | $number + 1, 47 | $line, 48 | ); 49 | } 50 | 51 | return NullViolation::create(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Rule/ValidUseStatements.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Value\Lines; 17 | use App\Value\NullViolation; 18 | use App\Value\RuleGroup; 19 | use App\Value\Violation; 20 | use App\Value\ViolationInterface; 21 | 22 | final class ValidUseStatements extends AbstractRule implements LineContentRule 23 | { 24 | public static function getGroups(): array 25 | { 26 | return [ 27 | RuleGroup::Sonata(), 28 | RuleGroup::Symfony(), 29 | ]; 30 | } 31 | 32 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 33 | { 34 | $lines->seek($number); 35 | $line = $lines->current(); 36 | 37 | /* 38 | * @todo do it in one regex instead of regex + string search 39 | */ 40 | if ($line->clean()->match('/^use (.*);$/') && str_contains($line->clean()->toString(), '\\\\')) { 41 | return Violation::from( 42 | 'Please do not escape the backslashes in a use statement.', 43 | $filename, 44 | $number + 1, 45 | $line, 46 | ); 47 | } 48 | 49 | return NullViolation::create(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Rst/Value/DirectiveContent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rst\Value; 15 | 16 | final class DirectiveContent 17 | { 18 | public array $cleaned = []; 19 | 20 | public function __construct( 21 | public readonly array $raw, 22 | ) { 23 | $cleaned = []; 24 | 25 | // remove entries in the array which equals an empty string, but only at the start and at the end 26 | foreach ($this->raw as $line) { 27 | if (0 === \count($cleaned) && '' === $line) { 28 | continue; 29 | } 30 | 31 | $cleaned[] = $line; 32 | } 33 | 34 | // reverse $cleaned array to remove empty lines at the end 35 | $cleaned = array_reverse($cleaned); 36 | 37 | foreach ($cleaned as $key => $line) { 38 | if ('' === $line) { 39 | unset($cleaned[$key]); 40 | } else { 41 | break; 42 | } 43 | } 44 | 45 | // reverse again to get the original order 46 | $cleaned = array_reverse($cleaned); 47 | 48 | $this->cleaned = $cleaned; 49 | } 50 | 51 | public function numberOfLines(): int 52 | { 53 | return \count($this->cleaned); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Rule/NoFootnotes.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Attribute\Rule\Description; 17 | use App\Attribute\Rule\InvalidExample; 18 | use App\Rst\RstParser; 19 | use App\Value\Lines; 20 | use App\Value\NullViolation; 21 | use App\Value\RuleGroup; 22 | use App\Value\Violation; 23 | use App\Value\ViolationInterface; 24 | 25 | #[Description('Make sure there is no footnotes')] 26 | #[InvalidExample('.. [5] A numerical footnote. Note')] 27 | final class NoFootnotes extends AbstractRule implements LineContentRule 28 | { 29 | public static function getGroups(): array 30 | { 31 | return [ 32 | RuleGroup::Symfony(), 33 | ]; 34 | } 35 | 36 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 37 | { 38 | $lines->seek($number); 39 | $line = $lines->current(); 40 | 41 | if (RstParser::isFootnote($lines->current())) { 42 | return Violation::from( 43 | "Please don't use footnotes as they are not supported", 44 | $filename, 45 | $number + 1, 46 | $line, 47 | ); 48 | } 49 | 50 | return NullViolation::create(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Rule/RemoveTrailingWhitespace.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Rule; 15 | 16 | use App\Attribute\Rule\Description; 17 | use App\Attribute\Rule\InvalidExample; 18 | use App\Attribute\Rule\ValidExample; 19 | use App\Value\Lines; 20 | use App\Value\NullViolation; 21 | use App\Value\RuleGroup; 22 | use App\Value\Violation; 23 | use App\Value\ViolationInterface; 24 | 25 | #[Description('Make sure there is not trailing whitespace.')] 26 | #[InvalidExample('Invalid sentence ')] 27 | #[ValidExample('Valid sentence')] 28 | final class RemoveTrailingWhitespace extends AbstractRule implements LineContentRule 29 | { 30 | public static function getGroups(): array 31 | { 32 | return [RuleGroup::Symfony()]; 33 | } 34 | 35 | public function check(Lines $lines, int $number, string $filename): ViolationInterface 36 | { 37 | $lines->seek($number); 38 | $line = $lines->current(); 39 | 40 | if ($line->raw()->match('/.+ $/')) { 41 | return Violation::from( 42 | 'Please remove trailing whitespace', 43 | $filename, 44 | $number + 1, 45 | $line, 46 | ); 47 | } 48 | 49 | return NullViolation::create(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Helper/XmlHelperTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace App\Tests\Helper; 15 | 16 | use App\Helper\XmlHelper; 17 | use App\Tests\UnitTestCase; 18 | use App\Value\Line; 19 | use PHPUnit\Framework\Attributes\DataProvider; 20 | use PHPUnit\Framework\Attributes\Test; 21 | 22 | final class XmlHelperTest extends UnitTestCase 23 | { 24 | #[Test] 25 | #[DataProvider('isCommentProvider')] 26 | public function isComment(bool $expected, string $line, ?bool $closed): void 27 | { 28 | self::assertSame( 29 | $expected, 30 | XmlHelper::isComment(new Line($line), $closed), 31 | ); 32 | } 33 | 34 | /** 35 | * @return \Generator 36 | */ 37 | public static function isCommentProvider(): iterable 38 | { 39 | yield [true, '', null]; 41 | 42 | yield [true, '', true]; 43 | yield [false, '', false]; 44 | yield [true, '