├── tests ├── reports │ └── .gitkeep ├── stubs │ ├── foo.twig │ ├── dummy.yml │ └── dummy.php ├── bootstrap.php ├── scanner_test_config_nodev.php ├── scanner_test_config_dev.php ├── scanner_test_config_reported.php ├── scanner_test_config_reported_custom.php ├── ConfigTest.php ├── stub_composer.json ├── stubs2 │ └── Foo.php ├── ComposerReaderTest.php ├── RunnerTest.php └── ScannerTest.php ├── unused.png ├── .gitignore ├── Exceptions └── InvalidConfigException.php ├── issue_template.md ├── box.json ├── phpunit.xml ├── LICENSE ├── composer.json ├── Lib ├── ComposerReader.php ├── DependencyMapper.php ├── Runner.php ├── Config.php └── Scanner.php ├── README.md ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── CHANGELOG.md ├── unused_scanner └── scanner_config.example.php /tests/reports/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/stubs/foo.twig: -------------------------------------------------------------------------------- 1 | use('Foo/Bar'); -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | $projectPath . '/stub_composer.json', 5 | 'vendorPath' => $projectPath . '/../vendor/', 6 | 'scanDirectories' => [$projectPath . '/stubs/'], 7 | 'requireDev' => false 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/scanner_test_config_dev.php: -------------------------------------------------------------------------------- 1 | $projectPath . '/stub_composer.json', 5 | 'vendorPath' => $projectPath . '/../vendor/', 6 | 'scanDirectories' => [$projectPath . '/stubs/', $projectPath.'/not_existed/', $projectPath.'/missing'], 7 | 'requireDev' => true 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/scanner_test_config_reported.php: -------------------------------------------------------------------------------- 1 | $projectPath . '/stub_composer.json', 5 | 'vendorPath' => $projectPath . '/../vendor/', 6 | 'scanDirectories' => [$projectPath . '/stubs/'], 7 | 'requireDev' => false, 8 | 'reportPath'=>$projectPath.'/reports/' 9 | ]; 10 | -------------------------------------------------------------------------------- /tests/stubs/dummy.yml: -------------------------------------------------------------------------------- 1 | a2i_geo.command.convert_city_layer_to_fixtures: 2 | class: A2I\GeoBundle\Command\ConvertToFixtures\ConvertCityLayerToFixturesCommand 3 | arguments: 4 | - '@a2i_geo.helper.file.ogr_feature_data_collector' 5 | - '@a2i_geo.factory.dto.skipped_item' 6 | - '@bazinga_geocoder.geocoder' 7 | tags: 8 | - name: console.command -------------------------------------------------------------------------------- /tests/scanner_test_config_reported_custom.php: -------------------------------------------------------------------------------- 1 | $projectPath . '/stub_composer.json', 5 | 'vendorPath' => $projectPath . '/../vendor/', 6 | 'scanDirectories' => [$projectPath . '/stubs/'], 7 | 'requireDev' => false, 8 | 'reportPath'=>$projectPath.'/reports/', 9 | 'reportFormatter'=>function(array $report){ 10 | return print_r($report, true); 11 | }, 12 | 'reportExtension' => 'txt' 13 | ]; 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | getScanDirectories(); 15 | $this->assertContains(__DIR__ . '/stubs', $dirs); 16 | $this->assertNotContains(__DIR__ . '/not_existed', $dirs); 17 | $this->assertNotContains(__DIR__ . '/missing', $dirs); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/stub_composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insolita/unused-scanner-test" , 3 | "description": "Detect unused composer dependencies" , 4 | "type": "library" , 5 | "license": "mit" , 6 | "authors": [ 7 | { 8 | "name": "insolita" , 9 | "email": "webmaster100500@ya.ru" 10 | } 11 | ] , 12 | "minimum-stability": "dev" , 13 | "require": { 14 | "php": ">=7.0" , 15 | "ext-gd": "*" , 16 | "symfony/finder": "^3.4|^4.0" 17 | } , 18 | "require-dev": { 19 | "phpunit/phpunit": "~7.0" , 20 | "symfony/thanks": "1.0.*" 21 | } , 22 | "bin": [ 23 | "unused_scanner" 24 | ] , 25 | "autoload": { 26 | "psr-4": { 27 | "insolita\\Scanner\\": "" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/stubs2/Foo.php: -------------------------------------------------------------------------------- 1 | Config::class, 17 | 'b'=> insolita\Scanner\Lib\ComposerReader::class, 18 | 'c'=> 'Symfony\Component\Finder\Exception\AccessDeniedException', 19 | 'd'=> 'Symfony\\Component\\Finder\\Finder', 20 | 'e'=> Text_Template::class, 21 | 'f'=> ['\PHPUnit\Runner\PhptTestCase', 'PHP_Token_AMPERSAND'], 22 | 'g'=>['\Exception',Composer\Autoload\ClassLoader::class], 23 | 'h'=>'\\SebastianBergmann\\ObjectReflector\\TestFixture\\ParentClass', 24 | 'i'=>PHPUnit\Util\Filesystem::class 25 | ]; 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Insolita 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insolita/unused-scanner" , 3 | "description": "Detect unused composer dependencies" , 4 | "type": "library" , 5 | "license": "mit" , 6 | "authors": [ 7 | { 8 | "name": "insolita" , 9 | "email": "webmaster100500@ya.ru" 10 | } 11 | ] , 12 | "minimum-stability": "dev" , 13 | "prefer-stable": true , 14 | "require": { 15 | "php": ">=7.1" , 16 | "ext-json": "*" , 17 | "ext-mbstring": "*" , 18 | "symfony/finder": "^3.4|^4.0|^5.0|^6.0" 19 | } , 20 | "require-dev": { 21 | "phpunit/phpunit": "^7.0|^8.0|^9.0", 22 | "symfony/thanks": "^1.2", 23 | "php-mock/php-mock-phpunit": "^2.6" 24 | } , 25 | "bin": [ 26 | "unused_scanner" 27 | ] , 28 | "autoload": { 29 | "psr-4": { 30 | "insolita\\Scanner\\Lib\\": "Lib" , 31 | "insolita\\Scanner\\Exceptions\\": "Exceptions", 32 | "tests\\": "tests" 33 | } 34 | } , 35 | "extra": { 36 | "branch-alias": { 37 | "dev-legacy": "1.3.x-dev" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Lib/ComposerReader.php: -------------------------------------------------------------------------------- 1 | config = $config; 20 | } 21 | 22 | public function fetchDependencies(): array 23 | { 24 | $composerData = $this->readComposerJson(); 25 | $packages = $composerData['require']; 26 | if ($this->config->getRequireDev()===true) { 27 | $packages = array_merge($packages, $composerData['require-dev'] ?? []); 28 | } 29 | $packages = array_keys($packages); 30 | return array_filter($packages, function ($package) { 31 | $packageHasVendor = mb_strpos($package, '/') !== false; 32 | $packageNotSkipped = !in_array($package, $this->config->getSkipPackages(), true); 33 | return $packageHasVendor && $packageNotSkipped; 34 | }); 35 | } 36 | 37 | private function readComposerJson(): array 38 | { 39 | $file = file_get_contents($this->config->getComposerJsonPath()); 40 | return $file ? json_decode($file, true) : []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project scanner for detect unused composer dependencies 2 | 3 | ![unused-scanner](https://github.com/Insolita/unused-scanner/workflows/unused-scanner/badge.svg?branch=master) 4 | 5 | ### Versions 6 | 7 | Use 1.3.x@dev versions for projects with php 5.6, 7.0 8 | 9 | Use 2.x versions for projects with php >= 7.1 10 | 11 | ### ChangeLog 12 | 13 | see [CHANGELOG.md](CHANGELOG.md) 14 | 15 | ### Installation 16 | 17 | `composer global require insolita/unused-scanner` 18 | 19 | Ensure that your ~/.composer/vendor/bin directory declared in $PATH 20 | 21 | `echo $PATH` 22 | 23 | if not - you should add it in ~/.bashrc or ~/.profile 24 | 25 | ### Update 26 | 27 | `composer global update` 28 | 29 | ### Usage 30 | 31 | prepare configuration file, see [scanner_config.example.php](scanner_config.example.php) 32 | 33 | put it in project root (or other place) 34 | 35 | run `composer dumpautoload` in your project directory 36 | 37 | run `unused_scanner /path/to/configuration/file/scanner_config.php` 38 | 39 | since 1.1 you can run it without argument, if scanner_config.php existed in current working directory, it will be used 40 | by default 41 | 42 | **For auto-testing**: 43 | 44 | Add --silent option for skip progress output and return exit code = 16, when unused packages detected 45 | 46 | run `unused_scanner --silent /path/to/configuration/file/scanner_config.php` 47 | 48 | **Docker**: 49 | 50 | https://github.com/juanmrad/docker-unused-scanner 51 | 52 | ![Demo screenshot](unused.png) 53 | 54 | ### Licence 55 | 56 | This project uses the [MIT licence](https://choosealicense.com/licenses/mit/). 57 | -------------------------------------------------------------------------------- /tests/ComposerReaderTest.php: -------------------------------------------------------------------------------- 1 | setRequireDev(true); 14 | $reader = new ComposerReader($config); 15 | $packages = $reader->fetchDependencies(); 16 | $this->assertNotContains('php', $packages); 17 | $this->assertNotContains('ext-gd', $packages); 18 | $this->assertContains('symfony/finder', $packages); 19 | $this->assertContains('phpunit/phpunit', $packages); 20 | $this->assertContains('symfony/thanks', $packages); 21 | } 22 | 23 | public function testItShouldBeSkipCustomConfiguredPackages() 24 | { 25 | $config = new Config(__DIR__ . '/stub_composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs/']); 26 | $config->setRequireDev(true); 27 | $config->setSkipPackages(['symfony/finder', 'ext-gd', 'phpunit/phpunit']); 28 | $reader = new ComposerReader($config); 29 | $packages = $reader->fetchDependencies(); 30 | $this->assertNotContains('php', $packages); 31 | $this->assertNotContains('ext-gd', $packages); 32 | $this->assertNotContains('symfony/finder', $packages); 33 | $this->assertNotContains('phpunit/phpunit', $packages); 34 | $this->assertContains('symfony/thanks', $packages); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: unused-scanner 2 | on: 3 | push: 4 | branches: [ master, dev ] 5 | pull_request: 6 | branches: [ master ] 7 | paths-ignore: 8 | - 'docs/**' 9 | - '*.md' 10 | 11 | jobs: 12 | test: 13 | if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')" 14 | name: unused-scanner (PHP ${{ matrix.php-versions }}) 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | php-versions: ['7.2', '7.3', '7.4'] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Setup PHP, with composer and extensions 25 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 26 | with: 27 | php-version: ${{ matrix.php-versions }} 28 | extensions: mbstring, intl 29 | tools: composer:v2 30 | 31 | - name: Get composer cache directory 32 | id: composercache 33 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 34 | 35 | - name: Cache Composer packages 36 | id: composer-cache 37 | uses: actions/cache@v2 38 | with: 39 | path: ${{ steps.composercache.outputs.dir }} 40 | key: ${{ runner.os }}-php-${{ matrix.php-versions }}-${{ hashFiles('**/composer.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-php-${{ matrix.php-versions }} 43 | 44 | - name: Install deps 45 | run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader 46 | 47 | - name: Unit tests 48 | run: php vendor/bin/phpunit 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | - Add Symfony 6 support 3 | 4 | 2.3.0 5 | 6 | - Fix [#34](https://github.com/Insolita/unused-scanner/issues/34) 7 | - Fix [#36](https://github.com/Insolita/unused-scanner/issues/36) 8 | [#37](https://github.com/Insolita/unused-scanner/issues/37) 9 | - Add support --version option 10 | - Fix [#33](https://github.com/Insolita/unused-scanner/issues/33) - now .phar builds available 11 | 12 | 2.2.0 13 | 14 | - Fix dev dependencies for composer2.0 compatibility 15 | - code typehint fixes 16 | 17 | 2.1.1 18 | 19 | - Improve json output format 20 | - code style fixes 21 | 22 | 2.1 23 | 24 | - Support namespaces with group use declarations 25 | - Ensure php 7.4 support 26 | 27 | 2.0.4 28 | 29 | - Symfony 5.0 support 30 | 31 | 2.0.3 32 | 33 | - Added ext_mbstring dependency in composer.json 34 | - Cosmetic changes 35 | 36 | 2.0.2 37 | 38 | - Add License file 39 | - Fix travis tests config 40 | 41 | 2.0 42 | 43 | - PHP >=7.1 branch without legacy support 44 | 45 | 1.3 46 | 47 | - Added support for old php 5.6, php 7.0 versions 48 | 49 | 1.2 50 | 51 | - Window suppor improvement 52 | 53 | 1.1.1 54 | 55 | - Fix #13, use DIRECTORY_SEPARATOR constants for windows support 56 | - add tests for php 7.3 in travis.ci 57 | - move Changelog in separated file 58 | 59 | 1.1 60 | 61 | - Fix #10 - php extensions should be skipped without warnings 62 | 63 | - Fix #12 - check presence of scanner_config.php in current working directory and allow run without arguments 64 | 65 | - New config option - skipPackages for excluding packages from checking 66 | 67 | 1.0.9 68 | 69 | - Add ability for store usage report [@see](https://github.com/Insolita/unused-scanner/blob/master/scanner_config.example.php#L51) 70 | 71 | 1.0.8 72 | 73 | - Return different exitCodes [@see](https://github.com/Insolita/unused-scanner/blob/master/Lib/Runner.php#L18) 74 | -------------------------------------------------------------------------------- /unused_scanner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 1 && strpos($argv[1] ?? '', '-') !== 0) { 24 | $configPath = $argv[1]; 25 | $args = array_slice($argv, 1); 26 | $silentMode = in_array('-s', $args, true) || in_array('--silent', $args, true); 27 | $showVersion = in_array('--version', $args, true); 28 | } else { 29 | $options = getopt('s', ['silent', 'version'], $optIndex); 30 | $configPath = (int)$optIndex < $argc && $argc > 1 ? array_slice($argv, $optIndex)[0] : null; 31 | $silentMode = isset($options['s']) || isset($options['silent']); 32 | $showVersion = isset($options['version']); 33 | } 34 | 35 | if ($showVersion) { 36 | echo $VERSION . PHP_EOL; 37 | exit(Runner::SUCCESS_CODE); 38 | } 39 | 40 | if (!$configPath && file_exists($defaultConfigPath)) { 41 | $configPath = $defaultConfigPath; 42 | echo 'Default detected config will be used at ' . $defaultConfigPath . PHP_EOL; 43 | } 44 | if (!$configPath) { 45 | echo 'Missing required argument - path to config' . PHP_EOL; 46 | exit(Runner::ARGUMENT_ERROR_CODE); 47 | } 48 | 49 | if (!file_exists($configPath)) { 50 | echo 'Configuration file "' . $configPath . '" not found' . PHP_EOL; 51 | exit(Runner::ARGUMENT_ERROR_CODE); 52 | } 53 | 54 | $exitCode = (new Runner((string)$configPath, $silentMode))->run(); 55 | 56 | exit($exitCode); 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | # on: workflow_dispatch 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: '7.1' 17 | extensions: intl, zip, zlib, mbstring 18 | coverage: none 19 | ini-values: memory_limit=1G, phar.readonly=0 20 | 21 | - name: Get composer cache directory 22 | id: composer_release_cachedir 23 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 24 | 25 | - name: Cache Composer packages 26 | id: composer-release-cache 27 | uses: actions/cache@v2 28 | with: 29 | path: ${{ steps.composer_release_cachedir.outputs.dir }} 30 | key: ${{ runner.os }}-release-${{ hashFiles('**/composer.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-release- 33 | 34 | - name: Install 35 | run: composer install --prefer-dist --no-interaction --no-ansi --no-progress --no-dev 36 | 37 | - name: Install Box 38 | run: composer global require humbug/box 39 | 40 | - name: Validate config 41 | run: box validate -i || exit 1 42 | 43 | - name: Phar building 44 | run: box compile -v || exit 1 45 | 46 | - name: Phar test 47 | run: test -f "./unused_scanner.phar" || exit 1 48 | 49 | - name: Getting Tag Name 50 | id: get-version 51 | run: echo ::set-output name=version::${GITHUB_REF#refs/tags/} 52 | 53 | - name: Self-Test 54 | run: ./unused_scanner.phar --version 55 | 56 | - name: Release 57 | uses: ncipollo/release-action@v1 58 | with: 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | name: ${{ steps.get-version.outputs.version }} 61 | tag: ${{ steps.get-version.outputs.version }} 62 | body: 'Next stable release.' 63 | allowUpdates: true 64 | artifacts: unused_scanner.phar 65 | artifactContentType: application/x-php -------------------------------------------------------------------------------- /tests/RunnerTest.php: -------------------------------------------------------------------------------- 1 | run(); 17 | $this->assertEquals(Runner::SUCCESS_CODE, $exitCode); 18 | } 19 | 20 | public function testItShouldBeReturnUnusedExitCode() 21 | { 22 | $exitCode = (new Runner(__DIR__ . '/scanner_test_config_dev.php', false))->run(); 23 | $this->assertEquals(Runner::HAS_UNUSED_CODE, $exitCode); 24 | } 25 | 26 | public function testItShouldBeStoreJsonReport() 27 | { 28 | $reportFile = __DIR__ . '/reports/package_usage_report_2018-01-02_03_04.json'; 29 | if (file_exists($reportFile)) { 30 | unlink($reportFile); 31 | } 32 | $exitCode = (new Runner(__DIR__ . '/scanner_test_config_reported.php', false))->run(); 33 | $this->assertEquals(Runner::SUCCESS_CODE, $exitCode); 34 | $this->assertFileExists($reportFile); 35 | $fileData = json_decode(file_get_contents($reportFile), true); 36 | print_r($fileData); 37 | $this->assertNotEmpty($fileData); 38 | } 39 | 40 | public function testItShouldBeStoreCustomFormattedReport() 41 | { 42 | $reportFile = __DIR__ . '/reports/package_usage_report_2018-01-02_03_04.txt'; 43 | if (file_exists($reportFile)) { 44 | unlink($reportFile); 45 | } 46 | $exitCode = (new Runner(__DIR__ . '/scanner_test_config_reported_custom.php', false))->run(); 47 | $this->assertEquals(Runner::SUCCESS_CODE, $exitCode); 48 | $this->assertFileExists($reportFile); 49 | $fileData = file_get_contents($reportFile); 50 | print_r($fileData); 51 | $this->assertNotEmpty($fileData); 52 | } 53 | 54 | protected function setUp():void 55 | { 56 | parent::setUp(); 57 | $mock = (new MockBuilder()) 58 | ->setNamespace('insolita\Scanner\Lib') 59 | ->setName("date")->setFunctionProvider(new FixedValueFunction('2018-01-02_03_04')) 60 | ->build(); 61 | $mock->enable(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scanner_config.example.php: -------------------------------------------------------------------------------- 1 | $projectPath . '/composer.json', 41 | 'vendorPath' => $projectPath . '/vendor/', 42 | 'scanDirectories' => $scanDirectories, 43 | 44 | /** 45 | * Optional params 46 | **/ 47 | 'skipPackages' => [], //List of packages that must be excluded from verification 48 | 'excludeDirectories' => $excludeDirectories, 49 | 'scanFiles' => $scanFiles, 50 | 'extensions' => ['*.php'], 51 | 'requireDev' => false, //Check composer require-dev section, default false 52 | /** 53 | * Optional, custom logic for check is file contains definitions of package 54 | * 55 | * @example 56 | * 'customMatch'=> function($definition, $packageName, \Symfony\Component\Finder\SplFileInfo $file):bool{ 57 | * $isPresent = false; 58 | * if($packageName === 'phpunit/phpunit'){ 59 | * $isPresent = true; 60 | * } 61 | * if($file->getExtension()==='twig'){ 62 | * $isPresent = customCheck(); 63 | * } 64 | * return $isPresent; 65 | * } 66 | **/ 67 | 'customMatch'=> null, 68 | 69 | /** 70 | * Report mode options 71 | * Report mode enabled, when reportPath value is valid directory path 72 | * !!!Note!!! The scanning time and memory usage will be increased when report mode enabled, 73 | * it sensitive especially for big projects and when requireDev option enabled 74 | **/ 75 | 76 | 'reportPath' => null, //path in directory, where usage report will be stores; 77 | 78 | /** 79 | * Optional custom formatter (by default report stored as json) 80 | * $report array format 81 | * [ 82 | * 'packageName'=> [ 83 | * 'definition'=>['fileNames',...] 84 | * .... 85 | * ] 86 | * ... 87 | * ] 88 | * 89 | * @example 90 | * 'reportFormatter'=>function(array $report):string{ 91 | * return print_r($report, true); 92 | * } 93 | **/ 94 | 'reportFormatter' => null, 95 | 'reportExtension' => null, //by default - json, set own, if use custom formatter 96 | ]; 97 | -------------------------------------------------------------------------------- /Lib/DependencyMapper.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | $this->dependencies = $this->prepareDependencies($dependencies); 28 | } 29 | 30 | public function build(): array 31 | { 32 | foreach ($this->loadNamespaces() as $definition => $pathMap) { 33 | foreach ($this->dependencies as $packageName => $pathPart) { 34 | if ($this->isPathMatched(implode(',', $pathMap), $pathPart)) { 35 | $this->addToMap($definition, $packageName); 36 | break; 37 | } 38 | } 39 | } 40 | 41 | foreach ($this->loadPsr() as $definition => $pathMap) { 42 | foreach ($this->dependencies as $packageName => $pathPart) { 43 | if ($this->isPathMatched(implode(',', $pathMap), $pathPart)) { 44 | $this->addToMap($definition, $packageName); 45 | break; 46 | } 47 | } 48 | } 49 | 50 | foreach ($this->loadClassmap() as $definition => $path) { 51 | foreach ($this->dependencies as $packageName => $pathPart) { 52 | if ($this->isPathMatched($path, $pathPart)) { 53 | $this->addToMap($definition, $packageName); 54 | break; 55 | } 56 | } 57 | } 58 | return $this->map; 59 | } 60 | 61 | public function prepareDependencies(array $dependencies) 62 | { 63 | return array_reduce($dependencies, 64 | function ($carry, $name) { 65 | $carry[$name] = $this->config->getVendorPath($name); 66 | return $carry; 67 | }, 68 | []); 69 | } 70 | 71 | private function addToMap($definition, $packageName):void 72 | { 73 | $this->map[$definition] = $packageName; 74 | } 75 | 76 | private function loadNamespaces(): array 77 | { 78 | if (file_exists($this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR . 'autoload_namespaces.php'))) { 79 | return require_once $this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR. 'autoload_namespaces.php'); 80 | } 81 | return []; 82 | } 83 | 84 | private function loadPsr(): array 85 | { 86 | if (file_exists($this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR . 'autoload_psr4.php'))) { 87 | return require_once $this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR . 'autoload_psr4.php'); 88 | } 89 | return []; 90 | } 91 | 92 | private function loadClassmap(): array 93 | { 94 | if (file_exists($this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR . 'autoload_classmap.php'))) { 95 | return require_once $this->config->getVendorPath('composer' . DIRECTORY_SEPARATOR . 'autoload_classmap.php'); 96 | } 97 | return []; 98 | } 99 | 100 | private function isPathMatched(string $path, string $pathPart): bool 101 | { 102 | return mb_strpos($this->normalizePath($path), $this->normalizePath($pathPart)) !== false; 103 | } 104 | 105 | private function normalizePath(string $path): string 106 | { 107 | $ds = DIRECTORY_SEPARATOR; 108 | $path = rtrim(strtr($path, '/\\', $ds . $ds), $ds); 109 | if (strpos($ds . $path, "{$ds}.") === false && strpos($path, "{$ds}{$ds}") === false) { 110 | return $path; 111 | } 112 | if (strpos($path, "{$ds}{$ds}") === 0 && $ds === '\\') { 113 | $parts = [$ds]; 114 | } else { 115 | $parts = []; 116 | } 117 | foreach (explode($ds, $path) as $part) { 118 | if ($part === '..' && !empty($parts) && end($parts) !== '..') { 119 | array_pop($parts); 120 | } elseif ($part === '.' || ($part === '' && !empty($parts))) { 121 | continue; 122 | } else { 123 | $parts[] = $part; 124 | } 125 | } 126 | $path = implode($ds, $parts); 127 | return $path === '' ? '.' : $path; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Lib/Runner.php: -------------------------------------------------------------------------------- 1 | configFile = $configFile; 38 | $this->silentMode = $silentMode; 39 | } 40 | 41 | public function run(): int 42 | { 43 | try { 44 | $config = $this->makeConfig(); 45 | $this->output(' - config prepared' . PHP_EOL); 46 | $map = $this->makeDependencyMap($config); 47 | $this->output(' - search patterns prepared' . PHP_EOL); 48 | } catch (Throwable $e) { 49 | echo 'Error! ' . $e->getMessage() . PHP_EOL; 50 | echo $e->getTraceAsString() . PHP_EOL; 51 | return $e instanceof InvalidConfigException ? self::CONFIG_ERROR_CODE : self::GENERAL_ERROR_CODE; 52 | } 53 | try { 54 | $scanner = (new Scanner($map, $config, new Finder(), [$this, 'onNextDirectory'], [$this, 'onProgress'])); 55 | $scanResult = $scanner->scan(); 56 | } catch (Throwable $e) { 57 | echo 'Error! ' . $e->getMessage() . PHP_EOL; 58 | echo $e->getTraceAsString() . PHP_EOL; 59 | return self::SCANNING_ERROR_CODE; 60 | } 61 | if ($config->getReportPath() !== null) { 62 | $this->storeReport($scanner->getUsageReport(), $config); 63 | } 64 | return $this->showScanReport($map, $scanResult); 65 | } 66 | 67 | public function onNextDirectory(string $directory): void 68 | { 69 | $this->output(PHP_EOL . ' - Scan ' . $directory . PHP_EOL); 70 | } 71 | 72 | public function onProgress(int $done, int $total): void 73 | { 74 | $width = 60; 75 | $percentage = round(($done * 100) / ($total <= 0 ? 1 : $total)); 76 | $bar = (int)round(($width * $percentage) / 100); 77 | $this->output(sprintf("%s%%[%s>%s]\r", $percentage, str_repeat("=", $bar), str_repeat(" ", $width - $bar))); 78 | } 79 | 80 | private function makeConfig(): Config 81 | { 82 | $params = require $this->configFile; 83 | return Config::create($params); 84 | } 85 | 86 | private function makeDependencyMap(Config $config): array 87 | { 88 | $dependencies = (new ComposerReader($config))->fetchDependencies(); 89 | return (new DependencyMapper($config, $dependencies))->build(); 90 | } 91 | 92 | private function output(string $message): void 93 | { 94 | if ($this->silentMode === false) { 95 | echo $message; 96 | } 97 | } 98 | 99 | private function showScanReport(array $map, array $scanResult): int 100 | { 101 | $result = array_values(array_diff(array_unique(array_values($map)), $scanResult)); 102 | if (empty($result)) { 103 | $this->output(PHP_EOL . 'No unused dependencies found!' . PHP_EOL); 104 | return self::SUCCESS_CODE; 105 | } 106 | 107 | $this->output(PHP_EOL . 'Unused dependencies found!' . PHP_EOL); 108 | array_walk($result, 109 | function($packageName) { 110 | $this->output(' -' . $packageName . PHP_EOL); 111 | }); 112 | return self::HAS_UNUSED_CODE; 113 | } 114 | 115 | private function storeReport(array $usageReport, Config $config): void 116 | { 117 | $formattedReport = $config->getReportFormatter() !== null 118 | ? (string)call_user_func($config->getReportFormatter(), $usageReport) 119 | : json_encode($usageReport, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); 120 | $reportFileName = sprintf( 121 | '%spackage_usage_report_%s%s', 122 | $config->getReportPath(), 123 | date('Y-m-d_H_i'), 124 | $config->getReportExtension() 125 | ); 126 | file_put_contents($reportFileName, $formattedReport); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/ScannerTest.php: -------------------------------------------------------------------------------- 1 | 0, 15 | 'insolita\Scanner\Lib\Config' => 1, 16 | 'insolita\Scanner\Lib\ComposerReader' => 2, 17 | 'Symfony\Component\Finder\Exception' => 3, 18 | 'Symfony\Component\Finder\Finder' => 4, 19 | 'Text_Template' => 5, 20 | 'PHPUnit\\Runner\\' => 6, 21 | 'PHP_Token_AMPERSAND' => 7, 22 | 'Exception' => 8, 23 | 'Composer\\Autoload\\' => 9, 24 | 'SebastianBergmann\\' => 10, 25 | 'PHPUnit\\Util\\' => 11, 26 | 'DeepCopy\\Filter\\' => 12, 27 | 'phpDocumentor\\Reflection\\' => 13, 28 | 'Webmozart\\Assert\\Tests\\' => 14, 29 | 'Webmozart\Assert\Assert' => 15, 30 | 'Prophecy\Exception' => 16, 31 | 'A2I\\GeoBundle\\' => 17, 32 | 'Bazinga\\GeocoderBundle\\' => 18, 33 | ]; 34 | 35 | public function testDetection() 36 | { 37 | $config = new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs/']); 38 | $scanner = new Scanner(self::$map, $config, new Finder(), function () { 39 | }, function () { 40 | }); 41 | $founds = $scanner->scan(); 42 | sort($founds); 43 | $this->assertEquals([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], $founds); 44 | } 45 | 46 | public function testScanAdditionalFiles() 47 | { 48 | $config = new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', []); 49 | $config->setScanFiles([__DIR__ . '/stubs/dummy.php']); 50 | $scanner = new Scanner(self::$map, $config, new Finder(), function () { 51 | }, function () { 52 | }); 53 | $founds = $scanner->scan(); 54 | sort($founds); 55 | $this->assertEquals([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], $founds); 56 | } 57 | 58 | public function testScanWithCustomMatch() 59 | { 60 | $map = array_merge(self::$map, ['Foo\Bar' => 17, 'Bar\Baz' => 18]); 61 | $config = new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs/']); 62 | $config->setCustomMatch(function ($definition, $packageName, SplFileInfo $file) { 63 | if ($packageName === 18) { 64 | return true; 65 | } 66 | 67 | if ($file->getExtension() === 'twig') { 68 | $definition = str_replace('\\', '/', $definition); 69 | if (mb_strpos($file->getContents(), $definition) !== false) { 70 | return true; 71 | } 72 | } 73 | return false; 74 | })->setExtensions(['*.php', '*.twig']); 75 | $scanner = new Scanner($map, $config, new Finder(), function () { 76 | }, function () { 77 | }); 78 | $founds = $scanner->scan(); 79 | sort($founds); 80 | $this->assertEquals([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], $founds); 81 | } 82 | 83 | public function testYmlScan() 84 | { 85 | $config = new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs/']); 86 | $config->setExtensions(['*.yml']); 87 | $fn = function () { 88 | }; 89 | $scanner = new Scanner(self::$map, $config, new Finder(), $fn, $fn); 90 | $founds = $scanner->scan(); 91 | sort($founds); 92 | $this->assertEquals([17], $founds); 93 | } 94 | 95 | public function testSymfonyCustomScan() 96 | { 97 | $config = (new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs/'])) 98 | ->setExtensions(['*.yml']) 99 | ->setCustomMatch(function ($definition, $packageName, SplFileInfo $file) { 100 | if ($file->getExtension() === 'yml') { 101 | $bundleDefinition = '@'.mb_strtolower( 102 | preg_replace('/\\\\/', '_', str_replace('Bundle', '', $definition),1) 103 | ); 104 | $bundleDefinition = rtrim($bundleDefinition, '\\'); 105 | if (mb_strpos($file->getContents(), $bundleDefinition) !== false) { 106 | return true; 107 | } 108 | } 109 | }); 110 | $fn = function () { 111 | }; 112 | $scanner = new Scanner(self::$map, $config, new Finder(), $fn, $fn); 113 | $founds = $scanner->scan(); 114 | sort($founds); 115 | $this->assertEquals([17, 18], $founds); 116 | } 117 | 118 | public function testScanGroupedNamespaces() 119 | { 120 | $config = new Config(__DIR__ . '/../composer.json', __DIR__ . '/../vendor', [__DIR__ . '/stubs2/']); 121 | $patterns = [ 122 | 'Symfony\Thanks\GitHubClient' => 0, 123 | 'Symfony\Thanks\Thanks' => 1, 124 | 'TheSeer\Tokenizer\NamespaceUri' => 2, 125 | 'TheSeer\Tokenizer\NamespaceUriException' => 3, 126 | 'Symfony\Component\Console\Input\ArrayInput' => 4, 127 | 'TheSeer\Tokenizer\GitHubClient' => 9, //Should be not matched 128 | ]; 129 | $scanner = new Scanner($patterns, $config, new Finder(), function () {}, function () {}); 130 | $founds = $scanner->scan(); 131 | sort($founds); 132 | $this->assertEquals([0, 1, 2, 3, 4], $founds); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Lib/Config.php: -------------------------------------------------------------------------------- 1 | composerJsonPath = $composerJsonPath; 37 | $this->vendorPath = realpath(rtrim($vendorPath, DIRECTORY_SEPARATOR)) . DIRECTORY_SEPARATOR; 38 | $this->scanDirectories = array_filter(array_map(static function ($path) { 39 | return realpath(rtrim($path, DIRECTORY_SEPARATOR). DIRECTORY_SEPARATOR); 40 | }, $scanDirectories)); 41 | } 42 | 43 | public function getComposerJsonPath(): string 44 | { 45 | return $this->composerJsonPath; 46 | } 47 | 48 | public function getVendorPath(string $append = ''): string 49 | { 50 | return $this->vendorPath . $append; 51 | } 52 | 53 | public function getScanDirectories(): array 54 | { 55 | return $this->scanDirectories ?? []; 56 | } 57 | 58 | public function getRequireDev(): bool 59 | { 60 | return $this->requireDev; 61 | } 62 | 63 | public function setRequireDev(bool $requireDev): Config 64 | { 65 | $this->requireDev = $requireDev; 66 | return $this; 67 | } 68 | 69 | /** 70 | * @return array 71 | */ 72 | public function getScanFiles(): array 73 | { 74 | return $this->scanFiles ?? []; 75 | } 76 | 77 | /** 78 | * @param array $scanFiles 79 | * 80 | * @return Config 81 | */ 82 | public function setScanFiles(array $scanFiles): Config 83 | { 84 | $this->scanFiles = $scanFiles; 85 | return $this; 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getExcludeDirectories(): array 92 | { 93 | return $this->excludeDirectories ?? []; 94 | } 95 | 96 | /** 97 | * @param array $excludeDirectories 98 | * 99 | * @return Config 100 | */ 101 | public function setExcludeDirectories(array $excludeDirectories): Config 102 | { 103 | $this->excludeDirectories = $excludeDirectories; 104 | return $this; 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | public function getExtensions(): array 111 | { 112 | return $this->extensions; 113 | } 114 | 115 | /** 116 | * @param array $extensions 117 | * 118 | * @return Config 119 | */ 120 | public function setExtensions(array $extensions): Config 121 | { 122 | $this->extensions = $extensions; 123 | return $this; 124 | } 125 | 126 | /** 127 | * @return null|callable 128 | */ 129 | public function getCustomMatch(): ?callable 130 | { 131 | return $this->customMatch; 132 | } 133 | 134 | /** 135 | * @param callable $customMatch 136 | * 137 | * @return Config 138 | */ 139 | public function setCustomMatch(callable $customMatch): Config 140 | { 141 | $this->customMatch = $customMatch; 142 | return $this; 143 | } 144 | 145 | /** 146 | * @param string $reportPath 147 | * 148 | * @return Config 149 | */ 150 | public function setReportPath(string $reportPath): Config 151 | { 152 | $this->reportPath = $reportPath; 153 | return $this; 154 | } 155 | 156 | public function getReportPath(): ?string 157 | { 158 | return $this->reportPath; 159 | } 160 | 161 | /** 162 | * @param callable $reportFormatter 163 | * 164 | * @return Config 165 | */ 166 | public function setReportFormatter(callable $reportFormatter): Config 167 | { 168 | $this->reportFormatter = $reportFormatter; 169 | return $this; 170 | } 171 | 172 | public function getReportFormatter(): ?callable 173 | { 174 | return $this->reportFormatter; 175 | } 176 | 177 | /** 178 | * @param string $reportExtension 179 | */ 180 | public function setReportExtension(string $reportExtension): void 181 | { 182 | $this->reportExtension = $reportExtension; 183 | } 184 | 185 | /** 186 | * @return string 187 | */ 188 | public function getReportExtension(): string 189 | { 190 | return $this->reportExtension; 191 | } 192 | 193 | /** 194 | * @return array 195 | */ 196 | public function getSkipPackages(): array 197 | { 198 | return $this->skipPackages; 199 | } 200 | 201 | /** 202 | * @param array $skipPackages 203 | */ 204 | public function setSkipPackages(array $skipPackages): void 205 | { 206 | $this->skipPackages = $skipPackages; 207 | } 208 | 209 | /** 210 | * @param array $data 211 | * 212 | * @return \insolita\Scanner\Lib\Config 213 | * @throws \insolita\Scanner\Exceptions\InvalidConfigException 214 | */ 215 | public static function create(array $data): Config 216 | { 217 | if (!isset($data['composerJsonPath'], $data['vendorPath'], $data['scanDirectories'])) { 218 | throw new InvalidConfigException('missing required keys'); 219 | } 220 | $config = new self($data['composerJsonPath'], $data['vendorPath'], $data['scanDirectories']); 221 | if (isset($data['requireDev'])) { 222 | $config->setRequireDev((bool)$data['requireDev']); 223 | } 224 | if (isset($data['scanFiles'])) { 225 | $config->setScanFiles((array)$data['scanFiles']); 226 | } 227 | if (isset($data['excludeDirectories'])) { 228 | $config->setExcludeDirectories($data['excludeDirectories']); 229 | } 230 | if (isset($data['extensions']) && !empty($data['extensions'])) { 231 | $config->setExtensions($data['extensions']); 232 | } 233 | if (isset($data['customMatch']) && is_callable($data['customMatch'])) { 234 | $config->setCustomMatch($data['customMatch']); 235 | } 236 | if (isset($data['reportPath']) && is_dir($data['reportPath'])) { 237 | $path = rtrim($data['reportPath'], DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; 238 | $config->setReportPath($path); 239 | } 240 | if (isset($data['reportFormatter']) && is_callable($data['reportFormatter'])) { 241 | $config->setReportFormatter($data['reportFormatter']); 242 | } 243 | if (isset($data['reportExtension']) && is_string($data['reportExtension'])) { 244 | $config->setReportExtension('.'.ltrim($data['reportExtension'], '.')); 245 | } 246 | if (isset($data['skipPackages']) && is_array($data['skipPackages'])) { 247 | $config->setSkipPackages($data['skipPackages']); 248 | } 249 | return $config; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Lib/Scanner.php: -------------------------------------------------------------------------------- 1 | searchPatterns = $searchPatterns; 66 | $this->config = $config; 67 | $this->finder = $finder; 68 | $this->onNextDirectory = $onNextDirectory; 69 | $this->onDirectoryProgress = $onDirectoryProgress; 70 | $this->reportMode = $config->getReportPath() !== null; 71 | } 72 | 73 | public function scan(): array 74 | { 75 | foreach ($this->config->getScanDirectories() as $directory) { 76 | if (is_dir($directory)) { 77 | call_user_func($this->onNextDirectory, $directory); 78 | $this->scanDirectory($directory); 79 | } 80 | if (empty($this->searchPatterns)) { 81 | break; 82 | } 83 | } 84 | $this->scanAdditionalFiles(); 85 | return array_unique($this->usageFounds); 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getUsageReport(): array 92 | { 93 | return $this->usageReport; 94 | } 95 | 96 | private function scanAdditionalFiles(): void 97 | { 98 | if (!empty($this->searchPatterns) && !empty($this->config->getScanFiles())) { 99 | call_user_func($this->onNextDirectory, ' additional files'); 100 | $total = count($this->config->getScanFiles()); 101 | foreach ($this->config->getScanFiles() as $iteration => $filename) { 102 | if (is_file($filename)) { 103 | $file = new SplFileInfo($filename, $filename, basename($filename)); 104 | $this->reportMode === false 105 | ? $this->checkUsage($file) 106 | : $this->collectUsage($file); 107 | } 108 | if (empty($this->searchPatterns)) { 109 | call_user_func($this->onDirectoryProgress, $total, $total, $filename); 110 | break; 111 | } 112 | 113 | call_user_func($this->onDirectoryProgress, $iteration + 1, $total, $filename); 114 | } 115 | } 116 | } 117 | 118 | private function scanDirectory(string $directory): void 119 | { 120 | $finder = clone $this->finder; 121 | $files = $finder->files()->in([$directory])->exclude($this->config->getExcludeDirectories()); 122 | foreach ($this->config->getExtensions() as $extension) { 123 | $files->name($extension); 124 | } 125 | $total = $files->count(); 126 | $iteration = 0; 127 | foreach ($files as $file) { 128 | /**@var SplFileInfo $file * */ 129 | $this->reportMode === false 130 | ? $this->checkUsage($file) 131 | : $this->collectUsage($file); 132 | 133 | if (empty($this->searchPatterns)) { 134 | call_user_func($this->onDirectoryProgress, $total, $total, $file->getRealPath()); 135 | break; 136 | } 137 | 138 | $iteration++; 139 | call_user_func($this->onDirectoryProgress, $iteration, $total, $file->getRealPath()); 140 | } 141 | } 142 | 143 | private function checkUsage(SplFileInfo $file): void 144 | { 145 | $usageFounds = []; 146 | $fileContent = $file->getContents(); 147 | foreach ($this->searchPatterns as $definition => $packageName) { 148 | if (in_array($packageName, $usageFounds, true)) { 149 | continue; 150 | } 151 | $isMatched = $this->matchDefinition($definition, $packageName, $fileContent, $file); 152 | if ($isMatched) { 153 | $usageFounds[] = $packageName; 154 | } 155 | } 156 | $this->registerFounds($usageFounds); 157 | } 158 | 159 | private function collectUsage(SplFileInfo $file): void 160 | { 161 | $usageFounds = []; 162 | $fileContent = $file->getContents(); 163 | foreach ($this->searchPatterns as $definition => $packageName) { 164 | $isMatched = $this->matchDefinition($definition, $packageName, $fileContent, $file); 165 | if ($isMatched) { 166 | $usageFounds[] = $packageName; 167 | if($this->reportMode === true){ 168 | $this->collectFounds($packageName, $definition, $file->getRealPath()); 169 | } 170 | } 171 | } 172 | $this->registerFounds($usageFounds); 173 | } 174 | 175 | private function collectFounds(string $packageName, string $definition, string $fileName): void 176 | { 177 | if (!isset($this->usageReport[$packageName])) { 178 | $this->usageReport[$packageName] = []; 179 | } 180 | if (!isset($this->usageReport[$packageName][$definition])) { 181 | $this->usageReport[$packageName][$definition] = []; 182 | } 183 | $this->usageReport[$packageName][$definition][] = $fileName; 184 | } 185 | 186 | private function registerFounds(array $usageFounds): void 187 | { 188 | $this->usageFounds = array_merge($this->usageFounds, $usageFounds); 189 | if ($this->reportMode !== true) { 190 | $this->searchPatterns = array_filter($this->searchPatterns, function ($packageName) use (&$usageFounds) { 191 | return !in_array($packageName, $usageFounds, true); 192 | }); 193 | } 194 | } 195 | 196 | /** 197 | * @param $definition 198 | * @param $packageName 199 | * @param string $fileContent 200 | * @param \Symfony\Component\Finder\SplFileInfo $file 201 | * @return bool|false|int|mixed 202 | */ 203 | private function matchDefinition($definition, $packageName, string $fileContent, SplFileInfo $file) 204 | { 205 | $preparedDefinition = str_replace('\\', '\\\\', $definition); 206 | $pattern = "/[\s\t\n\.\,<=>\'\"\[\(;\\\\]{$preparedDefinition}/"; 207 | $isMatched = $this->config->getCustomMatch() !== null 208 | ? call_user_func($this->config->getCustomMatch(), $definition, $packageName, $file) 209 | : false; 210 | $content = str_replace('\\\\', '\\', $fileContent); 211 | if (!$isMatched) { 212 | $isMatched = preg_match($pattern, $content); 213 | } 214 | if (!$isMatched) { 215 | $parts = array_filter(explode('\\', str_replace('\\\\', '\\', $definition))); 216 | $partsCount = count($parts); 217 | if($partsCount > 1 && strpos($content, $parts[0].'\\') !== false){ 218 | $i = 1; 219 | while ($i < $partsCount && !$isMatched){ 220 | $head = implode('\\\\', array_slice($parts, 0, $partsCount - $i)); 221 | $tail = implode('\\\\', array_slice($parts, $partsCount - $i)); 222 | $pattern = "~{$head}\\\{[^\{]*{$tail}[^\{]*\}~"; 223 | $isMatched = preg_match($pattern, $content); 224 | $i++; 225 | } 226 | } 227 | } 228 | return $isMatched; 229 | } 230 | } 231 | --------------------------------------------------------------------------------