├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.properties ├── build.xml ├── composer.json ├── composer.lock ├── docs ├── bounded_contexts.rst └── extract_method.png ├── features ├── bootstrap │ └── FeatureContext.php ├── convert_local_to_instance_variable.feature ├── extract_method.feature ├── fix_class_names.feature ├── optimize_use.feature └── rename_local_variable.feature ├── phpunit.xml └── src ├── bin ├── compile └── refactor ├── config ├── autocomplete.bash └── behat.yml ├── main └── QafooLabs │ ├── Collections │ ├── Hashable.php │ └── Set.php │ └── Refactoring │ ├── Adapters │ ├── PHPParser │ │ ├── ParserPhpNameScanner.php │ │ ├── ParserVariableScanner.php │ │ └── Visitor │ │ │ ├── LineRangeStatementCollector.php │ │ │ ├── LocalVariableClassifier.php │ │ │ ├── NodeConnector.php │ │ │ └── PhpNameCollector.php │ ├── PatchBuilder │ │ ├── ApplyPatchCommand.php │ │ ├── PatchBuffer.php │ │ ├── PatchBuilder.php │ │ └── PatchEditor.php │ ├── Symfony │ │ ├── CliApplication.php │ │ ├── Commands │ │ │ ├── ConvertLocalToInstanceVariableCommand.php │ │ │ ├── ExtractMethodCommand.php │ │ │ ├── FixClassNamesCommand.php │ │ │ ├── OptimizeUseCommand.php │ │ │ └── RenameLocalVariableCommand.php │ │ ├── Compiler.php │ │ └── OutputPatchCommand.php │ └── TokenReflection │ │ └── StaticCodeAnalysis.php │ ├── Application │ ├── ConvertLocalToInstanceVariable.php │ ├── ExtractMethod.php │ ├── FixClassNames.php │ ├── OptimizeUse.php │ ├── RenameLocalVariable.php │ └── SingleFileRefactoring.php │ ├── Domain │ ├── Model │ │ ├── DefinedVariables.php │ │ ├── Directory.php │ │ ├── EditingAction.php │ │ ├── EditingAction │ │ │ ├── AddMethod.php │ │ │ ├── AddProperty.php │ │ │ ├── LocalVariableToInstance.php │ │ │ ├── RenameVariable.php │ │ │ └── ReplaceWithMethodCall.php │ │ ├── EditingSession.php │ │ ├── EditorBuffer.php │ │ ├── File.php │ │ ├── IndentationDetector.php │ │ ├── IndentingLineCollection.php │ │ ├── Line.php │ │ ├── LineCollection.php │ │ ├── LineRange.php │ │ ├── MethodSignature.php │ │ ├── PhpClass.php │ │ ├── PhpName.php │ │ ├── PhpNameChange.php │ │ ├── PhpNameOccurance.php │ │ ├── PhpNames │ │ │ └── NoImportedUsagesFilter.php │ │ ├── RefactoringException.php │ │ ├── UseStatement.php │ │ └── Variable.php │ └── Services │ │ ├── CodeAnalysis.php │ │ ├── Editor.php │ │ ├── PhpNameScanner.php │ │ └── VariableScanner.php │ ├── Utils │ ├── CallbackFilterIterator.php │ ├── CallbackTransformIterator.php │ ├── ToStringIterator.php │ ├── TransformIterator.php │ └── ValueObject.php │ └── Version.php └── test └── QafooLabs ├── Collections └── SetTest.php └── Refactoring ├── Adapters ├── PHPParser │ ├── ParserPhpNameScannerTest.php │ └── Visitor │ │ ├── LineRangeStatementCollectorTest.php │ │ └── LocalVariableClassifierTest.php ├── PatchBuilder │ └── PatchBuilderTest.php └── TokenReflection │ └── StaticCodeAnalysisTest.php ├── Application ├── RenameLocalVariableTest.php └── Service │ └── ExtractMethodTest.php ├── Domain └── Model │ ├── DefinedVariablesTest.php │ ├── DirectoryTest.php │ ├── EditingAction │ ├── AddMethodTest.php │ ├── AddPropertyTest.php │ ├── LocalVariableToInstanceTest.php │ ├── RenameVariableTest.php │ └── ReplaceWithMethodCallTest.php │ ├── EditingSessionTest.php │ ├── FileTest.php │ ├── IndentationDetectorTest.php │ ├── IndentingLineCollectionTest.php │ ├── LineCollectionTest.php │ ├── LineRangeTest.php │ ├── LineTest.php │ ├── MethodSignatureTest.php │ ├── PhpNameTest.php │ ├── PhpNames │ └── NoImportedUsagesFilterTest.php │ ├── UseStatementTest.php │ └── VariableTest.php └── Utils ├── CallbackFilterIteratorTest.php ├── ToStringIteratorTest.php └── TransformIteratorTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | refactor.phar 3 | .abc 4 | .pear 5 | build/ 6 | composer.phar 7 | 8 | /php-refactoring-browser.sublime-project 9 | 10 | /php-refactoring-browser.sublime-workspace 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "setup"] 2 | path = setup 3 | url = git://github.com/Qafoo/build-commons.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | before_script: 3 | - composer self-update 4 | - composer install --dev --prefer-dist 5 | script: php vendor/bin/phpunit; php vendor/bin/behat --format progress 6 | php: 7 | - 5.4 8 | - 5.5 9 | - 5.6 10 | - 7.0 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - php: hhvm 16 | - php: 7 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.4 2 | 3 | - Fix paths of fix-class-names generated patches not generated from working 4 | directory, preventing a direct pipe to |patch -p1 (GH-30) 5 | 6 | # 0.0.3 7 | 8 | - Fixed support for `fix-class-names` command. This includes 9 | various fixes that are aggreated under the GH-28, GH-29, GH-19 10 | and GH-20. The command is now much more robust and does 11 | not create false/positives anymore. 12 | 13 | - Added `optimize-use ` command that will convert all 14 | relative or absolute usages of namespaces into use statements, 15 | leaving only the last part at the occuring position. 16 | (by @pscheit) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Qafoo GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Refactoring Browser 2 | 3 | Note: This software is under development and in alpha state. Refactorings 4 | do not contain all necessary pre-conditions and might mess up your code. 5 | Check the diffs carefully before applying the patches. 6 | 7 | [![Build Status](https://travis-ci.org/QafooLabs/php-refactoring-browser.png)](https://travis-ci.org/QafooLabs/php-refactoring-browser) 8 | 9 | Automatic Refactorings for PHP Code by generating diffs that describe 10 | the refactorings steps. To prevent simple mistakes during refactorings, an automated tool 11 | is a great. 12 | 13 | See a [screenshot of extract-method in action](docs/extract_method.png). 14 | 15 | The library is standing on the shoulder of giants, using multiple existing libraries: 16 | 17 | * [PHP Parser](https://github.com/nikic/PHP-Parser) by Nikic 18 | * [PHP Token Reflection](https://github.com/Andrewsville/PHP-Token-Reflection) from Ondřej Nešpor 19 | 20 | Based on data from these sources the Refactoring Browser consists of two distinct components: 21 | 22 | * ``Patches`` allows to build patches based on change operations on a file. 23 | * ``Refactoring`` contains the actual Refactoring domain and adapters to third party libraries. 24 | * ``Collections`` adds some collection semantics on top of PHP arrays. Currently contains a Set type. 25 | 26 | ## Install & Basic Usage 27 | 28 | [Download PHAR](https://github.com/QafooLabs/php-refactoring-browser/releases) 29 | 30 | The refactoring browser is used with: 31 | 32 | php refactor.phar ... 33 | 34 | It outputs a diff to the screen and you can apply it to your code by piping it to ``patch -p1``: 35 | 36 | php refactor.phar ... | patch -p1 37 | 38 | ### Editor Plugins 39 | 40 | There are third-party plugins available for using PHP refactorings within 41 | different text editors. These separately maintained projects can be found at 42 | the links below: 43 | 44 | * vim : https://github.com/vim-php/vim-php-refactoring 45 | * emacs : https://github.com/keelerm84/php-refactor-mode.el 46 | 47 | ## Why? 48 | 49 | Users of PHPStorm (or Netbeans) might wonder why this project exists, all the 50 | refactorings are available in this IDE. We feel there are several reasons to have 51 | such a tool in PHP natively: 52 | 53 | * We are VIM users and don't want to use an IDE for refactorings. Also we 54 | are independent of an IDE and users of any (non PHP Storm) editor can now 55 | benefit from the practice of automated refactorings. 56 | * The non-existence of a simple refactoring tool leads to programmers not 57 | refactoring "just to be safe". This hurts long time maintainability of code. 58 | Refactoring is one of the most important steps during development and just come easy. 59 | * Generating patches for refactorings before applying them allows to easily 60 | verify the operation yourself or sending it to a colleague. 61 | * The libraries (see above) to build such a tool are available, so why not do it. 62 | * The project is an academic of sorts as well, as you can see in the Design Goals 63 | we try to be very strict about the Ports and Adapters architecture and a Domain 64 | Driven Design. 65 | 66 | ## Refactorings 67 | 68 | ### Extract Method 69 | 70 | Extract a range of lines into a new method and call this method from the original 71 | location: 72 | 73 | php refactor.phar extract-method 74 | 75 | This refactoring automatically detects all necessary inputs and outputs from the 76 | function and generates the argument list and return statement accordingly. 77 | 78 | ### Rename Local Variable 79 | 80 | Rename a local variable from one to another name: 81 | 82 | php refactor.phar rename-local-variable 83 | 84 | ### Convert Local to Instance Variable 85 | 86 | Converts a local variable into an instance variable, creates the property and renames 87 | all the occurrences in the selected method to use the instance variable: 88 | 89 | php refactor.phar convert-local-to-instance-variable 90 | 91 | ### Rename Class and Namespaces 92 | 93 | Batch Operation to rename classes and namespaces by syncing class-names (IS-state) 94 | to filesystem names (SHOULD-state) based on the assumption of PSR-0. 95 | 96 | Fix class and namespace names to correspond to the current filesystem layout, 97 | given that the project uses PSR-0. This means you can use this tool to 98 | rename classes and namespaces by renaming folders and files and then applying 99 | the command to fix class and namespaces. 100 | 101 | php refactor.phar fix-class-names 102 | 103 | ### Optimize use statements 104 | 105 | Optimizes the use of Fully qualified names in a file so that FQN is imported with 106 | "use" at the top of the file and the FQN is replaced with its classname. 107 | 108 | All other use statements will be untouched, only new ones will be added. 109 | 110 | php refactor.phar optimize-use 111 | 112 | ## Roadmap 113 | 114 | Not prioritized. 115 | 116 | Integration: 117 | 118 | * Vim Plugin to apply refactorings from within Vim. 119 | 120 | List of Refactorings to implement: 121 | 122 | * Extract Method (Prototype Done) 123 | * Rename Local Variable (Prototype Done) 124 | * Optimize use statements (Done) 125 | * Convert Local Variable to Instance Variable (Prototype Done) 126 | * Rename Class PSR-0 aware (Done) 127 | * Rename Namespace PSR-0 aware (Done) 128 | * Convert Magic Value to Constant 129 | * Rename Method 130 | * Private Methods Only first 131 | * Rename Instance Variable 132 | * Private Variables Only First 133 | * Extract Interface 134 | 135 | ## Internals 136 | 137 | ### Design Goals 138 | 139 | * Be independent of third-party libraries and any Type Inference Engine (PDepend, PHP Analyzer) via Ports+Adapters 140 | * Apply Domain-Driven-Design and find suitable Bounded Contexts and Ubiquitous Language within them 141 | * Avoid primitive obsession by introducing value objects for useful constructs in the domain 142 | 143 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | project.name = PHP Refactoring Browser 2 | project.version = 0.0.1 3 | project.stability = alpha 4 | 5 | # The commons based directory will be used to calculate several build related 6 | # paths and directories. Therefore we will keep it separated and independent for 7 | # each component in the component's basedir. 8 | commons.basedir = ${basedir} 9 | 10 | # Base directories with PHP source and test files 11 | commons.srcdir = ${basedir}/src/main 12 | commons.srcdir.php = ${basedir}/src/main 13 | commons.testdir.php = ${basedir}/src/test 14 | 15 | #package.archive.enabled=false 16 | phpmd.enabled=false 17 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qafoolabs/php-refactoring-browser", 3 | "description": "Refactoring Browser for PHP", 4 | "license": "Apache2", 5 | 6 | "authors": [ 7 | {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"} 8 | ], 9 | 10 | "require": { 11 | "nikic/php-parser": "@stable", 12 | "beberlei/assert": "@stable", 13 | "andrewsville/php-token-reflection": "@stable", 14 | "symfony/finder": "~2.4@stable", 15 | "symfony/console": "~2.4@stable", 16 | "tomphp/patch-builder": "~0.1" 17 | }, 18 | 19 | "require-dev": { 20 | "php": ">=5.4", 21 | "behat/behat": "~2.5@stable", 22 | "mikey179/vfsStream": "@stable", 23 | "phake/phake": "@stable", 24 | "symfony/process": "@stable", 25 | "phpunit/phpunit": "~4.6@stable" 26 | }, 27 | 28 | "autoload": { 29 | "psr-0": { 30 | "QafooLabs\\Refactoring": "src/main/", 31 | "QafooLabs\\Patches": "src/main/", 32 | "QafooLabs\\Collections": "src/main/" 33 | } 34 | }, 35 | 36 | "bin": ["src/bin/refactor"] 37 | } 38 | -------------------------------------------------------------------------------- /docs/bounded_contexts.rst: -------------------------------------------------------------------------------- 1 | Refactoring Domain 2 | ------------------ 3 | 4 | Automatic code refactoring based on PHP Parser ASTs. Data from the type 5 | inference domain is used in a format suitable for the refactoring domain. 6 | 7 | The refactoring domain is based on William Opdyke thesis on `"Refactoring 8 | Object-Oriented Frameworks" 9 | `_. 10 | 11 | Type Inference Domain 12 | --------------------- 13 | 14 | Just necessary because PHP is not statically typed. The type inference domain 15 | creates a database of artifacts, which is loaded into the refactoring domain 16 | through an anticorruption layer. 17 | -------------------------------------------------------------------------------- /docs/extract_method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QafooLabs/php-refactoring-browser/3ff0f7bc955ec253ad7fa1bfaf44cc02604aaa1d/docs/extract_method.png -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | root = vfsStream::setup('project'); 34 | $this->structure = array(); 35 | } 36 | 37 | /** 38 | * @Given /^a PHP File named "([^"]*)" with:$/ 39 | */ 40 | public function aPhpFileNamedWith($file, PyStringNode $code) 41 | { 42 | $this->structure = $this->appendToTree($this->structure, $file, (string)$code); 43 | } 44 | 45 | private function appendToTree($tree, $path, $code) 46 | { 47 | @list($head, $rest) = explode("/", $path, 2); // Muting notice that happens when there are no 2 elements left. 48 | 49 | if (!isset($tree[$head])) { 50 | $tree[$head] = array(); 51 | } 52 | 53 | if (empty($rest)) { 54 | $tree[$head] = $code; 55 | } else { 56 | $tree[$head] = $this->appendToTree($tree, $rest, $code); 57 | } 58 | 59 | return $tree; 60 | } 61 | 62 | /** 63 | * @When /^I use refactoring "([^"]*)" with:$/ 64 | */ 65 | public function iUseRefactoringWith($refactoringName, TableNode $table) 66 | { 67 | vfsStream::create($this->structure, $this->root); 68 | 69 | $data = array('command' => $refactoringName); 70 | foreach ($table->getHash() as $line) { 71 | $data[$line['arg']] = $line['value']; 72 | } 73 | 74 | if (isset($data['file'])) { 75 | $data['file'] = vfsStream::url('project/' . $data['file']); 76 | } 77 | if (isset($data['dir'])) { 78 | $data['dir'] = vfsStream::url('project/' . $data['dir']); 79 | } 80 | 81 | $data['--verbose'] = true; 82 | 83 | $fh = fopen("php://memory", "rw"); 84 | $input = new ArrayInput($data); 85 | $output = new \Symfony\Component\Console\Output\StreamOutput($fh); 86 | 87 | $app = new CliApplication(); 88 | $app->setAutoExit(false); 89 | $app->run($input, $output); 90 | 91 | rewind($fh); 92 | $this->output = stream_get_contents($fh); 93 | } 94 | 95 | /** 96 | * @Then /^the PHP File "([^"]*)" should be refactored:$/ 97 | */ 98 | public function thePhpFileShouldBeRefactored($file, PyStringNode $expectedPatch) 99 | { 100 | $output = array_map('trim', explode("\n", rtrim($this->output))); 101 | $formattedExpectedPatch = $this->formatExpectedPatch((string)$expectedPatch); 102 | 103 | PHPUnit::assertEquals( 104 | $formattedExpectedPatch, 105 | $output, 106 | "Expected File:\n" . (string)$expectedPatch . "\n\n" . 107 | "Refactored File:\n" . $this->output . "\n\n" . 108 | "Diff:\n" . print_r(array_diff($formattedExpectedPatch, $output), true) 109 | ); 110 | } 111 | 112 | /** 113 | * converts / paths in expectedPatch text to \ paths 114 | * 115 | * leaves the a/ b/ slashes untouched 116 | * returns an array of lines 117 | * @return array 118 | */ 119 | protected function formatExpectedPatch($patch) 120 | { 121 | if ('\\' === DIRECTORY_SEPARATOR) { 122 | $formatLine = function ($line) { 123 | // replace lines for diff-path-files starting with --- or +++ 124 | $line = preg_replace_callback('~^((?:---|\+\+\+)\s*(?:a|b)/)(.*)~', function ($match) { 125 | list($all, $diff, $path) = $match; 126 | 127 | // dont replace wrapped path separators 128 | if (! preg_match('~^[a-z]+://~i', $path)) { 129 | $path = str_replace('/', '\\', $path); 130 | } 131 | 132 | return $diff.$path; 133 | 134 | }, $line); 135 | 136 | return trim($line); 137 | }; 138 | 139 | } else { 140 | $formatLine = function ($line) { 141 | return trim($line); 142 | }; 143 | } 144 | 145 | return array_map($formatLine, explode("\n", rtrim($patch))); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /features/convert_local_to_instance_variable.feature: -------------------------------------------------------------------------------- 1 | Feature: Convert Local to Instance Variable 2 | To keep the my code base clean 3 | As a developer 4 | I want to convert local variables to instance variables 5 | 6 | Scenario: Convert Variable 7 | Given a PHP File named "src/Foo.php" with: 8 | """ 9 | operation(); 16 | 17 | return $service; 18 | } 19 | } 20 | """ 21 | When I use refactoring "convert-local-to-instance-variable" with: 22 | | arg | value | 23 | | file | src/Foo.php | 24 | | line | 6 | 25 | | variable | service | 26 | Then the PHP File "src/Foo.php" should be refactored: 27 | """ 28 | --- a/vfs://project/src/Foo.php 29 | +++ b/vfs://project/src/Foo.php 30 | @@ -1,11 +1,13 @@ 31 | operation(); 40 | + $this->service = new Service(); 41 | + $this->service->operation(); 42 | 43 | - return $service; 44 | + return $this->service; 45 | } 46 | } 47 | """ 48 | 49 | -------------------------------------------------------------------------------- /features/optimize_use.feature: -------------------------------------------------------------------------------- 1 | Feature: Optimize use 2 | To optimize the use statements in my code 3 | As a developer 4 | I need to convert every FQN in the code to a use statement in the file 5 | 6 | Scenario: Convert FQN and leave relative names 7 | Given a PHP File named "src/Foo.php" with: 8 | """ 9 | operation(new \Bar\Qux\Adapter($flag)); 23 | 24 | return $service; 25 | } 26 | } 27 | """ 28 | When I use refactoring "optimize-use" with: 29 | | arg | value | 30 | | file | src/Foo.php | 31 | Then the PHP File "src/Foo.php" should be refactored: 32 | """ 33 | --- a/vfs://project/src/Foo.php 34 | +++ b/vfs://project/src/Foo.php 35 | @@ -3,6 +3,7 @@ 36 | namespace Bar; 37 | 38 | use Bar\Baz\Service; 39 | +use Bar\Qux\Adapter; 40 | 41 | class Foo 42 | { 43 | @@ -11,7 +12,7 @@ 44 | $flag = Qux\Adapter::CONSTANT_ARG; 45 | 46 | $service = new Service(); 47 | - $service->operation(new \Bar\Qux\Adapter($flag)); 48 | + $service->operation(new Adapter($flag)); 49 | 50 | return $service; 51 | } 52 | """ 53 | Scenario: Organize use for file without namespace and other uses 54 | Given a PHP File named "src/Foo.php" with: 55 | """ 56 | 2 | 13 | 14 | 15 | src/test 16 | 17 | 18 | 19 | 20 | 21 | src/main 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | compile(); 16 | } catch (\Exception $e) { 17 | echo 'Failed to compile phar: ['.get_class($e).'] '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine(); 18 | exit(1); 19 | } 20 | -------------------------------------------------------------------------------- /src/bin/refactor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 12 | -------------------------------------------------------------------------------- /src/config/autocomplete.bash: -------------------------------------------------------------------------------- 1 | #!bash 2 | # 3 | # bash completion support for php-refactoring-browser 4 | # 5 | # Copyright (C) 2013 Tobias Schlitt 6 | # 7 | # Code copied and adjusted from https://github.com/KnpLabs/symfony2-autocomplete: 8 | # Copyright (C) 2011 Matthieu Bontemps 9 | # Distributed under the GNU General Public License, version 2.0. 10 | 11 | _console() 12 | { 13 | local cur prev cmd 14 | COMPREPLY=() 15 | cur="${COMP_WORDS[COMP_CWORD]}" 16 | prev="${COMP_WORDS[COMP_CWORD-1]}" 17 | cmd="${COMP_WORDS[0]}" 18 | PHP='$ret = shell_exec($argv[1] . " --no-debug --env=prod"); 19 | 20 | $comps = ""; 21 | $ret = preg_replace("/^.*Available commands:\n/s", "", $ret); 22 | if (preg_match_all("@^ ([^ ]+) @m", $ret, $m)) { 23 | $comps = $m[1]; 24 | } 25 | 26 | echo implode("\n", $comps); 27 | ' 28 | possible=$($(which php) -r "$PHP" $COMP_WORDS); 29 | COMPREPLY=( $(compgen -W "${possible}" -- ${cur}) ) 30 | return 0 31 | } 32 | 33 | complete -F _console refactor 34 | COMP_WORDBREAKS=${COMP_WORDBREAKS//:} 35 | -------------------------------------------------------------------------------- /src/config/behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | paths: 3 | features: ../../features 4 | bootstrap: %behat.paths.features%/bootstrap 5 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Collections/Hashable.php: -------------------------------------------------------------------------------- 1 | items[$item->hashCode()] = $item; 30 | 31 | return; 32 | } 33 | 34 | $this->items[(string)$item] = $item; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function count() 41 | { 42 | return count($this->items); 43 | } 44 | 45 | /** 46 | * @return Iterator 47 | */ 48 | public function getIterator() 49 | { 50 | return new ArrayIterator(array_values($this->items)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/ParserPhpNameScanner.php: -------------------------------------------------------------------------------- 1 | parse($file->getCode()); 28 | } catch (PHPParser_Error $e) { 29 | throw new \RuntimeException("Error parsing " . $file->getRelativePath() .": " . $e->getMessage(), 0, $e); 30 | } 31 | 32 | $traverser->addVisitor($collector); 33 | $traverser->traverse($stmts); 34 | 35 | return array_map(function ($use) use ($file) { 36 | $type = constant('QafooLabs\Refactoring\Domain\Model\PhpName::TYPE_' . strtoupper($use['type'])); 37 | return new PhpNameOccurance( 38 | new PhpName( 39 | $use['fqcn'], 40 | $use['alias'], 41 | $type 42 | ), 43 | $file, 44 | $use['line'] 45 | ); 46 | }, $collector->collectedNameDeclarations()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/ParserVariableScanner.php: -------------------------------------------------------------------------------- 1 | parse($file->getCode()); 39 | 40 | $collector = new LineRangeStatementCollector($range); 41 | 42 | $traverser = new PHPParser_NodeTraverser; 43 | $traverser->addVisitor(new NodeConnector); 44 | $traverser->addVisitor($collector); 45 | 46 | $traverser->traverse($stmts); 47 | 48 | $selectedStatements = $collector->getStatements(); 49 | 50 | if ( ! $selectedStatements) { 51 | throw new \RuntimeException("No statements found in line range."); 52 | } 53 | 54 | $localVariableClassifier = new LocalVariableClassifier(); 55 | $traverser = new PHPParser_NodeTraverser; 56 | $traverser->addVisitor($localVariableClassifier); 57 | $traverser->traverse($selectedStatements); 58 | 59 | $localVariables = $localVariableClassifier->getUsedLocalVariables(); 60 | $assignments = $localVariableClassifier->getAssignments(); 61 | 62 | return new DefinedVariables($localVariables, $assignments); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/LineRangeStatementCollector.php: -------------------------------------------------------------------------------- 1 | lineRange = $lineRange; 36 | $this->statements = new \SplObjectStorage(); 37 | } 38 | 39 | public function enterNode(PHPParser_Node $node) 40 | { 41 | if ( ! $this->lineRange->isInRange($node->getLine())) { 42 | return; 43 | } 44 | 45 | $parent = $node->getAttribute('parent'); 46 | 47 | // TODO: Expensive (?) 48 | do { 49 | if ($parent && $this->statements->contains($parent)) { 50 | return; 51 | } 52 | } while($parent && $parent = $parent->getAttribute('parent')); 53 | 54 | $this->statements->attach($node); 55 | } 56 | 57 | public function getStatements() 58 | { 59 | return iterator_to_array($this->statements); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/LocalVariableClassifier.php: -------------------------------------------------------------------------------- 1 | seenAssignmentVariables = new SplObjectStorage(); 38 | } 39 | 40 | public function enterNode(PHPParser_Node $node) 41 | { 42 | if ($node instanceof PHPParser_Node_Expr_Variable) { 43 | $this->enterVariableNode($node); 44 | } 45 | 46 | if ($node instanceof PHPParser_Node_Expr_Assign) { 47 | $this->enterAssignment($node); 48 | } 49 | 50 | if ($node instanceof PHPParser_Node_Param) { 51 | $this->enterParam($node); 52 | } 53 | } 54 | 55 | private function enterParam($node) 56 | { 57 | $this->assignments[$node->name][] = $node->getLine(); 58 | } 59 | 60 | private function enterAssignment($node) 61 | { 62 | if ($node->var instanceof PHPParser_Node_Expr_Variable) { 63 | $this->assignments[$node->var->name][] = $node->getLine(); 64 | $this->seenAssignmentVariables->attach($node->var); 65 | } else if ($node->var instanceof PHPParser_Node_Expr_ArrayDimFetch) { 66 | // $foo[] = "baz" is both a read and a write access to $foo 67 | $this->localVariables[$node->var->var->name][] = $node->getLine(); 68 | $this->assignments[$node->var->var->name][] = $node->getLine(); 69 | $this->seenAssignmentVariables->attach($node->var->var); 70 | } 71 | } 72 | 73 | private function enterVariableNode($node) 74 | { 75 | if ($node->name === "this" || $this->seenAssignmentVariables->contains($node)) { 76 | return; 77 | } 78 | 79 | $this->localVariables[$node->name][] = $node->getLine(); 80 | } 81 | 82 | public function getLocalVariables() 83 | { 84 | return $this->localVariables; 85 | } 86 | 87 | public function getUsedLocalVariables() 88 | { 89 | return $this->localVariables; 90 | } 91 | 92 | public function getAssignments() 93 | { 94 | return $this->assignments; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/NodeConnector.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class NodeConnector extends PHPParser_NodeVisitorAbstract 25 | { 26 | public function enterNode(PHPParser_Node $node) 27 | { 28 | $subNodes = array(); 29 | foreach ($node as $subNode) { 30 | if ($subNode instanceof PHPParser_Node) { 31 | $subNodes[] = $subNode; 32 | continue; 33 | } else if (!is_array($subNode)) { 34 | continue; 35 | } 36 | 37 | $subNodes = array_merge($subNodes, array_values($subNode)); 38 | } 39 | 40 | for ($i=0,$c=count($subNodes); $i<$c; $i++) { 41 | if (!$subNodes[$i] instanceof PHPParser_Node) { 42 | continue; 43 | } 44 | 45 | $subNodes[$i]->setAttribute('parent', $node); 46 | 47 | if ($i > 0) { 48 | $subNodes[$i]->setAttribute('previous', $subNodes[$i - 1]); 49 | } 50 | if ($i + 1 < $c) { 51 | $subNodes[$i]->setAttribute('next', $subNodes[$i + 1]); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/PhpNameCollector.php: -------------------------------------------------------------------------------- 1 | uses as $use) { 47 | if ($use instanceof PHPParser_Node_Stmt_UseUse) { 48 | $name = implode('\\', $use->name->parts); 49 | 50 | $this->useStatements[$use->alias] = $name; 51 | $this->nameDeclarations[] = array( 52 | 'alias' => $name, 53 | 'fqcn' => $name, 54 | 'line' => $use->getLine(), 55 | 'type' => 'use', 56 | ); 57 | } 58 | } 59 | } 60 | 61 | 62 | if ($node instanceof PHPParser_Node_Expr_New && $node->class instanceof PHPParser_Node_Name) { 63 | $usedAlias = implode('\\', $node->class->parts); 64 | 65 | $this->nameDeclarations[] = array( 66 | 'alias' => $usedAlias, 67 | 'fqcn' => $this->fullyQualifiedNameFor($usedAlias, $node->class->isFullyQualified()), 68 | 'line' => $node->getLine(), 69 | 'type' => 'usage', 70 | ); 71 | } 72 | 73 | if ($node instanceof PHPParser_Node_Expr_StaticCall && $node->class instanceof PHPParser_Node_Name) { 74 | $usedAlias = implode('\\', $node->class->parts); 75 | 76 | $this->nameDeclarations[] = array( 77 | 'alias' => $usedAlias, 78 | 'fqcn' => $this->fullyQualifiedNameFor($usedAlias, $node->class->isFullyQualified()), 79 | 'line' => $node->getLine(), 80 | 'type' => 'usage', 81 | ); 82 | } 83 | 84 | if ($node instanceof PHPParser_Node_Stmt_Class) { 85 | $className = $node->name; 86 | 87 | $this->nameDeclarations[] = array( 88 | 'alias' => $className, 89 | 'fqcn' => $this->fullyQualifiedNameFor($className, false), 90 | 'line' => $node->getLine(), 91 | 'type' => 'class', 92 | ); 93 | 94 | if ($node->extends) { 95 | $usedAlias = implode('\\', $node->extends->parts); 96 | 97 | $this->nameDeclarations[] = array( 98 | 'alias' => $usedAlias, 99 | 'fqcn' => $this->fullyQualifiedNameFor($usedAlias, $node->extends->isFullyQualified()), 100 | 'line' => $node->extends->getLine(), 101 | 'type' => 'usage', 102 | ); 103 | } 104 | 105 | foreach ($node->implements as $implement) { 106 | $usedAlias = implode('\\', $implement->parts); 107 | 108 | $this->nameDeclarations[] = array( 109 | 'alias' => $usedAlias, 110 | 'fqcn' => $this->fullyQualifiedNameFor($usedAlias, $implement->isFullyQualified()), 111 | 'line' => $implement->getLine(), 112 | 'type' => 'usage', 113 | ); 114 | } 115 | } 116 | 117 | if ($node instanceof PHPParser_Node_Stmt_Namespace) { 118 | $this->currentNamespace = implode('\\', $node->name->parts); 119 | $this->useStatements = array(); 120 | 121 | $this->nameDeclarations[] = array( 122 | 'alias' => $this->currentNamespace, 123 | 'fqcn' => $this->currentNamespace, 124 | 'line' => $node->name->getLine(), 125 | 'type' => 'namespace', 126 | ); 127 | } 128 | } 129 | 130 | private function fullyQualifiedNameFor($alias, $isFullyQualified) 131 | { 132 | $isAbsolute = $alias[0] === "\\"; 133 | 134 | if ($isAbsolute || $isFullyQualified) { 135 | $class = $alias; 136 | } else if (isset($this->useStatements[$alias])) { 137 | $class = $this->useStatements[$alias]; 138 | } else { 139 | $class = ltrim($this->currentNamespace . '\\' . $alias, '\\'); 140 | } 141 | 142 | return $class; 143 | } 144 | 145 | public function collectedNameDeclarations() 146 | { 147 | return $this->nameDeclarations; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PatchBuilder/ApplyPatchCommand.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 30 | } 31 | 32 | public function getLines(LineRange $range) 33 | { 34 | return $this->builder->getOriginalLines($range->getStart(), $range->getEnd()); 35 | } 36 | 37 | public function replace(LineRange $range, array $newLines) 38 | { 39 | $this->builder->replaceLines($range->getStart(), $range->getEnd(), $newLines); 40 | } 41 | 42 | public function append($line, array $newLines) 43 | { 44 | $this->builder->appendToLine($line, $newLines); 45 | } 46 | 47 | public function replaceString($line, $oldToken, $newToken) 48 | { 49 | if ($oldToken === $newToken) { 50 | return; 51 | } 52 | 53 | $this->builder->changeToken($line, $oldToken, $newToken); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PatchBuilder/PatchBuilder.php: -------------------------------------------------------------------------------- 1 | buffer = PatchBuilderBuffer::createWithContents($lines); 49 | 50 | $this->path = $path; 51 | } 52 | 53 | public function getOriginalLines($start, $end) 54 | { 55 | return array_slice( 56 | $this->buffer->getOriginalContents(), 57 | $start - 1, 58 | $end - $start + 1 59 | ); 60 | } 61 | 62 | /** 63 | * Change Token in given line from old to new. 64 | * 65 | * @param int $originalLine 66 | * @param string $oldToken 67 | * @param string $newToken 68 | * 69 | * @return void 70 | */ 71 | public function changeToken($originalLine, $oldToken, $newToken) 72 | { 73 | $newLine = $this->buffer->getLine($this->createLineNumber($originalLine)); 74 | 75 | $newLine = preg_replace( 76 | '!(^|[^a-z0-9])(' . preg_quote($oldToken) . ')([^a-z0-9]|$)!', 77 | '\1' . $newToken . '\3', 78 | $newLine 79 | ); 80 | 81 | $this->buffer->replace( 82 | $this->createSingleLineRange($originalLine), 83 | array($newLine) 84 | ); 85 | } 86 | 87 | /** 88 | * Append new lines to an original line of the file. 89 | * 90 | * @param int $originalLine 91 | * @param array $lines 92 | * 93 | * @return void 94 | */ 95 | public function appendToLine($originalLine, array $lines) 96 | { 97 | // Why is this one method different to the rest? 98 | $originalLine++; 99 | 100 | $this->buffer->insert($this->createLineNumber($originalLine), $lines); 101 | } 102 | 103 | /** 104 | * Change one line by replacing it with one or many new lines. 105 | * 106 | * @param int $originalLine 107 | * @param array $newLines 108 | * 109 | * @return void 110 | */ 111 | public function changeLines($originalLine, array $newLines) 112 | { 113 | $this->buffer->replace($this->createSingleLineRange($originalLine), $newLines); 114 | } 115 | 116 | /** 117 | * Remove one line 118 | * 119 | * @param int $originalLine 120 | * 121 | * @return void 122 | */ 123 | public function removeLine($originalLine) 124 | { 125 | $this->buffer->delete($this->createSingleLineRange($originalLine)); 126 | } 127 | 128 | /** 129 | * Replace a range of lines with a set of new lines. 130 | * 131 | * @param int $startOriginalLine 132 | * @param int $endOriginalLine 133 | * @param array $newLines 134 | * 135 | * @return void 136 | */ 137 | public function replaceLines($startOriginalLine, $endOriginalLine, array $newLines) 138 | { 139 | $this->buffer->replace($this->createLineRange($startOriginalLine, $endOriginalLine), $newLines); 140 | } 141 | 142 | /** 143 | * Generate a unified diff of all operations performed on the current file. 144 | * 145 | * @return string 146 | */ 147 | public function generateUnifiedDiff() 148 | { 149 | $builder = new PhpDiffBuilder(); 150 | 151 | return $builder->buildPatch( 152 | 'a/' . $this->path, 153 | 'b/' . $this->path, 154 | $this->buffer 155 | ); 156 | } 157 | 158 | /** 159 | * @param int $start 160 | * @param int $end 161 | * 162 | * @return LineRange 163 | */ 164 | private function createLineRange($start, $end) 165 | { 166 | return new LineRange( 167 | new OriginalLineNumber($start - 1), 168 | new OriginalLineNumber($end - 1) 169 | ); 170 | } 171 | 172 | /** 173 | * @param int $number 174 | * 175 | * @return LineRange 176 | */ 177 | private function createSingleLineRange($number) 178 | { 179 | $line = new OriginalLineNumber($number - 1); 180 | 181 | return new LineRange($line, $line); 182 | } 183 | 184 | /** 185 | * @param int $number 186 | * 187 | * @return OriginalLineNumber 188 | */ 189 | private function createLineNumber($number) 190 | { 191 | return new OriginalLineNumber($number - 1); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/PatchBuilder/PatchEditor.php: -------------------------------------------------------------------------------- 1 | command = $command; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function openBuffer(File $file) 38 | { 39 | if ( ! isset($this->builders[$file->getRelativePath()])) { 40 | $this->builders[$file->getRelativePath()] = new PatchBuilder( 41 | $file->getCode(), $file->getRelativePath() 42 | ); 43 | } 44 | 45 | return new PatchBuffer($this->builders[$file->getRelativePath()]); 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function save() 52 | { 53 | foreach ($this->builders as $builder) { 54 | $this->command->apply($builder->generateUnifiedDiff()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/CliApplication.php: -------------------------------------------------------------------------------- 1 | logo . parent::getHelp(); 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Commands/ConvertLocalToInstanceVariableCommand.php: -------------------------------------------------------------------------------- 1 | setName('convert-local-to-instance-variable') 37 | ->setDescription('Convert a local variable to an instance variable.') 38 | ->addArgument('file', InputArgument::REQUIRED, 'File that contains the local variable.') 39 | ->addArgument('line', InputArgument::REQUIRED, 'Line of one of the local variables occurrences.') 40 | ->addArgument('variable', InputArgument::REQUIRED, 'Name of the variable with or without $.') 41 | ->setHelp(<<It will: 47 | 48 | 1. Convert all occurrences of the same variable within the method into an instance variable of the same name. 49 | 2. Create the instance variable on the class. 50 | 51 | Pre-Conditions: 52 | 53 | 1. Selected Variable does not exist on class (NOT CHECKED YET) 54 | 2. Variable is a local variable 55 | 56 | Usage: 57 | 58 | php refactor.phar convert-local-to-instance-variable file.php 10 hello 59 | 60 | Will convert variable \$hello into an instance variable \$this->hello. 61 | HELP 62 | ) 63 | ; 64 | } 65 | 66 | protected function execute(InputInterface $input, OutputInterface $output) 67 | { 68 | $file = File::createFromPath($input->getArgument('file'), getcwd()); 69 | $line = (int)$input->getArgument('line'); 70 | $variable = new Variable($input->getArgument('variable')); 71 | 72 | $scanner = new ParserVariableScanner(); 73 | $codeAnalysis = new StaticCodeAnalysis(); 74 | $editor = new PatchEditor(new OutputPatchCommand($output)); 75 | 76 | $convertRefactoring = new ConvertLocalToInstanceVariable($scanner, $codeAnalysis, $editor); 77 | $convertRefactoring->refactor($file, $line, $variable); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Commands/ExtractMethodCommand.php: -------------------------------------------------------------------------------- 1 | setName('extract-method') 40 | ->setDescription('Extract a list of statements into a method.') 41 | ->addArgument('file', InputArgument::REQUIRED, 'File that contains list of statements to extract') 42 | ->addArgument('range', InputArgument::REQUIRED, 'Line Range of statements that should be extracted.') 43 | ->addArgument('newmethod', InputArgument::REQUIRED, 'Name of the new method.') 44 | ->setHelp(<<Operations: 53 | 54 | 1. Create a new method containing the selected code. 55 | 2. Add a return statement with all variables necessary to make caller work. 56 | 3. Pass all arguments to make the method work. 57 | 58 | Pre-Conditions: 59 | 60 | 1. Selected code is inside a single method. 61 | 2. New Method does not exist (NOT YET CHECKED). 62 | 63 | Usage: 64 | 65 | php refactor.phar extract-method file.php 10-16 newMethodName 66 | 67 | Will extract lines 10-16 from file.php into a new method called newMethodName. 68 | HELP 69 | ); 70 | ; 71 | } 72 | 73 | protected function execute(InputInterface $input, OutputInterface $output) 74 | { 75 | $file = File::createFromPath($input->getArgument('file'), getcwd()); 76 | $range = LineRange::fromString($input->getArgument('range')); 77 | $newMethodName = $input->getArgument('newmethod'); 78 | 79 | $scanner = new ParserVariableScanner(); 80 | $codeAnalysis = new StaticCodeAnalysis(); 81 | $editor = new PatchEditor(new OutputPatchCommand($output)); 82 | 83 | $extractMethod = new ExtractMethod($scanner, $codeAnalysis, $editor); 84 | $extractMethod->refactor($file, $range, $newMethodName); 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Commands/FixClassNamesCommand.php: -------------------------------------------------------------------------------- 1 | setName('fix-class-names') 35 | ->setDescription('Find all classes whose names don\'t match their required PSR-0 name and rename them.') 36 | ->addArgument('dir', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Directory that contains the source code to refactor') 37 | ->setHelp(<<Operations: 44 | 45 | 1. Find all PHP files in given directory. 46 | 2. Check every PHP file for class names and namespace definition 47 | 3. Change the namespaces and class names to match the current file name 48 | 49 | Pre-Conditions: 50 | 51 | This refactoring has no pre-conditions. 52 | 53 | Usage: 54 | 55 | php refactor.phar fix-class-names src/ 56 | HELP 57 | ) 58 | ; 59 | } 60 | 61 | protected function execute(InputInterface $input, OutputInterface $output) 62 | { 63 | $directory = new Directory($input->getArgument('dir'), getcwd()); 64 | 65 | $codeAnalysis = new StaticCodeAnalysis(); 66 | $phpNameScanner = new ParserPhpNameScanner(); 67 | $editor = new PatchEditor(new OutputPatchCommand($output)); 68 | 69 | $fixClassNames = new FixClassNames($codeAnalysis, $editor, $phpNameScanner); 70 | $fixClassNames->refactor($directory); 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Commands/OptimizeUseCommand.php: -------------------------------------------------------------------------------- 1 | setName('optimize-use') 41 | ->setDescription('Optimize use statements of a file. Replace FQNs with imported aliases.') 42 | ->addArgument('file', InputArgument::REQUIRED, 'File that contains the use statements to optimize') 43 | ->setHelp(<<Operations: 50 | 51 | 1. import found FQNs 52 | 2. replace FQNs with the imported classname 53 | 54 | Pre-Conditions: 55 | 56 | 1. File has a single namespace defined 57 | 58 | Known issues: 59 | 60 | 1. a FQN might be renamed with an conflicting name when the className of the renamend full qualified name is already in use 61 | 2. if there is no use statement in the whole file, new ones will be appended after the namespace 62 | 63 | Usage: 64 | 65 | php refactor.phar optimize-use file.php 66 | 67 | Will optimize the use statements in file.php. 68 | HELP 69 | ); 70 | ; 71 | } 72 | 73 | protected function execute(InputInterface $input, OutputInterface $output) 74 | { 75 | $file = File::createFromPath($input->getArgument('file'), getcwd()); 76 | 77 | $codeAnalysis = new StaticCodeAnalysis(); 78 | $editor = new PatchEditor(new OutputPatchCommand($output)); 79 | $phpNameScanner = new ParserPhpNameScanner(); 80 | 81 | $optimizeUse = new OptimizeUse($codeAnalysis, $editor, $phpNameScanner); 82 | $optimizeUse->refactor($file); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Commands/RenameLocalVariableCommand.php: -------------------------------------------------------------------------------- 1 | setName('rename-local-variable') 38 | ->setDescription('Rename a local variable inside a method') 39 | ->addArgument('file', InputArgument::REQUIRED, 'File that contains list of statements to extract') 40 | ->addArgument('line', InputArgument::REQUIRED, 'Line where the local variable is defined.') 41 | ->addArgument('name', InputArgument::REQUIRED, 'Current name of the variable, with or without the $') 42 | ->addArgument('new-name', InputArgument::REQUIRED, 'New name of the variable') 43 | ->setHelp(<<Operations: 47 | 48 | 1. Renames a local variable by giving it a new name inside the method. 49 | 50 | Pre-Conditions: 51 | 52 | 1. Check that new variable name does not exist (NOT YET CHECKED). 53 | 54 | Usage: 55 | 56 | php refactor.phar rename-local-variable file.php 17 hello newHello 57 | 58 | Renames \$hello in line 17 of file.php into \$newHello. 59 | 60 | HELP 61 | ); 62 | ; 63 | } 64 | 65 | protected function execute(InputInterface $input, OutputInterface $output) 66 | { 67 | $file = File::createFromPath($input->getArgument('file'), getcwd()); 68 | $line = (int)$input->getArgument('line'); 69 | $name = new Variable($input->getArgument('name')); 70 | $newName = new Variable($input->getArgument('new-name')); 71 | 72 | $scanner = new ParserVariableScanner(); 73 | $codeAnalysis = new StaticCodeAnalysis(); 74 | $editor = new PatchEditor(new OutputPatchCommand($output)); 75 | 76 | $renameLocalVariable = new RenameLocalVariable($scanner, $codeAnalysis, $editor); 77 | $renameLocalVariable->refactor($file, $line, $name, $newName); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/Symfony/Compiler.php: -------------------------------------------------------------------------------- 1 | 25 | * @author Jordi Boggiano 26 | */ 27 | class Compiler 28 | { 29 | private $version; 30 | private $directory; 31 | 32 | public function __construct($directory) 33 | { 34 | $this->directory = realpath($directory); 35 | } 36 | 37 | /** 38 | * Compiles composer into a single phar file 39 | * 40 | * @throws \RuntimeException 41 | */ 42 | public function compile() 43 | { 44 | $pharFile = 'refactor.phar'; 45 | 46 | if (file_exists($pharFile)) { 47 | unlink($pharFile); 48 | } 49 | 50 | $process = new Process('git log --pretty="%H" -n1 HEAD', $this->directory); 51 | if ($process->run() != 0) { 52 | throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from git repository clone and that git binary is available.'); 53 | } 54 | $this->version = trim($process->getOutput()); 55 | 56 | $process = new Process('git describe --tags HEAD'); 57 | if ($process->run() == 0) { 58 | $this->version = trim($process->getOutput()); 59 | } 60 | 61 | $phar = new \Phar($pharFile, 0, 'refactor.phar'); 62 | $phar->setSignatureAlgorithm(\Phar::SHA1); 63 | 64 | $phar->startBuffering(); 65 | 66 | $finder = new Finder(); 67 | $finder->files() 68 | ->ignoreVCS(true) 69 | ->name('*.php') 70 | ->notName('Compiler.php') 71 | ->in($this->directory.'/src/main') 72 | ; 73 | 74 | foreach ($finder as $file) { 75 | $this->addFile($phar, $file); 76 | } 77 | 78 | $finder = new Finder(); 79 | $finder->files() 80 | ->ignoreVCS(true) 81 | ->name('*.php') 82 | ->exclude('test') 83 | ->exclude('features') 84 | ->in($this->directory . '/vendor/') 85 | ; 86 | 87 | foreach ($finder as $file) { 88 | $this->addFile($phar, $file); 89 | } 90 | 91 | $this->addRefactorBin($phar); 92 | 93 | // Stubs 94 | $phar->setStub($this->getStub()); 95 | 96 | $phar->stopBuffering(); 97 | 98 | unset($phar); 99 | } 100 | 101 | private function addFile($phar, $file, $strip = true) 102 | { 103 | $path = str_replace($this->directory . DIRECTORY_SEPARATOR, '', $file->getRealPath()); 104 | 105 | $content = file_get_contents($file); 106 | if ($strip) { 107 | $content = $this->stripWhitespace($content); 108 | } elseif ('LICENSE' === basename($file)) { 109 | $content = "\n".$content."\n"; 110 | } 111 | 112 | $content = str_replace('@package_version@', $this->version, $content); 113 | 114 | $phar->addFromString($path, $content); 115 | } 116 | 117 | private function addRefactorBin($phar) 118 | { 119 | $content = file_get_contents($this->directory . '/src/bin/refactor'); 120 | $content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content); 121 | $phar->addFromString('src/bin/refactor', $content); 122 | } 123 | 124 | /** 125 | * Removes whitespace from a PHP source string while preserving line numbers. 126 | * 127 | * @param string $source A PHP string 128 | * @return string The PHP string with the whitespace removed 129 | */ 130 | private function stripWhitespace($source) 131 | { 132 | if (!function_exists('token_get_all')) { 133 | return $source; 134 | } 135 | 136 | $output = ''; 137 | foreach (token_get_all($source) as $token) { 138 | if (is_string($token)) { 139 | $output .= $token; 140 | } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { 141 | $output .= str_repeat("\n", substr_count($token[1], "\n")); 142 | } elseif (T_WHITESPACE === $token[0]) { 143 | // reduce wide spaces 144 | $whitespace = preg_replace('{[ \t]+}', ' ', $token[1]); 145 | // normalize newlines to \n 146 | $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); 147 | // trim leading spaces 148 | $whitespace = preg_replace('{\n +}', "\n", $whitespace); 149 | $output .= $whitespace; 150 | } else { 151 | $output .= $token[1]; 152 | } 153 | } 154 | 155 | return $output; 156 | } 157 | 158 | private function getStub() 159 | { 160 | return <<<'EOF' 161 | #!/usr/bin/env php 162 | output = $output; 33 | } 34 | 35 | /** 36 | * @var string 37 | */ 38 | public function apply($patch) 39 | { 40 | if (empty($patch)) { 41 | return; 42 | } 43 | 44 | $this->output->writeln($patch); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Adapters/TokenReflection/StaticCodeAnalysis.php: -------------------------------------------------------------------------------- 1 | findMatchingMethod($file, $range); 39 | 40 | return $method ? $method->isStatic() : false; 41 | } 42 | 43 | public function getMethodEndLine(File $file, LineRange $range) 44 | { 45 | $method = $this->findMatchingMethod($file, $range); 46 | 47 | if ($method === null) { 48 | throw new \InvalidArgumentException("Could not find method end line."); 49 | } 50 | 51 | return $method->getEndLine(); 52 | } 53 | 54 | public function getMethodStartLine(File $file, LineRange $range) 55 | { 56 | $method = $this->findMatchingMethod($file, $range); 57 | 58 | if ($method === null) { 59 | throw new \InvalidArgumentException("Could not find method start line."); 60 | } 61 | 62 | return $method->getStartLine(); 63 | } 64 | 65 | public function getLineOfLastPropertyDefinedInScope(File $file, $lastLine) 66 | { 67 | $this->broker = new Broker(new Memory); 68 | $file = $this->broker->processString($file->getCode(), $file->getRelativePath(), true); 69 | 70 | foreach ($file->getNamespaces() as $namespace) { 71 | foreach ($namespace->getClasses() as $class) { 72 | $lastPropertyDefinitionLine = $class->getStartLine() + 1; 73 | 74 | foreach ($class->getMethods() as $method) { 75 | if ($method->getStartLine() < $lastLine && $lastLine < $method->getEndLine()) { 76 | foreach ($class->getProperties() as $property) { 77 | $lastPropertyDefinitionLine = max($lastPropertyDefinitionLine, $property->getEndLine()); 78 | } 79 | 80 | return $lastPropertyDefinitionLine; 81 | } 82 | } 83 | } 84 | } 85 | 86 | throw new \InvalidArgumentException("Could not find method start line."); 87 | } 88 | 89 | public function isInsideMethod(File $file, LineRange $range) 90 | { 91 | return $this->findMatchingMethod($file, $range) !== null; 92 | } 93 | 94 | /** 95 | * @param File $file 96 | * @return PhpClass[] 97 | */ 98 | public function findClasses(File $file) 99 | { 100 | $classes = array(); 101 | 102 | $this->broker = new Broker(new Memory); 103 | 104 | $file = $this->broker->processString($file->getCode(), $file->getRelativePath(), true); 105 | foreach ($file->getNamespaces() as $namespace) { 106 | $noNamespace = ReflectionNamespace::NO_NAMESPACE_NAME === $namespace->getName(); 107 | foreach ($namespace->getClasses() as $class) { 108 | $classes[] = new PhpClass( 109 | PhpName::createDeclarationName($class->getName()), 110 | $class->getStartLine(), 111 | $noNamespace ? 0 : $namespace->getStartLine() 112 | ); 113 | } 114 | } 115 | 116 | return $classes; 117 | } 118 | 119 | private function findMatchingMethod(File $file, LineRange $range) 120 | { 121 | $foundMethod = null; 122 | 123 | $this->broker = new Broker(new Memory); 124 | $file = $this->broker->processString($file->getCode(), $file->getRelativePath(), true); 125 | $lastLine = $range->getEnd(); 126 | 127 | foreach ($file->getNamespaces() as $namespace) { 128 | foreach ($namespace->getClasses() as $class) { 129 | foreach ($class->getMethods() as $method) { 130 | if ($method->getStartLine() < $lastLine && $lastLine < $method->getEndLine()) { 131 | $foundMethod = $method; 132 | break; 133 | } 134 | } 135 | } 136 | } 137 | 138 | return $foundMethod; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/ConvertLocalToInstanceVariable.php: -------------------------------------------------------------------------------- 1 | file = $file; 32 | $this->line = $line; 33 | $this->convertVariable = $convertVariable; 34 | 35 | $this->assertIsInsideMethod(); 36 | 37 | $this->startEditingSession(); 38 | $this->addProperty(); 39 | $this->convertVariablesToInstanceVariables(); 40 | $this->completeEditingSession(); 41 | } 42 | 43 | private function addProperty() 44 | { 45 | $line = $this->codeAnalysis->getLineOfLastPropertyDefinedInScope($this->file, $this->line); 46 | 47 | $this->session->addEdit( 48 | new AddProperty($line, $this->convertVariable->getName()) 49 | ); 50 | } 51 | 52 | private function convertVariablesToInstanceVariables() 53 | { 54 | $definedVariables = $this->getDefinedVariables(); 55 | 56 | if ( ! $definedVariables->contains($this->convertVariable)) { 57 | throw RefactoringException::variableNotInRange($this->convertVariable, $selectedMethodLineRange); 58 | } 59 | 60 | $this->session->addEdit(new LocalVariableToInstance($definedVariables, $this->convertVariable)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/ExtractMethod.php: -------------------------------------------------------------------------------- 1 | file = $file; 39 | $this->extractRange = $extractRange; 40 | 41 | $this->assertIsInsideMethod(); 42 | 43 | $this->createNewMethodSignature($newMethodName); 44 | 45 | $this->startEditingSession(); 46 | $this->replaceCodeWithMethodCall(); 47 | $this->addNewMethod(); 48 | $this->completeEditingSession(); 49 | } 50 | 51 | protected function assertIsInsideMethod() 52 | { 53 | if ( ! $this->codeAnalysis->isInsideMethod($this->file, $this->extractRange)) { 54 | throw RefactoringException::rangeIsNotInsideMethod($this->extractRange); 55 | } 56 | } 57 | 58 | private function createNewMethodSignature($newMethodName) 59 | { 60 | $extractVariables = $this->variableScanner->scanForVariables($this->file, $this->extractRange); 61 | $methodVariables = $this->variableScanner->scanForVariables($this->file, $this->findMethodRange()); 62 | 63 | $isStatic = $this->codeAnalysis->isMethodStatic($this->file, $this->extractRange); 64 | 65 | $this->newMethod = new MethodSignature( 66 | $newMethodName, 67 | $isStatic ? MethodSignature::IS_STATIC : 0, 68 | $methodVariables->variablesFromSelectionUsedBefore($extractVariables), 69 | $methodVariables->variablesFromSelectionUsedAfter($extractVariables) 70 | ); 71 | } 72 | 73 | private function addNewMethod() 74 | { 75 | $this->session->addEdit(new AddMethod( 76 | $this->findMethodRange()->getEnd(), 77 | $this->newMethod, 78 | $this->getSelectedCode() 79 | )); 80 | } 81 | 82 | private function replaceCodeWithMethodCall() 83 | { 84 | $this->session->addEdit(new ReplaceWithMethodCall( 85 | $this->extractRange, 86 | $this->newMethod 87 | )); 88 | } 89 | 90 | private function findMethodRange() 91 | { 92 | return $this->codeAnalysis->findMethodRange($this->file, $this->extractRange); 93 | } 94 | 95 | private function getSelectedCode() 96 | { 97 | return LineCollection::createFromArray( 98 | $this->extractRange->sliceCode($this->file->getCode()) 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/FixClassNames.php: -------------------------------------------------------------------------------- 1 | codeAnalysis = $codeAnalysis; 32 | $this->editor = $editor; 33 | $this->nameScanner = $nameScanner; 34 | } 35 | 36 | public function refactor(Directory $directory) 37 | { 38 | $phpFiles = $directory->findAllPhpFilesRecursivly(); 39 | 40 | $renames = new Set(); 41 | $occurances = array(); 42 | $noImportedUsages = new NoImportedUsagesFilter(); 43 | 44 | foreach ($phpFiles as $phpFile) { 45 | $classes = $this->codeAnalysis->findClasses($phpFile); 46 | 47 | $occurances = array_merge( 48 | $noImportedUsages->filter($this->nameScanner->findNames($phpFile)), 49 | $occurances 50 | ); 51 | 52 | if (count($classes) !== 1) { 53 | continue; 54 | } 55 | 56 | $class = $classes[0]; 57 | $currentClassName = $class->declarationName(); 58 | $expectedClassName = $phpFile->extractPsr0ClassName(); 59 | 60 | $buffer = $this->editor->openBuffer($phpFile); // This is weird to be required here 61 | 62 | if ($expectedClassName->shortName() !== $currentClassName->shortName()) { 63 | $renames->add(new PhpNameChange($currentClassName, $expectedClassName)); 64 | } 65 | 66 | if (!$expectedClassName->namespaceName()->equals($currentClassName->namespaceName())) { 67 | $renames->add(new PhpNameChange($currentClassName->fullyQualified(), $expectedClassName->fullyQualified())); 68 | 69 | $buffer->replaceString( 70 | $class->namespaceDeclarationLine(), 71 | $currentClassName->namespaceName()->fullyQualifiedName(), 72 | $expectedClassName->namespaceName()->fullyQualifiedName() 73 | ); 74 | } 75 | } 76 | 77 | $occurances = array_filter($occurances, function ($occurance) { 78 | return $occurance->name()->type() !== PhpName::TYPE_NAMESPACE; 79 | }); 80 | 81 | foreach ($occurances as $occurance) { 82 | $name = $occurance->name(); 83 | 84 | foreach ($renames as $rename) { 85 | if ($rename->affects($name)) { 86 | $buffer = $this->editor->openBuffer($occurance->file()); 87 | $buffer->replaceString($occurance->declarationLine(), $name->relativeName(), $rename->change($name)->relativeName()); 88 | continue 2; 89 | } 90 | } 91 | } 92 | 93 | $this->editor->save(); 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/OptimizeUse.php: -------------------------------------------------------------------------------- 1 | codeAnalysis = $codeAnalysis; 30 | $this->editor = $editor; 31 | $this->phpNameScanner = $phpNameScanner; 32 | } 33 | 34 | public function refactor(File $file) 35 | { 36 | $classes = $this->codeAnalysis->findClasses($file); 37 | $occurances = $this->phpNameScanner->findNames($file); 38 | $class = $classes[0]; 39 | 40 | $appendNewLine = 0 === $class->namespaceDeclarationLine(); 41 | $lastUseStatementLine = $class->namespaceDeclarationLine() + 2; 42 | $usedNames = array(); 43 | $fqcns = array(); 44 | 45 | foreach ($occurances as $occurance) { 46 | $name = $occurance->name(); 47 | 48 | if ($name->type() === PhpName::TYPE_NAMESPACE || $name->type() === PhpName::TYPE_CLASS) { 49 | continue; 50 | } 51 | 52 | if ($name->isUse()) { 53 | $lastUseStatementLine = $occurance->declarationLine(); 54 | $usedNames[] = $name->fullyQualifiedName(); 55 | } elseif ($name->isFullyQualified()) { 56 | $fqcns[] = $occurance; 57 | } 58 | } 59 | 60 | if (!$fqcns) { 61 | return; 62 | } 63 | 64 | $buffer = $this->editor->openBuffer($file); 65 | 66 | foreach ($fqcns as $occurance) { 67 | $name = $occurance->name(); 68 | $buffer->replaceString($occurance->declarationLine(), '\\'.$name->fullyQualifiedName(), $name->shortname()); 69 | 70 | if (!in_array($name->fullyQualifiedName(), $usedNames)) { 71 | $lines = array(sprintf('use %s;', $name->fullyQualifiedName())); 72 | if ($appendNewLine) { 73 | $appendNewLine = FALSE; 74 | $lines[] = ''; 75 | } 76 | 77 | $buffer->append($lastUseStatementLine, $lines); 78 | $lastUseStatementLine++; 79 | } 80 | } 81 | 82 | $this->editor->save(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/RenameLocalVariable.php: -------------------------------------------------------------------------------- 1 | file = $file; 38 | $this->line = $line; 39 | $this->newName = $newName; 40 | $this->oldName = $oldName; 41 | 42 | $this->assertIsInsideMethod(); 43 | 44 | $this->assertVariableIsLocal($this->oldName); 45 | $this->assertVariableIsLocal($this->newName); 46 | 47 | $this->startEditingSession(); 48 | $this->renameLocalVariable(); 49 | $this->completeEditingSession(); 50 | } 51 | 52 | private function assertVariableIsLocal(Variable $variable) 53 | { 54 | if ( ! $variable->isLocal()) { 55 | throw RefactoringException::variableNotLocal($variable); 56 | } 57 | } 58 | 59 | private function renameLocalVariable() 60 | { 61 | $definedVariables = $this->getDefinedVariables(); 62 | 63 | if ( ! $definedVariables->contains($this->oldName)) { 64 | throw RefactoringException::variableNotInRange($this->oldName, $selectedMethodLineRange); 65 | } 66 | 67 | $this->session->addEdit(new RenameVariable($definedVariables, $this->oldName, $this->newName)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Application/SingleFileRefactoring.php: -------------------------------------------------------------------------------- 1 | variableScanner = $variableScanner; 51 | $this->codeAnalysis = $codeAnalysis; 52 | $this->editor = $editor; 53 | } 54 | 55 | protected function assertIsInsideMethod() 56 | { 57 | if ( ! $this->codeAnalysis->isInsideMethod($this->file, LineRange::fromSingleLine($this->line))) { 58 | throw RefactoringException::rangeIsNotInsideMethod(LineRange::fromSingleLine($this->line)); 59 | } 60 | } 61 | 62 | protected function startEditingSession() 63 | { 64 | $buffer = $this->editor->openBuffer($this->file); 65 | 66 | $this->session = new EditingSession($buffer); 67 | } 68 | 69 | protected function completeEditingSession() 70 | { 71 | $this->session->performEdits(); 72 | 73 | $this->editor->save(); 74 | } 75 | 76 | protected function getDefinedVariables() 77 | { 78 | $selectedMethodLineRange = $this->codeAnalysis->findMethodRange($this->file, LineRange::fromSingleLine($this->line)); 79 | 80 | $definedVariables = $this->variableScanner->scanForVariables( 81 | $this->file, $selectedMethodLineRange 82 | ); 83 | 84 | return $definedVariables; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/DefinedVariables.php: -------------------------------------------------------------------------------- 1 | readAccess = $readAccess; 41 | $this->changeAccess = $changeAccess; 42 | } 43 | 44 | public function read() 45 | { 46 | return array_keys($this->readAccess); 47 | } 48 | 49 | public function changed() 50 | { 51 | return array_keys($this->changeAccess); 52 | } 53 | 54 | public function all() 55 | { 56 | $all = $this->readAccess; 57 | 58 | foreach ($this->changeAccess as $name => $lines) { 59 | if ( ! isset($all[$name])) { 60 | $all[$name] = array(); 61 | } 62 | 63 | $all[$name] = array_unique(array_merge($all[$name], $lines)); 64 | 65 | sort($all[$name]); 66 | } 67 | 68 | return $all; 69 | } 70 | 71 | /** 72 | * Does list contain the given variable? 73 | * 74 | * @return bool 75 | */ 76 | public function contains(Variable $variable) 77 | { 78 | return ( 79 | isset($this->readAccess[$variable->getName()]) || 80 | isset($this->changeAccess[$variable->getName()]) 81 | ); 82 | } 83 | 84 | public function variablesFromSelectionUsedAfter(DefinedVariables $selection) 85 | { 86 | return $this->filterVariablesFromSelection( 87 | $selection->changed(), 88 | $selection, 89 | function ($lastUsedLine, $endLine) { 90 | return $lastUsedLine > $endLine; 91 | }, 'max'); 92 | } 93 | 94 | public function variablesFromSelectionUsedBefore(DefinedVariables $selection) 95 | { 96 | return $this->filterVariablesFromSelection( 97 | $selection->read(), 98 | $selection, 99 | function ($lastUsedLine, $endLine) { 100 | return $lastUsedLine < $endLine; 101 | }, 'min'); 102 | } 103 | 104 | private function filterVariablesFromSelection($selectedVariables, DefinedVariables $selection, Closure $filter, $reducer) 105 | { 106 | $variablesUsed = array(); 107 | 108 | $compareLine = $reducer == 'max' 109 | ? $selection->endLine() 110 | : $selection->startLine(); 111 | $knownVariables = $this->all(); 112 | 113 | foreach ($selectedVariables as $variable) { 114 | if ( ! isset($knownVariables[$variable])) { 115 | continue; 116 | } 117 | 118 | $lastUsedLine = $reducer($knownVariables[$variable]); 119 | 120 | if ($filter($lastUsedLine, $compareLine)) { 121 | $variablesUsed[] = $variable; 122 | } 123 | } 124 | 125 | return $variablesUsed; 126 | } 127 | 128 | private function endLine() 129 | { 130 | if (!$this->readAccess && !$this->changeAccess) { 131 | return 0; 132 | } 133 | 134 | return max(array_merge(array_map('max', $this->readAccess), array_map('max', $this->changeAccess))); 135 | } 136 | 137 | private function startLine() 138 | { 139 | if (!$this->readAccess && !$this->changeAccess) { 140 | return 0; 141 | } 142 | 143 | return min(array_merge(array_map('min', $this->readAccess), array_map('min', $this->changeAccess))); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/Directory.php: -------------------------------------------------------------------------------- 1 | paths = $paths; 46 | $this->workingDirectory = $workingDirectory; 47 | } 48 | 49 | /** 50 | * @return File[] 51 | */ 52 | public function findAllPhpFilesRecursivly() 53 | { 54 | $workingDirectory = $this->workingDirectory; 55 | 56 | $iterator = new AppendIterator; 57 | 58 | foreach ($this->paths as $path) { 59 | $iterator->append( 60 | new CallbackTransformIterator( 61 | new CallbackFilterIterator( 62 | new RecursiveIteratorIterator( 63 | new RecursiveDirectoryIterator($path), 64 | RecursiveIteratorIterator::LEAVES_ONLY 65 | ), 66 | function (SplFileInfo $file) { 67 | return substr($file->getFilename(), -4) === ".php"; 68 | } 69 | ), 70 | function ($file) use ($workingDirectory) { 71 | return File::createFromPath($file->getPathname(), $workingDirectory); 72 | } 73 | ) 74 | ); 75 | } 76 | 77 | $files = iterator_to_array($iterator); 78 | return new StandardCallbackFilterIterator($iterator, function($file, $filename) use ($files) { 79 | return !in_array($filename, $files); 80 | }); 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingAction.php: -------------------------------------------------------------------------------- 1 | lineNumber = $lineNumber; 45 | $this->newMethod = $newMethod; 46 | $this->selectedCode = $selectedCode; 47 | } 48 | 49 | public function performEdit(EditorBuffer $buffer) 50 | { 51 | $this->newCode = new IndentingLineCollection(); 52 | 53 | $this->newCode->addIndentation(); 54 | 55 | $this->addMethodOpening(); 56 | $this->addMethodBody(); 57 | $this->addReturnStatement(); 58 | $this->addMethodClosing(); 59 | 60 | $buffer->append($this->lineNumber, $this->getNewCodeAsStringArray()); 61 | } 62 | 63 | private function addMethodOpening() 64 | { 65 | $this->newCode->appendBlankLine(); 66 | 67 | $this->newCode->appendString($this->getNewMethodSignatureString()); 68 | $this->newCode->appendString('{'); 69 | 70 | $this->newCode->addIndentation(); 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | private function getNewMethodSignatureString() 77 | { 78 | return sprintf( 79 | 'private %sfunction %s(%s)', 80 | ($this->newMethod->isStatic() ? 'static ' : ''), 81 | $this->newMethod->getName(), 82 | $this->createVariableList($this->newMethod->arguments()) 83 | ); 84 | } 85 | 86 | /** 87 | * @param string[] $variables 88 | * 89 | * @return string 90 | */ 91 | private function createVariableList(array $variables) 92 | { 93 | return implode(', ', array_map(function ($variableName) { 94 | return '$' . $variableName; 95 | }, $variables)); 96 | } 97 | 98 | private function addMethodBody() 99 | { 100 | $this->newCode->appendLines($this->getUnindentedSelectedCode()); 101 | } 102 | 103 | private function getUnindentedSelectedCode() 104 | { 105 | $detector = new IndentationDetector($this->selectedCode); 106 | 107 | $lines = array_map(function ($line) use ($detector) { 108 | return substr($line, $detector->getMinIndentation()); 109 | }, iterator_to_array(new ToStringIterator($this->selectedCode->getIterator()))); 110 | 111 | return LineCollection::createFromArray($lines); 112 | } 113 | 114 | private function addReturnStatement() 115 | { 116 | $returnVars = $this->newMethod->returnVariables(); 117 | 118 | $numVariables = count($returnVars); 119 | 120 | if ($numVariables === 0) { 121 | return; 122 | } 123 | 124 | $returnVariable = '$' . reset($returnVars); 125 | 126 | if ($numVariables > 1) { 127 | $returnVariable = 'array(' . $this->createVariableList($returnVars) . ')'; 128 | } 129 | 130 | $this->newCode->appendBlankLine(); 131 | $this->newCode->appendString('return ' . $returnVariable . ';'); 132 | } 133 | 134 | private function addMethodClosing() 135 | { 136 | $this->newCode->removeIndentation(); 137 | $this->newCode->appendString('}'); 138 | } 139 | 140 | 141 | /** 142 | * @return string[] 143 | */ 144 | private function getNewCodeAsStringArray() 145 | { 146 | $toString = new ToStringIterator($this->newCode->getIterator()); 147 | 148 | return iterator_to_array($toString); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingAction/AddProperty.php: -------------------------------------------------------------------------------- 1 | line = $line; 27 | $this->propertyName = $propertyName; 28 | } 29 | 30 | public function performEdit(EditorBuffer $buffer) 31 | { 32 | $buffer->append($this->line, array( 33 | ' private $' . $this->propertyName . ';', 34 | '' 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingAction/LocalVariableToInstance.php: -------------------------------------------------------------------------------- 1 | definedVars = $definedVars; 25 | $this->variable = $variable; 26 | } 27 | 28 | public function performEdit(EditorBuffer $buffer) 29 | { 30 | $renamer = new RenameVariable( 31 | $this->definedVars, 32 | $this->variable, 33 | $this->variable->convertToInstance() 34 | ); 35 | 36 | $renamer->performEdit($buffer); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingAction/RenameVariable.php: -------------------------------------------------------------------------------- 1 | definedVars = $definedVars; 30 | $this->oldName = $oldName; 31 | $this->newName = $newName; 32 | } 33 | 34 | public function performEdit(EditorBuffer $buffer) 35 | { 36 | foreach ($this->getLinesVariableIsUsedOn() as $line) { 37 | $buffer->replaceString( 38 | $line, 39 | $this->oldName->getToken(), 40 | $this->newName->getToken() 41 | ); 42 | } 43 | } 44 | 45 | /** 46 | * @return int[] 47 | */ 48 | private function getLinesVariableIsUsedOn() 49 | { 50 | $variables = $this->definedVars->all(); 51 | $variableName = $this->oldName->getName(); 52 | 53 | $lines = array(); 54 | 55 | if (isset($variables[$variableName])) { 56 | $lines = $variables[$variableName]; 57 | } 58 | 59 | return $lines; 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingAction/ReplaceWithMethodCall.php: -------------------------------------------------------------------------------- 1 | range = $range; 27 | $this->newMethod = $newMethod; 28 | } 29 | 30 | public function performEdit(EditorBuffer $buffer) 31 | { 32 | $extractedCode = $buffer->getLines($this->range); 33 | 34 | $buffer->replace($this->range, array($this->getIndent($extractedCode) . $this->getMethodCall())); 35 | } 36 | 37 | /** 38 | * @param string[] $lines 39 | * 40 | * @return string 41 | */ 42 | private function getIndent(array $lines) 43 | { 44 | $detector = new IndentationDetector( 45 | LineCollection::createFromArray($lines) 46 | ); 47 | 48 | return str_repeat(' ', $detector->getFirstLineIndentation()); 49 | } 50 | 51 | private function getMethodCall() 52 | { 53 | return sprintf( 54 | '%s%s%s(%s);', 55 | $this->getReturnVariables(), 56 | ($this->newMethod->isStatic() ? 'self::' : '$this->'), 57 | $this->newMethod->getName(), 58 | $this->createVariableList($this->newMethod->arguments()) 59 | ); 60 | } 61 | 62 | private function getReturnVariables() 63 | { 64 | $returnVars = $this->newMethod->returnVariables(); 65 | 66 | $numVariables = count($returnVars); 67 | 68 | if ($numVariables === 0) { 69 | return; 70 | } 71 | 72 | $returnVariable = '$' . reset($returnVars); 73 | 74 | if ($numVariables > 1) { 75 | $returnVariable = 'list(' . $this->createVariableList($returnVars) . ')'; 76 | } 77 | 78 | return $returnVariable . ' = '; 79 | } 80 | 81 | /** 82 | * @param string[] $variables 83 | * 84 | * @return string 85 | */ 86 | private function createVariableList(array $variables) 87 | { 88 | return implode(', ', array_map(function ($variableName) { 89 | return '$' . $variableName; 90 | }, $variables)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditingSession.php: -------------------------------------------------------------------------------- 1 | buffer = $buffer; 32 | } 33 | 34 | public function addEdit(EditingAction $action) 35 | { 36 | $this->actions[] = $action; 37 | } 38 | 39 | public function performEdits() 40 | { 41 | foreach ($this->actions as $action) { 42 | $action->performEdit($this->buffer); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/EditorBuffer.php: -------------------------------------------------------------------------------- 1 | relativePath = $relativePath; 51 | $this->code = $code; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getRelativePath() 58 | { 59 | return $this->relativePath; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getCode() 66 | { 67 | return $this->code; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getBaseName() 74 | { 75 | return basename($this->relativePath); 76 | } 77 | 78 | /** 79 | * Extract the PhpName for the class contained in this file assuming PSR-0 naming. 80 | * 81 | * @return PhpName 82 | */ 83 | public function extractPsr0ClassName() 84 | { 85 | $shortName = $this->parseFileForPsr0ClassShortName(); 86 | 87 | return new PhpName( 88 | ltrim($this->parseFileForPsr0NamespaceName() . '\\' . $shortName, '\\'), 89 | $shortName 90 | ); 91 | } 92 | 93 | private function parseFileForPsr0ClassShortName() 94 | { 95 | return str_replace(".php", "", $this->getBaseName()); 96 | } 97 | 98 | private function parseFileForPsr0NamespaceName() 99 | { 100 | $file = ltrim($this->getRelativePath(), DIRECTORY_SEPARATOR); 101 | 102 | $separator = DIRECTORY_SEPARATOR; 103 | if (preg_match('(^([a-z]+:\/\/))', $file, $matches)) { 104 | $file = substr($file, strlen($matches[1])); 105 | $separator = '/'; 106 | } 107 | 108 | $parts = explode($separator, $file); 109 | $namespace = array(); 110 | 111 | foreach ($parts as $part) { 112 | if ($this->startsWithLowerCase($part)) { 113 | $namespace = array(); 114 | continue; 115 | } 116 | 117 | $namespace[] = $part; 118 | } 119 | 120 | array_pop($namespace); 121 | 122 | return str_replace(".php", "", implode("\\", $namespace)); 123 | } 124 | 125 | private function startsWithLowerCase($string) 126 | { 127 | return isset($string[0]) && strtolower($string[0]) === $string[0]; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/IndentationDetector.php: -------------------------------------------------------------------------------- 1 | lines = $lines; 15 | } 16 | 17 | /** 18 | * @return int 19 | */ 20 | public function getMinIndentation() 21 | { 22 | return array_reduce( 23 | iterator_to_array($this->lines), 24 | function ($minIndentation, $line) { 25 | $indentation = $line->getIndentation(); 26 | 27 | if ($line->isEmpty()) { 28 | return $minIndentation; 29 | } 30 | 31 | if ($minIndentation === null) { 32 | return $indentation; 33 | } 34 | 35 | return min($minIndentation, $indentation); 36 | } 37 | ); 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getFirstLineIndentation() 44 | { 45 | $indentation = null; 46 | 47 | foreach ($this->lines as $line) { 48 | if (!$line->isEmpty()) { 49 | $indentation = $line->getIndentation(); 50 | break; 51 | } 52 | } 53 | 54 | return $indentation; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/IndentingLineCollection.php: -------------------------------------------------------------------------------- 1 | indentation++; 17 | } 18 | 19 | public function removeIndentation() 20 | { 21 | $this->indentation--; 22 | } 23 | 24 | public function append(Line $line) 25 | { 26 | parent::append(new Line($this->createIndentationString() . (string) $line)); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | private function createIndentationString() 33 | { 34 | return str_repeat(' ', $this->indentation * self::INDENTATION_SIZE); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/Line.php: -------------------------------------------------------------------------------- 1 | line = (string) $line; 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function __toString() 24 | { 25 | return $this->line; 26 | } 27 | 28 | /** 29 | * @return bool 30 | */ 31 | public function isEmpty() 32 | { 33 | return trim($this->line) === ''; 34 | } 35 | 36 | /** 37 | * @return int 38 | */ 39 | public function getIndentation() 40 | { 41 | return strlen($this->line) - strlen(ltrim($this->line)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/LineCollection.php: -------------------------------------------------------------------------------- 1 | lines = $lines; 21 | } 22 | 23 | public function getIterator() 24 | { 25 | return new ArrayIterator($this->lines); 26 | } 27 | 28 | /** 29 | * @return Line[] 30 | */ 31 | public function getLines() 32 | { 33 | return $this->lines; 34 | } 35 | 36 | public function append(Line $line) 37 | { 38 | $this->lines[] = $line; 39 | } 40 | 41 | /** 42 | * @param string $line 43 | */ 44 | public function appendString($line) 45 | { 46 | $this->append(new Line($line)); 47 | } 48 | 49 | public function appendLines(LineCollection $lines) 50 | { 51 | foreach ($lines as $line) { 52 | $this->append($line); 53 | } 54 | } 55 | 56 | public function appendBlankLine() 57 | { 58 | $this->lines[] = new Line(''); 59 | } 60 | 61 | /** 62 | * @param string[] $lines 63 | * 64 | * @return LineCollection 65 | */ 66 | public static function createFromArray(array $lines) 67 | { 68 | return new self(array_map(function ($line) { 69 | return new Line($line); 70 | }, $lines)); 71 | } 72 | 73 | /** 74 | * @param string $code 75 | * 76 | * @return LineCollection 77 | */ 78 | public static function createFromString($code) 79 | { 80 | return self::createFromArray(explode("\n", $code)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/LineRange.php: -------------------------------------------------------------------------------- 1 | lines[$line] = $line; 31 | 32 | return $list; 33 | } 34 | 35 | /** 36 | * @return LineRange 37 | */ 38 | static public function fromLines($start, $end) 39 | { 40 | $list = new self(); 41 | 42 | for ($i = $start; $i <= $end; $i++) { 43 | $list->lines[$i] = $i; 44 | } 45 | 46 | return $list; 47 | } 48 | 49 | /** 50 | * @return LineRange 51 | */ 52 | static public function fromString($range) 53 | { 54 | list($start, $end) = explode("-", $range); 55 | 56 | return self::fromLines($start, $end); 57 | } 58 | 59 | public function isInRange($line) 60 | { 61 | return isset($this->lines[$line]); 62 | } 63 | 64 | public function getStart() 65 | { 66 | return (int)min($this->lines); 67 | } 68 | 69 | public function getEnd() 70 | { 71 | return (int)max($this->lines); 72 | } 73 | 74 | public function sliceCode($code) 75 | { 76 | $selectedCode = explode("\n", $code); 77 | $numLines = count($selectedCode); 78 | 79 | for ($i = 0; $i < $numLines; $i++) { 80 | if ( ! $this->isInRange($i+1)) { 81 | unset($selectedCode[$i]); 82 | } 83 | } 84 | 85 | return array_values($selectedCode); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/MethodSignature.php: -------------------------------------------------------------------------------- 1 | name = $name; 35 | $this->flags = $this->change($flags); 36 | $this->arguments = $arguments; 37 | $this->returnVariables = $returnVariables; 38 | } 39 | 40 | private function change($flags) 41 | { 42 | $visibility = (self::IS_PRIVATE | self::IS_PROTECTED | self::IS_PUBLIC); 43 | $allowedVisibilities = array(self::IS_PRIVATE, self::IS_PROTECTED, self::IS_PUBLIC); 44 | 45 | if (($flags & $visibility) === 0) { 46 | $flags = $flags | self::IS_PRIVATE; 47 | } 48 | 49 | if ( ! in_array(($flags & $visibility), $allowedVisibilities)) { 50 | throw new \InvalidArgumentException("Mix of visibilities is not allowed."); 51 | } 52 | 53 | return $flags; 54 | } 55 | 56 | public function getName() 57 | { 58 | return $this->name; 59 | } 60 | 61 | /** 62 | * Is this method private? 63 | * 64 | * @return bool 65 | */ 66 | public function isPrivate() 67 | { 68 | return ($this->flags & self::IS_PRIVATE) > 0; 69 | } 70 | 71 | /** 72 | * Is this method static? 73 | * 74 | * @return bool 75 | */ 76 | public function isStatic() 77 | { 78 | return ($this->flags & self::IS_STATIC) > 0; 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function returnVariables() 85 | { 86 | return $this->returnVariables; 87 | } 88 | 89 | /** 90 | * @return array 91 | */ 92 | public function arguments() 93 | { 94 | return $this->arguments; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/PhpClass.php: -------------------------------------------------------------------------------- 1 | declarationName = $declarationName; 39 | $this->declarationLine = $declarationLine; 40 | $this->namespaceDeclarationLine = $namespaceDeclarationLine; 41 | } 42 | 43 | /** 44 | * PhpName for the declaration of this class. 45 | * 46 | * @return PhpName 47 | */ 48 | public function declarationName() 49 | { 50 | return $this->declarationName; 51 | } 52 | 53 | public function declarationLine() 54 | { 55 | return $this->declarationLine; 56 | } 57 | 58 | public function namespaceDeclarationLine() 59 | { 60 | return $this->namespaceDeclarationLine; 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/PhpNameChange.php: -------------------------------------------------------------------------------- 1 | fromName = $fromName; 26 | $this->toName = $toName; 27 | } 28 | 29 | public function affects(PhpName $name) 30 | { 31 | return $name->isAffectedByChangesTo($this->fromName); 32 | } 33 | 34 | public function change(PhpName $name) 35 | { 36 | return $name->change($this->fromName, $this->toName); 37 | } 38 | 39 | public function hashCode() 40 | { 41 | return "1373136290" . $this->fromName->hashCode() . $this->toName->hashCode(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/PhpNameOccurance.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->file = $file; 38 | $this->declarationLine = $declarationLine; 39 | } 40 | 41 | public function name() 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function declarationLine() 47 | { 48 | return $this->declarationLine; 49 | } 50 | 51 | public function file() 52 | { 53 | return $this->file; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/PhpNames/NoImportedUsagesFilter.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @return array 16 | */ 17 | public function filter(array $phpNames) 18 | { 19 | $fileUseOccurances = array_flip( 20 | array_map( 21 | function ($useOccurance) { 22 | return $useOccurance->name()->fullyQualifiedName(); 23 | }, 24 | array_filter( 25 | $phpNames, 26 | function ($occurance) { 27 | return $occurance->name()->type() === PhpName::TYPE_USE; 28 | } 29 | ) 30 | ) 31 | ); 32 | 33 | return array_filter( 34 | $phpNames, 35 | function ($occurance) use ($fileUseOccurances) { 36 | return ( 37 | $occurance->name()->type() !== PhpName::TYPE_USAGE || 38 | !isset($fileUseOccurances[$occurance->name()->fullyQualifiedName()]) 39 | ); 40 | } 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/RefactoringException.php: -------------------------------------------------------------------------------- 1 | getToken(), $range->getStart(), $range->getEnd() 30 | )); 31 | } 32 | 33 | static public function variableNotLocal(Variable $variable) 34 | { 35 | return new self(sprintf( 36 | 'Given variable "%s" is required to be local to the current method.', 37 | $variable->getToken() 38 | )); 39 | } 40 | 41 | static public function rangeIsNotInsideMethod(LineRange $range) 42 | { 43 | return new self(sprintf( 44 | 'The range %d-%d is not inside one single method.', 45 | $range->getStart(), $range->getEnd() 46 | )); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/UseStatement.php: -------------------------------------------------------------------------------- 1 | file = $file; 32 | $this->declaredLines = $declaredLines; 33 | } 34 | 35 | public function getEndLine() 36 | { 37 | return $this->declaredLines->getEnd(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Model/Variable.php: -------------------------------------------------------------------------------- 1 | name = ltrim($name, '$'); 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getName() 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getToken() 45 | { 46 | return '$' . $this->name; 47 | } 48 | 49 | /** 50 | * @return bool 51 | */ 52 | public function isLocal() 53 | { 54 | return ! $this->isInstance(); 55 | } 56 | 57 | /** 58 | * @return bool 59 | */ 60 | public function isInstance() 61 | { 62 | return strpos($this->name, 'this->') === 0; 63 | } 64 | 65 | /** 66 | * Create a new variable of the local variable that is an instance variable. 67 | */ 68 | public function convertToInstance() 69 | { 70 | if ( ! $this->isLocal()) { 71 | throw RefactoringException::variableNotLocal($this); 72 | } 73 | 74 | return new Variable('$this->' . $this->name); 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Services/CodeAnalysis.php: -------------------------------------------------------------------------------- 1 | getMethodStartLine($file, $range); 78 | $methodEndLine = $this->getMethodEndLine($file, $range); 79 | 80 | return LineRange::fromLines($methodStartLine, $methodEndLine); 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Domain/Services/Editor.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 32 | } 33 | 34 | public function accept() 35 | { 36 | $filter = $this->filter; 37 | return $filter($this->getInnerIterator()->current()); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Utils/CallbackTransformIterator.php: -------------------------------------------------------------------------------- 1 | transformer = $transformer; 24 | } 25 | 26 | protected function transform($value) 27 | { 28 | $transformer = $this->transformer; 29 | return $transformer($value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Utils/ToStringIterator.php: -------------------------------------------------------------------------------- 1 | iterator = $it; 17 | } 18 | 19 | /** 20 | * @return string 21 | */ 22 | public function current() 23 | { 24 | return (string) $this->iterator->current(); 25 | } 26 | 27 | /** 28 | * @return scalar 29 | */ 30 | public function key() 31 | { 32 | return $this->iterator->key(); 33 | } 34 | 35 | public function next() 36 | { 37 | $this->iterator->next(); 38 | } 39 | 40 | public function rewind() 41 | { 42 | $this->iterator->rewind(); 43 | } 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function valid() 49 | { 50 | return $this->iterator->valid(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Utils/TransformIterator.php: -------------------------------------------------------------------------------- 1 | iterator = $iterator; 29 | } 30 | 31 | abstract protected function transform($value); 32 | 33 | public function next() 34 | { 35 | return $this->iterator->next(); 36 | } 37 | 38 | public function valid() 39 | { 40 | return $this->iterator->valid(); 41 | } 42 | 43 | public function current() 44 | { 45 | return $this->transform($this->iterator->current()); 46 | } 47 | 48 | public function rewind() 49 | { 50 | return $this->iterator->rewind(); 51 | } 52 | 53 | public function key() 54 | { 55 | return $this->iterator->key(); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Utils/ValueObject.php: -------------------------------------------------------------------------------- 1 | $name)) { 32 | throw new \RuntimeException(sprintf("Variable %s does not exist on %s.", $name, get_class($this))); 33 | } 34 | 35 | return $this->$name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/QafooLabs/Refactoring/Version.php: -------------------------------------------------------------------------------- 1 | add($item); 16 | $set->add($item); 17 | 18 | $this->assertEquals(1, count($set)); 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function whenAddingMultipleItems_ThenCountThemUniquely() 25 | { 26 | $item1 = 'A'; 27 | $item2 = 'B'; 28 | 29 | $set = new Set(); 30 | $set->add($item1); 31 | $set->add($item1); 32 | $set->add($item2); 33 | 34 | $this->assertEquals(2, count($set)); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function whenAddingHashableObjectMultipleTimes_ThenOnlyAddItOnce() 41 | { 42 | $item1 = new FooObject(1); 43 | $item2 = new FooObject(2); 44 | 45 | $set = new Set(); 46 | $set->add($item1); 47 | $set->add($item1); 48 | $set->add($item2); 49 | $set->add($item2); 50 | 51 | $this->assertEquals(2, count($set)); 52 | } 53 | 54 | /** 55 | * @test 56 | */ 57 | public function whenIteratingOverSet_ThenReturnAllUniqueItems() 58 | { 59 | $item1 = 'A'; 60 | $item2 = 'B'; 61 | 62 | $set = new Set(); 63 | $set->add($item1); 64 | $set->add($item2); 65 | 66 | $values = array(); 67 | 68 | foreach ($set as $key => $value) { 69 | $values[$key] = $value; 70 | } 71 | 72 | $this->assertEquals(array(0 => 'A', 1 => 'B'), $values); 73 | } 74 | } 75 | 76 | class FooObject implements Hashable 77 | { 78 | private $value; 79 | 80 | public function __construct($value) 81 | { 82 | $this->value = $value; 83 | } 84 | 85 | public function hashCode() 86 | { 87 | return md5($this->value); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Adapters/PHPParser/ParserPhpNameScannerTest.php: -------------------------------------------------------------------------------- 1 | findNames($file); 16 | 17 | $this->assertEquals( 18 | array( 19 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Adapters\PHPParser', 'QafooLabs\Refactoring\Adapters\PHPParser', PhpName::TYPE_NAMESPACE), $file, 3), 20 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Domain\Model\File', 'QafooLabs\Refactoring\Domain\Model\File', PhpName::TYPE_USE), $file, 5), 21 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Domain\Model\PhpName', 'QafooLabs\Refactoring\Domain\Model\PhpName', PhpName::TYPE_USE), $file, 6), 22 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Domain\Model\PhpNameOccurance', 'QafooLabs\Refactoring\Domain\Model\PhpNameOccurance', PhpName::TYPE_USE), $file, 7), 23 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Adapters\PHPParser\ParserPhpNameScannerTest', 'ParserPhpNameScannerTest', PhpName::TYPE_CLASS), $file, 9), 24 | new PhpNameOccurance(new PhpName('PHPUnit_Framework_TestCase', 'PHPUnit_Framework_TestCase', PhpName::TYPE_USAGE), $file, 9), 25 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Domain\Model\File', 'File', PhpName::TYPE_USAGE), $file, 13), 26 | new PhpNameOccurance(new PhpName('QafooLabs\Refactoring\Adapters\PHPParser\ParserPhpNameScanner', 'ParserPhpNameScanner', PhpName::TYPE_USAGE), $file, 14), 27 | ), 28 | array_slice($names, 0, 8) 29 | ); 30 | } 31 | 32 | public function testRegressionFindNamesDetectsFQCNCorrectly() 33 | { 34 | $file = new File("Fqcn.php", <<<'PHP' 35 | findNames($file), 52 | function ($occurance) { 53 | return $occurance->name()->type() === PhpName::TYPE_USAGE; 54 | } 55 | )); 56 | 57 | $this->assertEquals( 58 | array( 59 | new PhpNameOccurance( 60 | new PhpName('Bar\Qux\Adapter', 'Bar\Qux\Adapter'), 61 | $file, 62 | 9 63 | ) 64 | ), 65 | $names 66 | ); 67 | } 68 | 69 | 70 | public function testFindNamesFindsParentForPhpNameInSingleLineUseStatement() 71 | { 72 | $file = new File("Fqcn.php", <<<'PHP' 73 | findNames($file); 81 | 82 | $this->assertEquals( 83 | array( 84 | new PhpNameOccurance( 85 | new PhpName( 86 | 'Bar\Qux\Adapter', 87 | 'Bar\Qux\Adapter', 88 | PhpName::TYPE_USE 89 | ), 90 | $file, 91 | 3 92 | ) 93 | ), 94 | $names 95 | ); 96 | } 97 | 98 | public function testFindNamesFindsParentForPhpNameInMultiLineUseStatement() 99 | { 100 | $file = new File("Fqcn.php", <<<'PHP' 101 | findNames($file); 110 | 111 | $this->assertEquals( 112 | array( 113 | new PhpNameOccurance( 114 | new PhpName('Bar\Qux\Adapter', 'Bar\Qux\Adapter', PhpName::TYPE_USE), 115 | $file, 116 | 3 117 | ), 118 | new PhpNameOccurance( 119 | new PhpName('Bar\Qux\Foo', 'Bar\Qux\Foo', PhpName::TYPE_USE), 120 | $file, 121 | 4 122 | ) 123 | ), 124 | $names 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/LineRangeStatementCollectorTest.php: -------------------------------------------------------------------------------- 1 | statements('$this->foo(bar(baz()));'); 20 | 21 | $collector = new LineRangeStatementCollector($this->range("2-2")); 22 | 23 | $this->traverse($stmts, $collector); 24 | 25 | $collectedStatements = $collector->getStatements(); 26 | 27 | $this->assertCount(1, $collectedStatements); 28 | $this->assertInstanceOf('PHPParser_Node_Expr_MethodCall', $collectedStatements[0]); 29 | } 30 | 31 | private function traverse($stmts, $visitor) 32 | { 33 | $this->connect($stmts); 34 | 35 | $traverser = new PHPParser_NodeTraverser; 36 | $traverser->addVisitor(new NodeConnector); 37 | $traverser->addVisitor($visitor); 38 | $traverser->traverse($stmts); 39 | 40 | return $stmts; 41 | } 42 | 43 | private function connect($stmts) 44 | { 45 | $traverser = new PHPParser_NodeTraverser; 46 | $traverser->addVisitor(new NodeConnector); 47 | return $traverser->traverse($stmts); 48 | } 49 | 50 | private function range($range) 51 | { 52 | return LineRange::fromString($range); 53 | } 54 | 55 | private function statements($code) 56 | { 57 | if (strpos($code, "parse($code); 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Adapters/PHPParser/Visitor/LocalVariableClassifierTest.php: -------------------------------------------------------------------------------- 1 | enterNode($variable); 16 | 17 | $this->assertEquals(array('foo' => array(-1)), $classifier->getLocalVariables()); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function givenAssignment_WhenClassification_ThenAssignmentFound() 24 | { 25 | $classifier = new LocalVariableClassifier(); 26 | $assign = new \PHPParser_Node_Expr_Assign( 27 | new \PHPParser_Node_Expr_Variable("foo"), 28 | new \PHPParser_Node_Expr_Variable("bar") 29 | ); 30 | 31 | $classifier->enterNode($assign); 32 | 33 | $this->assertEquals(array('foo' => array(-1)), $classifier->getAssignments()); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function givenAssignmentAndReadOfSameVariable_WhenClassification_ThenFindBoth() 40 | { 41 | $classifier = new LocalVariableClassifier(); 42 | $assign = new \PHPParser_Node_Expr_Assign( 43 | new \PHPParser_Node_Expr_Variable("foo"), 44 | new \PHPParser_Node_Expr_Variable("foo") 45 | ); 46 | 47 | $traverser = new \PHPParser_NodeTraverser; 48 | $traverser->addVisitor($classifier); 49 | $traverser->traverse(array($assign)); 50 | 51 | $this->assertEquals(array('foo' => array(-1)), $classifier->getAssignments()); 52 | $this->assertEquals(array('foo' => array(-1)), $classifier->getLocalVariables()); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function givenThisVariable_WhenClassification_ThenNoLocalVariables() 59 | { 60 | $classifier = new LocalVariableClassifier(); 61 | $variable = new \PHPParser_Node_Expr_Variable("this"); 62 | 63 | $classifier->enterNode($variable); 64 | 65 | $this->assertEquals(array(), $classifier->getLocalVariables()); 66 | } 67 | 68 | /** 69 | * @test 70 | */ 71 | public function givenParam_WhenClassification_FindAsAssignment() 72 | { 73 | $classifier = new LocalVariableClassifier(); 74 | $variable = new \PHPParser_Node_Param("foo"); 75 | 76 | $classifier->enterNode($variable); 77 | 78 | $this->assertEquals(array('foo' => array(-1)), $classifier->getAssignments()); 79 | } 80 | 81 | /** 82 | * @test 83 | * @group GH-4 84 | */ 85 | public function givenArrayDimFetchASsignment_WhenClassification_FindAsAssignmentAndRead() 86 | { 87 | $classifier = new LocalVariableClassifier(); 88 | 89 | $assign = new \PHPParser_Node_Expr_Assign( 90 | new \PHPParser_Node_Expr_ArrayDimFetch( 91 | new \PHPParser_Node_Expr_Variable("foo") 92 | ), 93 | new \PHPParser_Node_Expr_Variable("bar") 94 | ); 95 | 96 | $classifier->enterNode($assign); 97 | 98 | $this->assertEquals(array('foo' => array(-1)), $classifier->getLocalVariables()); 99 | $this->assertEquals(array('foo' => array(-1)), $classifier->getAssignments()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Adapters/PatchBuilder/PatchBuilderTest.php: -------------------------------------------------------------------------------- 1 | builder = new PatchBuilder( 15 | "line1\n" . 16 | "line2\n" . 17 | "line3\n" . 18 | "line4\n" . 19 | "line5\n" . 20 | "line6\n" . 21 | "line7\n" . 22 | "line8\n" . 23 | "line9\n" 24 | ); 25 | } 26 | 27 | public function testChangeTokenOnLineAlone() 28 | { 29 | $this->builder->changeToken(4, 'line4', 'linefour'); 30 | 31 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 47 | } 48 | 49 | public function testChangeIsCaseSensitive() 50 | { 51 | $this->builder = new PatchBuilder('$bar = new Bar();'); 52 | $this->builder->changeToken(1, 'Bar', 'Foo'); 53 | 54 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 63 | } 64 | 65 | public function testChangeTokenAloneOnIndentedLine() 66 | { 67 | $this->builder = new PatchBuilder( 68 | "line1\n" . 69 | " line2\n" . 70 | "line3\n" 71 | ); 72 | 73 | $this->builder->changeToken(2, 'line2', 'linetwo'); 74 | 75 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 86 | } 87 | 88 | public function testChangeTokenWithMultipleTokensOneOneLine() 89 | { 90 | $this->builder = new PatchBuilder( 91 | "line1\n" . 92 | " echo \$var . ' = ' . \$var;\n" . 93 | "line3\n" 94 | ); 95 | 96 | $this->builder->changeToken(2, 'var', 'variable'); 97 | 98 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 109 | } 110 | 111 | public function testChangeTokenWithUnderscore() 112 | { 113 | $this->builder = new PatchBuilder( 114 | "line1\n" . 115 | " echo \$my_variable;\n" . 116 | "line3\n" 117 | ); 118 | 119 | $this->builder->changeToken(2, 'my_variable', 'myVariable'); 120 | 121 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 132 | } 133 | 134 | public function testAppendToLine() 135 | { 136 | $this->builder->appendToLine(5, array('line5.1', 'line5.2')); 137 | 138 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 153 | } 154 | 155 | public function testChangeLines() 156 | { 157 | $this->builder->changeLines(5, array('linefive', 'linefive.five')); 158 | 159 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 175 | } 176 | 177 | public function testRemoveLine() 178 | { 179 | $this->builder->removeLine(5); 180 | 181 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 195 | } 196 | 197 | public function testReplaceLines() 198 | { 199 | $this->builder->replaceLines(4, 6, array('hello', 'world')); 200 | 201 | $expected = <<assertEquals($expected, $this->builder->generateUnifiedDiff()); 219 | } 220 | 221 | public function testGetOriginalLines() 222 | { 223 | $this->assertEquals( 224 | array('line4', 'line5', 'line6'), 225 | $this->builder->getOriginalLines(4, 6) 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Adapters/TokenReflection/StaticCodeAnalysisTest.php: -------------------------------------------------------------------------------- 1 | findClasses($file); 24 | $class = $classes[0]; 25 | 26 | $this->assertEquals(0, $class->namespaceDeclarationLine(), 'namespace declaration line for file without namespace'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Application/RenameLocalVariableTest.php: -------------------------------------------------------------------------------- 1 | scanner = \Phake::mock('QafooLabs\Refactoring\Domain\Services\VariableScanner'); 19 | $this->codeAnalysis = \Phake::mock('QafooLabs\Refactoring\Domain\Services\CodeAnalysis'); 20 | $this->editor = \Phake::mock('QafooLabs\Refactoring\Domain\Services\Editor'); 21 | $this->refactoring = new RenameLocalVariable($this->scanner, $this->codeAnalysis, $this->editor); 22 | 23 | \Phake::when($this->codeAnalysis)->isInsideMethod(\Phake::anyParameters())->thenReturn(true); 24 | } 25 | 26 | public function testRenameLocalVariable() 27 | { 28 | $buffer = \Phake::mock('QafooLabs\Refactoring\Domain\Model\EditorBuffer'); 29 | 30 | \Phake::when($this->scanner)->scanForVariables(\Phake::anyParameters())->thenReturn( 31 | new DefinedVariables(array('helloWorld' => array(6))) 32 | ); 33 | \Phake::when($this->editor)->openBuffer(\Phake::anyParameters())->thenReturn($buffer); 34 | \Phake::when($this->codeAnalysis)->findMethodRange(\Phake::anyParameters())->thenReturn(LineRange::fromSingleLine(1)); 35 | 36 | $patch = $this->refactoring->refactor(new File("foo.php", <<<'PHP' 37 | replaceString(6, '$helloWorld', '$var'); 49 | } 50 | 51 | public function testRenameNonLocalVariable_ThrowsException() 52 | { 53 | $this->setExpectedException('QafooLabs\Refactoring\Domain\Model\RefactoringException', 'Given variable "$this->foo" is required to be local to the current method.'); 54 | 55 | $this->refactoring->refactor( 56 | new File("foo.php", ''), 6, 57 | new Variable('$this->foo'), 58 | new Variable('$foo') 59 | ); 60 | } 61 | 62 | public function testRenameIntoNonLocalVariable_ThrowsException() 63 | { 64 | $this->setExpectedException('QafooLabs\Refactoring\Domain\Model\RefactoringException', 'Given variable "$this->foo" is required to be local to the current method.'); 65 | 66 | $this->refactoring->refactor( 67 | new File("foo.php", ''), 6, 68 | new Variable('$foo'), 69 | new Variable('$this->foo') 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Application/Service/ExtractMethodTest.php: -------------------------------------------------------------------------------- 1 | applyCommand = \Phake::mock('QafooLabs\Refactoring\Adapters\PatchBuilder\ApplyPatchCommand'); 18 | 19 | $scanner = new ParserVariableScanner(); 20 | $codeAnalysis = new StaticCodeAnalysis(); 21 | $editor = new PatchEditor($this->applyCommand); 22 | 23 | $this->refactoring = new ExtractMethod($scanner, $codeAnalysis, $editor); 24 | } 25 | 26 | /** 27 | * @group integration 28 | */ 29 | public function testRefactorSimpleMethod() 30 | { 31 | $patch = $this->refactoring->refactor(new File("foo.php", <<<'PHP' 32 | applyCommand)->apply(<<<'CODE' 45 | --- a/foo.php 46 | +++ b/foo.php 47 | @@ -3,6 +3,11 @@ 48 | { 49 | public function main() 50 | { 51 | + $this->helloWorld(); 52 | + } 53 | + 54 | + private function helloWorld() 55 | + { 56 | echo "Hello World"; 57 | } 58 | } 59 | 60 | CODE 61 | ); 62 | } 63 | 64 | /** 65 | * @group regression 66 | * @group GH-4 67 | */ 68 | public function testVariableUsedBeforeAndAfterExtractedSlice() 69 | { 70 | $this->markTestIncomplete('Failing over some invisible whitespace issue?'); 71 | 72 | $patch = $this->refactoring->refactor(new File("foo.php", <<<'PHP' 73 | applyCommand)->apply(<<<'CODE' 92 | --- a/foo.php 93 | +++ b/foo.php 94 | @@ -6,9 +6,16 @@ 95 | $foo = "bar"; 96 | $baz = array(); 97 | 98 | + list($foo, $baz) = $this->extract($foo, $baz); 99 | + 100 | + return new Something($foo, $baz); 101 | + } 102 | + 103 | + private function extract($foo, $baz) 104 | + { 105 | $foo = strtolower($foo); 106 | $baz[] = $foo; 107 | 108 | - return new Something($foo, $baz); 109 | + return array($foo, $baz); 110 | } 111 | } 112 | 113 | CODE 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/DefinedVariablesTest.php: -------------------------------------------------------------------------------- 1 | array(1)), array('foo' => array(1))); 13 | $methodRange = new DefinedVariables(array('foo' => array(1, 2)), array('foo' => array(1, 2))); 14 | 15 | $variables = $methodRange->variablesFromSelectionUsedAfter($selectedRange); 16 | 17 | $this->assertEquals(array('foo'), $variables); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | findAllPhpFilesRecursivly(); 14 | 15 | $this->assertContainsOnly('QafooLabs\Refactoring\Domain\Model\File', $files); 16 | } 17 | 18 | public function testRemovesDuplicates() 19 | { 20 | vfsStreamWrapper::register(); 21 | 22 | $structure = array( 23 | 'src' => array( 24 | 'src' => array(), 25 | 'Foo' => array( 26 | 'src' => array(), 27 | 'Foo' => array(), 28 | 'Bar.php' => 'findAllPhpFilesRecursivly(); 38 | 39 | $foundFiles = array(); 40 | foreach ($files as $f => $file) { 41 | $foundFiles[] = $f; 42 | } 43 | 44 | $this->assertEquals(array('vfs://project/src/Foo/Bar.php'), $foundFiles); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/EditingAction/AddPropertyTest.php: -------------------------------------------------------------------------------- 1 | buffer = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditorBuffer'); 12 | } 13 | 14 | public function testItIsAnEditingAction() 15 | { 16 | $this->assertInstanceOf( 17 | 'QafooLabs\Refactoring\Domain\Model\EditingAction', 18 | new AddProperty(5, 'testProperty') 19 | ); 20 | } 21 | 22 | public function testPropertyIsAppenedAtGivenLine() 23 | { 24 | $line = 27; 25 | 26 | $action = new AddProperty($line, ''); 27 | 28 | $this->buffer 29 | ->expects($this->once()) 30 | ->method('append') 31 | ->with($line, $this->anything()); 32 | 33 | $action->performEdit($this->buffer); 34 | } 35 | 36 | public function testPropertyCodeIsCorrect() 37 | { 38 | $action = new AddProperty(5, 'testProperty'); 39 | 40 | $this->buffer 41 | ->expects($this->once()) 42 | ->method('append') 43 | ->with($this->anything(), $this->equalTo(array( 44 | ' private $testProperty;', 45 | '' 46 | ))); 47 | 48 | $action->performEdit($this->buffer); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/EditingAction/LocalVariableToInstanceTest.php: -------------------------------------------------------------------------------- 1 | buffer = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditorBuffer'); 15 | } 16 | 17 | public function testItIsAnEditingAction() 18 | { 19 | $this->assertInstanceOf( 20 | 'QafooLabs\Refactoring\Domain\Model\EditingAction', 21 | new LocalVariableToInstance( 22 | new DefinedVariables(array(), array()), 23 | new Variable('testVar') 24 | ) 25 | ); 26 | } 27 | 28 | public function testItReplacesVariableWithInstanceVariableVersion() 29 | { 30 | $variable = new Variable('varName'); 31 | 32 | $action = new LocalVariableToInstance( 33 | new DefinedVariables(array('varName' => array(1)), array()), 34 | $variable 35 | ); 36 | 37 | $this->buffer 38 | ->expects($this->once()) 39 | ->method('replaceString') 40 | ->with($this->anything(), $this->equalTo('$varName'), $this->equalTo('$this->varName')); 41 | 42 | $action->performEdit($this->buffer); 43 | } 44 | 45 | public function testItReplacesOnLineForReadOnlyVariable() 46 | { 47 | $definedVars = new DefinedVariables(array('theVar' => array(12)), array()); 48 | $variable = new Variable('theVar'); 49 | 50 | $action = new LocalVariableToInstance($definedVars, $variable); 51 | 52 | $this->buffer 53 | ->expects($this->once()) 54 | ->method('replaceString') 55 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 56 | 57 | $action->performEdit($this->buffer); 58 | } 59 | 60 | public function testItReplacesOn2LinesForReadOnlyVariable() 61 | { 62 | $definedVars = new DefinedVariables(array('theVar' => array(12, 15)), array()); 63 | $variable = new Variable('theVar'); 64 | 65 | $action = new LocalVariableToInstance($definedVars, $variable); 66 | 67 | $this->buffer 68 | ->expects($this->at(0)) 69 | ->method('replaceString') 70 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 71 | 72 | $this->buffer 73 | ->expects($this->at(1)) 74 | ->method('replaceString') 75 | ->with($this->equalTo(15), $this->anything(), $this->anything()); 76 | 77 | $action->performEdit($this->buffer); 78 | } 79 | 80 | public function testItReplacesOnLineForChangedVariable() 81 | { 82 | $definedVars = new DefinedVariables(array(), array('theVar' => array(12))); 83 | $variable = new Variable('theVar'); 84 | 85 | $action = new LocalVariableToInstance($definedVars, $variable); 86 | 87 | $this->buffer 88 | ->expects($this->once()) 89 | ->method('replaceString') 90 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 91 | 92 | $action->performEdit($this->buffer); 93 | } 94 | 95 | public function testItReplacesOn2LinesForChangedVariable() 96 | { 97 | $definedVars = new DefinedVariables(array(), array('theVar' => array(12, 15))); 98 | $variable = new Variable('theVar'); 99 | 100 | $action = new LocalVariableToInstance($definedVars, $variable); 101 | 102 | $this->buffer 103 | ->expects($this->at(0)) 104 | ->method('replaceString') 105 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 106 | 107 | $this->buffer 108 | ->expects($this->at(1)) 109 | ->method('replaceString') 110 | ->with($this->equalTo(15), $this->anything(), $this->anything()); 111 | 112 | $action->performEdit($this->buffer); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/EditingAction/RenameVariableTest.php: -------------------------------------------------------------------------------- 1 | buffer = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditorBuffer'); 15 | } 16 | 17 | public function testItIsAnEditingAction() 18 | { 19 | $this->assertInstanceOf( 20 | 'QafooLabs\Refactoring\Domain\Model\EditingAction', 21 | new RenameVariable( 22 | new DefinedVariables(array(), array()), 23 | new Variable('testVar'), 24 | new Variable('newVar') 25 | ) 26 | ); 27 | } 28 | 29 | public function testItReplacesVariableWithInstanceVariableVersion() 30 | { 31 | $oldName = new Variable('varName'); 32 | $newName = new Variable('newName'); 33 | 34 | $action = new RenameVariable( 35 | new DefinedVariables(array('varName' => array(1)), array()), 36 | $oldName, 37 | $newName 38 | ); 39 | 40 | $this->buffer 41 | ->expects($this->once()) 42 | ->method('replaceString') 43 | ->with($this->anything(), $this->equalTo('$varName'), $this->equalTo('$newName')); 44 | 45 | $action->performEdit($this->buffer); 46 | } 47 | 48 | public function testItReplacesOnLineForReadOnlyVariable() 49 | { 50 | $definedVars = new DefinedVariables(array('theVar' => array(12)), array()); 51 | $variable = new Variable('theVar'); 52 | 53 | $action = new RenameVariable($definedVars, $variable, $variable); 54 | 55 | $this->buffer 56 | ->expects($this->once()) 57 | ->method('replaceString') 58 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 59 | 60 | $action->performEdit($this->buffer); 61 | } 62 | 63 | public function testItReplacesOn2LinesForReadOnlyVariable() 64 | { 65 | $definedVars = new DefinedVariables(array('theVar' => array(12, 15)), array()); 66 | $variable = new Variable('theVar'); 67 | 68 | $action = new RenameVariable($definedVars, $variable, $variable); 69 | 70 | $this->buffer 71 | ->expects($this->at(0)) 72 | ->method('replaceString') 73 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 74 | 75 | $this->buffer 76 | ->expects($this->at(1)) 77 | ->method('replaceString') 78 | ->with($this->equalTo(15), $this->anything(), $this->anything()); 79 | 80 | $action->performEdit($this->buffer); 81 | } 82 | 83 | public function testItReplacesOnLineForChangedVariable() 84 | { 85 | $definedVars = new DefinedVariables(array(), array('theVar' => array(12))); 86 | $variable = new Variable('theVar'); 87 | 88 | $action = new RenameVariable($definedVars, $variable, $variable); 89 | 90 | $this->buffer 91 | ->expects($this->once()) 92 | ->method('replaceString') 93 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 94 | 95 | $action->performEdit($this->buffer); 96 | } 97 | 98 | public function testItReplacesOn2LinesForChangedVariable() 99 | { 100 | $definedVars = new DefinedVariables(array(), array('theVar' => array(12, 15))); 101 | $variable = new Variable('theVar'); 102 | 103 | $action = new RenameVariable($definedVars, $variable, $variable); 104 | 105 | $this->buffer 106 | ->expects($this->at(0)) 107 | ->method('replaceString') 108 | ->with($this->equalTo(12), $this->anything(), $this->anything()); 109 | 110 | $this->buffer 111 | ->expects($this->at(1)) 112 | ->method('replaceString') 113 | ->with($this->equalTo(15), $this->anything(), $this->anything()); 114 | 115 | $action->performEdit($this->buffer); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/EditingSessionTest.php: -------------------------------------------------------------------------------- 1 | buffer = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditorBuffer'); 25 | 26 | $this->session = new EditingSession($this->buffer); 27 | } 28 | 29 | public function testEditActionsArePerformed() 30 | { 31 | $action1 = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditingAction'); 32 | $action2 = $this->getMock('QafooLabs\Refactoring\Domain\Model\EditingAction'); 33 | 34 | $action1->expects($this->once()) 35 | ->method('performEdit') 36 | ->with($this->equalTo($this->buffer)); 37 | 38 | $action2->expects($this->once()) 39 | ->method('performEdit') 40 | ->with($this->equalTo($this->buffer)); 41 | 42 | $this->session->addEdit($action1); 43 | $this->session->addEdit($action2); 44 | 45 | $this->session->performEdits(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/FileTest.php: -------------------------------------------------------------------------------- 1 | 14 | array( 15 | 'Foo'=> 16 | array( 17 | 'Bar.php'=>'' 18 | ) 19 | ) 20 | ) 21 | ); 22 | } 23 | 24 | public function testGetRelativePathRespectsMixedWindowsPathsAndWorkingDirectoryTrailingSlashs() 25 | { 26 | $root = $this->createFileSystem(); 27 | $workingDir = $root->getChild('src')->url().'/'; 28 | 29 | $file = File::createFromPath( 30 | $root->getChild('src')->url().'\Foo\Bar.php', 31 | $workingDir 32 | ); 33 | 34 | $this->assertEquals("Foo\Bar.php", $file->getRelativePath()); 35 | } 36 | 37 | public function testRelativePathConstructionForAbsoluteVFSFiles() 38 | { 39 | $src = $this->createFileSystem()->getChild('src')->url(); 40 | $bar = $src.DIRECTORY_SEPARATOR.'Foo'.DIRECTORY_SEPARATOR.'Bar.php'; 41 | 42 | $file = File::createFromPath($bar, $notRelatedWorkingDir = __DIR__); 43 | $this->assertEquals('vfs://project/src/Foo/Bar.php', $file->getRelativePath()); 44 | } 45 | 46 | static public function dataExtractPsr0ClassName() 47 | { 48 | return array( 49 | array(new PhpName('Foo', 'Foo'), 'src'.DIRECTORY_SEPARATOR.'Foo.php'), 50 | array(new PhpName('Foo\Bar', 'Bar'), 'src'.DIRECTORY_SEPARATOR.'Foo'.DIRECTORY_SEPARATOR.'Bar.php'), 51 | ); 52 | } 53 | 54 | /** 55 | * @dataProvider dataExtractPsr0ClassName 56 | */ 57 | public function testExtractPsr0ClassName($expectedClassName, $fileName) 58 | { 59 | $file = new File($fileName, 'extractPsr0ClassName(); 61 | 62 | $this->assertTrue($expectedClassName->equals($actualClassName), "- $expectedClassName\n+ $actualClassName"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/IndentationDetectorTest.php: -------------------------------------------------------------------------------- 1 | createDetector(array(' echo "test";')); 10 | 11 | $this->assertEquals(4, $detector->getMinIndentation()); 12 | } 13 | 14 | public function testGetMinIndentationForFirstLine() 15 | { 16 | $detector = $this->createDetector(array( 17 | ' echo "Line 1";', 18 | ' echo "Line 2";', 19 | )); 20 | 21 | $this->assertEquals(2, $detector->getMinIndentation()); 22 | } 23 | 24 | public function testGetMinIntentationForLaterLine() 25 | { 26 | $detector = $this->createDetector(array( 27 | ' echo "Line 1";', 28 | ' echo "Line 2";', 29 | )); 30 | 31 | $this->assertEquals(2, $detector->getMinIndentation()); 32 | } 33 | 34 | public function testGetMinIndentationWithBlankLines() 35 | { 36 | $detector = $this->createDetector(array( 37 | '', 38 | ' echo "test";', 39 | )); 40 | 41 | $this->assertEquals(4, $detector->getMinIndentation()); 42 | } 43 | 44 | public function testGetFirstLineIndentation() 45 | { 46 | $detector = $this->createDetector(array( 47 | ' echo "line 1";', 48 | ' echo "line 2";', 49 | )); 50 | 51 | $this->assertEquals(4, $detector->getFirstLineIndentation()); 52 | } 53 | 54 | public function testGetFirstLineIndentationWithBlankLines() 55 | { 56 | $detector = $this->createDetector(array( 57 | '', 58 | ' echo "test";', 59 | )); 60 | 61 | $this->assertEquals(2, $detector->getFirstLineIndentation()); 62 | } 63 | 64 | private function createDetector(array $lines) 65 | { 66 | return new IndentationDetector(LineCollection::createFromArray($lines)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/IndentingLineCollectionTest.php: -------------------------------------------------------------------------------- 1 | lines = new IndentingLineCollection(); 14 | } 15 | 16 | public function testIsALineCollection() 17 | { 18 | $this->assertInstanceOf( 19 | 'QafooLabs\Refactoring\Domain\Model\LineCollection', 20 | $this->lines 21 | ); 22 | } 23 | 24 | public function testAppendAddsIndentation() 25 | { 26 | $this->lines->addIndentation(); 27 | 28 | $this->lines->append(new Line('echo "test";')); 29 | 30 | $this->assertLinesMatch(array( 31 | ' echo "test";' 32 | )); 33 | } 34 | 35 | public function testAppendAddsMulitpleIndentation() 36 | { 37 | $this->lines->append(new Line('echo "line1";')); 38 | $this->lines->addIndentation(); 39 | $this->lines->append(new Line('echo "line2";')); 40 | $this->lines->addIndentation(); 41 | $this->lines->append(new Line('echo "line3";')); 42 | 43 | $this->assertLinesMatch(array( 44 | 'echo "line1";', 45 | ' echo "line2";', 46 | ' echo "line3";' 47 | )); 48 | } 49 | 50 | public function testAppendRemovesIndentation() 51 | { 52 | $this->lines->append(new Line('echo "line1";')); 53 | $this->lines->addIndentation(); 54 | $this->lines->append(new Line('echo "line2";')); 55 | $this->lines->removeIndentation(); 56 | $this->lines->append(new Line('echo "line3";')); 57 | 58 | $this->assertLinesMatch(array( 59 | 'echo "line1";', 60 | ' echo "line2";', 61 | 'echo "line3";' 62 | )); 63 | } 64 | 65 | public function testAppendStringObeysIndentation() 66 | { 67 | $this->lines->appendString('echo "line1";'); 68 | $this->lines->addIndentation(); 69 | $this->lines->appendString('echo "line2";'); 70 | $this->lines->removeIndentation(); 71 | $this->lines->appendString('echo "line3";'); 72 | 73 | $this->assertLinesMatch(array( 74 | 'echo "line1";', 75 | ' echo "line2";', 76 | 'echo "line3";' 77 | )); 78 | } 79 | 80 | public function testAppendLinesObeysIndentation() 81 | { 82 | $this->lines->addIndentation(); 83 | 84 | $this->lines->appendLines(LineCollection::createFromArray(array( 85 | 'echo "line1";', 86 | 'echo "line2";' 87 | ))); 88 | 89 | $this->assertLinesMatch(array( 90 | ' echo "line1";', 91 | ' echo "line2";', 92 | )); 93 | } 94 | 95 | public function testAddBlankLineContainsNoIndentation() 96 | { 97 | $this->lines->appendBlankLine(); 98 | 99 | $this->assertLinesMatch(array('')); 100 | } 101 | 102 | private function assertLinesMatch(array $expected) 103 | { 104 | $this->assertEquals( 105 | $expected, 106 | iterator_to_array(new ToStringIterator($this->lines->getIterator())) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/LineCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertSame($lineObjects, $lines->getLines()); 19 | } 20 | 21 | public function testAppendAddsALine() 22 | { 23 | $line1 = new Line('line 1'); 24 | $line2 = new Line('line 2'); 25 | 26 | $lines = new LineCollection(array($line1)); 27 | 28 | $lines->append($line2); 29 | 30 | $this->assertSame(array($line1, $line2), $lines->getLines()); 31 | } 32 | 33 | public function testAppendStringAddsALine() 34 | { 35 | $line1 = 'line 1'; 36 | $line2 = 'line 2'; 37 | 38 | $lines = new LineCollection(array(new Line($line1))); 39 | 40 | $lines->appendString($line2); 41 | 42 | $this->assertEquals( 43 | array(new Line($line1), new Line($line2)), 44 | $lines->getLines() 45 | ); 46 | } 47 | 48 | public function testCreateFromArray() 49 | { 50 | $lines = LineCollection::createFromArray(array( 51 | 'line1', 52 | 'line2', 53 | )); 54 | 55 | $this->assertEquals( 56 | array(new Line('line1'), new Line('line2')), 57 | $lines->getLines() 58 | ); 59 | } 60 | 61 | public function testCreateFromString() 62 | { 63 | $lines = LineCollection::createFromString( 64 | "line1\nline2" 65 | ); 66 | 67 | $this->assertEquals( 68 | array(new Line('line1'), new Line('line2')), 69 | $lines->getLines() 70 | ); 71 | } 72 | 73 | public function testIsIterable() 74 | { 75 | $lineObjects = array( 76 | new Line('line 1'), 77 | new Line('line 2') 78 | ); 79 | 80 | $lines = new LineCollection($lineObjects); 81 | 82 | $this->assertEquals($lineObjects, iterator_to_array($lines)); 83 | } 84 | 85 | public function testAppendLinesAddsGivenLines() 86 | { 87 | $lines = LineCollection::createFromArray(array( 88 | 'line1', 89 | 'line2', 90 | )); 91 | 92 | $lines->appendLines(LineCollection::createFromArray(array( 93 | 'line3', 94 | 'line4', 95 | ))); 96 | 97 | $this->assertEquals( 98 | array('line1', 'line2', 'line3', 'line4'), 99 | iterator_to_array(new ToStringIterator($lines->getIterator())) 100 | ); 101 | } 102 | 103 | public function testAppendlankLine() 104 | { 105 | $lines = new LineCollection(); 106 | 107 | $lines->appendBlankLine(); 108 | 109 | $this->assertEquals( 110 | array(''), 111 | iterator_to_array(new ToStringIterator($lines->getIterator())) 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/LineRangeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, $range->getStart()); 12 | $this->assertEquals(1, $range->getEnd()); 13 | 14 | $this->assertTrue($range->isInRange(1)); 15 | $this->assertFalse($range->isInRange(2)); 16 | } 17 | 18 | public function testCreateFromString() 19 | { 20 | $range = LineRange::fromString("1-4"); 21 | 22 | $this->assertEquals(1, $range->getStart()); 23 | $this->assertEquals(4, $range->getEnd()); 24 | 25 | $this->assertTrue($range->isInRange(1)); 26 | $this->assertFalse($range->isInRange(5)); 27 | } 28 | 29 | public function testCreateFromLines() 30 | { 31 | $range = LineRange::fromLines(1, 4); 32 | 33 | $this->assertEquals(1, $range->getStart()); 34 | $this->assertEquals(4, $range->getEnd()); 35 | 36 | $this->assertTrue($range->isInRange(1)); 37 | $this->assertFalse($range->isInRange(5)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/LineTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($content, (string) $line); 14 | } 15 | 16 | public function testIsEmptyForEmptyLine() 17 | { 18 | $line = new Line(''); 19 | 20 | $this->assertTrue($line->isEmpty()); 21 | } 22 | 23 | public function testIsEmptyForLineWithContent() 24 | { 25 | $line = new Line('$a = 5;'); 26 | 27 | $this->assertFalse($line->isEmpty()); 28 | } 29 | 30 | public function testGetIndentationFor2Spaces() 31 | { 32 | $line = new Line(' echo "Test";'); 33 | 34 | $this->assertEquals(2, $line->getIndentation()); 35 | } 36 | 37 | public function testGetIndentationFor4Spaces() 38 | { 39 | $line = new Line(' echo "Test";'); 40 | 41 | $this->assertEquals(4, $line->getIndentation()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/MethodSignatureTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($method->isPrivate()); 15 | $this->assertFalse($method->isStatic()); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function whenCreateMethodSignatureWithInvalidVisibility_ThenThrowException() 22 | { 23 | $this->setExpectedException("InvalidArgumentException"); 24 | 25 | $method = new MethodSignature("foo", MethodSignature::IS_PRIVATE | MethodSignature::IS_PUBLIC); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function whenCreateMethodSignatureWithStaticOnly_ThenAssumePrivateVisibility() 32 | { 33 | $method = new MethodSignature("foo", MethodSignature::IS_STATIC); 34 | 35 | $this->assertTrue($method->isPrivate()); 36 | $this->assertTrue($method->isStatic()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/PhpNames/NoImportedUsagesFilterTest.php: -------------------------------------------------------------------------------- 1 | filter(array( 20 | new PhpNameOccurance(new PhpName('Foo\Bar', 'Foo\Bar', PhpName::TYPE_USE), $file, 12), 21 | new PhpNameOccurance(new PhpName('Foo\Bar', 'Bar', PhpName::TYPE_USAGE), $file, 12), 22 | )); 23 | 24 | $this->assertEquals(1, count($names)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/UseStatementTest.php: -------------------------------------------------------------------------------- 1 | useStatement = new UseStatement($file, LineRange::fromLines(3,5)); 15 | } 16 | 17 | public function testReturnsEndLineFromLineRange() 18 | { 19 | $this->assertEquals(5, $this->useStatement->getEndLine()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Domain/Model/VariableTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('QafooLabs\Refactoring\Domain\Model\RefactoringException', 'The given variable name "(); " is not valid in PHP.'); 10 | 11 | new Variable('(); '); 12 | } 13 | 14 | public function testGetNameOrToken() 15 | { 16 | $variable = new Variable('$var'); 17 | 18 | $this->assertEquals('var', $variable->getName()); 19 | $this->assertEquals('$var', $variable->getToken()); 20 | } 21 | 22 | public function testCreateInstanceVariable() 23 | { 24 | $variable = new Variable('$this->var'); 25 | 26 | $this->assertEquals('this->var', $variable->getName()); 27 | $this->assertEquals('$this->var', $variable->getToken()); 28 | 29 | $this->assertTrue($variable->isInstance()); 30 | $this->assertFalse($variable->isLocal()); 31 | } 32 | 33 | public function testCreateLocalVariable() 34 | { 35 | $variable = new Variable('$var'); 36 | 37 | $this->assertFalse($variable->isInstance()); 38 | $this->assertTrue($variable->isLocal()); 39 | } 40 | 41 | public function testCreateInstanceFromLocal() 42 | { 43 | $local = new Variable('$var'); 44 | $instance = $local->convertToInstance(); 45 | 46 | $this->assertTrue($instance->isInstance()); 47 | $this->assertFalse($local->isInstance()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Utils/CallbackFilterIteratorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(array(1, 2), array_values(iterator_to_array($values))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Utils/ToStringIteratorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 30 | array('value1', 'value2'), 31 | iterator_to_array($it) 32 | ); 33 | } 34 | } 35 | 36 | class StringableClass 37 | { 38 | private $value; 39 | 40 | public function __construct($value) 41 | { 42 | $this->value = $value; 43 | } 44 | 45 | public function __toString() 46 | { 47 | return (string) $this->value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/QafooLabs/Refactoring/Utils/TransformIteratorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(array('olleH', 'dlroW'), iterator_to_array($strings)); 25 | } 26 | } 27 | 28 | class ReverseStringTransformIterator extends TransformIterator 29 | { 30 | protected function transform($value) 31 | { 32 | return strrev($value); 33 | } 34 | } 35 | --------------------------------------------------------------------------------