├── .gitattributes ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE.md ├── bin └── phpinsights ├── composer.json ├── config ├── container.php └── routes │ └── console.php ├── phpstan.neon ├── phpunit.xml ├── rector.php ├── schema.json ├── src ├── Application │ ├── Adapters │ │ ├── Drupal │ │ │ └── Preset.php │ │ ├── Laravel │ │ │ ├── Commands │ │ │ │ └── InsightsCommand.php │ │ │ ├── InsightsServiceProvider.php │ │ │ └── Preset.php │ │ ├── Magento2 │ │ │ └── Preset.php │ │ ├── Symfony │ │ │ └── Preset.php │ │ ├── WordPress │ │ │ └── Preset.php │ │ └── Yii │ │ │ └── Preset.php │ ├── Composer.php │ ├── ConfigResolver.php │ ├── Console │ │ ├── Analyser.php │ │ ├── Commands │ │ │ ├── AnalyseCommand.php │ │ │ ├── FixCommand.php │ │ │ ├── InternalProcessorCommand.php │ │ │ └── InvokableCommand.php │ │ ├── Contracts │ │ │ ├── Formatter.php │ │ │ └── Style.php │ │ ├── Definitions │ │ │ ├── AnalyseDefinition.php │ │ │ ├── BaseDefinition.php │ │ │ ├── DefinitionBinder.php │ │ │ ├── FixDefinition.php │ │ │ └── InternalProcessorDefinition.php │ │ ├── Formatters │ │ │ ├── Checkstyle.php │ │ │ ├── CodeClimate.php │ │ │ ├── Console.php │ │ │ ├── FormatResolver.php │ │ │ ├── GithubAction.php │ │ │ ├── Json.php │ │ │ ├── Multiple.php │ │ │ └── PathShortener.php │ │ ├── OutputDecorator.php │ │ ├── Style.php │ │ └── Styles │ │ │ ├── Bold.php │ │ │ └── Title.php │ ├── DefaultPreset.php │ ├── Injectors │ │ ├── Cache.php │ │ ├── Configuration.php │ │ ├── FileProcessors.php │ │ ├── InsightLoaders.php │ │ └── Repositories.php │ └── PathResolver.php ├── Domain │ ├── Analyser.php │ ├── Collector.php │ ├── Configuration.php │ ├── Container.php │ ├── Contracts │ │ ├── DetailsCarrier.php │ │ ├── FileLinkFormatter.php │ │ ├── FileProcessor.php │ │ ├── Fixable.php │ │ ├── GlobalInsight.php │ │ ├── HasAvg.php │ │ ├── HasDetails.php │ │ ├── HasInsights.php │ │ ├── HasMax.php │ │ ├── HasPercentage.php │ │ ├── HasValue.php │ │ ├── Insight.php │ │ ├── InsightLoader.php │ │ ├── Metric.php │ │ ├── Preset.php │ │ ├── Repositories │ │ │ └── FilesRepository.php │ │ └── Sniffer.php │ ├── Details.php │ ├── DetailsComparator.php │ ├── Differ.php │ ├── Exceptions │ │ ├── ComposerNotFound.php │ │ ├── DirectoryNotFound.php │ │ ├── InsightClassNotFound.php │ │ ├── InternetConnectionNotFound.php │ │ ├── InvalidConfiguration.php │ │ ├── PresetNotFound.php │ │ └── SniffClassNotFound.php │ ├── File.php │ ├── FileFactory.php │ ├── FileProcessors │ │ ├── FixerFileProcessor.php │ │ └── SniffFileProcessor.php │ ├── Helper │ │ └── Files.php │ ├── InsightLoader │ │ ├── FixerLoader.php │ │ ├── InsightLoader.php │ │ └── SniffLoader.php │ ├── Insights │ │ ├── ClassMethodAverageCyclomaticComplexityIsHigh.php │ │ ├── CyclomaticComplexityIsHigh.php │ │ ├── FixPerFileCollector.php │ │ ├── FixerDecorator.php │ │ ├── ForbiddenDefineFunctions.php │ │ ├── ForbiddenDefineGlobalConstants.php │ │ ├── ForbiddenFinalClasses.php │ │ ├── ForbiddenGlobals.php │ │ ├── ForbiddenNormalClasses.php │ │ ├── ForbiddenPrivateMethods.php │ │ ├── ForbiddenSecurityIssues.php │ │ ├── ForbiddenTraits.php │ │ ├── ForbiddenUsingGlobals.php │ │ ├── Insight.php │ │ ├── InsightCollection.php │ │ ├── InsightCollectionFactory.php │ │ ├── InsightFactory.php │ │ ├── MethodCyclomaticComplexityIsHigh.php │ │ ├── SniffDecorator.php │ │ └── SyntaxCheck.php │ ├── Kernel.php │ ├── LinkFormatter │ │ ├── FileLinkFormatter.php │ │ └── NullFileLinkFormatter.php │ ├── Metrics │ │ ├── Architecture │ │ │ ├── Classes.php │ │ │ ├── Constants.php │ │ │ ├── Files.php │ │ │ ├── Functions.php │ │ │ ├── Globally.php │ │ │ ├── Interfaces.php │ │ │ ├── Namespaces.php │ │ │ └── Traits.php │ │ ├── Code │ │ │ ├── Classes.php │ │ │ ├── Code.php │ │ │ ├── Comments.php │ │ │ ├── Functions.php │ │ │ └── Globally.php │ │ ├── Complexity │ │ │ └── Complexity.php │ │ ├── Security │ │ │ └── Security.php │ │ └── Style │ │ │ └── Style.php │ ├── MetricsFinder.php │ ├── Reflection.php │ ├── Results.php │ ├── Runner.php │ └── Sniffs │ │ └── ForbiddenSetterSniff.php └── Infrastructure │ └── Repositories │ └── LocalFilesRepository.php └── stubs ├── config.php ├── drupal.php ├── laravel.php ├── magento2.php ├── symfony.php └── wordpress.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | .dockerignore export-ignore 6 | .editorconfig export-ignore 7 | .phpcs.xml.dist export-ignore 8 | .styleci.yml export-ignore 9 | .travis.yml export-ignore 10 | phpinsights.php export-ignore 11 | phpstan.neon.dist export-ignore 12 | phpunit.xml.dist export-ignore 13 | CHANGELOG.md export-ignore 14 | CONTRIBUTING.md export-ignore 15 | README.md export-ignore 16 | RELEASE.md export-ignore 17 | .github export-ignore 18 | docker export-ignore 19 | docs export-ignore 20 | tests export-ignore 21 | art export-ignore 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /docs/.vuepress/dist 3 | /docs/.vuepress/public/* 4 | .idea/* 5 | .env 6 | .phpunit.result.cache 7 | .phpunit.cache/ 8 | /composer.lock 9 | .php_cs.cache 10 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setUsingCache(false) 6 | ->setRiskyAllowed(true) 7 | ->setRules([ 8 | '@PSR2' => true, 9 | 'array_syntax' => ['syntax' => 'short'], 10 | 'no_empty_statement' => true, 11 | 'no_unneeded_control_parentheses' => true, 12 | 'no_unneeded_braces' => true, 13 | 'no_unused_imports' => true, 14 | 'ordered_imports' => true, 15 | 'protected_to_private' => true, 16 | ]) 17 | ->setFinder(PhpCsFixer\Finder::create() 18 | ->in(__DIR__ . '/src/') 19 | ) 20 | ; 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuno Maduro 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 | -------------------------------------------------------------------------------- /bin/phpinsights: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | all() as $command) { 36 | $command->setHidden(true); 37 | } 38 | 39 | $application->addCommands($commands); 40 | $application->setDefaultCommand('analyse'); 41 | 42 | $application->run(); 43 | })(); 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nunomaduro/phpinsights", 3 | "description": "Instant PHP quality checks from your console.", 4 | "keywords": [ 5 | "php", 6 | "insights", 7 | "console", 8 | "quality", 9 | "source", 10 | "code" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Nuno Maduro", 16 | "email": "enunomaduro@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "ext-iconv": "*", 22 | "ext-json": "*", 23 | "ext-mbstring": "*", 24 | "ext-tokenizer": "*", 25 | "cmgmyr/phploc": "^8.0.6", 26 | "composer/semver": "^3.4.3", 27 | "friendsofphp/php-cs-fixer": "^3.74.0", 28 | "justinrainbow/json-schema": "^6.3.1", 29 | "league/container": "^5.0.1", 30 | "php-parallel-lint/php-parallel-lint": "^1.4.0", 31 | "psr/container": "^2.0.2", 32 | "psr/simple-cache": "^2.0|^3.0", 33 | "sebastian/diff": "^5.1.1|^6.0.2|^7.0.0", 34 | "slevomat/coding-standard": "^8.16.2", 35 | "squizlabs/php_codesniffer": "^3.12.0", 36 | "symfony/cache": "^6.4.20|^7.2.5", 37 | "symfony/console": "^6.4.20|^7.2.5", 38 | "symfony/finder": "^6.4.17|^7.2.2", 39 | "symfony/http-client": "^6.4.19|^7.2.4", 40 | "symfony/process": "^6.4.20|^7.2.5" 41 | }, 42 | "require-dev": { 43 | "illuminate/console": "^10.48.28|^11.44.2|^12.4", 44 | "illuminate/support": "^10.48.28|^11.44.2|^12.4", 45 | "mockery/mockery": "^1.6.12", 46 | "phpstan/phpstan": "^2.1.11", 47 | "phpunit/phpunit": "^10.5.45|^11.5.15", 48 | "symfony/var-dumper": "^6.4.18|^7.2.3" 49 | }, 50 | "suggest": { 51 | "ext-simplexml": "It is needed for the checkstyle formatter" 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "Tests\\": "tests/" 56 | } 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true, 60 | "autoload": { 61 | "psr-4": { 62 | "NunoMaduro\\PhpInsights\\": "src" 63 | } 64 | }, 65 | "config": { 66 | "sort-packages": true, 67 | "preferred-install": "dist", 68 | "allow-plugins": { 69 | "dealerdirect/phpcodesniffer-composer-installer": true 70 | } 71 | }, 72 | "extra": { 73 | "laravel": { 74 | "providers": [ 75 | "NunoMaduro\\PhpInsights\\Application\\Adapters\\Laravel\\InsightsServiceProvider" 76 | ] 77 | } 78 | }, 79 | "bin": [ 80 | "bin/phpinsights" 81 | ], 82 | "scripts": { 83 | "website:copy-changelog": "@php -r \"copy('CHANGELOG.md', 'docs/changelog.md');\"", 84 | "website:copy-logo": "@php -r \"(is_dir('docs/.vuepress/public') || mkdir('docs/.vuepress/public')) && copy('art/logo_mixed.gif', 'docs/.vuepress/public/logo.gif') && copy('art/heart.svg', 'docs/.vuepress/public/heart.svg') && copy('art/heart.png', 'docs/.vuepress/public/heart.png');\"", 85 | "csfixer:test": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix -v", 86 | "phpstan:test": "phpstan analyse --ansi", 87 | "phpunit:test": "phpunit --colors=always", 88 | "insights": "bin/phpinsights analyse --ansi -v --no-interaction", 89 | "test": [ 90 | "@phpstan:test", 91 | "@csfixer:test --dry-run", 92 | "@phpunit:test", 93 | "@insights" 94 | ], 95 | "fix": [ 96 | "@csfixer:test", 97 | "@insights --fix --quiet" 98 | ], 99 | "post-install-cmd": [ 100 | "@website:copy-changelog", 101 | "@website:copy-logo" 102 | ], 103 | "post-update-cmd": [ 104 | "@website:copy-changelog", 105 | "@website:copy-logo" 106 | ] 107 | }, 108 | "scripts-descriptions": { 109 | "website:copy-changelog": "Copy package changelog to the website", 110 | "website:copy-logo": "Copy logo from art directory to the website", 111 | "csfixer:test": "Run the PhpCsFixer tests.", 112 | "phpstan:test": "Run the phpstan tests.", 113 | "phpunit:test": "Run the phpunit tests.", 114 | "insights": "Run the phpinsights tests", 115 | "test": "Run all tests including phpstan, phpunit and phpcs.", 116 | "fix": "Run ecs, phpinsights and rector fixers." 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | FileProcessor::FILE_PROCESSOR_TAG, 26 | InsightLoaders::class => InsightLoader::INSIGHT_LOADER_TAG, 27 | ]; 28 | 29 | $container = (new Container())->delegate(new ReflectionContainer()); 30 | 31 | foreach ($injectors as $injector) { 32 | foreach ((new $injector())() as $id => $concrete) { 33 | $definition = $container->add($id, $concrete); 34 | 35 | if (isset($tagsDefinition[$injector])) { 36 | $definition->addTag($tagsDefinition[$injector]); 37 | } 38 | } 39 | } 40 | 41 | return $container; 42 | })(); 43 | -------------------------------------------------------------------------------- /config/routes/console.php: -------------------------------------------------------------------------------- 1 | get(AnalyseCommand::class), 19 | AnalyseDefinition::get() 20 | ); 21 | 22 | $fixCommand = new InvokableCommand( 23 | 'fix', 24 | $container->get(FixCommand::class), 25 | FixDefinition::get() 26 | ); 27 | 28 | $internalProcessorCommand = new InvokableCommand( 29 | InternalProcessorCommand::NAME, 30 | $container->get(InternalProcessorCommand::class), 31 | InternalProcessorDefinition::get() 32 | ); 33 | $internalProcessorCommand->setHidden(true); 34 | 35 | return [ 36 | $analyseCommand, 37 | $fixCommand, 38 | $internalProcessorCommand, 39 | ]; 40 | })(); 41 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - config 5 | - src 6 | - stubs 7 | 8 | bootstrapFiles: 9 | - %rootDir%/../../squizlabs/php_codesniffer/autoload.php 10 | 11 | excludePaths: 12 | - %rootDir%/../../../tests/*/Fixtures/* 13 | - %rootDir%/../../../tests/Fixtures/* 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__.'/src', 10 | ]) 11 | ->withPreparedSets( 12 | deadCode: true, 13 | codeQuality: true, 14 | typeDeclarations: true, 15 | privatization: true, 16 | earlyReturn: true, 17 | strictBooleans: true, 18 | )->withPhpSets(php81: true); 19 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "readOnly": true, 5 | "title": "The JSON format for phpinsights", 6 | "required": [ 7 | "summary", 8 | "Code", 9 | "Complexity", 10 | "Architecture", 11 | "Style", 12 | "Security" 13 | ], 14 | "definitions": { 15 | "percentage": { 16 | "type": "number", 17 | "minimum": 0, 18 | "maximum": 100 19 | }, 20 | "insight": { 21 | "type": "object", 22 | "required": [ 23 | "title", 24 | "insightClass" 25 | ], 26 | "properties": { 27 | "title": { 28 | "type": "string" 29 | }, 30 | "insightClass": { 31 | "type": "string" 32 | }, 33 | "file": { 34 | "type": "string" 35 | }, 36 | "line": { 37 | "type": "integer", 38 | "minimum": 1 39 | }, 40 | "message": { 41 | "type": "string" 42 | }, 43 | "diff": { 44 | "type": "string" 45 | } 46 | } 47 | } 48 | }, 49 | "properties": { 50 | "summary": { 51 | "$id": "#/properties/summary", 52 | "type": "object", 53 | "title": "The Summary Schema", 54 | "properties": { 55 | "code": { 56 | "$ref": "#/definitions/percentage" 57 | }, 58 | "complexity": { 59 | "$ref": "#/definitions/percentage" 60 | }, 61 | "architecture": { 62 | "$ref": "#/definitions/percentage" 63 | }, 64 | "style": { 65 | "$ref": "#/definitions/percentage" 66 | }, 67 | "security issues": { 68 | "type": "integer", 69 | "minimum": 0 70 | }, 71 | "fixed issues": { 72 | "type": "integer", 73 | "minimum": 0 74 | } 75 | } 76 | }, 77 | "Code": { 78 | "type": "array", 79 | "items": { 80 | "$ref": "#/definitions/insight" 81 | } 82 | }, 83 | "Complexity": { 84 | "type": "array", 85 | "items": { 86 | "$ref": "#/definitions/insight" 87 | } 88 | }, 89 | "Architecture": { 90 | "type": "array", 91 | "items": { 92 | "$ref": "#/definitions/insight" 93 | } 94 | }, 95 | "Style": { 96 | "type": "array", 97 | "items": { 98 | "$ref": "#/definitions/insight" 99 | } 100 | }, 101 | "Security": { 102 | "type": "array", 103 | "items": { 104 | "$ref": "#/definitions/insight" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Application/Adapters/Drupal/Preset.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'core', 28 | 'modules/contrib', 29 | 'sites', 30 | 'profiles/contrib', 31 | 'themes/contrib', 32 | ], 33 | 'config' => [ 34 | ForbiddenFunctionsSniff::class => [ 35 | 'forbiddenFunctions' => [ 36 | 'dd' => null, 37 | 'dump' => null, 38 | ], 39 | ], 40 | ], 41 | ]; 42 | 43 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), $config); 44 | } 45 | 46 | public static function shouldBeApplied(Composer $composer): bool 47 | { 48 | $requirements = $composer->getRequirements(); 49 | $replace = $composer->getReplacements(); 50 | 51 | foreach (array_keys(array_merge($requirements, $replace)) as $requirement) { 52 | if (strpos($requirement, 'drupal/core') !== false) { 53 | return true; 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Application/Adapters/Laravel/Commands/InsightsCommand.php: -------------------------------------------------------------------------------- 1 | https://github.com/nunomaduro/phpinsights', 27 | ' - Sponsor the maintainers:', 28 | ' https://github.com/sponsors/nunomaduro', 29 | ' - Subscribe to my YouTube channel:', 30 | ' https://www.youtube.com/@nunomaduro', 31 | ]; 32 | 33 | protected $name = 'insights'; 34 | 35 | protected $description = 'Analyze the code quality'; 36 | 37 | public function handle(): int 38 | { 39 | Kernel::bootstrap(); 40 | 41 | $configPath = ConfigResolver::resolvePath($this->input); 42 | 43 | if (! file_exists($configPath)) { 44 | $this->output->error('First, publish the configuration using: php artisan vendor:publish'); 45 | return 1; 46 | } 47 | 48 | $configuration = require $configPath; 49 | $configuration['fix'] = $this->hasOption('fix') && (bool) $this->option('fix') === true; 50 | try { 51 | $configuration = ConfigResolver::resolve($configuration, $this->input); 52 | } catch (InvalidConfiguration $exception) { 53 | $this->output->writeln([ 54 | '', 55 | ' Invalid configuration ', 56 | ' ' . $exception->getMessage() . '', 57 | '', 58 | ]); 59 | return 1; 60 | } 61 | 62 | $container = Container::make(); 63 | if (! $container instanceof \League\Container\Container) { 64 | throw new RuntimeException('Container should be League Container instance'); 65 | } 66 | 67 | $configurationDefinition = $container->extend(Configuration::class); 68 | $configurationDefinition->setConcrete($configuration); 69 | 70 | $analyseCommand = $container->get(AnalyseCommand::class); 71 | 72 | $output = (new Reflection($this->output))->get('output'); 73 | 74 | $result = $analyseCommand->__invoke($this->input, $output); 75 | 76 | if ($output instanceof ConsoleOutputInterface) { 77 | foreach (self::FUNDING_MESSAGES as $message) { 78 | $output->getErrorOutput()->writeln($message); 79 | } 80 | } 81 | 82 | return $result; 83 | } 84 | 85 | public function configure(): void 86 | { 87 | parent::configure(); 88 | 89 | $this->setDefinition( 90 | AnalyseDefinition::get() 91 | ); 92 | 93 | $this->getDefinition() 94 | ->getOption('config-path') 95 | ->setDefault('config/insights.php'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Application/Adapters/Laravel/InsightsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(Repositories::class)->__invoke() as $abstract => $concrete) { 20 | $this->app->bind($abstract, $concrete); 21 | } 22 | } 23 | 24 | public function boot(): void 25 | { 26 | /** 27 | * @noRector Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector 28 | */ 29 | if ($this->app instanceof Application) { // @phpstan-ignore-line 30 | $this->publishes([ 31 | __DIR__ . '/../../../../stubs/laravel.php' => $this->app->configPath('insights.php'), 32 | ], 'config'); 33 | } 34 | 35 | $this->commands([ 36 | InsightsCommand::class, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Application/Adapters/Laravel/Preset.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'config', 33 | 'storage', 34 | 'resources', 35 | 'bootstrap', 36 | 'nova', 37 | 'database', 38 | 'server.php', 39 | '_ide_helper.php', 40 | '_ide_helper_models.php', 41 | 'app/Providers/TelescopeServiceProvider.php', 42 | 'public', 43 | ], 44 | 'add' => [ 45 | // ... 46 | ], 47 | 'remove' => [ 48 | ProtectedToPrivateFixer::class, 49 | VoidReturnFixer::class, 50 | StaticClosureSniff::class, 51 | ], 52 | 'config' => [ 53 | ForbiddenDefineGlobalConstants::class => [ 54 | 'ignore' => ['LARAVEL_START'], 55 | ], 56 | ForbiddenFunctionsSniff::class => [ 57 | 'forbiddenFunctions' => [ 58 | 'dd' => null, 59 | 'dump' => null, 60 | 'ddd' => null, 61 | 'tinker' => null, 62 | ], 63 | ], 64 | ForbiddenSetterSniff::class => [ 65 | 'allowedMethodRegex' => [ 66 | '/^set.*?Attribute$/', 67 | ], 68 | ], 69 | ], 70 | ]; 71 | 72 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), $config); 73 | } 74 | 75 | public static function shouldBeApplied(Composer $composer): bool 76 | { 77 | $requirements = $composer->getRequirements(); 78 | 79 | foreach (array_keys($requirements) as $requirement) { 80 | if (strpos($requirement, 'laravel/framework') !== false) { 81 | return true; 82 | } 83 | if (strpos($requirement, 'illuminate/') !== false) { 84 | return true; 85 | } 86 | } 87 | 88 | return $composer->getName() === 'laravel/framework'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Application/Adapters/Magento2/Preset.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'bin', 20 | 'dev', 21 | 'generated', 22 | 'lib', 23 | 'phpserver', 24 | 'pub', 25 | 'setup', 26 | 'update', 27 | 'var', 28 | 'app/autoload.php', 29 | 'app/bootstrap.php', 30 | 'app/functions.php', 31 | 'index.php', 32 | ], 33 | 'add' => [ 34 | // ... 35 | ], 36 | 'remove' => [ 37 | // ... 38 | ], 39 | 'config' => [ 40 | // ... 41 | ], 42 | ]; 43 | public static function getName(): string 44 | { 45 | return 'magento2'; 46 | } 47 | 48 | public static function get(Composer $composer): array 49 | { 50 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), self::CONFIG); 51 | } 52 | 53 | public static function shouldBeApplied(Composer $composer): bool 54 | { 55 | $requirements = $composer->getRequirements(); 56 | 57 | foreach (array_keys($requirements) as $requirement) { 58 | if (strpos($requirement, 'magento/magento-cloud-metapackage') !== false) { 59 | return true; 60 | } 61 | if (strpos($requirement, 'magento/product-community-edition') !== false) { 62 | return true; 63 | } 64 | if (strpos($requirement, 'magento/product-enterprise-edition') !== false) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Application/Adapters/Symfony/Preset.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'var', 28 | 'translations', 29 | 'config', 30 | 'public', 31 | ], 32 | 'config' => [ 33 | ForbiddenFunctionsSniff::class => [ 34 | 'forbiddenFunctions' => [ 35 | 'dd' => null, 36 | 'dump' => null, 37 | ], 38 | ], 39 | ], 40 | ]; 41 | 42 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), $config); 43 | } 44 | 45 | public static function shouldBeApplied(Composer $composer): bool 46 | { 47 | $requirements = $composer->getRequirements(); 48 | 49 | foreach (array_keys($requirements) as $requirement) { 50 | if (strpos($requirement, 'symfony/framework-bundle') !== false) { 51 | return true; 52 | } 53 | if (strpos($requirement, 'symfony/flex') !== false) { 54 | return true; 55 | } 56 | if (strpos($requirement, 'symfony/symfony') !== false) { 57 | return true; 58 | } 59 | } 60 | 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Application/Adapters/WordPress/Preset.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'web/wp', 28 | 'web/.htaccess', 29 | 'web/app/mu-plugins/', 30 | 'web/app/upgrade', 31 | 'web/app/uploads/', 32 | 'web/app/plugins/', 33 | ], 34 | 'config' => [ 35 | ForbiddenFunctionsSniff::class => [ 36 | 'forbiddenFunctions' => [ 37 | 'eval' => null, 38 | 'error_log' => null, 39 | 'print_r' => null, 40 | ], 41 | ], 42 | ], 43 | ]; 44 | 45 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), $config); 46 | } 47 | 48 | public static function shouldBeApplied(Composer $composer): bool 49 | { 50 | $requirements = $composer->getRequirements(); 51 | 52 | foreach (array_keys($requirements) as $requirement) { 53 | if (strpos($requirement, 'johnpbloch/wordpress') !== false) { 54 | return true; 55 | } 56 | 57 | if (strpos($requirement, 'roots/wordpress') !== false) { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Application/Adapters/Yii/Preset.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'web', 20 | 'views', 21 | 'vagrant', 22 | 'runtime', 23 | ], 24 | 'add' => [ 25 | // ... 26 | ], 27 | 'remove' => [ 28 | // ... 29 | ], 30 | 'config' => [ 31 | // ... 32 | ], 33 | ]; 34 | public static function getName(): string 35 | { 36 | return 'yii'; 37 | } 38 | 39 | public static function get(Composer $composer): array 40 | { 41 | return ConfigResolver::mergeConfig(DefaultPreset::get($composer), self::CONFIG); 42 | } 43 | 44 | public static function shouldBeApplied(Composer $composer): bool 45 | { 46 | $requirements = $composer->getRequirements(); 47 | 48 | foreach (array_keys($requirements) as $requirement) { 49 | if (strpos($requirement, 'yiisoft/yii2') !== false) { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Application/Composer.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $config; 14 | 15 | /** 16 | * Composer constructor. 17 | * 18 | * @param array $data 19 | */ 20 | public function __construct(array $data) 21 | { 22 | $this->config = $data; 23 | } 24 | 25 | public static function fromPath(string $path): self 26 | { 27 | return new self(json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR)); 28 | } 29 | 30 | /** 31 | * @return array 32 | */ 33 | public function getRequirements(): array 34 | { 35 | return $this->config['require'] ?? []; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getReplacements(): array 42 | { 43 | return $this->config['replace'] ?? []; 44 | } 45 | 46 | public function getName(): string 47 | { 48 | return $this->config['name'] ?? ''; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Application/Console/Analyser.php: -------------------------------------------------------------------------------- 1 | insightCollectionFactory = $insightCollectionFactory; 26 | } 27 | 28 | /** 29 | * Analyse the given dirs. 30 | */ 31 | public function analyse(Formatter $formatter, OutputInterface $consoleOutput): Results 32 | { 33 | $metrics = MetricsFinder::find(); 34 | 35 | $insightCollection = $this->insightCollectionFactory 36 | ->get($metrics, $consoleOutput); 37 | 38 | $formatter->format($insightCollection, $metrics); 39 | 40 | return $insightCollection->results(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Application/Console/Commands/AnalyseCommand.php: -------------------------------------------------------------------------------- 1 | analyser = $analyser; 36 | $this->configuration = $configuration; 37 | } 38 | 39 | /** 40 | * Handle the given input. 41 | */ 42 | public function __invoke(InputInterface $input, OutputInterface $output): int 43 | { 44 | $consoleOutput = $output; 45 | $format = $input->getOption('format'); 46 | 47 | if ($consoleOutput instanceof ConsoleOutputInterface 48 | && is_array($format) 49 | && ! in_array('console', $format, true)) { 50 | $consoleOutput = $consoleOutput->getErrorOutput(); 51 | $consoleOutput->setDecorated($output->isDecorated()); 52 | } 53 | 54 | $consoleStyle = new Style($input, $consoleOutput); 55 | 56 | $output = OutputDecorator::decorate($output); 57 | 58 | $formatter = FormatResolver::resolve($input, $output, $consoleOutput); 59 | 60 | // flush cache before processing if asked 61 | if ($input->getOption('flush-cache') === true) { 62 | Container::make()->get(CacheInterface::class)->clear(); 63 | } 64 | 65 | $results = $this->analyser->analyse($formatter, $consoleOutput); 66 | 67 | $hasError = false; 68 | if ($this->configuration->getMinQuality() > $results->getCodeQuality()) { 69 | $consoleStyle->error('The code quality score is too low'); 70 | $hasError = true; 71 | } 72 | 73 | if ($this->configuration->getMinComplexity() > $results->getComplexity()) { 74 | $consoleStyle->error('The complexity score is too low'); 75 | $hasError = true; 76 | } 77 | 78 | if ($this->configuration->getMinArchitecture() > $results->getStructure()) { 79 | $consoleStyle->error('The architecture score is too low'); 80 | $hasError = true; 81 | } 82 | 83 | if ($this->configuration->getMinStyle() > $results->getStyle()) { 84 | $consoleStyle->error('The style score is too low'); 85 | $hasError = true; 86 | } 87 | 88 | if (! $this->configuration->isSecurityCheckDisabled() && $results->getTotalSecurityIssues() > 0) { 89 | $hasError = true; 90 | } 91 | 92 | $consoleStyle->newLine(); 93 | 94 | return (int) $hasError; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Application/Console/Commands/FixCommand.php: -------------------------------------------------------------------------------- 1 | collectionFactory = $collectionFactory; 24 | } 25 | 26 | public function __invoke(InputInterface $input, OutputInterface $output): int 27 | { 28 | $metrics = MetricsFinder::find(); 29 | $collection = $this->collectionFactory->get($metrics, $output); 30 | 31 | $output = OutputDecorator::decorate($output); 32 | $formatter = new Console($input, $output); 33 | 34 | $formatter->formatFix($collection, $metrics); 35 | 36 | return 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Application/Console/Commands/InvokableCommand.php: -------------------------------------------------------------------------------- 1 | https://github.com/nunomaduro/phpinsights', 18 | ' - Sponsor the maintainers:', 19 | ' https://github.com/sponsors/nunomaduro', 20 | ' - Subscribe to my YouTube channel:', 21 | ' https://www.youtube.com/@nunomaduro', 22 | ]; 23 | 24 | /** 25 | * @var callable 26 | */ 27 | private $callable; 28 | 29 | /** 30 | * Creates a new instance of the Invokable Command. 31 | */ 32 | public function __construct(string $name, callable $callable, InputDefinition $definition) 33 | { 34 | parent::__construct($name); 35 | 36 | $this->setDefinition($definition); 37 | 38 | $this->callable = $callable; 39 | } 40 | 41 | public function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $result = call_user_func($this->callable, $input, $output); 44 | 45 | if ($output instanceof ConsoleOutputInterface) { 46 | foreach (self::FUNDING_MESSAGES as $message) { 47 | $output->getErrorOutput()->writeln($message); 48 | } 49 | } 50 | 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Application/Console/Contracts/Formatter.php: -------------------------------------------------------------------------------- 1 | $metrics 20 | */ 21 | public function format(InsightCollection $insightCollection, array $metrics): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Console/Contracts/Style.php: -------------------------------------------------------------------------------- 1 | addOptions([ 20 | new InputOption( 21 | 'min-quality', 22 | null, 23 | InputOption::VALUE_OPTIONAL, 24 | 'Minimal Quality level to reach without throw error', 25 | '0' 26 | ), 27 | new InputOption( 28 | 'min-complexity', 29 | null, 30 | InputOption::VALUE_OPTIONAL, 31 | 'Minimal Complexity level to reach without throw error', 32 | '0' 33 | ), 34 | new InputOption( 35 | 'min-architecture', 36 | null, 37 | InputOption::VALUE_OPTIONAL, 38 | 'Minimal Architecture level to reach without throw error', 39 | '0' 40 | ), 41 | new InputOption( 42 | 'min-style', 43 | null, 44 | InputOption::VALUE_OPTIONAL, 45 | 'Minimal Style level to reach without throw error', 46 | '0' 47 | ), 48 | new InputOption( 49 | 'disable-security-check', 50 | null, 51 | InputOption::VALUE_NONE, 52 | 'Disable Security issues check to not throw error if vulnerability is found' 53 | ), 54 | new InputOption( 55 | 'format', 56 | null, 57 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 58 | 'Format to output the result in [console, json, checkstyle, codeclimate, github-action]', 59 | ['console'] 60 | ), 61 | new InputOption( 62 | 'composer', 63 | null, 64 | InputOption::VALUE_OPTIONAL, 65 | 'The composer file path' 66 | ), 67 | new InputOption( 68 | 'fix', 69 | null, 70 | InputOption::VALUE_NONE, 71 | 'Enable auto-fix for fixable insights' 72 | ), 73 | new InputOption( 74 | 'flush-cache', 75 | null, 76 | InputOption::VALUE_NONE, 77 | 'Flush cache results before processing' 78 | ), 79 | ]); 80 | 81 | return $definition; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Application/Console/Definitions/BaseDefinition.php: -------------------------------------------------------------------------------- 1 | getDefinition(); 19 | 20 | $commandDefinition = BaseDefinition::get(); 21 | if ($input->getFirstArgument() !== 'fix') { 22 | $commandDefinition = AnalyseDefinition::get(); 23 | } 24 | 25 | $definition->addArguments($commandDefinition->getArguments()); 26 | $definition->addOptions($commandDefinition->getOptions()); 27 | 28 | $input->bind($definition); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Application/Console/Definitions/FixDefinition.php: -------------------------------------------------------------------------------- 1 | output = $output; 27 | } 28 | 29 | /** 30 | * Format the result to the desired format. 31 | * 32 | * @param array $metrics 33 | */ 34 | public function format(InsightCollection $insightCollection, array $metrics): void 35 | { 36 | if (! extension_loaded('simplexml')) { 37 | throw new RuntimeException('To use checkstyle format install simplexml extension.'); 38 | } 39 | 40 | $checkstyle = new SimpleXMLElement(''); 41 | $detailsComparator = new DetailsComparator(); 42 | 43 | foreach ($metrics as $metricClass) { 44 | /** @var Insight $insight */ 45 | foreach ($insightCollection->allFrom(new $metricClass()) as $insight) { 46 | if (! $insight instanceof HasDetails) { 47 | continue; 48 | } 49 | if (! $insight->hasIssue()) { 50 | continue; 51 | } 52 | $details = $insight->getDetails(); 53 | usort($details, $detailsComparator); 54 | 55 | /** @var Details $detail */ 56 | foreach ($details as $detail) { 57 | $fileName = PathShortener::fileName($detail, $insightCollection->getCollector()->getCommonPath()); 58 | 59 | if (property_exists($checkstyle, 'file') && $checkstyle->file !== null && (string) $checkstyle->file->attributes()['name'] === $fileName) { 60 | $file = $checkstyle->file; 61 | } else { 62 | $file = $checkstyle->addChild('file'); 63 | $file->addAttribute('name', $fileName); 64 | } 65 | 66 | $error = $file->addChild('error'); 67 | $error->addAttribute('severity', 'error'); 68 | $error->addAttribute('source', str_replace('\\', '.', $insight->getInsightClass())); 69 | $error->addAttribute('line', $detail->hasLine() ? (string) $detail->getLine() : ''); 70 | $error->addAttribute('message', $detail->hasMessage() ? $detail->getMessage() : $insight->getTitle()); 71 | } 72 | } 73 | } 74 | 75 | $this->output->write((string) $checkstyle->asXML()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/CodeClimate.php: -------------------------------------------------------------------------------- 1 | output = $output; 26 | } 27 | 28 | /** 29 | * Format the result to the desired format. 30 | * 31 | * @param array $metrics 32 | * 33 | * @throws Exception 34 | */ 35 | public function format(InsightCollection $insightCollection, array $metrics): void 36 | { 37 | $data = $this->issues($insightCollection, $metrics); 38 | 39 | $this->output->write(json_encode($data, JSON_THROW_ON_ERROR)); 40 | } 41 | 42 | /** 43 | * Outputs the issues errors according to the format. 44 | * 45 | * @param array $metrics 46 | * 47 | * @return array|string>|string|null>> 48 | */ 49 | private function issues(InsightCollection $insightCollection, array $metrics): array 50 | { 51 | $data = []; 52 | 53 | $climateCategories = [ 54 | 'Code' => 'Clarity', 55 | 'Complexity' => 'Complexity', 56 | 'Architecture' => 'Bug Risk', 57 | 'Style' => 'Style', 58 | 'Security' => 'Security', 59 | ]; 60 | 61 | $detailsComparator = new DetailsComparator(); 62 | 63 | foreach ($metrics as $metricClass) { 64 | $category = explode('\\', $metricClass); 65 | $category = $category[count($category) - 2]; 66 | 67 | /** @var Insight $insight */ 68 | foreach ($insightCollection->allFrom(new $metricClass()) as $insight) { 69 | if (! $insight->hasIssue()) { 70 | continue; 71 | } 72 | 73 | if (! $insight instanceof HasDetails) { 74 | continue; 75 | } 76 | 77 | $details = $insight->getDetails(); 78 | 79 | usort($details, $detailsComparator); 80 | 81 | /** @var Details $detail */ 82 | foreach ($details as $detail) { 83 | $data[] = [ 84 | 'check_name' => $insight->getInsightClass(), 85 | 'description' => $detail->hasMessage() ? $detail->getMessage() : null, 86 | 'fingerprint' => md5( 87 | implode( 88 | [ 89 | PathShortener::fileName($detail, $insightCollection->getCollector()->getCommonPath()), 90 | $detail->hasLine() ? $detail->getLine() : null, 91 | $detail->hasMessage() ? $detail->getMessage() : null, 92 | ] 93 | ) 94 | ), 95 | 'location' => [ 96 | 'path' => PathShortener::fileName($detail, $insightCollection->getCollector()->getCommonPath()), 97 | 'lines' => [ 98 | 'begin' => $detail->hasLine() ? $detail->getLine() : null, 99 | ], 100 | ], 101 | 'category' => $climateCategories[$category], 102 | 'severity' => 'minor', 103 | ]; 104 | } 105 | } 106 | } 107 | 108 | return $data; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/FormatResolver.php: -------------------------------------------------------------------------------- 1 | Console::class, 19 | 'json' => Json::class, 20 | 'checkstyle' => Checkstyle::class, 21 | 'github-action' => GithubAction::class, 22 | 'codeclimate' => CodeClimate::class, 23 | ]; 24 | 25 | public static function resolve( 26 | InputInterface $input, 27 | OutputInterface $output, 28 | OutputInterface $consoleOutput 29 | ): Formatter { 30 | $requestedFormats = $input->getOption('format'); 31 | if (! is_array($requestedFormats)) { 32 | $consoleOutput->writeln( 33 | 'Could not understand requested format, using fallback [console] instead.' 34 | ); 35 | 36 | $requestedFormats = ['console']; 37 | } 38 | 39 | $formatters = []; 40 | foreach ($requestedFormats as $requestedFormat) { 41 | try { 42 | $formatter = self::stringToFormatterClass($requestedFormat); 43 | $formatterConstructor = new \ReflectionMethod($formatter, '__construct'); 44 | 45 | $instance = $formatterConstructor->getNumberOfParameters() === 1 46 | ? new $formatter($output) 47 | : new $formatter($input, $output); 48 | 49 | if (! ($instance instanceof Formatter)) { 50 | $consoleOutput->writeln( 51 | "The formatter [{$formatter}] is not implementing the interface." 52 | ); 53 | 54 | continue; 55 | } 56 | 57 | $formatters[] = $instance; 58 | } catch (InvalidArgumentException $exception) { 59 | $consoleOutput->writeln("Could not find requested format [{$requestedFormat}]."); 60 | } 61 | } 62 | 63 | if ($formatters === []) { 64 | $consoleOutput->writeln( 65 | 'No requested formats were found, using fallback [console] instead.' 66 | ); 67 | 68 | return new Console($input, $output); 69 | } 70 | 71 | return new Multiple($formatters); 72 | } 73 | private static function stringToFormatterClass(string $requestedFormat): string 74 | { 75 | if (class_exists($requestedFormat)) { 76 | return $requestedFormat; 77 | } 78 | 79 | if (array_key_exists($requestedFormat, self::FORMATTERS)) { 80 | return self::FORMATTERS[strtolower($requestedFormat)]; 81 | } 82 | 83 | throw new InvalidArgumentException('Could not find a formatter from string.'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/GithubAction.php: -------------------------------------------------------------------------------- 1 | '%0D', 25 | "\n" => '%0A', 26 | ]; 27 | 28 | private Console $decorated; 29 | 30 | private OutputInterface $output; 31 | 32 | private string $baseDir; 33 | 34 | public function __construct(InputInterface $input, OutputInterface $output) 35 | { 36 | $this->decorated = new Console($input, $output); 37 | $this->output = $output; 38 | } 39 | 40 | /** 41 | * Format the result to the desired format. 42 | * 43 | * @param array $metrics 44 | */ 45 | public function format(InsightCollection $insightCollection, array $metrics): void 46 | { 47 | $this->baseDir = Container::make()->get(Configuration::class)->getCommonPath(); 48 | 49 | // Call The Console Formatter to get summary and recap, 50 | // not issues by passing an empty array for metrics. 51 | $this->decorated->format($insightCollection, []); 52 | $detailsComparator = new DetailsComparator(); 53 | 54 | $errors = []; 55 | 56 | foreach ($insightCollection->all() as $insight) { 57 | if (! $insight instanceof HasDetails) { 58 | continue; 59 | } 60 | if (! $insight->hasIssue()) { 61 | continue; 62 | } 63 | $details = $insight->getDetails(); 64 | usort($details, $detailsComparator); 65 | 66 | /** @var Details $detail */ 67 | foreach ($details as $detail) { 68 | if (! $detail->hasFile()) { 69 | continue; 70 | } 71 | 72 | $file = $this->getRelativePath($detail->getFile()); 73 | if (! array_key_exists($file, $errors)) { 74 | $errors[$file] = []; 75 | } 76 | 77 | $message = $this->formatMessage($detail, $insight); 78 | // replace line 0 to line 1 79 | // github action write it at line 1 otherwise 80 | $line = $detail->hasLine() ? $detail->getLine() : 1; 81 | 82 | if (! array_key_exists($line, $errors[$file])) { 83 | $errors[$file][$line] = $message; 84 | 85 | continue; 86 | } 87 | 88 | $errors[$file][$line] .= "\n" . $message; 89 | } 90 | } 91 | 92 | foreach ($errors as $file => $lines) { 93 | foreach ($lines as $line => $message) { 94 | // @see https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#set-an-error-message-error 95 | $this->output->writeln(sprintf( 96 | '::error file=%s,line=%s::%s', 97 | $this->escapeData($file), 98 | $line, 99 | $this->escapeData($message) 100 | )); 101 | } 102 | } 103 | } 104 | 105 | private function getRelativePath(string $file): string 106 | { 107 | return str_replace($this->baseDir . DIRECTORY_SEPARATOR, '', $file); 108 | } 109 | 110 | private function formatMessage(Details $detail, Insight $insight): string 111 | { 112 | $message = '* [' . $insight->getTitle() . '] '; 113 | 114 | if ($detail->hasMessage()) { 115 | $message .= $detail->getMessage(); 116 | } 117 | 118 | return $message; 119 | } 120 | 121 | private function escapeData(string $data): string 122 | { 123 | return strtr($data, self::TEMPLATES); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/Json.php: -------------------------------------------------------------------------------- 1 | output = $output; 28 | if ($input->hasOption('summary')) { 29 | $this->summaryOnly = (bool) $input->getOption('summary'); 30 | } 31 | } 32 | 33 | /** 34 | * Format the result to the desired format. 35 | * 36 | * @param array $metrics 37 | * 38 | * @throws Exception 39 | */ 40 | public function format(InsightCollection $insightCollection, array $metrics): void 41 | { 42 | $results = $insightCollection->results(); 43 | 44 | $data = [ 45 | 'summary' => [ 46 | 'code' => $results->getCodeQuality(), 47 | 'complexity' => $results->getComplexity(), 48 | 'architecture' => $results->getStructure(), 49 | 'style' => $results->getStyle(), 50 | 'security issues' => $results->getTotalSecurityIssues(), 51 | 'fixed issues' => $results->getTotalFix(), 52 | ], 53 | ]; 54 | 55 | if (! $this->summaryOnly) { 56 | $data += $this->issues($insightCollection, $metrics); 57 | } 58 | 59 | $json = json_encode($data, JSON_THROW_ON_ERROR); 60 | 61 | $this->output->write($json); 62 | } 63 | 64 | /** 65 | * Outputs the issues errors according to the format. 66 | * 67 | * @param array $metrics 68 | * 69 | * @return array>|null> 70 | */ 71 | private function issues(InsightCollection $insightCollection, array $metrics): array 72 | { 73 | $data = []; 74 | $detailsComparator = new DetailsComparator(); 75 | 76 | foreach ($metrics as $metricClass) { 77 | $category = explode('\\', $metricClass); 78 | $category = $category[count($category) - 2]; 79 | 80 | if (! isset($data[$category])) { 81 | $data[$category] = []; 82 | } 83 | 84 | $current = $data[$category]; 85 | 86 | /** @var Insight $insight */ 87 | foreach ($insightCollection->allFrom(new $metricClass()) as $insight) { 88 | if (! $insight->hasIssue()) { 89 | continue; 90 | } 91 | 92 | if (! $insight instanceof HasDetails) { 93 | $current[] = [ 94 | 'title' => $insight->getTitle(), 95 | 'insightClass' => $insight->getInsightClass(), 96 | ]; 97 | 98 | continue; 99 | } 100 | 101 | $details = $insight->getDetails(); 102 | usort($details, $detailsComparator); 103 | 104 | /** @var Details $detail */ 105 | foreach ($details as $detail) { 106 | $current[] = array_filter([ 107 | 'title' => $insight->getTitle(), 108 | 'insightClass' => $insight->getInsightClass(), 109 | 'file' => PathShortener::fileName($detail, $insightCollection->getCollector()->getCommonPath()), 110 | 'line' => $detail->hasLine() ? $detail->getLine() : null, 111 | 'function' => $detail->hasFunction() ? $detail->getFunction() : null, 112 | 'message' => $detail->hasMessage() ? $detail->getMessage() : null, 113 | 'diff' => $detail->hasDiff() ? $detail->getDiff() : null, 114 | ]); 115 | } 116 | } 117 | 118 | $data[$category] = $current; 119 | } 120 | 121 | return $data; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/Multiple.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $formatters; 14 | 15 | /** 16 | * @param array $formatters 17 | */ 18 | public function __construct(array $formatters) 19 | { 20 | $this->formatters = $formatters; 21 | } 22 | 23 | /** 24 | * Format the result to the desired format. 25 | * 26 | * @param array $metrics 27 | */ 28 | public function format(InsightCollection $insightCollection, array $metrics): void 29 | { 30 | /** @var Formatter $formatter */ 31 | foreach ($this->formatters as $formatter) { 32 | $formatter->format($insightCollection, $metrics); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Application/Console/Formatters/PathShortener.php: -------------------------------------------------------------------------------- 1 | $paths 18 | */ 19 | public static function extractCommonPath(array $paths): string 20 | { 21 | $paths = array_values($paths); 22 | $paths = self::sanitizePath($paths); 23 | sort($paths); 24 | 25 | $first = $paths[0]; 26 | $last = $paths[count($paths) - 1]; 27 | $length = min(\strlen($first), \strlen($last)); 28 | 29 | for ($index = 0; $index < $length && $first[$index] === $last[$index];) { 30 | $index++; 31 | } 32 | 33 | $prefix = mb_substr($first, 0, $index ? $index : 0); 34 | 35 | return mb_substr($prefix, 0, (int) mb_strrpos($prefix, DIRECTORY_SEPARATOR) + 1); 36 | } 37 | 38 | public static function fileName(Details $detail, string $commonPath): string 39 | { 40 | if ($detail->hasFile()) { 41 | $file = $detail->getFile(); 42 | 43 | return mb_strpos($file, $commonPath) !== false 44 | ? str_replace($commonPath, '', $file) 45 | : $file; 46 | } 47 | 48 | return ''; 49 | } 50 | 51 | /** 52 | * @param array $paths 53 | * 54 | * @return array 55 | */ 56 | private static function sanitizePath(array $paths): array 57 | { 58 | return array_map(static function ($path): string { 59 | $path = rtrim($path, DIRECTORY_SEPARATOR); 60 | 61 | return is_dir($path) ? $path . DIRECTORY_SEPARATOR : $path; 62 | }, $paths); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Application/Console/OutputDecorator.php: -------------------------------------------------------------------------------- 1 | addTo($output); 32 | } 33 | 34 | return $output; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Application/Console/Style.php: -------------------------------------------------------------------------------- 1 | input = $input, $this->output = $output); 28 | } 29 | 30 | /** 31 | * Waits for Enter key. 32 | */ 33 | public function waitForKey(string $category): Style 34 | { 35 | $stdin = fopen('php://stdin', 'rb'); 36 | 37 | if ($stdin !== false && $this->output instanceof ConsoleOutput && $this->input->isInteractive()) { 38 | $this->newLine(); 39 | /** @var ConsoleSectionOutput $section */ 40 | $section = $this->output->section(); // @phpstan-ignore-line 41 | $section->writeln(sprintf('Press enter to see %s issues...', strtolower($category))); 42 | fgetc($stdin); 43 | $section->clear(3); 44 | } 45 | 46 | return $this; 47 | } 48 | 49 | public function getOutput(): OutputInterface 50 | { 51 | return $this->output; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Application/Console/Styles/Bold.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('bold', $outputStyle); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Console/Styles/Title.php: -------------------------------------------------------------------------------- 1 | getFormatter()->setStyle('title', $outputStyle); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/DefaultPreset.php: -------------------------------------------------------------------------------- 1 | [ 28 | 'bower_components', 29 | 'node_modules', 30 | 'vendor', 31 | 'vendor-bin', 32 | '.phpstorm.meta.php', 33 | ], 34 | 'add' => [ 35 | // ... 36 | ], 37 | 'remove' => [ 38 | // ... 39 | ], 40 | 'config' => [ 41 | DocCommentSpacingSniff::class => [ 42 | 'linesCountBetweenDifferentAnnotationsTypes' => 1, 43 | ], 44 | DeclareStrictTypesSniff::class => [ 45 | 'linesCountBeforeDeclare' => 1, 46 | 'spacesCountAroundEqualsSign' => 0, 47 | ], 48 | UnusedUsesSniff::class => [ 49 | 'searchAnnotations' => true, 50 | ], 51 | UnusedVariableSniff::class => [ 52 | 'ignoreUnusedValuesWhenOnlyKeysAreUsedInForeach' => true, 53 | ], 54 | PropertyTypeHintSniff::class => [ 55 | 'enableNativeTypeHint' => true, 56 | ], 57 | ], 58 | ]; 59 | } 60 | 61 | public static function shouldBeApplied(Composer $composer): bool 62 | { 63 | return true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Application/Injectors/Cache.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function __invoke(): array 22 | { 23 | return [ 24 | CacheInterface::class => static fn (): CacheInterface => new Psr16Cache( 25 | new FilesystemAdapter('phpinsights') 26 | ), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Injectors/Configuration.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function __invoke(): array 30 | { 31 | return [ 32 | DomainConfiguration::class => static function (): DomainConfiguration { 33 | $input = new ArgvInput(); 34 | if ( 35 | $input->getFirstArgument() === InternalProcessorCommand::NAME && 36 | Container::make()->get(CacheInterface::class)->has('current_configuration') 37 | ) { 38 | // Use cache only for internal:processor, not other commands 39 | return Container::make()->get(CacheInterface::class)->get('current_configuration'); 40 | } 41 | 42 | DefinitionBinder::bind($input); 43 | $configPath = ConfigResolver::resolvePath($input); 44 | $config = []; 45 | 46 | if ($configPath !== '' && file_exists($configPath)) { 47 | $config = require $configPath; 48 | } 49 | 50 | $fixOption = $input->hasOption('fix') && (bool) $input->getOption('fix') === true; 51 | 52 | $config['fix'] = $fixOption || $input->getFirstArgument() === 'fix'; 53 | 54 | try { 55 | return ConfigResolver::resolve($config, $input); 56 | } catch (InvalidConfiguration $exception) { 57 | (new ConsoleOutput())->getErrorOutput() 58 | ->writeln([ 59 | '', 60 | ' Invalid configuration ', 61 | ' ' . $exception->getMessage() . '', 62 | '', 63 | ]); 64 | die(1); 65 | } 66 | }, 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Application/Injectors/FileProcessors.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function __invoke(): array 23 | { 24 | return [ 25 | SniffFileProcessor::class => static fn (): SniffFileProcessor => new SniffFileProcessor( 26 | new FileFactory() 27 | ), 28 | FixerFileProcessor::class => static fn (): FixerFileProcessor => new FixerFileProcessor( 29 | new Differ() 30 | ), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/Injectors/InsightLoaders.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function __invoke(): array 22 | { 23 | return [ 24 | InsightLoader::class => static fn (): InsightLoader => new InsightLoader(), 25 | SniffLoader::class => static fn (): SniffLoader => new SniffLoader(), 26 | FixerLoader::class => static fn (): FixerLoader => new FixerLoader(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Injectors/Repositories.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function __invoke(): array 22 | { 23 | return [ 24 | FilesRepository::class => static function (): LocalFilesRepository { 25 | $finder = Finder::create(); 26 | 27 | return new LocalFilesRepository($finder); 28 | }, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/PathResolver.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function resolve(InputInterface $input): array 18 | { 19 | /** @var array|null $paths */ 20 | $paths = $input->getArgument('paths'); 21 | 22 | if ($paths === [] || $paths === null) { 23 | $paths = [(string) getcwd()]; 24 | } 25 | 26 | $pathList = []; 27 | foreach ($paths as $path) { 28 | $pathList[] = $path[0] !== DIRECTORY_SEPARATOR && preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) === 0 29 | ? getcwd() . DIRECTORY_SEPARATOR . $path 30 | : $path; 31 | } 32 | 33 | return $pathList; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Domain/Container.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getFixPerFile(): array; 18 | 19 | public function addFileFixed(string $file): void; 20 | } 21 | -------------------------------------------------------------------------------- /src/Domain/Contracts/GlobalInsight.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getDetails(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Contracts/HasInsights.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function getInsights(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Contracts/HasMax.php: -------------------------------------------------------------------------------- 1 | $config Related to $insightClass 25 | */ 26 | public function load(string $insightClass, string $dir, array $config, Collector $collector): Insight; 27 | } 28 | -------------------------------------------------------------------------------- /src/Domain/Contracts/Metric.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public static function get(Composer $composer): array; 25 | 26 | /** 27 | * Determines if the preset should be applied. 28 | */ 29 | public static function shouldBeApplied(Composer $composer): bool; 30 | } 31 | -------------------------------------------------------------------------------- /src/Domain/Contracts/Repositories/FilesRepository.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function getFiles(): array; 23 | 24 | /** 25 | * Sets the current files paths. 26 | * 27 | * @param array $paths 28 | * @param array $exclude 29 | */ 30 | public function within(array $paths, array $exclude): FilesRepository; 31 | } 32 | -------------------------------------------------------------------------------- /src/Domain/Contracts/Sniffer.php: -------------------------------------------------------------------------------- 1 | $error 24 | */ 25 | public function collect(Collector $collector, array $error): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/Domain/Details.php: -------------------------------------------------------------------------------- 1 | file = $file; 29 | 30 | return $this; 31 | } 32 | 33 | public function setOriginal(mixed $original): Details 34 | { 35 | $this->original = $original; 36 | 37 | return $this; 38 | } 39 | 40 | public function setLine(int $line): Details 41 | { 42 | $this->line = $line; 43 | 44 | return $this; 45 | } 46 | 47 | public function setMessage(string $message): Details 48 | { 49 | $this->message = $message; 50 | 51 | return $this; 52 | } 53 | 54 | public function setFunction(string $function): Details 55 | { 56 | $this->function = $function; 57 | 58 | return $this; 59 | } 60 | 61 | public function setDiff(string $diff): Details 62 | { 63 | $this->diff = $diff; 64 | 65 | return $this; 66 | } 67 | 68 | public function getFile(): string 69 | { 70 | return $this->file ?? ''; 71 | } 72 | 73 | public function hasFile(): bool 74 | { 75 | return $this->file !== null; 76 | } 77 | 78 | public function getLine(): int 79 | { 80 | return $this->line ?? 0; 81 | } 82 | 83 | public function hasLine(): bool 84 | { 85 | return $this->line !== null; 86 | } 87 | 88 | public function getMessage(): string 89 | { 90 | return $this->message ?? ''; 91 | } 92 | 93 | public function hasMessage(): bool 94 | { 95 | return $this->message !== null; 96 | } 97 | 98 | public function getFunction(): string 99 | { 100 | return $this->function ?? ''; 101 | } 102 | 103 | public function hasFunction(): bool 104 | { 105 | return $this->function !== null; 106 | } 107 | 108 | public function getOriginal(): mixed 109 | { 110 | return $this->original; 111 | } 112 | 113 | public function hasOriginal(): bool 114 | { 115 | return $this->original !== null; 116 | } 117 | 118 | public function getDiff(): string 119 | { 120 | return $this->diff ?? ''; 121 | } 122 | 123 | public function hasDiff(): bool 124 | { 125 | return $this->diff !== null; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Domain/DetailsComparator.php: -------------------------------------------------------------------------------- 1 | fileComparison($first, $second), 18 | $this->lineComparison($first, $second), 19 | $this->functionComparison($first, $second), 20 | $this->messageComparison($first, $second), 21 | ]; 22 | 23 | foreach ($comparisons as $comparison) { 24 | if ($comparison !== 0) { 25 | return $comparison; 26 | } 27 | } 28 | 29 | return 0; 30 | } 31 | 32 | private function fileComparison(Details $first, Details $second): int 33 | { 34 | return ($first->hasFile() ? $first->getFile() : null) <=> ($second->hasFile() ? $second->getFile() : null); 35 | } 36 | 37 | private function lineComparison(Details $first, Details $second): int 38 | { 39 | return ($first->hasLine() ? $first->getLine() : null) <=> ($second->hasLine() ? $second->getLine() : null); 40 | } 41 | 42 | private function functionComparison(Details $first, Details $second): int 43 | { 44 | return ($first->hasFunction() ? $first->getFunction() : null) 45 | <=> 46 | ($second->hasFunction() ? $second->getFunction() : null); 47 | } 48 | 49 | private function messageComparison(Details $first, Details $second): int 50 | { 51 | return ($first->hasMessage() ? $first->getMessage() : null) 52 | <=> 53 | ($second->hasMessage() ? $second->getMessage() : null); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Domain/Differ.php: -------------------------------------------------------------------------------- 1 | get(Configuration::class)->getDiffContext(); 18 | 19 | $outputBuilder = new StrictUnifiedDiffOutputBuilder([ 20 | 'collapseRanges' => true, 21 | 'commonLineThreshold' => 1, 22 | 'contextLines' => $diffContext, 23 | 'fromFile' => '', 24 | 'toFile' => '', 25 | ]); 26 | 27 | $this->differ = new BaseDiffer($outputBuilder); 28 | } 29 | 30 | public function diff(string $old, string $new, ?\SplFileInfo $file = null): string 31 | { 32 | return $this->differ->diff($old, $new); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Domain/Exceptions/ComposerNotFound.php: -------------------------------------------------------------------------------- 1 | > 19 | */ 20 | private array $tokenListeners = []; 21 | 22 | private SniffDecorator $activeSniff; 23 | 24 | private SplFileInfo $fileInfo; 25 | 26 | private bool $isFixable; 27 | 28 | private bool $fixEnabled = false; 29 | 30 | public function __construct(string $path, string $content, Config $config, Ruleset $ruleset) 31 | { 32 | $this->content = $content; 33 | 34 | $this->eolChar = Common::detectLineEndings($content); 35 | 36 | parent::__construct($path, $ruleset, $config); 37 | } 38 | 39 | public function process(): void 40 | { 41 | $this->parse(); 42 | $this->fixer->startFile($this); 43 | 44 | foreach ($this->tokens as $stackPtr => $token) { 45 | if (! isset($this->tokenListeners[$token['code']])) { 46 | continue; 47 | } 48 | 49 | /** @var \NunoMaduro\PhpInsights\Domain\Insights\SniffDecorator $sniff */ 50 | foreach ($this->tokenListeners[$token['code']] as $sniff) { 51 | $this->activeSniff = $sniff; 52 | 53 | try { 54 | $sniff->process($this, $stackPtr); 55 | } catch (Throwable $e) { 56 | $this->addError('Unparsable php code: syntax error or wrong phpdocs.', $stackPtr, $token['code']); 57 | } 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @param array> $tokenListeners 64 | */ 65 | public function processWithTokenListenersAndFileInfo( 66 | array $tokenListeners, 67 | SplFileInfo $fileInfo, 68 | bool $isFixable 69 | ): void { 70 | $this->tokenListeners = $tokenListeners; 71 | $this->fileInfo = $fileInfo; 72 | $this->isFixable = $isFixable; 73 | 74 | $this->process(); 75 | } 76 | 77 | /** 78 | * Get's the file info from the file. 79 | */ 80 | public function getFileInfo(): SplFileInfo 81 | { 82 | return $this->fileInfo; 83 | } 84 | 85 | /** 86 | * Enable fix mode. It's used to prevent report twice 87 | * details because fixer relaunch process method. 88 | */ 89 | public function enableFix(): void 90 | { 91 | $this->fixEnabled = true; 92 | } 93 | 94 | public function disableFix(): void 95 | { 96 | $this->fixEnabled = false; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | protected function addMessage( 103 | $isError, 104 | $message, 105 | $line, 106 | $column, 107 | $sniffClassOrCode, 108 | $data, 109 | $severity, 110 | $isFixable = false 111 | ): bool { 112 | $message = $data !== [] ? vsprintf($message, $data) : $message; 113 | 114 | if ($isFixable && $this->isFixable) { 115 | if ($this->fixEnabled) { 116 | $this->activeSniff->addFileFixed($this->fileInfo->getRelativePathname()); 117 | } else { 118 | $this->fixableCount++; 119 | } 120 | 121 | return true; 122 | } 123 | 124 | if ($this->fixEnabled) { 125 | // detail already added 126 | return true; 127 | } 128 | 129 | $this->activeSniff->addDetails( 130 | Details::make() 131 | ->setLine($line) 132 | ->setMessage($message) 133 | ->setFile($this->path) 134 | ); 135 | 136 | return true; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Domain/FileFactory.php: -------------------------------------------------------------------------------- 1 | restoreDefaults(); 26 | $config->__set('tabWidth', 4); 27 | $config->__set('annotations', false); 28 | $config->__set('encoding', 'UTF-8'); 29 | // Include only 1 sniff, they are register later 30 | $config->__set('sniffs', ['Generic.Files.LineEndings']); 31 | 32 | $this->config = $config; 33 | $this->ruleset = new Ruleset($this->config); 34 | } 35 | 36 | public function createFromFileInfo(SplFileInfo $smartFileInfo): File 37 | { 38 | $path = $smartFileInfo->getRealPath(); 39 | 40 | if ($path === false) { 41 | throw new RuntimeException( 42 | "{$smartFileInfo->getPath()} Does not exist." 43 | ); 44 | } 45 | 46 | return new File( 47 | $path, 48 | $smartFileInfo->getContents(), 49 | $this->config, 50 | $this->ruleset 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Domain/FileProcessors/FixerFileProcessor.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | private array $fixers = []; 28 | 29 | private DifferInterface $differ; 30 | 31 | private bool $fixEnabled; 32 | 33 | public function __construct(DifferInterface $differ) 34 | { 35 | $this->differ = $differ; 36 | $this->fixEnabled = Container::make()->get(Configuration::class)->hasFixEnabled(); 37 | } 38 | 39 | public function support(InsightContract $insight): bool 40 | { 41 | return $insight instanceof FixerDecorator; 42 | } 43 | 44 | public function addChecker(InsightContract $insight): void 45 | { 46 | if (! $insight instanceof FixerDecorator) { 47 | throw new RuntimeException(sprintf( 48 | 'Unable to add %s, not a Fixer instance', 49 | $insight::class 50 | )); 51 | } 52 | 53 | $this->fixers[] = $insight; 54 | } 55 | 56 | public function processFile(SplFileInfo $splFileInfo): void 57 | { 58 | $filePath = $splFileInfo->getRealPath(); 59 | if ($filePath === false) { 60 | throw new LogicException('Unable to found file ' . $splFileInfo->getFilename()); 61 | } 62 | 63 | $oldContent = $splFileInfo->getContents(); 64 | $needFix = false; 65 | 66 | try { 67 | $tokens = @Tokens::fromCode($oldContent); 68 | $originalTokens = clone $tokens; 69 | 70 | /** @var FixerDecorator $fixer */ 71 | foreach ($this->fixers as $fixer) { 72 | $fixer->fix($splFileInfo, $tokens); 73 | 74 | if (! $tokens->isChanged()) { 75 | continue; 76 | } 77 | 78 | if ($this->fixEnabled) { 79 | $needFix = true; 80 | // Register diff will be applied 81 | $fixer->addFileFixed($splFileInfo->getRelativePathname()); 82 | // Tokens has changed, so we need to clear cache 83 | @Tokens::clearCache(); 84 | $tokens = @Tokens::fromCode($oldContent); 85 | 86 | continue; 87 | } 88 | 89 | $fixer->addDiff($filePath, $this->differ->diff($oldContent, $tokens->generateCode())); 90 | // Tokens has changed, so we need to clear cache 91 | Tokens::clearCache(); 92 | $tokens = clone $originalTokens; 93 | } 94 | if (! $this->fixEnabled) { 95 | return; 96 | } 97 | if (! $needFix) { 98 | return; 99 | } 100 | 101 | $tokens = @Tokens::fromCode($oldContent); 102 | // Iterate on fixer to get full tokens to change 103 | foreach ($this->fixers as $fixer) { 104 | $fixer->fix($splFileInfo, $tokens); 105 | } 106 | 107 | file_put_contents($splFileInfo->getPathname(), $tokens->generateCode()); 108 | } catch (Throwable $e) { 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Domain/FileProcessors/SniffFileProcessor.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | private array $tokenListeners = []; 25 | 26 | private FileFactory $fileFactory; 27 | 28 | private bool $fixEnabled; 29 | 30 | /** 31 | * FileProcessor constructor. 32 | */ 33 | public function __construct(FileFactory $fileFactory) 34 | { 35 | $this->fileFactory = $fileFactory; 36 | $this->fixEnabled = Container::make()->get(Configuration::class)->hasFixEnabled(); 37 | } 38 | 39 | public function support(InsightContract $insight): bool 40 | { 41 | return $insight instanceof SniffDecorator; 42 | } 43 | 44 | public function addChecker(InsightContract $insight): void 45 | { 46 | if (! $insight instanceof SniffDecorator) { 47 | throw new RuntimeException(sprintf( 48 | 'Unable to add %s, not an Sniff instance', 49 | $insight::class 50 | )); 51 | } 52 | 53 | foreach ($insight->register() as $token) { 54 | $this->tokenListeners[$token][] = $insight; 55 | } 56 | } 57 | 58 | public function processFile(SplFileInfo $splFileInfo): void 59 | { 60 | $file = $this->fileFactory->createFromFileInfo($splFileInfo); 61 | 62 | $file->processWithTokenListenersAndFileInfo( 63 | $this->tokenListeners, 64 | $splFileInfo, 65 | $this->fixEnabled 66 | ); 67 | 68 | if ($this->fixEnabled && $file->getFixableCount() !== 0) { 69 | $file->enableFix(); 70 | $file->fixer->fixFile(); 71 | file_put_contents($splFileInfo->getPathname(), $file->fixer->getContents()); 72 | $file->disableFix(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Domain/Helper/Files.php: -------------------------------------------------------------------------------- 1 | $list 19 | * 20 | * @return array 21 | */ 22 | public static function find(string $basedir, array $list): array 23 | { 24 | $files = []; 25 | $userFinder = false; 26 | $finder = Finder::create()->in($basedir); 27 | 28 | /** @var string $file */ 29 | foreach ($list as $file) { 30 | if (is_file($file)) { 31 | $path = realpath($file); 32 | if ($path === false) { 33 | $path = $file; 34 | } 35 | $info = pathinfo($file); 36 | $files[$path] = new SplFileInfo($path, $info['dirname'], $info['basename']); 37 | 38 | continue; 39 | } 40 | 41 | $userFinder = true; 42 | $finder->path($file); 43 | } 44 | 45 | if ($userFinder) { 46 | $finder->name('*.php')->files(); 47 | /** 48 | * @noRector Rector\Php74\Rector\FuncCall\ArraySpreadInsteadOfArrayMergeRector 49 | * 50 | * @var array $files 51 | */ 52 | $files = array_merge($files, iterator_to_array($finder, true)); 53 | } 54 | 55 | return $files; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Domain/InsightLoader/FixerLoader.php: -------------------------------------------------------------------------------- 1 | $excludeConfig */ 37 | $excludeConfig = $config['exclude']; 38 | unset($config['exclude']); 39 | } 40 | 41 | if (isset($config['indent'])) { 42 | if ($fixer instanceof WhitespacesAwareFixerInterface && is_string($config['indent'])) { 43 | $fixerConfig = new WhitespacesFixerConfig($config['indent']); 44 | $fixer->setWhitespacesConfig($fixerConfig); 45 | } 46 | 47 | unset($config['indent']); 48 | } 49 | 50 | if ($fixer instanceof ConfigurableFixerInterface && $config !== []) { 51 | $fixer->configure($config); 52 | } 53 | 54 | return new FixerDecorator($fixer, $dir, $excludeConfig); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Domain/InsightLoader/InsightLoader.php: -------------------------------------------------------------------------------- 1 | $excludeConfig */ 35 | $excludeConfig = $config['exclude']; 36 | unset($config['exclude']); 37 | } 38 | 39 | foreach ($config as $property => $value) { 40 | $sniff->{$property} = $value; 41 | } 42 | 43 | return new SniffDecorator($sniff, $dir, $excludeConfig); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Domain/Insights/ClassMethodAverageCyclomaticComplexityIsHigh.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $details = []; 20 | 21 | public function hasIssue(): bool 22 | { 23 | return $this->details !== []; 24 | } 25 | 26 | public function getTitle(): string 27 | { 28 | return sprintf( 29 | 'Having `classes` with average method cyclomatic complexity more than %s is prohibited - Consider refactoring', 30 | $this->getMaxComplexity() 31 | ); 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getDetails(): array 38 | { 39 | return $this->details; 40 | } 41 | 42 | public function process(): void 43 | { 44 | // Exclude in collector all excluded files 45 | if ($this->excludedFiles !== []) { 46 | $this->collector->excludeComplexityFiles($this->excludedFiles); 47 | } 48 | 49 | $averageClassComplexity = $this->getAverageClassComplexity(); 50 | 51 | // Exclude the ones which didn't pass the threshold 52 | $complexityLimit = $this->getMaxComplexity(); 53 | $averageClassComplexity = array_filter( 54 | $averageClassComplexity, 55 | static fn ($complexity): bool => $complexity > $complexityLimit 56 | ); 57 | 58 | $this->details = array_map( 59 | static fn ($class, $complexity): Details => Details::make() 60 | ->setFile($class) 61 | ->setMessage(sprintf('%.2f cyclomatic complexity', $complexity)), 62 | array_keys($averageClassComplexity), 63 | $averageClassComplexity 64 | ); 65 | } 66 | 67 | private function getMaxComplexity(): float 68 | { 69 | return (float) ($this->config['maxClassMethodAverageComplexity'] ?? 5.0); 70 | } 71 | 72 | private function getFile(string $classMethod): string 73 | { 74 | $colonPosition = strpos($classMethod, ':'); 75 | 76 | if ($colonPosition !== false) { 77 | return substr($classMethod, 0, $colonPosition); 78 | } 79 | 80 | return $classMethod; 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | private function getAverageClassComplexity(): array 87 | { 88 | // Group method complexities by files 89 | $classComplexities = []; 90 | 91 | foreach ($this->collector->getMethodComplexity() as $classMethod => $complexity) { 92 | $classComplexities[$this->getFile($classMethod)][] = $complexity; 93 | } 94 | 95 | // Calculate average complexity of each file 96 | $averageClassComplexity = []; 97 | 98 | foreach ($classComplexities as $file => $complexities) { 99 | $averageClassComplexity[$file] = array_sum($complexities) / count($complexities); 100 | } 101 | 102 | return $averageClassComplexity; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Domain/Insights/CyclomaticComplexityIsHigh.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $details = []; 20 | 21 | public function hasIssue(): bool 22 | { 23 | return $this->details !== []; 24 | } 25 | 26 | public function getTitle(): string 27 | { 28 | return sprintf( 29 | 'Having `classes` with total cyclomatic complexity more than %s is prohibited - Consider refactoring', 30 | $this->getMaxComplexity() 31 | ); 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getDetails(): array 38 | { 39 | return $this->details; 40 | } 41 | 42 | public function process(): void 43 | { 44 | // Exclude in collector all excluded files 45 | if ($this->excludedFiles !== []) { 46 | $this->collector->excludeComplexityFiles($this->excludedFiles); 47 | } 48 | $complexityLimit = $this->getMaxComplexity(); 49 | 50 | $classesComplexity = array_filter( 51 | $this->collector->getClassComplexity(), 52 | static fn ($complexity): bool => $complexity > $complexityLimit 53 | ); 54 | 55 | $this->details = array_map( 56 | static fn ($class, $complexity): Details => Details::make() 57 | ->setFile($class) 58 | ->setMessage(sprintf('%d cyclomatic complexity', $complexity)), 59 | array_keys($classesComplexity), 60 | $classesComplexity 61 | ); 62 | } 63 | 64 | private function getMaxComplexity(): int 65 | { 66 | return (int) ($this->config['maxComplexity'] ?? 5); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Domain/Insights/FixPerFileCollector.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $fixPerFile = []; 20 | 21 | public function addFileFixed(string $file): void 22 | { 23 | if (! \array_key_exists($file, $this->fixPerFile)) { 24 | $this->fixPerFile[$file] = 0; 25 | } 26 | 27 | $this->fixPerFile[$file]++; 28 | $this->totalFixed++; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getFixPerFile(): array 35 | { 36 | $details = []; 37 | foreach ($this->fixPerFile as $file => $count) { 38 | $message = 'issues fixed'; 39 | 40 | if ($count === 1) { 41 | $message = 'issue fixed'; 42 | } 43 | 44 | $details[] = (new Details()) 45 | ->setMessage(sprintf('%s %s', $count, $message)) 46 | ->setFile($file); 47 | } 48 | 49 | return $details; 50 | } 51 | 52 | public function getTotalFix(): int 53 | { 54 | return $this->totalFixed; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Domain/Insights/FixerDecorator.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | private array $exclude; 34 | 35 | /** 36 | * @var array<\NunoMaduro\PhpInsights\Domain\Details> 37 | */ 38 | private array $errors = []; 39 | 40 | /** 41 | * @param array $exclude 42 | */ 43 | public function __construct(FixerInterface $fixer, string $dir, array $exclude) 44 | { 45 | $this->fixer = $fixer; 46 | $this->exclude = []; 47 | 48 | if ($exclude !== []) { 49 | $this->exclude = Files::find($dir, $exclude); 50 | } 51 | } 52 | 53 | public function isCandidate(Tokens $tokens): bool 54 | { 55 | return $this->fixer->isCandidate($tokens); 56 | } 57 | 58 | public function isRisky(): bool 59 | { 60 | return $this->fixer->isRisky(); 61 | } 62 | 63 | public function fix(SplFileInfo $file, Tokens $tokens): void 64 | { 65 | if ($this->skipFilesFromExcludedFiles($file)) { 66 | return; 67 | } 68 | 69 | $this->fixer->fix($file, $tokens); 70 | } 71 | 72 | public function getName(): string 73 | { 74 | return $this->fixer->getName(); 75 | } 76 | 77 | public function getPriority(): int 78 | { 79 | return $this->fixer->getPriority(); 80 | } 81 | 82 | public function supports(SplFileInfo $file): bool 83 | { 84 | if ($this->skipFilesFromExcludedFiles($file)) { 85 | return false; 86 | } 87 | 88 | return $this->fixer->supports($file); 89 | } 90 | 91 | /** 92 | * Checks if the insight detects an issue. 93 | */ 94 | public function hasIssue(): bool 95 | { 96 | return $this->errors !== []; 97 | } 98 | 99 | /** 100 | * Gets the title of the insight. 101 | */ 102 | public function getTitle(): string 103 | { 104 | $fixerClass = $this->getInsightClass(); 105 | $path = explode('\\', $fixerClass); 106 | $name = array_pop($path); 107 | $name = str_replace('Fixer', '', $name); 108 | 109 | return ucfirst(mb_strtolower(trim((string) preg_replace('/(?fixer); 118 | } 119 | 120 | /** 121 | * Returns the details of the insight. 122 | * 123 | * @return array 124 | */ 125 | public function getDetails(): array 126 | { 127 | return $this->errors; 128 | } 129 | 130 | public function addDetails(Details $details): void 131 | { 132 | $this->errors[] = $details; 133 | } 134 | 135 | public function addDiff(string $file, string $diff): void 136 | { 137 | $diff = trim(substr($diff, 8)); 138 | 139 | $this->errors[] = Details::make()->setFile($file)->setDiff($diff)->setMessage($diff); 140 | } 141 | 142 | public function getDefinition(): FixerDefinitionInterface 143 | { 144 | return $this->fixer->getDefinition(); 145 | } 146 | 147 | private function skipFilesFromExcludedFiles(SplFileInfo $file): bool 148 | { 149 | $path = $file->getRealPath(); 150 | if ($path === false) { 151 | return false; 152 | } 153 | return isset($this->exclude[$path]); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenDefineFunctions.php: -------------------------------------------------------------------------------- 1 | getDetails() !== []; 15 | } 16 | 17 | public function getTitle(): string 18 | { 19 | return 'Defining global helpers is prohibited'; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getDetails(): array 26 | { 27 | $namedFunctionsPerFile = $this->collector->getNamedFunctions(); 28 | 29 | $details = []; 30 | foreach ($namedFunctionsPerFile as $file => $namedFunctions) { 31 | if ($this->shouldSkipFile($file)) { 32 | continue; 33 | } 34 | 35 | foreach ($namedFunctions as $key => $namedFunction) { 36 | $number = $key + 1; 37 | $details[] = Details::make() 38 | ->setFile($file) 39 | ->setLine($number) 40 | ->setFunction($namedFunction); 41 | } 42 | } 43 | 44 | return $details; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenDefineGlobalConstants.php: -------------------------------------------------------------------------------- 1 | getDetails() !== []; 15 | } 16 | 17 | public function getTitle(): string 18 | { 19 | return 'Define `globals` is prohibited'; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getDetails(): array 26 | { 27 | /** @var array $ignore */ 28 | $ignore = $this->config['ignore'] ?? []; 29 | 30 | $globalConstants = array_diff($this->collector->getGlobalConstants(), $ignore); 31 | $globalConstants = $this->filterFilesWithoutExcluded($globalConstants); 32 | 33 | return array_map(static fn ($file, $constant): Details => Details::make() 34 | ->setFile($file) 35 | ->setMessage($constant), array_keys($globalConstants), $globalConstants); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenFinalClasses.php: -------------------------------------------------------------------------------- 1 | getDetails()); 15 | } 16 | 17 | public function getTitle(): string 18 | { 19 | return array_key_exists('title', $this->config) 20 | ? (string) $this->config['title'] 21 | : 'The use of `final` classes is prohibited'; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getDetails(): array 28 | { 29 | $concreteFinalClasses = $this->collector->getConcreteFinalClasses(); 30 | $concreteFinalClasses = array_flip($this->filterFilesWithoutExcluded(array_flip($concreteFinalClasses))); 31 | 32 | return array_values(array_map( 33 | static fn (string $name): Details => Details::make()->setFile($name), 34 | $concreteFinalClasses 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenGlobals.php: -------------------------------------------------------------------------------- 1 | getDetails() !== []; 18 | } 19 | 20 | public function getTitle(): string 21 | { 22 | return "{$this->collector->getGlobalAccesses()} globals accesses detected"; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getDetails(): array 29 | { 30 | $details = []; 31 | 32 | foreach ($this->collector->getGlobalVariableAccesses() as $file => $global) { 33 | $filePath = current(explode(':', $file)); 34 | if ($this->shouldSkipFile($filePath)) { 35 | continue; 36 | } 37 | 38 | $details[] = Details::make()->setFile($file)->setMessage( 39 | "Usage of {$global} found; Usage of GLOBALS are discouraged consider not relying on global scope" 40 | ); 41 | } 42 | 43 | return $details; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenNormalClasses.php: -------------------------------------------------------------------------------- 1 | getDetails() !== []; 15 | } 16 | 17 | public function getTitle(): string 18 | { 19 | return (string) ($this->config['title'] ?? 'Normal classes are forbidden. Classes must be final or abstract'); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getDetails(): array 26 | { 27 | $nonFinalClasses = $this->collector->getConcreteNonFinalClasses(); 28 | /** @var array $nonFinalClasses */ 29 | $nonFinalClasses = array_flip($this->filterFilesWithoutExcluded( 30 | array_flip($nonFinalClasses) 31 | )); 32 | 33 | return array_map(static fn (string $file): Details => Details::make()->setFile($file), $nonFinalClasses); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenPrivateMethods.php: -------------------------------------------------------------------------------- 1 | collector->getPrivateMethods() > 0; 12 | } 13 | 14 | public function getTitle(): string 15 | { 16 | return (string) ($this->config['title'] ?? 'The use of `private` methods is prohibited'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenTraits.php: -------------------------------------------------------------------------------- 1 | getDetails() !== []; 18 | } 19 | 20 | public function getTitle(): string 21 | { 22 | return 'The use of `traits` is prohibited'; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getDetails(): array 29 | { 30 | $traits = $this->collector->getTraits(); 31 | $traits = array_flip($this->filterFilesWithoutExcluded(array_flip($traits))); 32 | 33 | return array_values(array_map( 34 | static fn (string $name): Details => Details::make()->setFile($name), 35 | $traits 36 | )); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Domain/Insights/ForbiddenUsingGlobals.php: -------------------------------------------------------------------------------- 1 | collector->getGlobalAccesses(); 12 | } 13 | 14 | public function getTitle(): string 15 | { 16 | return 'The usage of globals is prohibited - Consider relying in abstractions'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Insights/Insight.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected array $config; 19 | 20 | /** 21 | * @var array 22 | */ 23 | protected array $excludedFiles; 24 | 25 | /** 26 | * Creates an new instance of the Insight. 27 | * 28 | * @param array $config 29 | */ 30 | final public function __construct(Collector $collector, array $config) 31 | { 32 | $this->collector = $collector; 33 | $this->config = $config; 34 | $this->excludedFiles = []; 35 | 36 | /** @var array $exclude */ 37 | $exclude = $config['exclude'] ?? []; 38 | 39 | if ($exclude !== []) { 40 | $this->excludedFiles = Files::find( 41 | (string) (getcwd() ?: $collector->getCommonPath()), 42 | $exclude 43 | ); 44 | } 45 | } 46 | 47 | final public function getInsightClass(): string 48 | { 49 | return static::class; 50 | } 51 | 52 | final protected function shouldSkipFile(string $file): bool 53 | { 54 | return \array_key_exists($file, $this->excludedFiles); 55 | } 56 | 57 | /** 58 | * @param array $files File path must be in key. 59 | * 60 | * @return array 61 | */ 62 | final protected function filterFilesWithoutExcluded(array $files): array 63 | { 64 | return array_filter($files, fn (string $file): bool => ! $this->shouldSkipFile($file), ARRAY_FILTER_USE_KEY); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Domain/Insights/InsightCollection.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | private array $insightsPerMetric; 20 | 21 | private Collector $collector; 22 | 23 | private ?Results $results = null; 24 | 25 | /** 26 | * Creates a new instance of the Insight Collection. 27 | * 28 | * @param array> $insightsPerMetric 29 | */ 30 | public function __construct(Collector $collector, array $insightsPerMetric) 31 | { 32 | $this->collector = $collector; 33 | $this->insightsPerMetric = $insightsPerMetric; 34 | } 35 | 36 | public function getCollector(): Collector 37 | { 38 | return $this->collector; 39 | } 40 | 41 | /** 42 | * Gets all insights. 43 | * 44 | * @return array<\NunoMaduro\PhpInsights\Domain\Contracts\Insight> 45 | */ 46 | public function all(): array 47 | { 48 | $all = []; 49 | 50 | foreach ($this->insightsPerMetric as $insights) { 51 | foreach ($insights as $insight) { 52 | $all[] = $insight; 53 | } 54 | } 55 | 56 | return $all; 57 | } 58 | 59 | /** 60 | * Gets all insights from given metric. 61 | * 62 | * @return array<\NunoMaduro\PhpInsights\Domain\Contracts\Insight> 63 | */ 64 | public function allFrom(Metric $metric): array 65 | { 66 | return $this->insightsPerMetric[$metric::class] ?? []; 67 | } 68 | 69 | /** 70 | * Returns the results of the code taking in consideration the current insights. 71 | */ 72 | public function results(): Results 73 | { 74 | if ($this->results !== null) { 75 | return $this->results; 76 | } 77 | 78 | $perCategory = []; 79 | foreach ($this->insightsPerMetric as $metric => $insights) { 80 | $category = explode('\\', $metric); 81 | $category = $category[count($category) - 2]; 82 | 83 | if (! array_key_exists($category, $perCategory)) { 84 | $perCategory[$category] = []; 85 | } 86 | 87 | $perCategory[$category] = [...$perCategory[$category], ...$insights]; 88 | } 89 | 90 | $this->results = new Results($this->collector, $perCategory); 91 | 92 | return $this->results; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Domain/Insights/InsightCollectionFactory.php: -------------------------------------------------------------------------------- 1 | filesRepository = $filesRepository; 34 | $this->analyser = $analyser; 35 | $this->config = $config; 36 | } 37 | 38 | /** 39 | * @param array $metrics 40 | */ 41 | public function get(array $metrics, OutputInterface $consoleOutput): InsightCollection 42 | { 43 | $paths = $this->config->getPaths(); 44 | $commonPath = $this->config->getCommonPath(); 45 | 46 | try { 47 | $files = array_map( 48 | static fn (SplFileInfo $file) => $file->getPath() . DIRECTORY_SEPARATOR . $file->getFilename(), 49 | $this->filesRepository->within($paths, $this->config->getExcludes())->getFiles() 50 | ); 51 | } catch (InvalidArgumentException $exception) { 52 | throw new DirectoryNotFound($exception->getMessage(), 0, $exception); 53 | } 54 | 55 | $collector = $this->analyser->analyse($paths, $files, $commonPath); 56 | 57 | $insightsClasses = []; 58 | foreach ($metrics as $metricClass) { 59 | $insightsClasses = [...$insightsClasses, ...$this->getInsights($metricClass)]; 60 | } 61 | 62 | $insightFactory = new InsightFactory($this->filesRepository, $insightsClasses, $this->config, $collector); 63 | $insightsForCollection = []; 64 | 65 | foreach ($metrics as $metricClass) { 66 | $insightsForCollection[$metricClass] = array_map( 67 | static fn (string $insightClass): Insight => $insightFactory->makeFrom($insightClass, $consoleOutput), 68 | $this->getInsights($metricClass) 69 | ); 70 | } 71 | 72 | return new InsightCollection($collector, $insightsForCollection); 73 | } 74 | 75 | /** 76 | * Returns the `Insights` from the given metric class. 77 | * 78 | * @return array 79 | */ 80 | private function getInsights(string $metricClass): array 81 | { 82 | $metric = new $metricClass(); 83 | 84 | $insights = $metric instanceof HasInsights 85 | ? $metric->getInsights() 86 | : []; 87 | 88 | $toAdd = $this->config->getAddedInsightsByMetric($metricClass); 89 | $insights = [...$insights, ...$toAdd]; 90 | 91 | // Remove insights based on config. 92 | return array_diff($insights, $this->config->getRemoves()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Domain/Insights/InsightFactory.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | private array $insightsClasses; 31 | 32 | /** 33 | * @var array 34 | */ 35 | private array $insights = []; 36 | 37 | /** 38 | * @var array 39 | */ 40 | private array $insightLoaders; 41 | 42 | private Configuration $config; 43 | 44 | private bool $ran = false; 45 | 46 | private Collector $collector; 47 | 48 | /** 49 | * Creates a new instance of Insight Factory. 50 | * 51 | * @param array $insightsClasses 52 | */ 53 | public function __construct(FilesRepository $filesRepository, array $insightsClasses, Configuration $config, Collector $collector) 54 | { 55 | $this->filesRepository = $filesRepository; 56 | $this->insightsClasses = $insightsClasses; 57 | $this->insightLoaders = Container::make()->get(InsightLoader::INSIGHT_LOADER_TAG); 58 | $this->config = $config; 59 | $this->collector = $collector; 60 | } 61 | 62 | /** 63 | * Creates a Insight from the given error class. 64 | * 65 | * @throws Exception 66 | */ 67 | public function makeFrom(string $errorClass, OutputInterface $consoleOutput): InsightContract 68 | { 69 | $this->runInsightCollector($consoleOutput); 70 | 71 | /** @var InsightContract $insight */ 72 | foreach ($this->insights as $insight) { 73 | if ($insight->getInsightClass() === $errorClass) { 74 | return $insight; 75 | } 76 | } 77 | 78 | throw new RuntimeException(sprintf('Insight `%s` is not instantiable.', $errorClass)); 79 | } 80 | 81 | private function runInsightCollector(OutputInterface $consoleOutput): void 82 | { 83 | if ($this->ran) { 84 | return; 85 | } 86 | 87 | $runner = new Runner( 88 | $consoleOutput, 89 | $this->filesRepository 90 | ); 91 | 92 | // Add insights 93 | $insights = $this->loadInsights($this->insightsClasses); 94 | $this->insights = $insights; 95 | $runner->addInsights($insights); 96 | 97 | // Run it. 98 | $runner->run(); 99 | $this->ran = true; 100 | } 101 | 102 | /** 103 | * Return instantiated insights. 104 | * 105 | * @param array $insights 106 | * 107 | * @return array 108 | */ 109 | private function loadInsights(array $insights): array 110 | { 111 | $insightsAdded = []; 112 | $path = (string) (getcwd() ?: $this->config->getCommonPath()); 113 | 114 | foreach ($insights as $insight) { 115 | /** @var InsightLoader $loader */ 116 | foreach ($this->insightLoaders as $loader) { 117 | if ($loader->support($insight)) { 118 | $insightsAdded[] = $loader->load( 119 | $insight, 120 | $path, 121 | $this->config->getConfigForInsight($insight), 122 | $this->collector 123 | ); 124 | } 125 | } 126 | } 127 | 128 | return $insightsAdded; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Domain/Insights/MethodCyclomaticComplexityIsHigh.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $details = []; 20 | 21 | public function hasIssue(): bool 22 | { 23 | return $this->details !== []; 24 | } 25 | 26 | public function getTitle(): string 27 | { 28 | return sprintf( 29 | 'Having `methods` with cyclomatic complexity more than %s is prohibited - Consider refactoring', 30 | $this->getMaxComplexity() 31 | ); 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getDetails(): array 38 | { 39 | return $this->details; 40 | } 41 | 42 | public function process(): void 43 | { 44 | // Exclude in collector all excluded files 45 | if ($this->excludedFiles !== []) { 46 | $this->collector->excludeComplexityFiles($this->excludedFiles); 47 | } 48 | $complexityLimit = $this->getMaxComplexity(); 49 | 50 | $methodComplexity = array_filter( 51 | $this->collector->getMethodComplexity(), 52 | static fn ($complexity): bool => $complexity > $complexityLimit 53 | ); 54 | 55 | $this->details = array_map( 56 | fn ($class, $complexity): Details => $this->getDetailsForClassMethod($class, $complexity), 57 | array_keys($methodComplexity), 58 | $methodComplexity 59 | ); 60 | } 61 | 62 | private function getMaxComplexity(): int 63 | { 64 | return (int) ($this->config['maxMethodComplexity'] ?? 5); 65 | } 66 | 67 | private function getDetailsForClassMethod(string $class, int $complexity): Details 68 | { 69 | $file = $class; 70 | $function = null; 71 | $colonPosition = strpos($class, ':'); 72 | 73 | if ($colonPosition !== false) { 74 | $file = substr($class, 0, $colonPosition); 75 | $function = substr($class, $colonPosition + 1); 76 | } 77 | 78 | $details = Details::make() 79 | ->setFile($file) 80 | ->setMessage(sprintf('%d cyclomatic complexity', $complexity)); 81 | 82 | if ($function !== null) { 83 | $details->setFunction($function); 84 | } 85 | 86 | return $details; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Domain/Insights/SniffDecorator.php: -------------------------------------------------------------------------------- 1 | */ 30 | private array $errors = []; 31 | 32 | /** 33 | * @var array 34 | */ 35 | private array $excludedFiles; 36 | 37 | /** 38 | * SniffDecorator constructor. 39 | * 40 | * @param array $exclude 41 | */ 42 | public function __construct(Sniff $sniff, string $dir, array $exclude) 43 | { 44 | $this->sniff = $sniff; 45 | $this->excludedFiles = []; 46 | 47 | if ($exclude !== []) { 48 | $this->excludedFiles = Files::find($dir, $exclude); 49 | } 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function register(): array 56 | { 57 | return $this->sniff->register(); 58 | } 59 | 60 | /** 61 | * @param int $stackPtr 62 | * 63 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 64 | */ 65 | public function process(File $file, $stackPtr): int|null 66 | { 67 | if ($file instanceof InsightFile && $this->skipFilesFromIgnoreFiles($file)) { 68 | return null; 69 | } 70 | 71 | return $this->sniff->process($file, $stackPtr); 72 | } 73 | 74 | /** 75 | * Returns the details of the insight. 76 | * 77 | * @return array 78 | */ 79 | public function getDetails(): array 80 | { 81 | return $this->errors; 82 | } 83 | 84 | /** 85 | * Checks if the insight detects an issue. 86 | */ 87 | public function hasIssue(): bool 88 | { 89 | return $this->errors !== []; 90 | } 91 | 92 | /** 93 | * Gets the title of the insight. 94 | */ 95 | public function getTitle(): string 96 | { 97 | $sniffClass = $this->getInsightClass(); 98 | 99 | $path = explode('\\', $sniffClass); 100 | $name = array_pop($path); 101 | 102 | $name = str_replace('Sniff', '', $name); 103 | 104 | return ucfirst( 105 | mb_strtolower( 106 | trim( 107 | (string) preg_replace( 108 | '/(?sniff); 123 | } 124 | 125 | public function addDetails(Details $details): void 126 | { 127 | $this->errors[] = $details; 128 | } 129 | 130 | private function skipFilesFromIgnoreFiles(InsightFile $file): bool 131 | { 132 | return array_key_exists( 133 | (string) $file->getFileInfo()->getRealPath(), 134 | $this->excludedFiles 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Domain/Kernel.php: -------------------------------------------------------------------------------- 1 | 58 | */ 59 | public static function getRequiredFiles(): array 60 | { 61 | return [ 62 | 'composer.json', 63 | 'composer.lock', 64 | // '.gitignore', 65 | ]; 66 | } 67 | 68 | /** 69 | * Returns the list of Insights required on root. 70 | * 71 | * @return array 72 | */ 73 | public static function getGlobalInsights(): array 74 | { 75 | return [ 76 | ForbiddenSecurityIssues::class, 77 | ]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Domain/LinkFormatter/FileLinkFormatter.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 26 | } 27 | 28 | public function format(string $file, int $line): string 29 | { 30 | return strtr($this->pattern, ['%f' => $file, '%l' => $line]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/LinkFormatter/NullFileLinkFormatter.php: -------------------------------------------------------------------------------- 1 | getClasses()); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getInsights(): array 31 | { 32 | return [ 33 | ForbiddenNormalClasses::class, 34 | ValidClassNameSniff::class, 35 | ClassDeclarationSniff::class, 36 | OneClassPerFileSniff::class, 37 | SuperfluousInterfaceNamingSniff::class, 38 | SuperfluousAbstractClassNamingSniff::class, 39 | ]; 40 | } 41 | 42 | public function getPercentage(Collector $collector): float 43 | { 44 | return $collector->getFiles() !== [] ? $collector->getClasses() / count($collector->getFiles()) * 100 : 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Constants.php: -------------------------------------------------------------------------------- 1 | getConstants()); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getInsights(): array 23 | { 24 | return [ 25 | ForbiddenDefineGlobalConstants::class, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Files.php: -------------------------------------------------------------------------------- 1 | getFiles()); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getInsights(): array 23 | { 24 | return [ 25 | SuperfluousExceptionNamingSniff::class, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Functions.php: -------------------------------------------------------------------------------- 1 | getFunctions()); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getInsights(): array 24 | { 25 | return [ 26 | FunctionLengthSniff::class, 27 | MethodArgumentSpaceFixer::class, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Globally.php: -------------------------------------------------------------------------------- 1 | getFiles()) 15 | - $collector->getClasses() 16 | - $collector->getInterfaces() 17 | - count($collector->getTraits()); 18 | 19 | return $collector->getFiles() !== [] ? $value / count($collector->getFiles()) * 100 : 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Interfaces.php: -------------------------------------------------------------------------------- 1 | getInterfaces()); 18 | } 19 | 20 | public function getPercentage(Collector $collector): float 21 | { 22 | return $collector->getFiles() !== [] 23 | ? $collector->getInterfaces() / count($collector->getFiles()) * 100 24 | : 0; 25 | } 26 | 27 | public function getInsights(): array 28 | { 29 | return [ 30 | OneInterfacePerFileSniff::class, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Namespaces.php: -------------------------------------------------------------------------------- 1 | getNamespaces())); 19 | } 20 | 21 | /** 22 | * Returns the insights classes applied on the metric. 23 | * 24 | * @return array 25 | */ 26 | public function getInsights(): array 27 | { 28 | return [ 29 | NamespaceDeclarationSniff::class, 30 | UselessAliasSniff::class, 31 | CompoundNamespaceDepthSniff::class, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Architecture/Traits.php: -------------------------------------------------------------------------------- 1 | getTraits())); 19 | } 20 | 21 | public function getPercentage(Collector $collector): float 22 | { 23 | return $collector->getFiles() !== [] 24 | ? count($collector->getTraits()) / count($collector->getFiles()) * 100 25 | : 0; 26 | } 27 | 28 | public function getInsights(): array 29 | { 30 | return [ 31 | ForbiddenTraits::class, 32 | OneTraitPerFileSniff::class, 33 | SuperfluousTraitNamingSniff::class, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Code/Classes.php: -------------------------------------------------------------------------------- 1 | getClassLines()); 32 | } 33 | 34 | public function getPercentage(Collector $collector): float 35 | { 36 | return $collector->getLines() > 0 ? $collector->getClassLines() / $collector->getLines() * 100 : 0; 37 | } 38 | 39 | public function getAvg(Collector $collector): string 40 | { 41 | return sprintf('%d', $collector->getAverageClassLength()); 42 | } 43 | 44 | public function getMax(Collector $collector): string 45 | { 46 | return sprintf(' % d', $collector->getMaximumClassLength()); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getInsights(): array 53 | { 54 | return [ 55 | //FullyQualifiedClassNameAfterKeywordSniff::class, 56 | ForbiddenPublicPropertySniff::class, 57 | ForbiddenSetterSniff::class, 58 | UnnecessaryFinalModifierSniff::class, 59 | PropertyDeclarationSniff::class, 60 | ClassConstantVisibilitySniff::class, 61 | DisallowLateStaticBindingForConstantsSniff::class, 62 | ModernClassNameReferenceSniff::class, 63 | UselessLateStaticBindingSniff::class, 64 | VisibilityRequiredFixer::class, 65 | ProtectedToPrivateFixer::class, 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Code/Comments.php: -------------------------------------------------------------------------------- 1 | getCommentLines()); 36 | } 37 | 38 | public function getPercentage(Collector $collector): float 39 | { 40 | return $collector->getLines() > 0 ? $collector->getCommentLines() / $collector->getLines() * 100 : 0; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getInsights(): array 47 | { 48 | return [ 49 | // FullyQualifiedClassNameInAnnotationSniff::class, 50 | NullableTypeForNullDefaultValueSniff::class, 51 | FixmeSniff::class, 52 | TodoSniff::class, 53 | ForbiddenCommentsSniff::class, 54 | InlineDocCommentDeclarationSniff::class, 55 | DisallowArrayTypeHintSyntaxSniff::class, 56 | DisallowMixedTypeHintSniff::class, 57 | LongTypeHintsSniff::class, 58 | NullTypeHintOnLastPositionSniff::class, 59 | ParameterTypeHintSniff::class, 60 | PropertyTypeHintSniff::class, 61 | ReturnTypeHintSniff::class, 62 | UselessFunctionDocCommentSniff::class, 63 | UselessConstantTypeHintSniff::class, 64 | UselessInheritDocCommentSniff::class, 65 | NoBreakCommentFixer::class, 66 | MultilineCommentOpeningClosingFixer::class, 67 | NoEmptyCommentFixer::class, 68 | PhpdocScalarFixer::class, 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Code/Functions.php: -------------------------------------------------------------------------------- 1 | getFunctionLines()); 29 | } 30 | 31 | public function getPercentage(Collector $collector): float 32 | { 33 | return $collector->getLines() > 0 ? $collector->getFunctionLines() / $collector->getLines() * 100 : 0; 34 | } 35 | 36 | public function getAvg(Collector $collector): string 37 | { 38 | return sprintf('%d', $collector->getAverageFunctionLength()); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getInsights(): array 45 | { 46 | return [ 47 | UnusedInheritedVariablePassedToClosureSniff::class, 48 | UnusedParameterSniff::class, 49 | CallTimePassByReferenceSniff::class, 50 | DeprecatedFunctionsSniff::class, 51 | NullableTypeDeclarationSniff::class, 52 | StaticClosureSniff::class, 53 | ForbiddenDefineFunctions::class, 54 | ForbiddenFunctionsSniff::class, 55 | NoSpacesAfterFunctionNameFixer::class, 56 | ReturnAssignmentFixer::class, 57 | VoidReturnFixer::class, 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Code/Globally.php: -------------------------------------------------------------------------------- 1 | getNotInClassesOrFunctions()); 19 | } 20 | 21 | public function getPercentage(Collector $collector): float 22 | { 23 | return $collector->getLines() > 0 ? $collector->getNotInClassesOrFunctions() / $collector->getLines() * 100 : 0; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getInsights(): array 30 | { 31 | return [ 32 | GlobalKeywordSniff::class, 33 | ForbiddenGlobals::class, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Complexity/Complexity.php: -------------------------------------------------------------------------------- 1 | getAverageComplexityPerMethod()); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getInsights(): array 25 | { 26 | return [ 27 | CyclomaticComplexityIsHigh::class, 28 | ClassMethodAverageCyclomaticComplexityIsHigh::class, 29 | MethodCyclomaticComplexityIsHigh::class, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Domain/Metrics/Security/Security.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public static function find(): array 33 | { 34 | return [ 35 | Code_Classes::class, 36 | Code_Code::class, 37 | Code_Comments::class, 38 | Code_Functions::class, 39 | Code_Globally::class, 40 | Complexity_Complexity::class, 41 | Architecture_Classes::class, 42 | Architecture_Constants::class, 43 | Architecture_Files::class, 44 | Architecture_Functions::class, 45 | Architecture_Globally::class, 46 | Architecture_Interfaces::class, 47 | Architecture_Namespaces::class, 48 | Architecture_Traits::class, 49 | Style_Style::class, 50 | Security_Security::class, 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Domain/Reflection.php: -------------------------------------------------------------------------------- 1 | instance = $instance; 25 | $this->reflectionClass = new ReflectionClass($instance); 26 | } 27 | 28 | /** 29 | * Sets an private attribute value on the given instance. 30 | */ 31 | public function set(string $attribute, mixed $value): Reflection 32 | { 33 | self::setProperty( 34 | $this->reflectionClass, 35 | $this->instance, 36 | $attribute, 37 | $value 38 | ); 39 | 40 | return $this; 41 | } 42 | 43 | /** 44 | * Gets an private attribute value on the given instance. 45 | */ 46 | public function get(string $attribute): mixed 47 | { 48 | $property = $this->reflectionClass->getProperty($attribute); 49 | 50 | $property->setAccessible(true); 51 | 52 | return $property->getValue($this->instance); 53 | } 54 | 55 | /** 56 | * @throws ReflectionException 57 | */ 58 | private static function setProperty( 59 | ReflectionClass $class, 60 | mixed $instance, 61 | string $attribute, 62 | mixed $value 63 | ): void { 64 | try { 65 | $property = $class->getProperty($attribute); 66 | $property->setAccessible(true); 67 | $property->setValue($instance, $value); 68 | } catch (ReflectionException $exception) { 69 | $parentClass = $class->getParentClass(); 70 | 71 | if ($parentClass === false) { 72 | throw $exception; 73 | } 74 | 75 | self::setProperty( 76 | $parentClass, 77 | $instance, 78 | $attribute, 79 | $value 80 | ); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Domain/Sniffs/ForbiddenSetterSniff.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public array $allowedMethodRegex; 25 | 26 | public function register(): array 27 | { 28 | return [T_FUNCTION]; 29 | } 30 | 31 | /** 32 | * Runs the sniff on a file. 33 | * 34 | * @param int $position 35 | * 36 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint 37 | */ 38 | public function process(File $file, $position): void 39 | { 40 | $methodName = $file->getDeclarationName($position); 41 | if ($methodName === null) { 42 | return; 43 | } 44 | 45 | // Check if method should be skipped. 46 | if ($this->shouldSkip($methodName)) { 47 | return; 48 | } 49 | 50 | // Check if method is a setter 51 | if (preg_match(self::SETTER_REGEX, $methodName) !== 1) { 52 | return; 53 | } 54 | 55 | $file->addError(self::ERROR_MESSAGE, $position, self::class); 56 | } 57 | 58 | /** 59 | * Checks if we should skip this method based on either the 60 | * method name or the class name. 61 | */ 62 | private function shouldSkip(string $methodName): bool 63 | { 64 | // Skip setUp method as often used in test classes 65 | if ($methodName === 'setUp') { 66 | return true; 67 | } 68 | 69 | foreach ($this->getAllowedMethodRegex() as $methodRegex) { 70 | if (preg_match($methodRegex, $methodName) === 1) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | private function getAllowedMethodRegex(): array 82 | { 83 | return $this->allowedMethodRegex ?? []; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Infrastructure/Repositories/LocalFilesRepository.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private ?array $files = null; 26 | 27 | /** 28 | * @var array 29 | */ 30 | private ?array $fileList = null; 31 | 32 | /** 33 | * @var array 34 | */ 35 | private ?array $directoryList = null; 36 | 37 | public function __construct(Finder $finder) 38 | { 39 | $this->finder = $finder; 40 | } 41 | 42 | public function getDefaultDirectory(): string 43 | { 44 | return (string) getcwd(); 45 | } 46 | 47 | public function getFiles(): array 48 | { 49 | if ($this->files === null) { 50 | $this->files = $this->getFilesList(); 51 | } 52 | 53 | return $this->files; 54 | } 55 | 56 | public function within(array $paths, array $exclude = []): FilesRepository 57 | { 58 | foreach ($paths as $path) { 59 | $pathInfo = pathinfo($path); 60 | 61 | if (! is_dir($path) && is_file($path)) { 62 | $this->fileList['dirname'][] = $pathInfo['dirname']; 63 | $this->fileList['basename'][] = $pathInfo['basename']; 64 | $this->fileList['full_path'][] = $pathInfo['dirname'] . DIRECTORY_SEPARATOR . $pathInfo['basename']; 65 | } else { 66 | $this->directoryList[] = rtrim($pathInfo['dirname'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $pathInfo['basename']; 67 | } 68 | } 69 | 70 | $directoryFiles = $this->directoryList === null ? [] : $this->getDirectoryFiles($exclude); 71 | $singleFiles = $this->fileList === null ? [] : $this->getSingleFiles(); 72 | 73 | $this->files = array_merge($directoryFiles, $singleFiles); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param array $exclude 80 | * 81 | * @return array 82 | */ 83 | private function getDirectoryFiles(array $exclude = []): array 84 | { 85 | $this->finder = Finder::create() 86 | ->files() 87 | ->name(['*.php']) 88 | ->exclude(self::DEFAULT_EXCLUDE) 89 | ->notName(['*.blade.php']) 90 | ->ignoreUnreadableDirs() 91 | ->in($this->directoryList ?? []) 92 | ->notPath($exclude); 93 | 94 | foreach ($exclude as $value) { 95 | if (substr($value, -4) === '.php') { 96 | $this->finder->notName($value); 97 | } 98 | } 99 | 100 | return $this->getFilesList(); 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | private function getSingleFiles(): array 107 | { 108 | $this->finder = Finder::create() 109 | ->in($this->fileList['dirname'] ?? []) 110 | ->name($this->fileList['basename'] ?? '') 111 | ->ignoreDotFiles(false) 112 | ->ignoreVCS(false) 113 | ->filter(fn (SplFileInfo $file): bool => in_array( 114 | $file->getPathname(), 115 | $this->fileList['full_path'] ?? '', 116 | true 117 | )); 118 | 119 | return $this->getFilesList(); 120 | } 121 | 122 | /** 123 | * @return array<\Symfony\Component\Finder\SplFileInfo> 124 | */ 125 | private function getFilesList(): array 126 | { 127 | return iterator_to_array($this->finder->getIterator(), true); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /stubs/config.php: -------------------------------------------------------------------------------- 1 | 'default', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | // ExampleInsight::class, 65 | ], 66 | 67 | 'config' => [ 68 | // ExampleInsight::class => [ 69 | // 'key' => 'value', 70 | // ], 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Requirements 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may define a level you want to reach per `Insights` category. 79 | | When a score is lower than the minimum level defined, then an error 80 | | code will be returned. This is optional and individually defined. 81 | | 82 | */ 83 | 84 | 'requirements' => [ 85 | // 'min-quality' => 0, 86 | // 'min-complexity' => 0, 87 | // 'min-architecture' => 0, 88 | // 'min-style' => 0, 89 | // 'disable-security-check' => false, 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Threads 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may adjust how many threads (core) PHPInsights can use to perform 98 | | the analysis. This is optional, don't provide it and the tool will guess 99 | | the max core number available. It accepts null value or integer > 0. 100 | | 101 | */ 102 | 103 | 'threads' => null, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Timeout 108 | |-------------------------------------------------------------------------- 109 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 110 | | a ProcessTimedOutException is thrown. 111 | | This accepts an int > 0. Default is 60 seconds, which is the default value 112 | | of Symfony's setTimeout function. 113 | | 114 | */ 115 | 116 | 'timeout' => 60, 117 | ]; 118 | -------------------------------------------------------------------------------- /stubs/drupal.php: -------------------------------------------------------------------------------- 1 | 'drupal', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | // ExampleInsight::class, 65 | ], 66 | 67 | 'config' => [ 68 | // ExampleInsight::class => [ 69 | // 'key' => 'value', 70 | // ], 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Requirements 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may define a level you want to reach per `Insights` category. 79 | | When a score is lower than the minimum level defined, then an error 80 | | code will be returned. This is optional and individually defined. 81 | | 82 | */ 83 | 84 | 'requirements' => [ 85 | // 'min-quality' => 0, 86 | // 'min-complexity' => 0, 87 | // 'min-architecture' => 0, 88 | // 'min-style' => 0, 89 | // 'disable-security-check' => false, 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Threads 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may adjust how many threads (core) PHPInsights can use to perform 98 | | the analysis. This is optional, don't provide it and the tool will guess 99 | | the max core number available. It accepts null value or integer > 0. 100 | | 101 | */ 102 | 103 | 'threads' => null, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Timeout 108 | |-------------------------------------------------------------------------- 109 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 110 | | a ProcessTimedOutException is thrown. 111 | | This accepts an int > 0. Default is 60 seconds, which is the default value 112 | | of Symfony's setTimeout function. 113 | | 114 | */ 115 | 116 | 'timeout' => 60, 117 | ]; 118 | -------------------------------------------------------------------------------- /stubs/magento2.php: -------------------------------------------------------------------------------- 1 | 'magento2', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | // ExampleInsight::class, 65 | ], 66 | 67 | 'config' => [ 68 | // ExampleInsight::class => [ 69 | // 'key' => 'value', 70 | // ], 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Requirements 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may define a level you want to reach per `Insights` category. 79 | | When a score is lower than the minimum level defined, then an error 80 | | code will be returned. This is optional and individually defined. 81 | | 82 | */ 83 | 84 | 'requirements' => [ 85 | // 'min-quality' => 0, 86 | // 'min-complexity' => 0, 87 | // 'min-architecture' => 0, 88 | // 'min-style' => 0, 89 | // 'disable-security-check' => false, 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Threads 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may adjust how many threads (core) PHPInsights can use to perform 98 | | the analysis. This is optional, don't provide it and the tool will guess 99 | | the max core number available. It accepts null value or integer > 0. 100 | | 101 | */ 102 | 103 | 'threads' => null, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Timeout 108 | |-------------------------------------------------------------------------- 109 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 110 | | a ProcessTimedOutException is thrown. 111 | | This accepts an int > 0. Default is 60 seconds, which is the default value 112 | | of Symfony's setTimeout function. 113 | | 114 | */ 115 | 116 | 'timeout' => 60, 117 | ]; 118 | -------------------------------------------------------------------------------- /stubs/symfony.php: -------------------------------------------------------------------------------- 1 | 'symfony', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | // ExampleInsight::class, 65 | ], 66 | 67 | 'config' => [ 68 | // ExampleInsight::class => [ 69 | // 'key' => 'value', 70 | // ], 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Requirements 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may define a level you want to reach per `Insights` category. 79 | | When a score is lower than the minimum level defined, then an error 80 | | code will be returned. This is optional and individually defined. 81 | | 82 | */ 83 | 84 | 'requirements' => [ 85 | // 'min-quality' => 0, 86 | // 'min-complexity' => 0, 87 | // 'min-architecture' => 0, 88 | // 'min-style' => 0, 89 | // 'disable-security-check' => false, 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Threads 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may adjust how many threads (core) PHPInsights can use to perform 98 | | the analysis. This is optional, don't provide it and the tool will guess 99 | | the max core number available. It accepts null value or integer > 0. 100 | | 101 | */ 102 | 103 | 'threads' => null, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Timeout 108 | |-------------------------------------------------------------------------- 109 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 110 | | a ProcessTimedOutException is thrown. 111 | | This accepts an int > 0. Default is 60 seconds, which is the default value 112 | | of Symfony's setTimeout function. 113 | | 114 | */ 115 | 116 | 'timeout' => 60, 117 | ]; 118 | -------------------------------------------------------------------------------- /stubs/wordpress.php: -------------------------------------------------------------------------------- 1 | 'wordpress', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | // ExampleInsight::class, 65 | ], 66 | 67 | 'config' => [ 68 | // ExampleInsight::class => [ 69 | // 'key' => 'value', 70 | // ], 71 | ], 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Requirements 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Here you may define a level you want to reach per `Insights` category. 79 | | When a score is lower than the minimum level defined, then an error 80 | | code will be returned. This is optional and individually defined. 81 | | 82 | */ 83 | 84 | 'requirements' => [ 85 | // 'min-quality' => 0, 86 | // 'min-complexity' => 0, 87 | // 'min-architecture' => 0, 88 | // 'min-style' => 0, 89 | // 'disable-security-check' => false, 90 | ], 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Threads 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Here you may adjust how many threads (core) PHPInsights can use to perform 98 | | the analysis. This is optional, don't provide it and the tool will guess 99 | | the max core number available. It accepts null value or integer > 0. 100 | | 101 | */ 102 | 103 | 'threads' => null, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Timeout 108 | |-------------------------------------------------------------------------- 109 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 110 | | a ProcessTimedOutException is thrown. 111 | | This accepts an int > 0. Default is 60 seconds, which is the default value 112 | | of Symfony's setTimeout function. 113 | | 114 | */ 115 | 116 | 'timeout' => 60, 117 | ]; 118 | --------------------------------------------------------------------------------