├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── code_analysis.yaml │ └── downgraded_release.yaml ├── LICENSE ├── bin ├── rule-doc-generator └── rule-doc-generator.php ├── build ├── build-scoped.sh ├── rector-downgrade-php-72.php └── target-repository │ ├── .github │ ├── FUNDING.yml │ └── workflows │ │ └── bare_run.yaml │ └── composer.json ├── composer.json ├── ecs.php ├── full-tool-build.sh ├── phpstan.neon ├── prefix-code.sh ├── rector.php ├── scoper.php ├── src ├── Command │ ├── GenerateCommand.php │ └── ValidateCommand.php ├── DependencyInjection │ └── ContainerFactory.php ├── DirectoryToMarkdownPrinter.php ├── Exception │ ├── ConfigurationBoundException.php │ └── ShouldNotHappenException.php ├── FileSystem │ ├── PathsHelper.php │ └── RuleDefinitionClassesFinder.php ├── Printer │ ├── CodeSamplePrinter │ │ ├── BadGoodCodeSamplePrinter.php │ │ ├── CodeSamplePrinter.php │ │ └── DiffCodeSamplePrinter.php │ ├── Markdown │ │ ├── MarkdownCodeWrapper.php │ │ └── MarkdownDiffer.php │ ├── NeonPrinter.php │ └── RuleDefinitionsPrinter.php ├── RuleCodeSamplePrinter │ ├── ECSRuleCodeSamplePrinter.php │ ├── PHPStanRuleCodeSamplePrinter.php │ └── RectorRuleCodeSamplePrinter.php ├── RuleDefinitionsResolver.php ├── Text │ └── KeywordHighlighter.php └── ValueObject │ ├── Option.php │ └── RuleClassWithFilePath.php └── stubs ├── PHPStan └── Rules │ └── Rule.php ├── PhpCsFixer ├── AbstractFixer.php └── Fixer │ └── FixerInterface.php └── Rector └── Core └── Contract └── Rector └── RectorInterface.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: tomasvotruba 3 | custom: https://www.paypal.me/rectorphp 4 | -------------------------------------------------------------------------------- /.github/workflows/code_analysis.yaml: -------------------------------------------------------------------------------- 1 | name: Code Analysis 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | code_analysis: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | actions: 15 | - 16 | name: 'PHPStan' 17 | run: composer phpstan --ansi 18 | 19 | - 20 | name: 'Composer Validate' 21 | run: composer validate --ansi 22 | 23 | - 24 | name: 'Rector' 25 | run: composer rector --ansi 26 | 27 | - 28 | name: 'Coding Standard' 29 | run: composer fix-cs --ansi 30 | 31 | - 32 | name: 'Tests' 33 | run: vendor/bin/phpunit 34 | 35 | - 36 | name: 'Check Active Classes' 37 | run: vendor/bin/class-leak check src --ansi 38 | 39 | name: ${{ matrix.actions.name }} 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | # see https://github.com/shivammathur/setup-php 45 | - uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: 8.2 48 | coverage: none 49 | 50 | # composer install cache - https://github.com/ramsey/composer-install 51 | - uses: "ramsey/composer-install@v2" 52 | 53 | - run: ${{ matrix.actions.run }} 54 | -------------------------------------------------------------------------------- /.github/workflows/downgraded_release.yaml: -------------------------------------------------------------------------------- 1 | name: Downgraded Release 2 | 3 | on: 4 | push: 5 | tags: 6 | # avoid infinite looping, skip tags that ends with ".72" 7 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-including-and-excluding-branches 8 | - '*' 9 | 10 | jobs: 11 | downgrade_release: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: "actions/checkout@v3" 16 | with: 17 | token: ${{ secrets.WORKFLOWS_TOKEN }} 18 | 19 | - 20 | uses: "shivammathur/setup-php@v2" 21 | with: 22 | php-version: 8.2 23 | coverage: none 24 | 25 | # invoke patches 26 | - run: composer install --ansi 27 | 28 | # but no dev packages 29 | - run: composer update --no-dev --ansi 30 | 31 | # get rector to "rector-local" directory, to avoid downgrading itself in the /vendor 32 | - run: mkdir rector-local 33 | - run: composer require rector/rector --working-dir rector-local --ansi 34 | 35 | # downgrade to PHP 7.2 36 | - run: rector-local/vendor/bin/rector process bin src vendor --config build/rector-downgrade-php-72.php --ansi 37 | 38 | # clear the dev files 39 | - run: rm -rf tests ecs.php phpstan.neon phpunit.xml .gitignore .editorconfig 40 | 41 | # prefix and scope 42 | - run: sh prefix-code.sh 43 | 44 | # copy PHP 7.2 composer + workflows 45 | - run: cp -r build/target-repository/. . 46 | 47 | # clear the dev files 48 | - run: rm -rf build prefix-code.sh full-tool-build.sh scoper.php rector.php rector-local 49 | 50 | # setup git user 51 | - 52 | run: | 53 | git config user.email "action@github.com" 54 | git config user.name "GitHub Action" 55 | # publish to the same repository with a new tag 56 | # see https://tomasvotruba.com/blog/how-to-release-php-81-and-72-package-in-the-same-repository/ 57 | - 58 | name: "Tag Downgraded Code" 59 | run: | 60 | # separate a "git add" to add untracked (new) files too 61 | git add --all 62 | git commit -m "release PHP 7.2 downgraded" 63 | 64 | # force push tag, so there is only 1 version 65 | git tag "${GITHUB_REF#refs/tags/}" --force 66 | git push origin "${GITHUB_REF#refs/tags/}" --force 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) 2016 Tomas Votruba (https://tomasvotruba.com) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /bin/rule-doc-generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | create(); 37 | 38 | 39 | $application = $container->make(\Symfony\Component\Console\Application::class); 40 | $exitCode = $application->run(); 41 | exit($exitCode); 42 | -------------------------------------------------------------------------------- /build/build-scoped.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # inspired from https://github.com/rectorphp/rector/blob/main/build/build-rector-scoped.sh 4 | 5 | # see https://stackoverflow.com/questions/66644233/how-to-propagate-colors-from-bash-script-to-github-action?noredirect=1#comment117811853_66644233 6 | export TERM=xterm-color 7 | 8 | # show errors 9 | set -e 10 | 11 | # script fails if trying to access to an undefined variable 12 | set -u 13 | 14 | 15 | # functions 16 | note() 17 | { 18 | MESSAGE=$1; 19 | printf "\n"; 20 | echo "\033[0;33m[NOTE] $MESSAGE\033[0m"; 21 | } 22 | 23 | 24 | # configure here 25 | BUILD_DIRECTORY=$1 26 | RESULT_DIRECTORY=$2 27 | 28 | # --------------------------- 29 | 30 | note "Starts" 31 | 32 | # 2. scope it 33 | note "Running scoper with '$RESULT_DIRECTORY' output directory" 34 | wget https://github.com/humbug/php-scoper/releases/download/0.17.5/php-scoper.phar -N --no-verbose 35 | 36 | # create directory 37 | mkdir "$RESULT_DIRECTORY" -p 38 | 39 | # Work around possible PHP memory limits 40 | php -d memory_limit=-1 php-scoper.phar add-prefix bin src stubs vendor composer.json --output-dir "../$RESULT_DIRECTORY" --config scoper.php --force --ansi --working-dir "$BUILD_DIRECTORY" 41 | 42 | note "Show prefixed files in '$RESULT_DIRECTORY'" 43 | ls -l $RESULT_DIRECTORY 44 | 45 | note "Dumping Composer Autoload" 46 | composer dump-autoload --working-dir "$RESULT_DIRECTORY" --ansi --classmap-authoritative --no-dev 47 | 48 | # make bin/rule-doc-generator runnable without "php" 49 | chmod 777 "$RESULT_DIRECTORY/bin/rule-doc-generator" 50 | chmod 777 "$RESULT_DIRECTORY/bin/rule-doc-generator.php" 51 | 52 | note "Finished" 53 | -------------------------------------------------------------------------------- /build/rector-downgrade-php-72.php: -------------------------------------------------------------------------------- 1 | sets([DowngradeLevelSetList::DOWN_TO_PHP_72]); 10 | 11 | $rectorConfig->skip([ 12 | '*/Tests/*', 13 | '*/tests/*', 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /build/target-repository/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: tomasvotruba 3 | custom: https://www.paypal.me/rectorphp 4 | -------------------------------------------------------------------------------- /build/target-repository/.github/workflows/bare_run.yaml: -------------------------------------------------------------------------------- 1 | name: Bare Run 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | bare_run: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php_version: ['7.2', '7.3', '7.4', '8.0', '8.2'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php_version }} 21 | coverage: none 22 | 23 | - run: php bin/rule-doc-generator list --ansi 24 | -------------------------------------------------------------------------------- /build/target-repository/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symplify/rule-doc-generator", 3 | "description": "Documentation generator for coding standard or static analysis rules", 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=7.2" 7 | }, 8 | "bin": [ 9 | "bin/rule-doc-generator" 10 | ], 11 | "autoload": { 12 | "classmap": [ 13 | "stubs" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symplify/rule-doc-generator", 3 | "description": "Documentation generator for coding standard or static analysis rules", 4 | "license": "MIT", 5 | "bin": [ 6 | "bin/rule-doc-generator" 7 | ], 8 | "require": { 9 | "php": ">=8.2", 10 | "symfony/console": "^6.4", 11 | "nette/robot-loader": "^4.0", 12 | "symplify/rule-doc-generator-contracts": "^11.1", 13 | "nette/utils": "^4.0", 14 | "sebastian/diff": "^6.0", 15 | "illuminate/container": "^11.0", 16 | "webmozart/assert": "^1.11", 17 | "symfony/yaml": "^7.0", 18 | "symfony/filesystem": "^7.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^11.0", 22 | "phpstan/phpstan": "^1.11", 23 | "symplify/easy-coding-standard": "^12.3", 24 | "rector/rector": "^1.1", 25 | "tracy/tracy": "^2.10", 26 | "tomasvotruba/class-leak": "^0.2" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Symplify\\RuleDocGenerator\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Symplify\\RuleDocGenerator\\Tests\\": "tests" 36 | }, 37 | "classmap": [ 38 | "stubs" 39 | ] 40 | }, 41 | "scripts": { 42 | "phpstan": "vendor/bin/phpstan analyse --ansi", 43 | "check-cs": "vendor/bin/ecs check --ansi", 44 | "fix-cs": "vendor/bin/ecs check --fix --ansi", 45 | "rector": "vendor/bin/rector process --dry-run --ansi" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | ->withRootFiles() 13 | ->withPreparedSets(psr12: true, common: true); 14 | -------------------------------------------------------------------------------- /full-tool-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # add patches 4 | composer install --ansi 5 | 6 | # but skip dev dependencies 7 | composer update --no-dev --ansi 8 | 9 | # remove tests and useless files, to make downgraded, scoped and deployed codebase as small as possible 10 | rm -rf tests 11 | 12 | # downgrade with rector 13 | mkdir rector-local 14 | composer require rector/rector --working-dir rector-local 15 | rector-local/vendor/bin/rector process bin src vendor --config build/rector-downgrade-php-72.php --ansi 16 | 17 | # prefix 18 | sh prefix-code.sh 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | 4 | paths: 5 | - src 6 | - tests 7 | 8 | excludePaths: 9 | - '*/tests/**/Source/*' 10 | - '*/tests/**/Fixture/*' 11 | 12 | ignoreErrors: 13 | - '#Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class-string\\|T of object, string given#' 14 | -------------------------------------------------------------------------------- /prefix-code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # inspired from https://github.com/rectorphp/rector/blob/main/build/build-rector-scoped.sh 4 | 5 | # see https://stackoverflow.com/questions/66644233/how-to-propagate-colors-from-bash-script-to-github-action?noredirect=1#comment117811853_66644233 6 | export TERM=xterm-color 7 | 8 | # show errors 9 | set -e 10 | 11 | # script fails if trying to access to an undefined variable 12 | set -u 13 | 14 | 15 | # functions 16 | note() 17 | { 18 | MESSAGE=$1; 19 | printf "\n"; 20 | echo "\033[0;33m[NOTE] $MESSAGE\033[0m"; 21 | } 22 | 23 | # --------------------------- 24 | 25 | # 2. scope it 26 | note "Downloading php-scoper 0.18.11" 27 | wget https://github.com/humbug/php-scoper/releases/download/0.18.11/php-scoper.phar -N --no-verbose 28 | 29 | 30 | note "Running php-scoper" 31 | 32 | # Work around possible PHP memory limits 33 | php -d memory_limit=-1 php-scoper.phar add-prefix bin src vendor composer.json --config scoper.php --force --ansi --output-dir scoped-code 34 | 35 | # the output code is in "/scoped-code", lets move it up 36 | # the local directories have to be empty to move easily 37 | rm -r bin src vendor composer.json stubs 38 | mv scoped-code/* . 39 | 40 | note "Dumping Composer Autoload" 41 | composer dump-autoload --ansi --classmap-authoritative --no-dev 42 | 43 | # make bin/ecs runnable without "php" 44 | chmod 777 "bin/rule-doc-generator" 45 | chmod 777 "bin/rule-doc-generator.php" 46 | 47 | note "Finished" 48 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__ . '/ecs.php', 13 | __DIR__ . '/rector.php', 14 | __DIR__ . '/src', 15 | __DIR__ . '/tests', 16 | ]); 17 | 18 | $rectorConfig->skip([ 19 | '*/Fixture/*', 20 | '*/Source/*', 21 | ]); 22 | 23 | $rectorConfig->importNames(); 24 | 25 | $rectorConfig->sets([ 26 | LevelSetList::UP_TO_PHP_81, 27 | SetList::CODING_STYLE, 28 | SetList::CODE_QUALITY, 29 | SetList::DEAD_CODE, 30 | SetList::PRIVATIZATION, 31 | PHPUnitSetList::PHPUNIT_100, 32 | ]); 33 | }; 34 | -------------------------------------------------------------------------------- /scoper.php: -------------------------------------------------------------------------------- 1 | format('Ym'); 9 | 10 | // see https://github.com/humbug/php-scoper 11 | return [ 12 | 'prefix' => 'RuleDocGenerator' . $timestamp, 13 | 'expose-constants' => ['#^SYMFONY\_[\p{L}_]+$#'], 14 | 'exclude-namespaces' => ['#^Symplify\\\\RuleDocGenerator#', '#^Symfony\\\\Polyfill#'], 15 | 'exclude-files' => [ 16 | // do not prefix "trigger_deprecation" from symfony - https://github.com/symfony/symfony/commit/0032b2a2893d3be592d4312b7b098fb9d71aca03 17 | // these paths are relative to this file location, so it should be in the root directory 18 | 'vendor/symfony/deprecation-contracts/function.php', 19 | 'stubs/PhpCsFixer/AbstractFixer.php', 20 | 'stubs/Rector/Core/Contract/Rector/RectorInterface.php', 21 | 22 | // check original types 23 | 'src/RuleCodeSamplePrinter/ECSRuleCodeSamplePrinter.php', 24 | 'src/RuleCodeSamplePrinter/PHPStanRuleCodeSamplePrinter.php', 25 | 'src/RuleCodeSamplePrinter/RectorRuleCodeSamplePrinter.php', 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /src/Command/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | '; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private const README_PLACEHOLDER_END = ''; 29 | 30 | public function __construct( 31 | private readonly DirectoryToMarkdownPrinter $directoryToMarkdownPrinter, 32 | private readonly SymfonyStyle $symfonyStyle, 33 | ) { 34 | parent::__construct(); 35 | } 36 | 37 | protected function configure(): void 38 | { 39 | $this->setName('generate'); 40 | 41 | $this->setDescription('Generated Markdown documentation based on documented rules found in directory'); 42 | 43 | $this->addArgument( 44 | Option::PATHS, 45 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 46 | 'Path to directory of your project' 47 | ); 48 | 49 | $this->addOption( 50 | Option::OUTPUT_FILE, 51 | null, 52 | InputOption::VALUE_REQUIRED, 53 | 'Path to output generated markdown file', 54 | getcwd() . '/docs/rules_overview.md' 55 | ); 56 | 57 | $this->addOption(Option::CATEGORIZE, null, InputOption::VALUE_REQUIRED, 'Group rules by namespace position'); 58 | $this->addOption(Option::SKIP_TYPE, null, InputOption::VALUE_REQUIRED, 'Skip specific type in filter'); 59 | $this->addOption(Option::README, null, InputOption::VALUE_NONE, 'Render contents to README using placeholders'); 60 | } 61 | 62 | protected function execute(InputInterface $input, OutputInterface $output): int 63 | { 64 | $paths = (array) $input->getArgument(Option::PATHS); 65 | $categorizeLevel = $input->getOption(Option::CATEGORIZE); 66 | if ($categorizeLevel !== null) { 67 | $categorizeLevel = (int) $categorizeLevel; 68 | } 69 | 70 | $skipTypes = (array) $input->getOption(Option::SKIP_TYPE); 71 | 72 | // dump markdown file 73 | $outputFilePath = (string) $input->getOption(Option::OUTPUT_FILE); 74 | 75 | $markdownFileDirectory = dirname($outputFilePath); 76 | 77 | // ensure directory exists 78 | if (! file_exists($markdownFileDirectory)) { 79 | FileSystem::createDir($markdownFileDirectory); 80 | } 81 | 82 | $markdownFileContent = $this->directoryToMarkdownPrinter->print( 83 | $markdownFileDirectory, 84 | $paths, 85 | $categorizeLevel, 86 | $skipTypes 87 | ); 88 | 89 | $isReadme = (bool) $input->getOption(Option::README); 90 | 91 | if ($isReadme) { 92 | $this->renderToReadme($markdownFileContent); 93 | } else { 94 | FileSystem::write($outputFilePath, $markdownFileContent); 95 | $this->symfonyStyle->success(\sprintf('File "%s" was created', $outputFilePath)); 96 | } 97 | 98 | return self::SUCCESS; 99 | } 100 | 101 | private function renderToReadme(string $markdownFileContent): void 102 | { 103 | $readmeFilepath = getcwd() . '/README.md'; 104 | Assert::fileExists($readmeFilepath); 105 | 106 | $readmeContents = FileSystem::read($readmeFilepath); 107 | 108 | Assert::contains($readmeContents, self::README_PLACEHOLDER_START); 109 | Assert::contains($readmeContents, self::README_PLACEHOLDER_END); 110 | 111 | /** @var string $readmeContents */ 112 | $readmeContents = preg_replace( 113 | '#' . preg_quote(self::README_PLACEHOLDER_START, '#') . '(.*?)' . 114 | preg_quote(self::README_PLACEHOLDER_END, '#') . '#s', 115 | self::README_PLACEHOLDER_START . PHP_EOL . $markdownFileContent . PHP_EOL . self::README_PLACEHOLDER_END, 116 | $readmeContents 117 | ); 118 | 119 | FileSystem::write($readmeFilepath, $readmeContents); 120 | $this->symfonyStyle->success('README.md was updated'); 121 | $this->symfonyStyle->newLine(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Command/ValidateCommand.php: -------------------------------------------------------------------------------- 1 | setName('validate'); 30 | 31 | $this->setDescription('Make sure all rule definitions are not empty and have at least one code sample'); 32 | 33 | $this->addArgument( 34 | Option::PATHS, 35 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 36 | 'Path to directory of your project' 37 | ); 38 | 39 | $this->addOption(Option::SKIP_TYPE, null, InputOption::VALUE_REQUIRED, 'Skip specific type in filter'); 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | $paths = (array) $input->getArgument(Option::PATHS); 45 | $input->getOption(Option::SKIP_TYPE); 46 | 47 | // 1. collect documented rules in provided path 48 | $classesByFilePaths = $this->ruleDefinitionClassesFinder->findInDirectories($paths); 49 | if ($classesByFilePaths === []) { 50 | $this->symfonyStyle->warning('No rules found in provided paths'); 51 | return self::FAILURE; 52 | } 53 | 54 | $isValid = true; 55 | 56 | foreach ($classesByFilePaths as $ruleClass) { 57 | $ruleClassReflection = new ReflectionClass($ruleClass); 58 | 59 | $documentedRule = $ruleClassReflection->newInstanceWithoutConstructor(); 60 | /** @var DocumentedRuleInterface $documentedRule */ 61 | $ruleDefinition = $documentedRule->getRuleDefinition(); 62 | 63 | if (strlen($ruleDefinition->getDescription()) < 10) { 64 | $this->symfonyStyle->error(sprintf( 65 | 'Rule definition "%s" of "%s" is too short. Make it at least 10 chars', 66 | $ruleDefinition->getDescription(), 67 | $ruleDefinition->getRuleClass(), 68 | )); 69 | 70 | $isValid = false; 71 | } 72 | 73 | if (count($ruleDefinition->getCodeSamples()) < 1) { 74 | $this->symfonyStyle->error(sprintf( 75 | 'Rule "%s" does not have any code samples. Ad at least one so documentation is clear', 76 | $ruleDefinition->getRuleClass(), 77 | )); 78 | 79 | $isValid = false; 80 | } 81 | } 82 | 83 | if ($isValid === false) { 84 | return self::FAILURE; 85 | } 86 | 87 | $this->symfonyStyle->success(sprintf('All "%d" rule definitions are valid', count($classesByFilePaths))); 88 | 89 | return self::SUCCESS; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DependencyInjection/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | singleton(Differ::class, static function (): Differ { 29 | $unifiedDiffOutputBuilder = new UnifiedDiffOutputBuilder(''); 30 | 31 | // this is required to show full diffs from start to end 32 | $contextLinesReflectionProperty = new ReflectionProperty($unifiedDiffOutputBuilder, 'contextLines'); 33 | $contextLinesReflectionProperty->setValue($unifiedDiffOutputBuilder, 10000); 34 | 35 | return new Differ($unifiedDiffOutputBuilder); 36 | }); 37 | 38 | $this->registerConsole($container); 39 | 40 | return $container; 41 | } 42 | 43 | private function registerConsole(Container $container): void 44 | { 45 | $container->singleton(SymfonyStyle::class, static function (): SymfonyStyle { 46 | $input = new ArrayInput([]); 47 | $output = defined('PHPUNIT_COMPOSER_INSTALL') ? new NullOutput() : new ConsoleOutput(); 48 | 49 | return new SymfonyStyle($input, $output); 50 | }); 51 | 52 | $container->singleton(Application::class, function (Container $container): Application { 53 | $application = new Application(); 54 | 55 | $generateCommand = $container->make(GenerateCommand::class); 56 | $application->add($generateCommand); 57 | 58 | $validateCommand = $container->make(ValidateCommand::class); 59 | $application->add($validateCommand); 60 | 61 | $this->propertyCallable($application, 'commands', static function (array $defaultCommands) { 62 | unset($defaultCommands['completion']); 63 | unset($defaultCommands['help']); 64 | return $defaultCommands; 65 | }); 66 | 67 | return $application; 68 | }); 69 | } 70 | 71 | private function propertyCallable(object $object, string $propertyName, callable $callable): void 72 | { 73 | $reflectionProperty = new ReflectionProperty($object, $propertyName); 74 | $reflectionProperty->setAccessible(true); 75 | 76 | $value = $reflectionProperty->getValue($object); 77 | $modifiedValue = $callable($value); 78 | 79 | $reflectionProperty->setValue($object, $modifiedValue); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/DirectoryToMarkdownPrinter.php: -------------------------------------------------------------------------------- 1 | classByTypeFinder->findAndCreateRuleWithFilePaths($directories, $workingDirectory); 34 | 35 | $ruleWithFilePaths = $this->filterOutSkippedTypes($ruleWithFilePaths, $skipTypes); 36 | if ($ruleWithFilePaths === []) { 37 | // we need at least some classes 38 | throw new ShouldNotHappenException(sprintf('No documented classes found in "%s" directories', implode('","', $directories))); 39 | } 40 | 41 | $message = sprintf('Found %d documented rule classes', count($ruleWithFilePaths)); 42 | $this->symfonyStyle->note($message); 43 | 44 | $classes = array_map( 45 | static fn (RuleClassWithFilePath $ruleClassWithFilePath): string => $ruleClassWithFilePath->getClass(), 46 | $ruleWithFilePaths 47 | ); 48 | 49 | $this->symfonyStyle->listing($classes); 50 | 51 | // 2. create rule definition collection 52 | $this->symfonyStyle->note('Resolving rule definitions'); 53 | 54 | $ruleDefinitions = $this->ruleDefinitionsResolver->resolveFromClassNames($ruleWithFilePaths); 55 | 56 | // 3. print rule definitions to markdown lines 57 | $this->symfonyStyle->note('Printing rule definitions'); 58 | $markdownLines = $this->ruleDefinitionsPrinter->print($ruleDefinitions, $categorizeLevel); 59 | 60 | $fileContent = ''; 61 | foreach ($markdownLines as $markdownLine) { 62 | $fileContent .= trim($markdownLine) . PHP_EOL . PHP_EOL; 63 | } 64 | 65 | return rtrim($fileContent) . PHP_EOL; 66 | } 67 | 68 | /** 69 | * @param RuleClassWithFilePath[] $ruleClassWithFilePaths 70 | * @param string[] $skipTypes 71 | * @return RuleClassWithFilePath[] 72 | */ 73 | private function filterOutSkippedTypes(array $ruleClassWithFilePaths, array $skipTypes): array 74 | { 75 | // remove deprecated classes, as no point in promoting them 76 | $ruleClassWithFilePaths = array_filter($ruleClassWithFilePaths, static fn (RuleClassWithFilePath $ruleClassWithFilePath): bool => $ruleClassWithFilePath->isDeprecated() === false); 77 | 78 | if ($skipTypes === []) { 79 | return $ruleClassWithFilePaths; 80 | } 81 | 82 | return array_filter( 83 | $ruleClassWithFilePaths, 84 | static function (RuleClassWithFilePath $ruleClassWithFilePath) use ($skipTypes): bool { 85 | foreach ($skipTypes as $skipType) { 86 | if (is_a($ruleClassWithFilePath->getClass(), $skipType, true)) { 87 | return false; 88 | } 89 | } 90 | 91 | // nothing to skip 92 | return true; 93 | } 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Exception/ConfigurationBoundException.php: -------------------------------------------------------------------------------- 1 | makePathRelative( 16 | (string) realpath($filePath), 17 | (string) realpath($directory) 18 | ); 19 | 20 | return rtrim($relativeFilePath, '/'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/FileSystem/RuleDefinitionClassesFinder.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function findInDirectories(array $directories): array 24 | { 25 | $robotLoader = new RobotLoader(); 26 | $robotLoader->setTempDirectory(sys_get_temp_dir() . '/robot_loader_temp'); 27 | $robotLoader->addDirectory(...$directories); 28 | $robotLoader->ignoreDirs[] = '*tests*'; 29 | $robotLoader->ignoreDirs[] = '*Fixture*'; 30 | $robotLoader->ignoreDirs[] = '*templates*'; 31 | 32 | $robotLoader->rebuild(); 33 | 34 | $classesByFilePath = []; 35 | foreach ($robotLoader->getIndexedClasses() as $class => $filePath) { 36 | if (! is_a($class, self::DOCUMENTED_RULE_INTERFACE, true)) { 37 | continue; 38 | } 39 | 40 | // skip abstract classes 41 | $reflectionClass = new ReflectionClass($class); 42 | if ($reflectionClass->isAbstract()) { 43 | continue; 44 | } 45 | 46 | $classesByFilePath[$filePath] = $class; 47 | } 48 | 49 | return $classesByFilePath; 50 | } 51 | 52 | /** 53 | * @param string[] $directories 54 | * @return RuleClassWithFilePath[] 55 | */ 56 | public function findAndCreateRuleWithFilePaths(array $directories, string $workingDirectory): array 57 | { 58 | $classesByFilePath = $this->findInDirectories($directories); 59 | 60 | $desiredClasses = []; 61 | foreach ($classesByFilePath as $filePath => $class) { 62 | $isClassDeprecated = $this->isClassDeprecated($class); 63 | 64 | $relativeFilePath = PathsHelper::relativeFromDirectory($filePath, $workingDirectory); 65 | $desiredClasses[] = new RuleClassWithFilePath($class, $relativeFilePath, $isClassDeprecated); 66 | } 67 | 68 | usort( 69 | $desiredClasses, 70 | static fn (RuleClassWithFilePath $left, RuleClassWithFilePath $right): int => $left->getClass() <=> $right->getClass() 71 | ); 72 | 73 | return $desiredClasses; 74 | } 75 | 76 | private function isClassDeprecated(string $class): bool 77 | { 78 | $reflectionClass = new ReflectionClass($class); 79 | 80 | return str_contains((string) $reflectionClass->getDocComment(), '@deprecated'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Printer/CodeSamplePrinter/BadGoodCodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | markdownCodeWrapper->printPhpCode($codeSample->getBadCode()), 24 | ':x:', 25 | '
', 26 | $this->markdownCodeWrapper->printPhpCode($codeSample->getGoodCode()), 27 | ':+1:', 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Printer/CodeSamplePrinter/CodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | ruleCodeSamplePrinters = [ 33 | $ecsRuleCodeSamplePrinter, 34 | $phpStanRuleCodeSamplePrinter, 35 | $rectorRuleCodeSamplePrinter, 36 | ]; 37 | } 38 | 39 | /** 40 | * @return string[] 41 | */ 42 | public function print(RuleDefinition $ruleDefinition): array 43 | { 44 | $lines = []; 45 | 46 | foreach ($ruleDefinition->getCodeSamples() as $codeSample) { 47 | $this->ensureConfigureRuleBoundsConfiguredCodeSample($codeSample, $ruleDefinition); 48 | 49 | foreach ($this->ruleCodeSamplePrinters as $ruleCodeSamplePrinter) { 50 | if (! $ruleCodeSamplePrinter->isMatch($ruleDefinition->getRuleClass())) { 51 | continue; 52 | } 53 | 54 | $newLines = $ruleCodeSamplePrinter->print($codeSample, $ruleDefinition); 55 | $lines = array_merge($lines, $newLines); 56 | break; 57 | } 58 | 59 | $lines[] = '
'; 60 | } 61 | 62 | return $lines; 63 | } 64 | 65 | private function ensureConfigureRuleBoundsConfiguredCodeSample( 66 | CodeSampleInterface $codeSample, 67 | RuleDefinition $ruleDefinition 68 | ): void { 69 | // ensure the configured rule + configure code sample are used 70 | if ($codeSample instanceof ConfiguredCodeSample) { 71 | if (is_a($ruleDefinition->getRuleClass(), ConfigurableRuleInterface::class, true)) { 72 | return; 73 | } 74 | 75 | $errorMessage = sprintf( 76 | 'The "%s" rule has configure code sample and must implements "%s" interface', 77 | $ruleDefinition->getRuleClass(), 78 | ConfigurableRuleInterface::class 79 | ); 80 | throw new ConfigurationBoundException($errorMessage); 81 | } 82 | 83 | if (! is_a($ruleDefinition->getRuleClass(), ConfigurableRuleInterface::class, true)) { 84 | return; 85 | } 86 | 87 | $errorMessage = sprintf( 88 | 'The "%s" rule implements "%s" and code sample must be "%s"', 89 | $ruleDefinition->getRuleClass(), 90 | ConfigurableRuleInterface::class, 91 | ConfiguredCodeSample::class, 92 | ); 93 | throw new ConfigurationBoundException($errorMessage); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Printer/CodeSamplePrinter/DiffCodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | markdownDiffer->diff( 23 | $codeSample->getBadCode(), 24 | $codeSample->getGoodCode() 25 | ); 26 | 27 | return [$diffCode]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Printer/Markdown/MarkdownCodeWrapper.php: -------------------------------------------------------------------------------- 1 | printCodeWrapped($content, self::SYNTAX_JSON); 27 | } 28 | 29 | public function printPhpCode(string $content): string 30 | { 31 | return $this->printCodeWrapped($content, self::SYNTAX_PHP); 32 | } 33 | 34 | public function printYamlCode(string $content): string 35 | { 36 | return $this->printCodeWrapped($content, self::SYNTAX_YAML); 37 | } 38 | 39 | private function printCodeWrapped(string $content, string $format): string 40 | { 41 | return sprintf('```%s%s%s%s```', $format, PHP_EOL, rtrim($content), PHP_EOL) . PHP_EOL; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Printer/Markdown/MarkdownDiffer.php: -------------------------------------------------------------------------------- 1 | differ->diff($old, $new); 39 | 40 | $diff = $this->clearUnifiedDiffOutputFirstLine($diff); 41 | $diff = $this->removeTrailingWhitespaces($diff); 42 | 43 | return $this->warpToDiffCode($diff); 44 | } 45 | 46 | /** 47 | * Removes UnifiedDiffOutputBuilder generated pre-spaces " \n" => "\n" 48 | */ 49 | private function removeTrailingWhitespaces(string $diff): string 50 | { 51 | $diff = Strings::replace($diff, self::SPACE_AND_NEWLINE_REGEX, PHP_EOL); 52 | 53 | return rtrim($diff); 54 | } 55 | 56 | private function warpToDiffCode(string $content): string 57 | { 58 | return '```diff' . PHP_EOL . $content . PHP_EOL . '```' . PHP_EOL; 59 | } 60 | 61 | private function clearUnifiedDiffOutputFirstLine(string $diff): string 62 | { 63 | return Strings::replace($diff, self::METADATA_REGEX, ''); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Printer/NeonPrinter.php: -------------------------------------------------------------------------------- 1 | .*?)$#ms'; 17 | 18 | /** 19 | * @param mixed[] $phpStanNeon 20 | */ 21 | public function printNeon(array $phpStanNeon): string 22 | { 23 | $printedContent = Yaml::dump($phpStanNeon, 1000); 24 | 25 | // inline single tags, dummy 26 | $printedContent = Strings::replace($printedContent, self::TAGS_REGEX, 'tags: [$1]'); 27 | 28 | return rtrim($printedContent) . PHP_EOL; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Printer/RuleDefinitionsPrinter.php: -------------------------------------------------------------------------------- 1 | groupDefinitionsByCategory($ruleDefinitions, $categorizeLevel); 34 | 35 | $categoryMenuLines = $this->createCategoryMenu($ruleDefinitionsByCategory); 36 | $lines = [...$lines, ...$categoryMenuLines]; 37 | 38 | foreach ($ruleDefinitionsByCategory as $category => $ruleDefinitions) { 39 | $lines[] = '## ' . $category; 40 | $lines = $this->printRuleDefinitions($ruleDefinitions, $lines, $categorizeLevel); 41 | } 42 | } else { 43 | $lines = $this->printRuleDefinitions($ruleDefinitions, $lines, null); 44 | } 45 | 46 | return $lines; 47 | } 48 | 49 | /** 50 | * @param RuleDefinition[] $ruleDefinitions 51 | * @return array 52 | */ 53 | private function groupDefinitionsByCategory(array $ruleDefinitions, int $categorizeLevel): array 54 | { 55 | $ruleDefinitionsByCategory = []; 56 | 57 | // have a convention from namespace :) 58 | foreach ($ruleDefinitions as $ruleDefinition) { 59 | $category = $this->resolveCategory($ruleDefinition, $categorizeLevel); 60 | $ruleDefinitionsByCategory[$category][] = $ruleDefinition; 61 | } 62 | 63 | ksort($ruleDefinitionsByCategory); 64 | 65 | return $ruleDefinitionsByCategory; 66 | } 67 | 68 | /** 69 | * @param RuleDefinition[] $ruleDefinitions 70 | * @param string[] $lines 71 | * @return string[] 72 | */ 73 | private function printRuleDefinitions(array $ruleDefinitions, array $lines, ?int $categorizeLevel): array 74 | { 75 | foreach ($ruleDefinitions as $ruleDefinition) { 76 | if ($categorizeLevel) { 77 | $lines[] = '### ' . $ruleDefinition->getRuleShortClass(); 78 | } else { 79 | $lines[] = '## ' . $ruleDefinition->getRuleShortClass(); 80 | } 81 | 82 | $lines[] = $this->keywordHighlighter->highlight($ruleDefinition->getDescription()); 83 | 84 | if ($ruleDefinition->isConfigurable()) { 85 | $lines[] = ':wrench: **configure it!**'; 86 | } 87 | 88 | $lines[] = '- class: [`' . $ruleDefinition->getRuleClass() . '`](' . $ruleDefinition->getRuleFilePath() . ')'; 89 | 90 | $codeSampleLines = $this->codeSamplePrinter->print($ruleDefinition); 91 | $lines = array_merge($lines, $codeSampleLines); 92 | } 93 | 94 | return $lines; 95 | } 96 | 97 | /** 98 | * @param array $ruleDefinitionsByCategory 99 | * @return string[] 100 | */ 101 | private function createCategoryMenu(array $ruleDefinitionsByCategory): array 102 | { 103 | $lines = []; 104 | $lines[] = '
'; 105 | $lines[] = '## Categories'; 106 | 107 | foreach ($ruleDefinitionsByCategory as $category => $ruleDefinitions) { 108 | $categoryLink = strtolower(Strings::webalize($category)); 109 | 110 | $lines[] = sprintf('- [%s](#%s) (%d)', $category, $categoryLink, count($ruleDefinitions)); 111 | } 112 | 113 | $lines[] = '
'; 114 | 115 | return $lines; 116 | } 117 | 118 | private function resolveCategory(RuleDefinition $ruleDefinition, int $categorizeLevel): string 119 | { 120 | $classNameParts = explode('\\', $ruleDefinition->getRuleClass()); 121 | 122 | // get one namespace before last by convention 123 | array_pop($classNameParts); 124 | 125 | $categoryName = null; 126 | while ($categorizeLevel > 0) { 127 | // get one namespace before last by convention 128 | $categoryName = array_pop($classNameParts); 129 | --$categorizeLevel; 130 | } 131 | 132 | // remove _ as not part of title 133 | Assert::string($categoryName); 134 | 135 | return trim($categoryName, '_'); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/RuleCodeSamplePrinter/ECSRuleCodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | getRuleClass(), 'PHP_CodeSniffer\Sniffs\Sniff', true)) { 36 | return $this->badGoodCodeSamplePrinter->print($codeSample); 37 | } 38 | 39 | return $this->diffCodeSamplePrinter->print($codeSample); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/RuleCodeSamplePrinter/PHPStanRuleCodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | printConfigurableCodeSample($codeSample, $ruleDefinition); 36 | } 37 | 38 | return $this->badGoodCodeSamplePrinter->print($codeSample); 39 | } 40 | 41 | /** 42 | * @return string[] 43 | */ 44 | private function printConfigurableCodeSample( 45 | ConfiguredCodeSample $configuredCodeSample, 46 | RuleDefinition $ruleDefinition 47 | ): array { 48 | $lines = []; 49 | 50 | $phpStanNeon = [ 51 | 'services' => [ 52 | [ 53 | 'class' => $ruleDefinition->getRuleClass(), 54 | 'tags' => ['phpstan.rules.rule'], 55 | 'arguments' => $configuredCodeSample->getConfiguration(), 56 | ], 57 | ], 58 | ]; 59 | 60 | $printedNeon = $this->neonPrinter->printNeon($phpStanNeon); 61 | $lines[] = $this->markdownCodeWrapper->printYamlCode($printedNeon); 62 | 63 | $lines[] = '↓'; 64 | 65 | $newLines = $this->badGoodCodeSamplePrinter->print($configuredCodeSample); 66 | return [...$lines, ...$newLines]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/RuleCodeSamplePrinter/RectorRuleCodeSamplePrinter.php: -------------------------------------------------------------------------------- 1 | diffCodeSamplePrinter->print($codeSample); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/RuleDefinitionsResolver.php: -------------------------------------------------------------------------------- 1 | getClass()); 25 | 26 | $documentedRule = $reflectionClass->newInstanceWithoutConstructor(); 27 | if (! $documentedRule instanceof DocumentedRuleInterface) { 28 | throw new ShouldNotHappenException(); 29 | } 30 | 31 | $ruleDefinition = $documentedRule->getRuleDefinition(); 32 | $ruleDefinition->setRuleClass($ruleClassWithFilePath->getClass()); 33 | 34 | $ruleDefinition->setRuleFilePath($ruleClassWithFilePath->getPath()); 35 | 36 | $ruleDefinitions[] = $ruleDefinition; 37 | } 38 | 39 | return $this->sortByClassName($ruleDefinitions); 40 | } 41 | 42 | /** 43 | * @param RuleDefinition[] $ruleDefinitions 44 | * @return RuleDefinition[] 45 | */ 46 | private function sortByClassName(array $ruleDefinitions): array 47 | { 48 | usort( 49 | $ruleDefinitions, 50 | static fn (RuleDefinition $firstRuleDefinition, RuleDefinition $secondRuleDefinition): int => $firstRuleDefinition->getRuleShortClass() <=> $secondRuleDefinition->getRuleShortClass() 51 | ); 52 | 53 | return $ruleDefinitions; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Text/KeywordHighlighter.php: -------------------------------------------------------------------------------- 1 | ]+)[^\]](\(\))?#'; 34 | 35 | /** 36 | * @var string 37 | * @see https://regex101.com/r/uxtJDA/1 38 | */ 39 | private const STATIC_CALL_REGEX = '#([A-Za-z::\-\>]+)(\(\))$#'; 40 | 41 | /** 42 | * @var string 43 | * @see https://regex101.com/r/9vnLcf/1 44 | */ 45 | private const ANNOTATION_REGEX = '#(\@\w+)$#'; 46 | 47 | /** 48 | * @var string 49 | * @see https://regex101.com/r/bwUIKb/1 50 | */ 51 | private const METHOD_NAME_REGEX = '#\w+\(\)#'; 52 | 53 | /** 54 | * @var string 55 | * @see https://regex101.com/r/18wjck/2 56 | */ 57 | private const COMMA_SPLIT_REGEX = '#(?\w+\(.*\))(\s{0,})(?,)(?\`)#'; 58 | 59 | public function highlight(string $content): string 60 | { 61 | $words = Strings::split($content, '# #'); 62 | foreach ($words as $key => $word) { 63 | if (! $this->isKeywordToHighlight($word)) { 64 | continue; 65 | } 66 | 67 | $words[$key] = Strings::replace( 68 | '`' . $word . '`', 69 | self::COMMA_SPLIT_REGEX, 70 | static fn (array $match): string => $match['call'] . $match['quote'] . $match['comma'] 71 | ); 72 | } 73 | 74 | return implode(' ', $words); 75 | } 76 | 77 | private function isKeywordToHighlight(string $word): bool 78 | { 79 | if (Strings::match($word, self::ANNOTATION_REGEX)) { 80 | return true; 81 | } 82 | 83 | // already in code quotes 84 | if (\str_starts_with($word, '`')) { 85 | return false; 86 | } 87 | 88 | if (\str_ends_with($word, '`')) { 89 | return false; 90 | } 91 | 92 | // part of normal text 93 | if (in_array($word, self::TEXT_WORDS, true)) { 94 | return false; 95 | } 96 | 97 | if ($this->isFunctionOrClass($word)) { 98 | return true; 99 | } 100 | 101 | if ($word === 'composer.json') { 102 | return true; 103 | } 104 | 105 | if ((bool) Strings::match($word, self::VARIABLE_CALL_OR_VARIABLE_REGEX)) { 106 | return true; 107 | } 108 | 109 | return (bool) Strings::match($word, self::STATIC_CALL_REGEX); 110 | } 111 | 112 | private function isFunctionOrClass(string $word): bool 113 | { 114 | if (Strings::match($word, self::METHOD_NAME_REGEX)) { 115 | return true; 116 | } 117 | 118 | if ($this->doesClassLikeExist($word)) { 119 | // not a className 120 | if (! \str_contains($word, '\\')) { 121 | return in_array($word, [Throwable::class, 'Exception'], true); 122 | } 123 | 124 | return true; 125 | } 126 | 127 | return false; 128 | } 129 | 130 | private function doesClassLikeExist(string $className): bool 131 | { 132 | if (class_exists($className)) { 133 | return true; 134 | } 135 | 136 | if (interface_exists($className)) { 137 | return true; 138 | } 139 | 140 | return trait_exists($className); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ValueObject/Option.php: -------------------------------------------------------------------------------- 1 | class; 19 | } 20 | 21 | public function getPath(): string 22 | { 23 | return $this->path; 24 | } 25 | 26 | public function isDeprecated(): bool 27 | { 28 | return $this->isDeprecated; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/PHPStan/Rules/Rule.php: -------------------------------------------------------------------------------- 1 |