├── .gitignore ├── .scrutinizer.yml ├── .travis.coverage.sh ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── composer.json ├── finalizer ├── phpunit.xml.dist ├── src └── Finalizer │ ├── Console │ └── CheckFinalClassesCommand.php │ ├── Constraint │ └── IsFinalizable.php │ ├── Iterator │ └── MapIterator.php │ ├── Reflection │ └── InheritanceClasses.php │ └── Scanner │ ├── DirectoryClassScanner.php │ └── DirectoryFileScanner.php └── test ├── FinalizerTest ├── Console │ └── CheckFinalClassesCommandTest.php ├── Constraint │ └── IsFinalizableTest.php ├── Iterator │ └── MapIteratorTest.php ├── Reflection │ └── InheritanceClassesTest.php └── Scanner │ ├── DirectoryClassScannerTest.php │ └── DirectoryFileScannerTest.php └── FinalizerTestAsset ├── BarMethodInterface.php ├── EmptyInterface.php ├── Finalizable ├── ClassWithConstructor.php ├── ClassWithNoMethods.php ├── ClassWithPrivateMethod.php ├── ClassWithProtectedMethod.php ├── EmptyChildClass.php ├── FooBarConstructorMethodClass.php ├── FooBarMethodClass.php ├── FooMethodClass.php ├── InvokableClass.php └── InvokableClassWithConstructor.php ├── FooMethodInterface.php ├── InvalidFinal └── ClassThatShouldNotBeFinal.php ├── NonFinalizable ├── AbstractEmptyClass.php ├── AbstractEmptyClassImplementingEmptyInterface.php ├── ClassWithNoMethods.php ├── EmptyParentClass.php ├── FooBarMethodClass.php └── InvokableClassWithAdditionalMethods.php └── Scanner └── DirectoryFileScanner ├── DirWithOneHhFile └── 1.hh ├── DirWithOnePhpFile └── 1.php ├── DirWithOneTxtFile └── 1.txt ├── DirWithTwoPhpFiles ├── 1.php └── 2.php └── EmptyDirectory └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | vendor/ 4 | clover.xml -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | before_commands: 2 | - "composer install --no-dev --prefer-source" 3 | 4 | tools: 5 | external_code_coverage: 6 | timeout: 600 7 | php_code_coverage: 8 | enabled: true 9 | php_code_sniffer: 10 | enabled: true 11 | config: 12 | standard: PSR2 13 | filter: 14 | paths: ["src/*", "test/*"] 15 | php_cpd: 16 | enabled: true 17 | excluded_dirs: ["test", "vendor"] 18 | php_cs_fixer: 19 | enabled: true 20 | config: 21 | level: all 22 | filter: 23 | paths: ["src/*", "test/*"] 24 | php_loc: 25 | enabled: true 26 | excluded_dirs: ["test", "vendor"] 27 | php_mess_detector: 28 | enabled: true 29 | filter: 30 | paths: ["src/*"] 31 | php_pdepend: 32 | enabled: true 33 | excluded_dirs: ["test", "vendor"] 34 | php_analyzer: 35 | enabled: true 36 | filter: 37 | paths: ["src/*", "test/*"] 38 | php_hhvm: 39 | enabled: true 40 | filter: 41 | paths: ["src/*", "test/*"] 42 | sensiolabs_security_checker: true 43 | -------------------------------------------------------------------------------- /.travis.coverage.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | if [ "$TRAVIS_PHP_VERSION" = 'hhvm' ] || [ "$TRAVIS_PHP_VERSION" = 'hhvm-nightly' ] ; then 3 | echo "skipping coverage submission for HHVM builds" 4 | else 5 | wget https://scrutinizer-ci.com/ocular.phar 6 | php ocular.phar code-coverage:upload --format=php-clover ./clover.xml 7 | fi 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - nightly 6 | 7 | before_script: 8 | - composer self-update 9 | - composer update 10 | 11 | script: 12 | - ./vendor/bin/phpunit --disallow-test-output --coverage-clover ./clover.xml 13 | - ./finalizer finalizer:check-final-classes src 14 | 15 | matrix: 16 | allow_failures: 17 | - php: nightly 18 | 19 | after_script: 20 | - sh .travis.coverage.sh 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * Coding standard for the project is [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) 4 | * The project will follow strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php) 5 | * Any contribution must provide tests for additional introduced conditions 6 | * Any un-confirmed issue needs a failing test case before being accepted 7 | * Pull requests must be sent from a new hotfix/feature branch, not from `master`. 8 | 9 | ## Installation 10 | 11 | To install the project and run the tests, you need to clone it first: 12 | 13 | ```sh 14 | $ git clone git://github.com/Ocramius/Finalizer.git 15 | ``` 16 | 17 | You will then need to run a composer installation: 18 | 19 | ```sh 20 | $ cd Finalizer 21 | $ curl -s https://getcomposer.org/installer | php 22 | $ php composer.phar update 23 | ``` 24 | 25 | ## Testing 26 | 27 | The PHPUnit version to be used is the one installed as a dev- dependency via composer: 28 | 29 | ```sh 30 | $ ./vendor/bin/phpunit 31 | ``` 32 | 33 | Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement 34 | won't be merged. 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finalizer 2 | 3 | This library aims at providing simple tools that help deciding whether 4 | a class should or shouldn't be declared as `final`. 5 | 6 | [![Build Status](https://travis-ci.org/Ocramius/Finalizer.svg?branch=master)](https://travis-ci.org/Ocramius/Finalizer) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Ocramius/Finalizer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Ocramius/Finalizer/?branch=master) 8 | [![Code Coverage](https://scrutinizer-ci.com/g/Ocramius/Finalizer/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ocramius/Finalizer/?branch=master) 9 | [![Latest Stable Version](https://poser.pugx.org/ocramius/finalizer/v/stable.png)](https://packagist.org/packages/ocramius/finalizer) 10 | [![Latest Unstable Version](https://poser.pugx.org/ocramius/finalizer/v/unstable.png)](https://packagist.org/packages/ocramius/finalizer) 11 | 12 | ## Help/Support 13 | 14 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Ocramius/Finalizer) 15 | 16 | ## Installation 17 | 18 | Install via [composer](https://getcomposer.org/): 19 | 20 | ```sh 21 | php composer.phar require ocramius/finalizer:~1.0 22 | ``` 23 | 24 | ## Usage 25 | 26 | In your console, simply type: 27 | 28 | ```php 29 | ./vendor/bin/finalizer finalizer:check-final-classes path/to/directory 30 | ./vendor/bin/finalizer finalizer:check-final-classes also/supports multiple/directories as/parameters 31 | ``` 32 | 33 | Note that finalizer will take decisions on whether classes should or 34 | shouldn't be `final` depending on the classes defined in the directories 35 | that you passed to it. 36 | 37 | Additionally, be aware that `finalizer` will (in its current state) require 38 | any of the PHP or Hack files in the given directories and include them via 39 | `require_once`. 40 | 41 | ## Reference 42 | 43 | If you need to know more about why I wrote this library, and what kind of 44 | decisions it is doing, then please read 45 | [this blogpost about the usage of the `final` keyword](http://goo.gl/4eCCIK). 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocramius/finalizer", 3 | "description": "A simple utility library that checks for PHP classes that should or shouldn't be marked as final", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/Ocramius/Finalizer", 7 | "keywords": [ 8 | "final", 9 | "syntax", 10 | "code quality" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Marco Pivetta", 15 | "email": "ocramius@gmail.com", 16 | "homepage": "http://ocramius.github.io/" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-4": { 21 | "Finalizer\\": "src/Finalizer" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "FinalizerTest\\": "test/FinalizerTest/", 27 | "FinalizerTestAsset\\": "test/FinalizerTestAsset/" 28 | } 29 | }, 30 | "require": { 31 | "php": "~7.2", 32 | "symfony/console": "~2.6|~3.0|~4.0" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "~7.0" 36 | }, 37 | "bin": [ 38 | "finalizer" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /finalizer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new CheckFinalClassesCommand()); 28 | 29 | $cli->run(); 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | ./test/FinalizerTest 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Finalizer/Console/CheckFinalClassesCommand.php: -------------------------------------------------------------------------------- 1 | setDescription( 26 | 'Checks the given paths for classes that should or shouldn\'t be final' 27 | ) 28 | ->setDefinition([new InputArgument( 29 | 'directories', 30 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 31 | 'Paths to be checked for existing classes' 32 | )]); 33 | $this->setHelp(<<%command.name% command generates a short report 35 | of classes that should be final and classes that shouldn't be final. 36 | 37 | You can use this command as following: 38 | %command.name% path/to/sources 39 | %command.name% path/to/first/sources/dir path/to/second/sources/dir 40 | EOT 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function execute(InputInterface $input, OutputInterface $output) : int 48 | { 49 | $classScanner = new DirectoryClassScanner(); 50 | $fileScanner = new DirectoryFileScanner(); 51 | $classes = $classScanner($fileScanner($input->getArgument('directories'))); 52 | 53 | $finalizeStatusCode = $this->renderClassesToFinalize($classes, $output); 54 | $deFinalizeStatusCode = $this->renderClassesToDeFinalize($classes, $output); 55 | 56 | return max($finalizeStatusCode, $deFinalizeStatusCode); 57 | } 58 | 59 | /** 60 | * @param \ReflectionClass[] $classes 61 | */ 62 | private function renderClassesToFinalize(array $classes, OutputInterface $output) : int 63 | { 64 | $tableHelper = new Table($output); 65 | 66 | $toFinalize = $this->getClassesToFinalize($classes); 67 | if (count($toFinalize) > 0) { 68 | $output->writeln('Following classes need to be made final:'); 69 | 70 | $tableHelper->addRows(array_map( 71 | function (\ReflectionClass $class) { 72 | return [$class->getName()]; 73 | }, 74 | $toFinalize 75 | )); 76 | 77 | $tableHelper->render(); 78 | 79 | return 1; 80 | } 81 | 82 | return 0; 83 | } 84 | 85 | /** 86 | * @param \ReflectionClass[] $classes 87 | */ 88 | private function renderClassesToDeFinalize(array $classes, OutputInterface $output) : int 89 | { 90 | $tableHelper = new Table($output); 91 | 92 | $toDeFinalize = $this->getClassesToDeFinalize($classes); 93 | if (count($toDeFinalize) > 0) { 94 | $output->writeln('Following classes are final and need to be made extensible again:'); 95 | 96 | $tableHelper->addRows(array_map( 97 | function (\ReflectionClass $class) { 98 | return [$class->getName()]; 99 | }, 100 | $toDeFinalize 101 | )); 102 | 103 | $tableHelper->render(); 104 | 105 | return 1; 106 | } 107 | 108 | return 0; 109 | } 110 | 111 | /** 112 | * @param \ReflectionClass[] $classes 113 | * 114 | * @return \ReflectionClass[] 115 | */ 116 | private function getClassesToFinalize(array $classes) 117 | { 118 | $isFinalizable = new IsFinalizable(); 119 | $toFinalize = []; 120 | 121 | foreach ($classes as $class) { 122 | if ($isFinalizable($class, ...$classes) && ! $class->isFinal()) { 123 | $toFinalize[] = $class; 124 | } 125 | } 126 | 127 | return $toFinalize; 128 | } 129 | 130 | /** 131 | * @param \ReflectionClass[] $classes 132 | * 133 | * @return \ReflectionClass[] 134 | */ 135 | private function getClassesToDeFinalize(array $classes) 136 | { 137 | $isFinalizable = new IsFinalizable(); 138 | $toDeFinalize = []; 139 | 140 | foreach ($classes as $class) { 141 | if ((! $isFinalizable($class, ...$classes)) && $class->isFinal()) { 142 | $toDeFinalize[] = $class; 143 | } 144 | } 145 | 146 | return $toDeFinalize; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Finalizer/Constraint/IsFinalizable.php: -------------------------------------------------------------------------------- 1 | isAbstract() 16 | && ! $this->hasChildClasses($class, $definedClasses) 17 | && ( 18 | ($class->getInterfaces() && $this->implementsOnlyInterfaceMethods($class)) 19 | || $this->isOnlyInvokable($class) 20 | ); 21 | } 22 | 23 | /** 24 | * Checks whether a given class is an invokable, and whether no other methods are implemented on it 25 | * 26 | * @param \ReflectionClass $class 27 | * 28 | * @return bool 29 | */ 30 | private function isOnlyInvokable(\ReflectionClass $class) 31 | { 32 | return ['__invoke'] === array_values(array_map('strtolower', $this->getNonConstructorMethodNames($class))) 33 | && ! $class->getMethod('__invoke')->isStatic(); 34 | } 35 | 36 | /** 37 | * @param \ReflectionClass $class 38 | * @param \ReflectionClass[] $definedClasses 39 | * 40 | * @return bool 41 | */ 42 | private function hasChildClasses(\ReflectionClass $class, array $definedClasses) 43 | { 44 | return array_filter( 45 | $definedClasses, 46 | function (\ReflectionClass $childClassCandidate) use ($class) { 47 | $parentClass = $childClassCandidate->getParentClass(); 48 | 49 | if (! $parentClass) { 50 | return false; 51 | } 52 | 53 | return $parentClass->getName() === $class->getName(); 54 | } 55 | ); 56 | } 57 | 58 | /** 59 | * Checks whether all methods implemented by $class are defined in 60 | * interfaces implemented by $class 61 | * 62 | * @param \ReflectionClass $class 63 | * 64 | * @return bool 65 | */ 66 | private function implementsOnlyInterfaceMethods(\ReflectionClass $class) 67 | { 68 | return ! array_diff( 69 | $this->getNonConstructorMethodNames($class), 70 | array_merge( 71 | [], 72 | [], 73 | ...array_values(array_map( 74 | [$this, 'getNonConstructorMethodNames'], 75 | $class->getInterfaces() 76 | )) 77 | ) 78 | ); 79 | } 80 | 81 | /** 82 | * @param \ReflectionClass $class 83 | * 84 | * @return string[] (indexed numerically) 85 | */ 86 | private function getNonConstructorMethodNames(\ReflectionClass $class) 87 | { 88 | return array_values(array_map( 89 | function (\ReflectionMethod $method) { 90 | return $method->getName(); 91 | }, 92 | array_filter( 93 | $class->getMethods(), 94 | function (\ReflectionMethod $method) { 95 | return $method->isPublic() && ! $method->isConstructor(); 96 | } 97 | ) 98 | )); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Finalizer/Iterator/MapIterator.php: -------------------------------------------------------------------------------- 1 | wrappedIterator = $wrappedIterator; 24 | $this->map = $map; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function current() 31 | { 32 | $map = $this->map; 33 | 34 | return $map($this->wrappedIterator->current()); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function next() 41 | { 42 | $this->wrappedIterator->next(); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function key() 49 | { 50 | return $this->wrappedIterator->key(); 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function valid() 57 | { 58 | return $this->wrappedIterator->valid(); 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | public function rewind() 65 | { 66 | $this->wrappedIterator->rewind(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Finalizer/Reflection/InheritanceClasses.php: -------------------------------------------------------------------------------- 1 | getParentClass()) { 15 | return array_merge( 16 | [$class->getName() => $class], 17 | $this->__invoke($parentClass) 18 | ); 19 | } 20 | 21 | return [$class->getName() => $class]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Finalizer/Scanner/DirectoryClassScanner.php: -------------------------------------------------------------------------------- 1 | getClassesDeclaredInFiles($this->includeFiles($files), get_declared_classes()); 15 | } 16 | 17 | /** 18 | * @param string[]|\Traversable $files 19 | * 20 | * @return bool[] the files that were loaded, indexed by file name 21 | */ 22 | private function includeFiles($files) 23 | { 24 | $loadedFiles = []; 25 | 26 | foreach ($files as $file) { 27 | require_once $file; 28 | 29 | $loadedFiles[realpath($file)] = true; 30 | } 31 | 32 | return $loadedFiles; 33 | } 34 | 35 | /** 36 | * @param string[] $files 37 | * @param string[] $classes 38 | * 39 | * @return \ReflectionClass[] 40 | */ 41 | private function getClassesDeclaredInFiles(array $files, array $classes) 42 | { 43 | return array_filter( 44 | array_map( 45 | function ($className) { 46 | return new \ReflectionClass($className); 47 | }, 48 | $classes 49 | ), 50 | function (\ReflectionClass $class) use ($files) { 51 | return isset($files[realpath($class->getFileName())]); 52 | } 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Finalizer/Scanner/DirectoryFileScanner.php: -------------------------------------------------------------------------------- 1 | run(new ArrayInput(['directories' => $paths]), $output); 28 | 29 | $this->assertCount(count(explode("\n", $expectedOutput)), explode(PHP_EOL, $output->fetch())); 30 | $this->assertSame($expectedExitCode, $exitCode); 31 | } 32 | 33 | /** 34 | * Data provider 35 | * 36 | * @return mixed[][] 37 | */ 38 | public function pathsProvider() : array 39 | { 40 | return [ 41 | [ 42 | [__DIR__ . '/../../../src'], 43 | '', 44 | 0, 45 | ], 46 | [ 47 | [__DIR__ . '/../../FinalizerTestAsset'], 48 | <<<'OUTPUT' 49 | Following classes need to be made final: 50 | +--------------------------------------------------------------+ 51 | | FinalizerTestAsset\Finalizable\ClassWithConstructor | 52 | | FinalizerTestAsset\Finalizable\ClassWithNoMethods | 53 | | FinalizerTestAsset\Finalizable\ClassWithPrivateMethod | 54 | | FinalizerTestAsset\Finalizable\ClassWithProtectedMethod | 55 | | FinalizerTestAsset\Finalizable\EmptyChildClass | 56 | | FinalizerTestAsset\Finalizable\FooBarConstructorMethodClass | 57 | | FinalizerTestAsset\Finalizable\FooBarMethodClass | 58 | | FinalizerTestAsset\Finalizable\FooMethodClass | 59 | | FinalizerTestAsset\Finalizable\InvokableClass | 60 | | FinalizerTestAsset\Finalizable\InvokableClassWithConstructor | 61 | +--------------------------------------------------------------+ 62 | Following classes are final and need to be made extensible again: 63 | +-----------------------------------------------------------+ 64 | | FinalizerTestAsset\InvalidFinal\ClassThatShouldNotBeFinal | 65 | +-----------------------------------------------------------+ 66 | 67 | OUTPUT 68 | , 69 | 1 70 | ], 71 | [ 72 | [__DIR__ . '/../../../src', __DIR__ . '/../../FinalizerTestAsset'], 73 | <<<'OUTPUT' 74 | Following classes need to be made final: 75 | +--------------------------------------------------------------+ 76 | | FinalizerTestAsset\Finalizable\ClassWithConstructor | 77 | | FinalizerTestAsset\Finalizable\ClassWithNoMethods | 78 | | FinalizerTestAsset\Finalizable\ClassWithPrivateMethod | 79 | | FinalizerTestAsset\Finalizable\ClassWithProtectedMethod | 80 | | FinalizerTestAsset\Finalizable\EmptyChildClass | 81 | | FinalizerTestAsset\Finalizable\FooBarConstructorMethodClass | 82 | | FinalizerTestAsset\Finalizable\FooBarMethodClass | 83 | | FinalizerTestAsset\Finalizable\FooMethodClass | 84 | | FinalizerTestAsset\Finalizable\InvokableClass | 85 | | FinalizerTestAsset\Finalizable\InvokableClassWithConstructor | 86 | +--------------------------------------------------------------+ 87 | Following classes are final and need to be made extensible again: 88 | +-----------------------------------------------------------+ 89 | | FinalizerTestAsset\InvalidFinal\ClassThatShouldNotBeFinal | 90 | +-----------------------------------------------------------+ 91 | 92 | OUTPUT 93 | , 94 | 1 95 | ], 96 | [ 97 | [ 98 | __DIR__ . '/../../FinalizerTestAsset/Finalizable', 99 | __DIR__ . '/../../FinalizerTestAsset/InvalidFinal', 100 | ], 101 | <<<'OUTPUT' 102 | Following classes need to be made final: 103 | +--------------------------------------------------------------+ 104 | | FinalizerTestAsset\Finalizable\ClassWithConstructor | 105 | | FinalizerTestAsset\Finalizable\ClassWithNoMethods | 106 | | FinalizerTestAsset\Finalizable\ClassWithPrivateMethod | 107 | | FinalizerTestAsset\Finalizable\ClassWithProtectedMethod | 108 | | FinalizerTestAsset\Finalizable\EmptyChildClass | 109 | | FinalizerTestAsset\Finalizable\FooBarConstructorMethodClass | 110 | | FinalizerTestAsset\Finalizable\FooBarMethodClass | 111 | | FinalizerTestAsset\Finalizable\FooMethodClass | 112 | | FinalizerTestAsset\Finalizable\InvokableClass | 113 | | FinalizerTestAsset\Finalizable\InvokableClassWithConstructor | 114 | +--------------------------------------------------------------+ 115 | Following classes are final and need to be made extensible again: 116 | +-----------------------------------------------------------+ 117 | | FinalizerTestAsset\InvalidFinal\ClassThatShouldNotBeFinal | 118 | +-----------------------------------------------------------+ 119 | 120 | OUTPUT 121 | , 122 | 1 123 | ], 124 | ]; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /test/FinalizerTest/Constraint/IsFinalizableTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 26 | $finalizable, 27 | (new IsFinalizable()) 28 | ->__invoke( 29 | new \ReflectionClass($className), 30 | ...array_map( 31 | function ($className) { 32 | return new \ReflectionClass($className); 33 | }, 34 | $definedClasses 35 | ) 36 | ) 37 | ); 38 | } 39 | 40 | /** 41 | * @return \ReflectionClass[][]|\ReflectionClass[][][] 42 | */ 43 | public function finalizableClassesProvider() 44 | { 45 | return [ 46 | NonFinalizable\EmptyParentClass::class => [ 47 | NonFinalizable\EmptyParentClass::class, 48 | [ 49 | Finalizable\EmptyChildClass::class, 50 | ], 51 | ], 52 | Finalizable\EmptyChildClass::class => [ 53 | Finalizable\EmptyChildClass::class, 54 | [], 55 | ], 56 | NonFinalizable\ClassWithNoMethods::class => [ 57 | NonFinalizable\ClassWithNoMethods::class, 58 | [], 59 | ], 60 | Finalizable\ClassWithNoMethods::class => [ 61 | Finalizable\ClassWithNoMethods::class, 62 | [], 63 | ], 64 | Finalizable\FooMethodClass::class => [ 65 | Finalizable\FooMethodClass::class, 66 | [], 67 | ], 68 | Finalizable\FooBarMethodClass::class => [ 69 | Finalizable\FooBarMethodClass::class, 70 | [], 71 | ], 72 | NonFinalizable\FooBarMethodClass::class => [ 73 | NonFinalizable\FooBarMethodClass::class, 74 | [], 75 | ], 76 | Finalizable\InvokableClass::class => [ 77 | Finalizable\InvokableClass::class, 78 | [], 79 | ], 80 | NonFinalizable\InvokableClassWithAdditionalMethods::class => [ 81 | NonFinalizable\InvokableClassWithAdditionalMethods::class, 82 | [], 83 | ], 84 | Finalizable\InvokableClassWithConstructor::class => [ 85 | Finalizable\InvokableClassWithConstructor::class, 86 | [], 87 | ], 88 | Finalizable\ClassWithConstructor::class => [ 89 | Finalizable\ClassWithConstructor::class, 90 | [], 91 | ], 92 | Finalizable\FooBarConstructorMethodClass::class => [ 93 | Finalizable\FooBarConstructorMethodClass::class, 94 | [], 95 | ], 96 | Finalizable\ClassWithPrivateMethod::class => [ 97 | Finalizable\ClassWithPrivateMethod::class, 98 | [], 99 | ], 100 | Finalizable\ClassWithProtectedMethod::class => [ 101 | Finalizable\ClassWithProtectedMethod::class, 102 | [], 103 | ], 104 | ]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/FinalizerTest/Iterator/MapIteratorTest.php: -------------------------------------------------------------------------------- 1 | createPartialMock(\stdClass::class, ['__invoke']); 17 | /* @var $wrappedIterator \Iterator|\PHPUnit_Framework_MockObject_MockObject */ 18 | $wrappedIterator = $this->createMock(\Iterator::class); 19 | 20 | $wrappedIterator->expects($this->at(0))->method('current')->will($this->returnValue('foo')); 21 | $wrappedIterator->expects($this->at(1))->method('current')->will($this->returnValue('bar')); 22 | 23 | $map->expects($this->at(0))->method('__invoke')->with('foo')->will($this->returnValue('baz')); 24 | $map->expects($this->at(1))->method('__invoke')->with('bar')->will($this->returnValue('tab')); 25 | 26 | $iterator = new MapIterator($wrappedIterator, $map); 27 | 28 | $this->assertSame('baz', $iterator->current()); 29 | $this->assertSame('tab', $iterator->current()); 30 | } 31 | 32 | public function testNext() 33 | { 34 | /* @var $map callable|\PHPUnit_Framework_MockObject_MockObject */ 35 | $map = $this->createPartialMock(\stdClass::class, ['__invoke']); 36 | /* @var $wrappedIterator \Iterator|\PHPUnit_Framework_MockObject_MockObject */ 37 | $wrappedIterator = $this->createMock(\Iterator::class); 38 | 39 | $wrappedIterator->expects($this->exactly(2))->method('next'); 40 | 41 | $map->expects($this->never())->method('__invoke'); 42 | 43 | $iterator = new MapIterator($wrappedIterator, $map); 44 | 45 | $iterator->next(); 46 | $iterator->next(); 47 | } 48 | 49 | public function testRewind() 50 | { 51 | /* @var $map callable|\PHPUnit_Framework_MockObject_MockObject */ 52 | $map = $this->createPartialMock(\stdClass::class, ['__invoke']); 53 | /* @var $wrappedIterator \Iterator|\PHPUnit_Framework_MockObject_MockObject */ 54 | $wrappedIterator = $this->createMock(\Iterator::class); 55 | 56 | $wrappedIterator->expects($this->exactly(2))->method('rewind'); 57 | 58 | $map->expects($this->never())->method('__invoke'); 59 | 60 | $iterator = new MapIterator($wrappedIterator, $map); 61 | 62 | $iterator->rewind(); 63 | $iterator->rewind(); 64 | } 65 | 66 | public function testKey() 67 | { 68 | /* @var $map callable|\PHPUnit_Framework_MockObject_MockObject */ 69 | $map = $this->createPartialMock(\stdClass::class, ['__invoke']); 70 | /* @var $wrappedIterator \Iterator|\PHPUnit_Framework_MockObject_MockObject */ 71 | $wrappedIterator = $this->createMock(\Iterator::class); 72 | 73 | $wrappedIterator->expects($this->at(0))->method('key')->will($this->returnValue('foo')); 74 | $wrappedIterator->expects($this->at(1))->method('key')->will($this->returnValue('bar')); 75 | 76 | $map->expects($this->never())->method('__invoke'); 77 | 78 | $iterator = new MapIterator($wrappedIterator, $map); 79 | 80 | $this->assertSame('foo', $iterator->key()); 81 | $this->assertSame('bar', $iterator->key()); 82 | } 83 | 84 | public function testValid() 85 | { 86 | /* @var $map callable|\PHPUnit_Framework_MockObject_MockObject */ 87 | $map = $this->createPartialMock(\stdClass::class, ['__invoke']); 88 | /* @var $wrappedIterator \Iterator|\PHPUnit_Framework_MockObject_MockObject */ 89 | $wrappedIterator = $this->createMock(\Iterator::class); 90 | 91 | $wrappedIterator->expects($this->at(0))->method('valid')->will($this->returnValue(true)); 92 | $wrappedIterator->expects($this->at(1))->method('valid')->will($this->returnValue(false)); 93 | 94 | $map->expects($this->never())->method('__invoke'); 95 | 96 | $iterator = new MapIterator($wrappedIterator, $map); 97 | 98 | $this->assertTrue($iterator->valid()); 99 | $this->assertFalse($iterator->valid()); 100 | } 101 | 102 | /** 103 | * @coversNothing 104 | */ 105 | public function testIterationWithActualArrayIterator() 106 | { 107 | $this->assertEquals( 108 | [2, 4, 6], 109 | iterator_to_array(new MapIterator( 110 | new \ArrayIterator([1, 2, 3]), 111 | function ($value) { 112 | return $value * 2; 113 | } 114 | )) 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/FinalizerTest/Reflection/InheritanceClassesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 32 | $indexedClasses, 33 | (new InheritanceClasses())->__invoke(new \ReflectionClass($className)) 34 | ); 35 | } 36 | 37 | /** 38 | * @return string[][]|string[][][] 39 | */ 40 | public function classesProvider() 41 | { 42 | return [ 43 | 'class with no parent class' => [ 44 | \Exception::class, 45 | [ 46 | \Exception::class, 47 | ], 48 | false, 49 | ], 50 | 'class with one parent class' => [ 51 | \LogicException::class, 52 | [ 53 | \LogicException::class, 54 | \Exception::class, 55 | ], 56 | false, 57 | ], 58 | 'class with multiple parent classes' => [ 59 | \InvalidArgumentException::class, 60 | [ 61 | \InvalidArgumentException::class, 62 | \LogicException::class, 63 | \Exception::class, 64 | ], 65 | false, 66 | ], 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/FinalizerTest/Scanner/DirectoryClassScannerTest.php: -------------------------------------------------------------------------------- 1 | getName(); 24 | }, 25 | (new DirectoryClassScanner())->__invoke($paths) 26 | )); 27 | 28 | sort($found); 29 | 30 | $this->assertEquals($expectedClasses, $found); 31 | } 32 | 33 | /** 34 | * @dataProvider pathsProvider 35 | * 36 | * @param string[]|array $paths 37 | * @param string[] $expectedClasses 38 | */ 39 | public function testDirectoryScannerWithIterator(array $paths, $expectedClasses) 40 | { 41 | $this->testDirectoryScannerWithPaths(new \ArrayIterator($paths), $expectedClasses); 42 | } 43 | 44 | /** 45 | * @return string[][][] 46 | */ 47 | public function pathsProvider() 48 | { 49 | return [ 50 | [ 51 | [__FILE__], 52 | [__CLASS__], 53 | ], 54 | [ 55 | [__FILE__, __DIR__ . '/DirectoryFileScannerTest.php'], 56 | [__CLASS__, DirectoryFileScannerTest::class], 57 | ], 58 | [ 59 | [ 60 | __FILE__, 61 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOnePhpFile/1.php' 62 | ], 63 | [__CLASS__], 64 | ], 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/FinalizerTest/Scanner/DirectoryFileScannerTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Traversable::class, (new DirectoryFileScanner())->__invoke($paths)); 22 | 23 | foreach ((new DirectoryFileScanner())->__invoke($paths) as $path) { 24 | $this->assertInternalType('string', $path); 25 | } 26 | } 27 | 28 | /** 29 | * @dataProvider pathsProvider 30 | * 31 | * @param string[] $paths 32 | */ 33 | public function testDiscoversCorrectAmountOfFiles(array $paths, $count) 34 | { 35 | $this->assertCount($count, iterator_to_array((new DirectoryFileScanner())->__invoke($paths))); 36 | } 37 | 38 | /** 39 | * Data provider 40 | * 41 | * @return string[][][]|int[][] 42 | */ 43 | public function pathsProvider() 44 | { 45 | return [ 46 | [ 47 | [__DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner'], 48 | 4 49 | ], 50 | [ 51 | [__DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/EmptyDirectory'], 52 | 0 53 | ], 54 | [ 55 | [__DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOnePhpFile'], 56 | 1 57 | ], 58 | [ 59 | [__DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithTwoPhpFiles'], 60 | 2 61 | ], 62 | [ 63 | [__DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOneHhFile'], 64 | 1 65 | ], 66 | [ 67 | [ 68 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOnePhpFile', 69 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOneHhFile', 70 | ], 71 | 2 72 | ], 73 | [ 74 | [ 75 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOnePhpFile', 76 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithOneHhFile', 77 | __DIR__ . '/../../FinalizerTestAsset/Scanner/DirectoryFileScanner/DirWithTwoPhpFiles', 78 | ], 79 | 4 80 | ], 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/FinalizerTestAsset/BarMethodInterface.php: -------------------------------------------------------------------------------- 1 |