├── .env.dev ├── .env.prod ├── .env.test ├── .github ├── dependabot.yml └── workflows │ ├── build-docker-image-after-release.yml │ ├── ci.yml │ └── deploy-website-after-release.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── bin └── console ├── build └── config │ ├── .php-cs-fixer.php │ ├── infection.json │ ├── phpstan.neon │ ├── phpunit.xml │ └── psalm.xml ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── cache.yaml │ └── framework.yaml ├── phpDocumentor │ └── template │ │ ├── argument.xml.twig │ │ ├── constant.xml.twig │ │ ├── docblock.xml.twig │ │ ├── method.xml.twig │ │ ├── namespace_tree.xml.twig │ │ ├── property.xml.twig │ │ ├── structure.xml.twig │ │ └── template.xml ├── preload.php ├── routes │ └── framework.yaml └── services.yaml ├── docker ├── composer.Dockerfile ├── infection.Dockerfile ├── install_composer.sh ├── php-cs-fixer.Dockerfile ├── phpstan.Dockerfile ├── phpunit.Dockerfile ├── project │ ├── Dockerfile │ ├── php.ini │ └── with_blackfire.Dockerfile └── psalm.Dockerfile ├── generated └── .gitkeep ├── php-dry ├── php-dry.xml.example ├── resources └── icons │ ├── php-dry.png │ └── php-dry.svg ├── src ├── Application.php ├── CloneDetection │ ├── CloneDetector.php │ ├── Type1CloneDetector.php │ ├── Type2CloneDetector.php │ ├── Type3CloneDetector.php │ └── Type4CloneDetector.php ├── Collection │ └── MethodsCollection.php ├── Command │ ├── DetectClonesCommand.php │ └── Output │ │ ├── DetectClonesCommandOutput.php │ │ └── Helper │ │ ├── OutputHelper.php │ │ └── VerboseOutputHelper.php ├── Compare │ └── MethodSignatureComparer.php ├── Configuration │ ├── Configuration.php │ ├── ConfigurationFactory.php │ ├── ReportConfiguration.php │ └── ReportConfiguration │ │ ├── Cli.php │ │ ├── Html.php │ │ └── Json.php ├── ContextDecider │ └── MethodContextDecider.php ├── Exception │ ├── CollectionCannotBeEmpty.php │ ├── ConfigurationError.php │ ├── NoParamRequestForParamType.php │ ├── PhpDocumentorFailed.php │ └── SubsequenceUtilNotFound.php ├── Factory │ ├── CodePosition │ │ ├── CodePositionFactory.php │ │ └── CodePositionRangeFactory.php │ ├── Collection │ │ └── MethodsCollectionFactory.php │ ├── SourceCloneCandidate │ │ ├── Type1SourceCloneCandidateFactory.php │ │ ├── Type2SourceCloneCandidateFactory.php │ │ ├── Type3SourceCloneCandidateFactory.php │ │ └── Type4SourceCloneCandidateFactory.php │ └── TokenSequenceFactory.php ├── Grouper │ ├── MethodTokenSequencesByTokenSequencesGrouper.php │ └── MethodsBySignatureGrouper.php ├── Kernel.php ├── Merge │ ├── Type2SourceCloneCandidatesMerger.php │ └── Type3SourceCloneCandidatesMerger.php ├── Model │ ├── CodePosition │ │ ├── CodePosition.php │ │ └── CodePositionRange.php │ ├── FilepathMethods │ │ └── FilepathMethods.php │ ├── Identity.php │ ├── Method │ │ ├── Method.php │ │ ├── MethodSignature.php │ │ ├── MethodSignatureGroup.php │ │ └── MethodTokenSequence.php │ ├── RunResult │ │ └── RunResultSet.php │ ├── SourceClone │ │ └── SourceClone.php │ └── SourceCloneCandidate │ │ ├── SourceCloneCandidate.php │ │ ├── Type1SourceCloneCandidate.php │ │ ├── Type2SourceCloneCandidate.php │ │ ├── Type3SourceCloneCandidate.php │ │ └── Type4SourceCloneCandidate.php ├── OutputFormatter │ └── Model │ │ ├── CodePosition │ │ ├── CodePositionOutputFormatter.php │ │ └── CodePositionRangeOutputFormatter.php │ │ ├── Method │ │ ├── MethodOutputFormatter.php │ │ └── MethodSignatureOutputFormatter.php │ │ └── SourceClone │ │ └── SourceCloneOutputFormatter.php ├── Report │ ├── Converter │ │ └── SourceClonesToArrayConverter.php │ ├── Formatter │ │ ├── CliReportFormatter.php │ │ ├── HtmlReportFormatter.php │ │ ├── JsonReportFormatter.php │ │ └── ReportFormatter.php │ ├── Report.php │ ├── ReportBuilder.php │ ├── Reporter.php │ └── Saver │ │ ├── CliReportReporter.php │ │ ├── HtmlReportReporter.php │ │ ├── JsonReportReporter.php │ │ └── ReportReporter.php ├── Service │ ├── DetectClonesService.php │ ├── FindMethodsInPathsService.php │ ├── IgnoreClonesService.php │ ├── PhpDocumentorRunner.php │ └── PhpDryVersionService.php ├── ServiceFactory │ ├── CrawlerFactory.php │ ├── EnvironmentFactory.php │ ├── FileSystemLoaderFactory.php │ ├── FinderFactory.php │ ├── StopwatchFactory.php │ └── SymfonyStyleFactory.php ├── Util │ ├── ArrayUtil.php │ └── Subsequence │ │ ├── LongestCommonSubsequenceUtil.php │ │ ├── SimilarTextSubsequenceUtil.php │ │ ├── SubsequenceUtil.php │ │ └── SubsequenceUtilPicker.php └── Wrapper │ └── PhpTokenWrapper.php ├── symfony.lock ├── templates └── php-dry │ ├── clone_card.html.twig │ ├── clone_instance_row.html.twig │ ├── nav.html.twig │ ├── php-dry.html.twig │ └── tabpanel.html.twig ├── tests ├── Functional │ ├── Command │ │ ├── DetectClonesCommandTest.php │ │ ├── expected_php-dry.html │ │ ├── expected_php-dry.json │ │ ├── php-dry.xml │ │ ├── with-DTOs │ │ │ ├── generated │ │ │ │ └── reports │ │ │ │ │ ├── php-dry.json │ │ │ │ │ ├── php-dry.json~ │ │ │ │ │ └── php-dry_html-report │ │ │ │ │ ├── php-dry.html │ │ │ │ │ └── resources │ │ │ │ │ └── icons │ │ │ │ │ └── php-dry.svg │ │ │ └── phpDocumentorReport │ │ │ │ └── structure.xml │ │ └── with-native-types │ │ │ ├── generated │ │ │ └── reports │ │ │ │ ├── php-dry.json │ │ │ │ ├── php-dry_html-report │ │ │ │ ├── php-dry.html │ │ │ │ └── resources │ │ │ │ │ └── icons │ │ │ │ │ └── php-dry.svg │ │ │ │ └── resources │ │ │ │ └── icons │ │ │ │ └── php-dry.svg │ │ │ └── phpDocumentorReport │ │ │ └── structure.xml │ └── Service │ │ ├── PhpDocumentorRunnerTest.php │ │ ├── expected_phpDocumentor_structure.xml │ │ └── phpDocumentorReport │ │ └── structure.xml ├── Unit │ ├── CloneDetection │ │ ├── CloneDetectorTest.php │ │ ├── Type1CloneDetectorTest.php │ │ ├── Type2CloneDetectorTest.php │ │ └── Type3CloneDetectorTest.php │ ├── Collection │ │ └── MethodsCollectionTest.php │ ├── Compare │ │ └── MethodSignatureComparerTest.php │ ├── Configuration │ │ └── ConfigurationFactoryTest.php │ ├── ContextDecider │ │ └── MethodContextDeciderTest.php │ ├── Exception │ │ ├── CollectionCannotBeEmptyTest.php │ │ ├── NoParamRequestForParamTypeTest.php │ │ └── SubsequenceUtilNotFoundTest.php │ ├── Factory │ │ ├── CodePosition │ │ │ ├── CodePositionFactoryTest.php │ │ │ └── CodePositionRangeFactoryTest.php │ │ ├── Collection │ │ │ └── MethodsCollectionFactoryTest.php │ │ ├── SourceCloneCandidate │ │ │ └── Type2SourceCloneCandidateFactoryTest.php │ │ └── TokenSequenceFactoryTest.php │ ├── Grouper │ │ ├── MethodTokenSequencesByTokenSequencesGrouperTest.php │ │ └── MethodsBySignatureGrouperTest.php │ ├── Merge │ │ ├── Type2SourceCloneCandidateMergerTest.php │ │ └── Type3SourceCloneCandidateMergerTest.php │ ├── Model │ │ ├── CodePosition │ │ │ ├── CodePositionRangeTest.php │ │ │ └── CodePositionTest.php │ │ ├── FilepathMethods │ │ │ └── FilepathMethodsTest.php │ │ ├── Method │ │ │ ├── MethodSignatureTest.php │ │ │ ├── MethodTest.php │ │ │ └── MethodTokenSequenceTest.php │ │ ├── SourceClone │ │ │ └── SourceCloneTest.php │ │ └── SourceCloneCandidate │ │ │ ├── Type1SourceCloneCandidateTest.php │ │ │ ├── Type2SourceCloneCandidateTest.php │ │ │ └── Type3SourceCloneCandidateTest.php │ ├── OutputFormatter │ │ └── Model │ │ │ ├── CodePosition │ │ │ ├── CodePositionOutputFormatterTest.php │ │ │ └── CodePositionRangeOutputFormatterTest.php │ │ │ ├── Method │ │ │ ├── MethodOutputFormatterTest.php │ │ │ └── MethodSignatureOutputFormatterTest.php │ │ │ └── SourceClone │ │ │ └── SourceCloneOutputFormatterTest.php │ ├── Service │ │ ├── FindMethodsInPathsServiceTest.php │ │ └── IgnoreClonesServiceTest.php │ ├── Util │ │ ├── ArrayUtilTest.php │ │ └── Subsequence │ │ │ ├── LongestCommonSubsequenceUtilTest.php │ │ │ ├── SimilarTextSubsequenceUtilTest.php │ │ │ └── SubsequenceUtilPickerTest.php │ └── Wrapper │ │ └── PhpTokenWrapperTest.php ├── bootstrap.php ├── generated │ └── reports │ │ └── .gitkeep └── testdata │ ├── clone-detection-testdata-with-native-types │ ├── 01_A.php │ ├── 02_B.php │ ├── 03_A_Exact_Copy.php │ ├── 04_A_Additional_Whitespaces.php │ ├── 05_A_Additional_Comments.php │ ├── 06_A_Changed_Layout.php │ ├── 07_A_Changed_Variable_Names.php │ ├── 08_A_Changed_Method_Names.php │ ├── 09_A_Changed_Literals.php │ ├── 10_A_Additional_Statements.php │ ├── 11_A_Removed_Statements.php │ ├── 12_A_Changed_Statement_Order.php │ ├── 13_A_Changed_Syntax.php │ └── 14_A_Changed_Param_Order.php │ ├── file │ └── file.txt │ ├── php-dry-01.xml │ ├── php-dry-02.xml │ ├── phpDocumentor │ ├── big_report │ │ └── structure.xml │ └── small_report │ │ └── structure.xml │ └── template │ ├── generated │ └── .gitkeep │ ├── run_class_method_template.php │ └── run_free_method_template.php ├── tools └── phpDocumentor.phar └── xsd └── php-dry.xsd /.env.dev: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | # define your env variables for the prod env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/build-docker-image-after-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ published ] 4 | workflow_dispatch: ~ 5 | 6 | jobs: 7 | Build_and_push: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get tag 11 | id: get_tag 12 | run: echo ::set-output name=TAG::${GITHUB_REF#refs/tags/v} 13 | - name: Login to DockerHub 14 | uses: docker/login-action@v1 15 | with: 16 | username: ${{ secrets.DOCKERHUB_USERNAME }} 17 | password: ${{ secrets.DOCKERHUB_TOKEN }} 18 | - name: "Check out repository code" 19 | uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.PAT_FOR_PHP_DRY_VERSION_UPDATING }} 22 | - name: Update VERSION file 23 | run: echo ${{ steps.get_tag.outputs.TAG }} > VERSION 24 | - uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | branch: main 27 | commit_message: Update VERSION file 28 | file_pattern: VERSION 29 | - name: "Install PHP" 30 | uses: "shivammathur/setup-php@v2" 31 | with: 32 | php-version: 8.1 33 | ini-values: memory_limit=-1 34 | tools: composer:v2.1 35 | extensions: ctype, iconv, mbstring 36 | - name: Setup env 37 | run: | 38 | cp .env.prod .env 39 | - name: Build and push image 40 | run: | 41 | make build_and_push_image tag=${{ steps.get_tag.outputs.TAG }} 42 | make build_and_push_image tag=latest 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Pipeline 2 | on: [ push ] 3 | jobs: 4 | Test: 5 | 6 | runs-on: ${{ matrix.operating-system }} 7 | 8 | strategy: 9 | matrix: 10 | operating-system: [ ubuntu-latest ] 11 | php-version: [ '8.0' ] 12 | include: 13 | - { operating-system: 'ubuntu-latest', php-version: '8.0'} 14 | - { operating-system: 'ubuntu-latest', php-version: '8.1'} 15 | 16 | name: CI on ${{ matrix.operating-system }} with PHP ${{ matrix.php-version }} 17 | 18 | steps: 19 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." 20 | - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" 21 | - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." 22 | - name: "Check out repository code" 23 | uses: actions/checkout@v3 24 | - name: List files in the repository 25 | run: | 26 | ls ${{ github.workspace }} 27 | - name: "Install requirements" 28 | run: | 29 | make setup_test_environment php_version=${{ matrix.php-version }} 30 | - name: "Static Analysis" 31 | run: | 32 | make phpstan 33 | make psalm 34 | - name: "Unit Testing" 35 | run: | 36 | make unit 37 | - name: "Functional Testing" 38 | run: | 39 | make functional 40 | - name: "Mutation Testing" 41 | run: | 42 | make infection -------------------------------------------------------------------------------- /.github/workflows/deploy-website-after-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ published ] 4 | workflow_dispatch: ~ 5 | 6 | jobs: 7 | deploy_website: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Deploy website 11 | run: | 12 | curl -XPOST -u "${{ secrets.DEPLOY_PAT_USERNAME}}:${{secrets.DEPLOY_PAT}}" -H "Accept:application/vnd.github.everest-preview+json" -H "Content-Type:application/json" https://api.github.com/repos/leovie/php-dry-website/dispatches --data '{"event_type": "deploy_website" }' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Composer 3 | # 4 | vendor 5 | bin/* 6 | !bin/console 7 | 8 | # 9 | # PHPStorm 10 | # 11 | .idea 12 | 13 | # 14 | # Test Output 15 | # 16 | /build/cache/* 17 | !build/cache/.gitkeep 18 | /build/config/base/* 19 | !build/config/base/.gitkeep 20 | /build/coverage/ 21 | /build/logs/* 22 | .phpunit.result.cache 23 | 24 | # 25 | # Symfony 26 | # 27 | /.env.local 28 | /.env 29 | /.env.local.php 30 | /.env.*.local 31 | /config/secrets/prod/prod.decrypt.private.php 32 | /public/bundles/ 33 | /var/ 34 | 35 | generated/* 36 | !generated/.gitkeep 37 | 38 | tests/testdata/template/generated/* 39 | !tests/testdata/template/generated/.gitkeep 40 | 41 | /report.html 42 | 43 | .phpdoc 44 | tests/generated/reports/* 45 | !/tests/generated/reports/.gitkeep 46 | 47 | tests/Functional/Command/phpDocumentorReport -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to php-dry 2 | 3 | First of all: You plan to contribute to php-dry. That's a very kind idea of you! 🥳 4 | 5 | ## GitHub 6 | 7 | Please open up an issue on the php-dry repository and describe, what you want to improve. We can talk about your plans, 8 | before you actually put too much effort into implementation. 9 | 10 | ## Development 11 | 12 | ### What you need 13 | 14 | - Git 15 | - Docker 16 | - Make 17 | 18 | ### Prepare 19 | 20 | 1. Fork GitHub repository 21 | 2. Clone your fork 22 | 3. `cd` to your fork 23 | 4. Setup dev environment: `make setup_dev_environment` 24 | 7. Run static analysis, tests and so on: `make test` 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Leo Viezens 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-dry – Clone Detection for PHP 2 | 3 | php-dry detects duplicated behaviour in your application, even if the duplicated passages are implemented completely 4 | different to each other. Likely you should read a bit about the [theoretical background](http://php-dry.org/news/0.html) 5 | for a better understanding. 6 | 7 | ## Run via Docker (recommended) 8 | 9 | ```bash 10 | docker run -v {path_to_project}:/project leovie/php-dry -h 11 | ``` 12 | 13 | ## Run via binary 14 | 15 | Install via composer 16 | 17 | ```bash 18 | composer require --dev leovie/php-dry 19 | ``` 20 | 21 | After installation, you can run php-dry via 22 | ```bash 23 | vendor/bin/php-dry {path_to_project} -h 24 | ``` 25 | 26 | ## Documentation 27 | see [here](http://php-dry.org/documentation) 28 | 29 | ## Thanks 30 | 31 | Special thank you belongs to [queo GmbH](https://www.queo.de) for sponsoring the development and maintenance of php-dry. -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.7.7 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | exclude('vendor') 5 | ->exclude('var') 6 | ->exclude('generated') 7 | ->exclude('tests/testdata') 8 | ->in(__DIR__ . '/../../'); 9 | 10 | $config = new PhpCsFixer\Config(); 11 | return $config 12 | ->setRules([ 13 | '@PSR12' => true, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | ])->setFinder($finder) 16 | ->setCacheFile(__DIR__ . '/../../build/cache/.php-cs-fixer.cache'); -------------------------------------------------------------------------------- /build/config/infection.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 10, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "phpUnit": { 9 | "configDir": "." 10 | }, 11 | "logs": { 12 | "text": "build/logs/infection.log" 13 | }, 14 | "mutators": { 15 | "@default": true 16 | }, 17 | "tmpDir": "../cache" 18 | } 19 | -------------------------------------------------------------------------------- /build/config/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - /composer/vendor/spaze/phpstan-disallowed-calls/extension.neon 3 | parameters: 4 | level: max 5 | paths: 6 | - %currentWorkingDirectory%/src 7 | - %currentWorkingDirectory%/tests/Unit 8 | excludePaths: 9 | - %currentWorkingDirectory%/template/generated/* 10 | - %currentWorkingDirectory%/src/ServiceFactory/CrawlerFactory.php 11 | ignoreErrors: 12 | - '#Method App\\Tests.+ has parameter .+ with no value type specified in iterable type .+#' 13 | - '#Method App\\Tests.+ return type has no value type specified in iterable type .+#' 14 | - '#Parameter \#2 \$callback of function array_filter expects callable\(mixed\)\: mixed, .+ given\.#' 15 | - '#Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, .+ given\.#' 16 | disallowedMethodCalls: 17 | - method: '*\*::__construct' 18 | message: "Don't use naive object instantiation." 19 | allowIn: 20 | - %currentWorkingDirectory%/src/ServiceFactory/* 21 | - %currentWorkingDirectory%/tests/* -------------------------------------------------------------------------------- /build/config/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ../../tests 15 | 16 | 17 | ../../tests/Unit 18 | 19 | 20 | ../../tests/Functional 21 | 22 | 23 | 24 | 26 | 27 | ../../src 28 | 29 | 30 | ../../src/Kernel.php 31 | ../../src/ServiceFactory 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /build/config/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leovie/php-dry", 3 | "description": "Detect clones in php code.", 4 | "type": "project", 5 | "license": "BSD-3-Clause", 6 | "minimum-stability": "stable", 7 | "prefer-stable": true, 8 | "bin": [ 9 | "php-dry" 10 | ], 11 | "require": { 12 | "php": ">=8.0.0", 13 | "ext-ctype": "*", 14 | "ext-dom": "*", 15 | "ext-iconv": "*", 16 | "ext-mbstring": "*", 17 | "leovie/php-construct-normalize": "^2.0", 18 | "leovie/php-filesystem": "^2.0", 19 | "leovie/php-grouper": "^2.0", 20 | "leovie/php-method-modifier": "^2.0", 21 | "leovie/php-method-runner": "^2.0", 22 | "leovie/php-param-generator": "^2.0", 23 | "leovie/php-token-normalize": "^2.0", 24 | "nikic/php-parser": "^4.13", 25 | "phpdocumentor/type-resolver": "^1.6", 26 | "symfony/console": "^6.0", 27 | "symfony/css-selector": "^6.0", 28 | "symfony/dom-crawler": "^6.0", 29 | "symfony/dotenv": "^6.0", 30 | "symfony/flex": "^2.1", 31 | "symfony/framework-bundle": "^6.0", 32 | "symfony/runtime": "^6.0", 33 | "symfony/stopwatch": "^6.0", 34 | "symfony/yaml": "^6.0", 35 | "thecodingmachine/safe": "^2.2", 36 | "twig/twig": "^3.4" 37 | }, 38 | "require-dev": { 39 | "ext-libxml": "*", 40 | "ergebnis/composer-normalize": "^2.26", 41 | "infection/infection": "^0.26.6", 42 | "jetbrains/phpstorm-attributes": "^1.0", 43 | "phpunit/phpunit": "^9.5", 44 | "psalm/plugin-symfony": "^3.1", 45 | "roave/security-advisories": "dev-latest", 46 | "vimeo/psalm": "^4.10" 47 | }, 48 | "config": { 49 | "optimize-autoloader": true, 50 | "preferred-install": { 51 | "*": "dist" 52 | }, 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "infection/extension-installer": true, 56 | "symfony/flex": true, 57 | "symfony/runtime": true, 58 | "ergebnis/composer-normalize": true 59 | } 60 | }, 61 | "autoload": { 62 | "psr-4": { 63 | "App\\": "src/" 64 | } 65 | }, 66 | "autoload-dev": { 67 | "psr-4": { 68 | "App\\Tests\\": "tests/" 69 | } 70 | }, 71 | "replace": { 72 | "symfony/polyfill-ctype": "*", 73 | "symfony/polyfill-iconv": "*", 74 | "symfony/polyfill-php72": "*" 75 | }, 76 | "scripts": { 77 | "psalm": "psalm -c build/config/psalm.xml --show-info=true", 78 | "phpunit": "phpunit -c build/config/phpunit.xml", 79 | "infection": [ 80 | "Composer\\Config::disableProcessTimeout", 81 | "infection --only-covered --configuration=build/config/infection.json --min-msi=97 --min-covered-msi=97 --coverage=../coverage --threads=4 --test-framework-options='--no-coverage --testsuite=unit'" 82 | ], 83 | "test": [ 84 | "@psalm", 85 | "@phpunit" 86 | ], 87 | "auto-scripts": { 88 | "cache:clear": "symfony-cmd" 89 | }, 90 | "post-install-cmd": [ 91 | "@auto-scripts" 92 | ], 93 | "post-update-cmd": [ 94 | "@auto-scripts" 95 | ] 96 | }, 97 | "conflict": { 98 | "symfony/symfony": "*" 99 | }, 100 | "extra": { 101 | "symfony": { 102 | "allow-contrib": false, 103 | "require": "5.3.*" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | LeoVie\PhpTokenNormalize\PhpTokenNormalizeBundle::class => ['all' => true], 6 | LeoVie\PhpFilesystem\PhpFilesystemBundle::class => ['all' => true], 7 | LeoVie\PhpGrouper\PhpGrouperBundle::class => ['all' => true], 8 | LeoVie\PhpParamGenerator\PhpParamGeneratorBundle::class => ['all' => true], 9 | LeoVie\PhpMethodRunner\PhpMethodRunnerBundle::class => ['all' => true], 10 | LeoVie\PhpMethodModifier\PhpMethodModifierBundle::class => ['all' => true], 11 | LeoVie\PhpConstructNormalize\PhpConstructNormalizeBundle::class => ['all' => true], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | 15 | #esi: true 16 | #fragments: true 17 | php_errors: 18 | log: true 19 | 20 | when@test: 21 | framework: 22 | test: true 23 | session: 24 | storage_factory_id: session.storage.factory.mock_file 25 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/argument.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var argument \phpDocumentor\Descriptor\ArgumentDescriptor #} 2 | 3 | {{ argument.name }} 4 | {{ argument.default }} 5 | {{ argument.type }} 6 | 7 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/constant.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var constant phpDocumentor\Descriptor\ConstantDescriptor #} 2 | 3 | {{ constant.name }} 4 | {{ constant.fullyQualifiedStructuralElementName }} 5 | {{ constant.value }} 6 | {% if inherited_from %}{{ inherited_from }}{% endif %} 7 | {{ include('docblock.xml.twig', {descriptor: constant}) }} 8 | 9 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/docblock.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var descriptor \phpDocumentor\Descriptor\DescriptorAbstract #} 2 | 3 | {{ descriptor.summary }} 4 | {{ descriptor.description }} 5 | {% for tags in descriptor.tags %} 6 | {% for tag in tags %} 7 | {% apply spaceless %} 8 | 17 | {% endapply %} 18 | 19 | {% endfor %} 20 | {% endfor %} 21 | 22 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/method.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var method phpDocumentor\Descriptor\MethodDescriptor #} 2 | 13 | {{ method.name }} 14 | {{ method.fullyQualifiedStructuralElementName }} 15 | {{ method.value }} 16 | {% if inherited_from %} 17 | {{ inherited_from }}{% endif %} 18 | {% for argument in method.arguments %} 19 | {{ include('argument.xml.twig', {descriptor: argument}) }} 20 | {% endfor %} 21 | {{ include('docblock.xml.twig', {descriptor: method}) }} 22 | 23 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/namespace_tree.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var namespace \phpDocumentor\Descriptor\NamespaceDescriptor #} 2 | {% if namespace.children.count > 0 %} 3 | 4 | {% for child in namespace.children %} 5 | {{ include('namespace_tree.xml.twig', {namespace: child}) }} 6 | {% endfor %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/property.xml.twig: -------------------------------------------------------------------------------- 1 | {# @var property phpDocumentor\Descriptor\PropertyDescriptor #} 2 | 3 | {{ property.name }} 4 | {{ property.fullyQualifiedStructuralElementName }} 5 | {{ property.default }} 6 | {% if inherited_from %}{{ inherited_from }}{% endif %} 7 | {{ include('docblock.xml.twig', {descriptor: property}) }} 8 | 9 | -------------------------------------------------------------------------------- /config/phpDocumentor/template/template.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | &2 echo 'ERROR: Invalid installer checksum' 10 | rm composer-setup.php 11 | exit 1 12 | fi 13 | 14 | php composer-setup.php --quiet 15 | RESULT=$? 16 | rm composer-setup.php 17 | exit $RESULT -------------------------------------------------------------------------------- /docker/php-cs-fixer.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.0 2 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as composer 3 | 4 | COPY install_composer.sh /install_composer.sh 5 | RUN chmod +x /install_composer.sh && /install_composer.sh 6 | RUN mv composer.phar /usr/bin/composer 7 | 8 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as runner 9 | COPY --from=composer /usr/bin/composer /usr/bin 10 | 11 | WORKDIR /tools 12 | RUN composer require friendsofphp/php-cs-fixer 13 | 14 | WORKDIR /app 15 | 16 | ENTRYPOINT ["/tools/vendor/bin/php-cs-fixer"] -------------------------------------------------------------------------------- /docker/phpstan.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/phpstan/phpstan:1.5.2 2 | RUN composer global require spaze/phpstan-disallowed-calls -------------------------------------------------------------------------------- /docker/phpunit.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.0 2 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as composer 3 | 4 | COPY install_composer.sh /install_composer.sh 5 | RUN chmod +x /install_composer.sh && /install_composer.sh 6 | RUN mv composer.phar /usr/bin/composer 7 | 8 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as runner 9 | COPY --from=composer /usr/bin/composer /usr/bin 10 | 11 | RUN apk --update-cache add autoconf gcc musl-dev make && pecl install xdebug \ 12 | && docker-php-ext-enable xdebug 13 | 14 | ENV XDEBUG_MODE=coverage 15 | 16 | WORKDIR /app 17 | 18 | ENTRYPOINT ["composer", "phpunit"] -------------------------------------------------------------------------------- /docker/project/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1.1-alpine 2 | 3 | COPY php-dry /var/www/php-dry 4 | COPY config /var/www/config 5 | COPY generated /var/www/generated 6 | COPY src /var/www/src 7 | COPY .env /var/www/.env 8 | COPY composer.json /var/www/composer.json 9 | COPY vendor /var/www/vendor 10 | COPY VERSION /var/www/VERSION 11 | COPY tools /var/www/tools 12 | COPY templates /var/www/templates 13 | COPY resources /var/www/resources 14 | 15 | COPY docker/project/php.ini "$PHP_INI_DIR/php.ini" 16 | 17 | ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 18 | 19 | RUN chmod +x /usr/local/bin/install-php-extensions && \ 20 | install-php-extensions intl 21 | 22 | ENTRYPOINT [ \ 23 | "php", "-d", "memory_limit=-1", \ 24 | "/var/www/php-dry" \ 25 | ] -------------------------------------------------------------------------------- /docker/project/with_blackfire.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1.1-fpm 2 | 3 | ARG CLIENT_ID 4 | ENV BLACKFIRE_CLIENT_ID=$CLIENT_ID 5 | ARG CLIENT_TOKEN 6 | ENV BLACKFIRE_CLIENT_TOKEN=$CLIENT_TOKEN 7 | ARG SERVER_ID 8 | ENV BLACKFIRE_SERVER_ID=$SERVER_ID 9 | ARG SERVER_TOKEN 10 | ENV BLACKFIRE_SERVER_TOKEN=$SERVER_TOKEN 11 | 12 | COPY php-dry /var/www/php-dry 13 | COPY config /var/www/config 14 | COPY generated /var/www/generated 15 | COPY src /var/www/src 16 | COPY .env /var/www/.env 17 | COPY composer.json /var/www/composer.json 18 | COPY vendor /var/www/vendor 19 | COPY VERSION /var/www/VERSION 20 | COPY tools /var/www/tools 21 | COPY templates /var/www/templates 22 | COPY resources /var/www/resources 23 | 24 | COPY docker/project/php.ini "$PHP_INI_DIR/php.ini" 25 | 26 | ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 27 | 28 | RUN chmod +x /usr/local/bin/install-php-extensions && \ 29 | install-php-extensions intl 30 | 31 | RUN mkdir -p /tmp/blackfire \ 32 | && architecture=$(uname -m) \ 33 | && curl -A "Docker" -L https://blackfire.io/api/v1/releases/cli/linux/$architecture | tar zxp -C /tmp/blackfire \ 34 | && mv /tmp/blackfire/blackfire /usr/bin/blackfire \ 35 | && rm -Rf /tmp/blackfire 36 | 37 | RUN blackfire php:install 38 | 39 | ENTRYPOINT [ \ 40 | "blackfire", "run", "--ignore-exit-status", "php", "-d", "memory_limit=-1", \ 41 | "/var/www/php-dry" \ 42 | ] -------------------------------------------------------------------------------- /docker/psalm.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.0 2 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as composer 3 | 4 | COPY install_composer.sh /install_composer.sh 5 | RUN chmod +x /install_composer.sh && /install_composer.sh 6 | RUN mv composer.phar /usr/bin/composer 7 | 8 | FROM php:${PHP_VERSION}-fpm-alpine3.14 as runner 9 | COPY --from=composer /usr/bin/composer /usr/bin 10 | 11 | WORKDIR /app 12 | 13 | ENTRYPOINT ["composer", "psalm"] -------------------------------------------------------------------------------- /generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoVie/php-dry/dcfc57d12b1f7ed7794d1fca4a66778dbf69a151/generated/.gitkeep -------------------------------------------------------------------------------- /php-dry: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | boot(); 53 | 54 | $container = $kernel->getContainer(); 55 | /** @var \App\Application $app */ 56 | $app = $container->get(\App\Application::class); 57 | 58 | $app->setDefaultCommand('php-dry:check', true); 59 | 60 | $app->run(); 61 | -------------------------------------------------------------------------------- /php-dry.xml.example: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/icons/php-dry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoVie/php-dry/dcfc57d12b1f7ed7794d1fca4a66778dbf69a151/resources/icons/php-dry.png -------------------------------------------------------------------------------- /resources/icons/php-dry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | $commands */ 11 | public function __construct(iterable $commands) 12 | { 13 | $commands = $commands instanceof \Traversable ? \iterator_to_array($commands) : $commands; 14 | 15 | foreach ($commands as $command) { 16 | $this->add($command); 17 | } 18 | 19 | parent::__construct('php-dry', \Safe\file_get_contents(__DIR__ . '/../VERSION')); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CloneDetection/CloneDetector.php: -------------------------------------------------------------------------------- 1 | createSourceClonesFromSourceCloneCandidates($type, $sourceCloneCandidates); 20 | } 21 | 22 | /** 23 | * @param SourceCloneCandidate[] $sourceCloneCandidates 24 | * 25 | * @return SourceClone[] 26 | */ 27 | private function createSourceClonesFromSourceCloneCandidates(string $type, array $sourceCloneCandidates): array 28 | { 29 | return array_map( 30 | fn (SourceCloneCandidate $scc): SourceClone => SourceClone::create($type, $scc->getMethodsCollection()), 31 | $this->findSourceCloneCandidatesWithMultipleMethods($sourceCloneCandidates) 32 | ); 33 | } 34 | 35 | /** 36 | * @param SourceCloneCandidate[] $sourceCloneCandidates 37 | * 38 | * @return SourceCloneCandidate[] 39 | */ 40 | private function findSourceCloneCandidatesWithMultipleMethods(array $sourceCloneCandidates): array 41 | { 42 | return array_filter( 43 | $sourceCloneCandidates, 44 | fn (SourceCloneCandidate $sc): bool => $sc->getMethodsCollection()->count() > 1 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CloneDetection/Type1CloneDetector.php: -------------------------------------------------------------------------------- 1 | cloneDetector->detect($type1SourceCloneCandidates, SourceClone::TYPE_1); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CloneDetection/Type2CloneDetector.php: -------------------------------------------------------------------------------- 1 | cloneDetector->detect($type2SourceCloneCandidates, SourceClone::TYPE_2); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CloneDetection/Type3CloneDetector.php: -------------------------------------------------------------------------------- 1 | cloneDetector->detect($type3SourceCloneCandidates, SourceClone::TYPE_3); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CloneDetection/Type4CloneDetector.php: -------------------------------------------------------------------------------- 1 | cloneDetector->detect($type4SourceCloneCandidates, SourceClone::TYPE_4); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Collection/MethodsCollection.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $methods; 18 | 19 | private string $hash = ''; 20 | 21 | private int $hashValidForMethodCount = 0; 22 | 23 | /** @throws CollectionCannotBeEmpty */ 24 | private function __construct(Method ...$methods) 25 | { 26 | if (empty($methods)) { 27 | throw CollectionCannotBeEmpty::create(); 28 | } 29 | 30 | $this->methods = $methods; 31 | } 32 | 33 | /** @throws CollectionCannotBeEmpty */ 34 | public static function create(Method ...$methods): self 35 | { 36 | $methods = self::sortMethods($methods); 37 | 38 | return new self(...$methods); 39 | } 40 | 41 | /** 42 | * @param array $methods 43 | * 44 | * @return array 45 | */ 46 | private static function sortMethods(array $methods): array 47 | { 48 | usort($methods, function (Method $m1, Method $m2): int { 49 | if ($m1->identity() === $m2->identity()) { 50 | return self::METHODS_EQUAL; 51 | } 52 | 53 | return $m1->identity() <= $m2->identity() ? self::METHOD_A_LOWER_B : self::METHOD_B_LOWER_A; 54 | }); 55 | 56 | return $methods; 57 | } 58 | 59 | public function getFirst(): Method 60 | { 61 | /** @var array-key $firstArrayKey */ 62 | $firstArrayKey = array_key_first($this->methods); 63 | 64 | return $this->methods[$firstArrayKey]; 65 | } 66 | 67 | /** @return array */ 68 | public function getAll(): array 69 | { 70 | $currentHash = $this->hash; 71 | $rebuiltHash = $this->getHash(); 72 | 73 | if ($currentHash === $rebuiltHash) { 74 | return $this->methods; 75 | } 76 | 77 | $this->methods = self::sortMethods($this->methods); 78 | 79 | return $this->methods; 80 | } 81 | 82 | public function equals(self $other): bool 83 | { 84 | return $this->getHash() === $other->getHash(); 85 | } 86 | 87 | public function add(Method $method): self 88 | { 89 | $this->methods[] = $method; 90 | 91 | return $this; 92 | } 93 | 94 | public function count(): int 95 | { 96 | return count($this->methods); 97 | } 98 | 99 | public function getHash(): string 100 | { 101 | if ($this->hashValidForMethodCount === $this->count()) { 102 | return $this->hash; 103 | } 104 | 105 | $this->hash = self::buildHash($this->methods); 106 | $this->hashValidForMethodCount = $this->count(); 107 | 108 | return $this->hash; 109 | } 110 | 111 | /** @param array $methods */ 112 | private static function buildHash(array $methods): string 113 | { 114 | return join('<->', array_map( 115 | fn(Method $method): string => $method->identity(), 116 | $methods 117 | )); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Command/Output/DetectClonesCommandOutput.php: -------------------------------------------------------------------------------- 1 | outputHelper = $outputHelper; 19 | 20 | return $this; 21 | } 22 | 23 | public function setStopwatch(Stopwatch $stopwatch): self 24 | { 25 | $this->stopwatch = $stopwatch; 26 | 27 | return $this; 28 | } 29 | 30 | public function start(string $version): self 31 | { 32 | return $this->single(sprintf('Running php-dry %s.', $version)); 33 | } 34 | 35 | public function runtime(StopwatchEvent $runtime): void 36 | { 37 | $this->outputHelper 38 | ->headline('Run information:') 39 | ->single($runtime->__toString()); 40 | } 41 | 42 | public function single(string $line): self 43 | { 44 | $this->outputHelper->single($line); 45 | 46 | return $this; 47 | } 48 | 49 | public function newLine(int $count = 1): self 50 | { 51 | $this->outputHelper->emptyLine($count); 52 | 53 | return $this; 54 | } 55 | 56 | /** @param string[] $items */ 57 | public function listing(array $items): self 58 | { 59 | $this->outputHelper->listing($items); 60 | 61 | return $this; 62 | } 63 | 64 | public function foundMethods(int $methodsCount): self 65 | { 66 | return $this 67 | ->single(sprintf('Found %s methods:', $methodsCount)) 68 | ->lapTime(); 69 | } 70 | 71 | public function lapTime(): self 72 | { 73 | return $this->single($this->stopwatch->lap('detect-clones')->__toString()); 74 | } 75 | 76 | public function stopTime(): self 77 | { 78 | return $this->single($this->stopwatch->stop('detect-clones')->__toString()); 79 | } 80 | 81 | public function noClonesFound(): self 82 | { 83 | return $this->single('No clones found.'); 84 | } 85 | 86 | public function detectionRunningForType(string $type): self 87 | { 88 | $this->outputHelper 89 | ->info(sprintf('Detecting type %s clones', $type)); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * @template T 96 | * 97 | * @param iterable $iterable 98 | * 99 | * @return iterable 100 | */ 101 | public function createProgressBarIterator(iterable $iterable): iterable 102 | { 103 | return $this->outputHelper->createProgressBarIterator($iterable); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Command/Output/Helper/OutputHelper.php: -------------------------------------------------------------------------------- 1 | io = SymfonyStyleFactory::create($input, $output); 19 | $this->io->setVerbosity($this->verbosityLevel); 20 | } 21 | 22 | public static function create(InputInterface $input, OutputInterface $output, int $verbosityLevel): static 23 | { 24 | return new static($input, $output, $verbosityLevel); 25 | } 26 | 27 | public function headline(string $headline, int $level = 0): self 28 | { 29 | $this->io->section($this->formatLine($headline, $level)); 30 | 31 | return $this; 32 | } 33 | 34 | /** @param string[] $items */ 35 | public function listing(array $items, int $level = 0): self 36 | { 37 | $this->io->listing(array_map(fn (string $l) => $this->formatLine($l, $level), $items)); 38 | 39 | return $this; 40 | } 41 | 42 | public function single(string $string, int $level = 0): self 43 | { 44 | $this->io->writeln($this->formatLine($string, $level)); 45 | 46 | return $this; 47 | } 48 | 49 | private function formatLine(string $message, int $level = 0): string 50 | { 51 | return str_repeat(" ", $level) . $message; 52 | } 53 | 54 | public function emptyLine(int $count = 1): self 55 | { 56 | $this->io->newLine($count); 57 | 58 | return $this; 59 | } 60 | 61 | public function info(string $message): self 62 | { 63 | $this->io->info($message); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @template T 70 | * @param iterable $iterable 71 | * 72 | * @return iterable 73 | */ 74 | public function createProgressBarIterator(iterable $iterable): iterable 75 | { 76 | return $this->io->createProgressBar()->iterate($iterable); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Command/Output/Helper/VerboseOutputHelper.php: -------------------------------------------------------------------------------- 1 | getHash() === $b->getHash(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Configuration/ReportConfiguration.php: -------------------------------------------------------------------------------- 1 | cli; 31 | } 32 | 33 | public function getHtml(): ?Html 34 | { 35 | return $this->html; 36 | } 37 | 38 | public function getJson(): ?Json 39 | { 40 | return $this->json; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Configuration/ReportConfiguration/Cli.php: -------------------------------------------------------------------------------- 1 | directory; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Configuration/ReportConfiguration/Json.php: -------------------------------------------------------------------------------- 1 | filepath; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Exception/CollectionCannotBeEmpty.php: -------------------------------------------------------------------------------- 1 | getStartLine(), $function->getStartFilePos()); 16 | } 17 | 18 | public function byEndClassMethodOrFunction(ClassMethod|Function_ $function): CodePosition 19 | { 20 | return CodePosition::create($function->getEndLine(), $function->getEndFilePos()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Factory/CodePosition/CodePositionRangeFactory.php: -------------------------------------------------------------------------------- 1 | codePositionFactory->byStartClassMethodOrFunction($function), 21 | $this->codePositionFactory->byEndClassMethodOrFunction($function), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Factory/Collection/MethodsCollectionFactory.php: -------------------------------------------------------------------------------- 1 | $mts->getMethod(), $methodTokenSequences)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Factory/SourceCloneCandidate/Type2SourceCloneCandidateFactory.php: -------------------------------------------------------------------------------- 1 | $type1SourceCloneCandidates 23 | * 24 | * @return Type2SourceCloneCandidate[] 25 | * @throws CollectionCannotBeEmpty 26 | */ 27 | public function createMultiple(iterable $type1SourceCloneCandidates): array 28 | { 29 | $type2SourceCloneCandidates = []; 30 | foreach ($type1SourceCloneCandidates as $type1SourceCloneCandidate) { 31 | $type2SourceCloneCandidates[] = $this->createFromType1SCC($type1SourceCloneCandidate); 32 | } 33 | 34 | return $this->type2SourceCloneCandidatesMerger->merge($type2SourceCloneCandidates); 35 | } 36 | 37 | private function createFromType1SCC(Type1SourceCloneCandidate $type1SourceCloneCandidate): Type2SourceCloneCandidate 38 | { 39 | return Type2SourceCloneCandidate::create( 40 | $this->tokenSequenceNormalizer->normalizeLevel2($type1SourceCloneCandidate->getTokenSequence()), 41 | $type1SourceCloneCandidate->getMethodsCollection(), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factory/SourceCloneCandidate/Type3SourceCloneCandidateFactory.php: -------------------------------------------------------------------------------- 1 | $type2SourceCloneCandidates 29 | * 30 | * @return Type3SourceCloneCandidate[] 31 | * @throws CollectionCannotBeEmpty 32 | * @throws SubsequenceUtilNotFound 33 | */ 34 | public function createMultiple(iterable $type2SourceCloneCandidates): array 35 | { 36 | $configuration = Configuration::instance(); 37 | 38 | $subsequenceUtil = $this->subsequenceUtilPicker->pick( 39 | $configuration->isEnableLcsAlgorithm() 40 | ? SubsequenceUtilPicker::STRATEGY_LCS 41 | : SubsequenceUtilPicker::STRATEGY_SIMILAR_TEXT 42 | ); 43 | 44 | /** @var array> $type2SourceCloneCandidateGroups */ 45 | $type2SourceCloneCandidateGroups = $this->grouper->groupByCallback( 46 | $type2SourceCloneCandidates, 47 | fn (Type2SourceCloneCandidate $a, Type2SourceCloneCandidate $b): bool => $a->identity() !== $b->identity() 48 | && $subsequenceUtil->percentageOfSimilarText( 49 | $a->identity(), 50 | $b->identity() 51 | ) > $configuration->getMinSimilarTokensPercentage() 52 | ); 53 | 54 | return $this->createMultipleFromGroups($type2SourceCloneCandidateGroups); 55 | } 56 | 57 | /** 58 | * @param array $type2SourceCloneCandidateGroups 59 | * 60 | * @return Type3SourceCloneCandidate[] 61 | * 62 | * @throws CollectionCannotBeEmpty 63 | */ 64 | private function createMultipleFromGroups(array $type2SourceCloneCandidateGroups): array 65 | { 66 | $type2SCCGroupsWithoutSubsetGroups = $this->arrayUtil->removeEntriesThatAreSubsetsOfOtherEntries($type2SourceCloneCandidateGroups); 67 | 68 | $type3SourceCloneCandidates = array_map( 69 | fn (array $type2SCCs): array => array_map( 70 | fn (Type2SourceCloneCandidate $type2SCC): Type3SourceCloneCandidate => Type3SourceCloneCandidate::create( 71 | [$type2SCC->getTokenSequence()], 72 | $type2SCC->getMethodsCollection() 73 | ), 74 | $type2SCCs 75 | ), 76 | $type2SCCGroupsWithoutSubsetGroups 77 | ); 78 | 79 | return $this->type3SourceCloneCandidatesMerger->merge($type3SourceCloneCandidates); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Factory/TokenSequenceFactory.php: -------------------------------------------------------------------------------- 1 | create('getContent()); 20 | } 21 | 22 | private function create(string $code): TokenSequence 23 | { 24 | return TokenSequence::create($this->phpTokenWrapper->tokenize($code)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Grouper/MethodTokenSequencesByTokenSequencesGrouper.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function group(array $methodTokenSequences): array 22 | { 23 | /** @var array $grouped */ 24 | $grouped = $this->grouper->groupByGroupID($methodTokenSequences); 25 | 26 | return $grouped; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Grouper/MethodsBySignatureGrouper.php: -------------------------------------------------------------------------------- 1 | getMethodSignature(), 35 | MethodsCollection::create($firstMethod) 36 | ), 37 | ]; 38 | 39 | foreach ($methods as $method) { 40 | $methodSignatureGroups = $this->addToExistingMatchingMethodSignatureGroupOrCreateNewOne($method, $methodSignatureGroups); 41 | } 42 | 43 | return $methodSignatureGroups; 44 | } 45 | 46 | /** 47 | * @param MethodSignatureGroup[] $methodSignatureGroups 48 | * 49 | * @return MethodSignatureGroup[] 50 | * @throws CollectionCannotBeEmpty 51 | */ 52 | private function addToExistingMatchingMethodSignatureGroupOrCreateNewOne(Method $method, array $methodSignatureGroups): array 53 | { 54 | $methodSignatureGroup = $this->findMatchingMethodSignatureGroup($method, $methodSignatureGroups); 55 | 56 | if ($methodSignatureGroup !== null) { 57 | $methodSignatureGroup->getMethodsCollection()->add($method); 58 | 59 | return $methodSignatureGroups; 60 | } 61 | 62 | $methodSignatureGroup = MethodSignatureGroup::create( 63 | $method->getMethodSignature(), 64 | MethodsCollection::create($method) 65 | ); 66 | $methodSignatureGroups[] = $methodSignatureGroup; 67 | 68 | return $methodSignatureGroups; 69 | } 70 | 71 | /** @param array $methodSignatureGroups */ 72 | private function findMatchingMethodSignatureGroup(Method $method, array $methodSignatureGroups): ?MethodSignatureGroup 73 | { 74 | foreach ($methodSignatureGroups as $methodSignatureGroup) { 75 | if ($this->matchesMethodSignatureGroup($method, $methodSignatureGroup)) { 76 | return $methodSignatureGroup; 77 | } 78 | } 79 | 80 | return null; 81 | } 82 | 83 | private function matchesMethodSignatureGroup(Method $method, MethodSignatureGroup $methodSignatureGroup): bool 84 | { 85 | return $this->methodSignatureComparer->areEqual( 86 | $method->getMethodSignature(), 87 | $methodSignatureGroup->getMethodSignature() 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | addToExistingType2SourceCloneCandidateOrCreateNewOne( 26 | $type2SourceCloneCandidate, 27 | $mergedType2SourceCloneCandidates 28 | ); 29 | } 30 | 31 | return $mergedType2SourceCloneCandidates; 32 | } 33 | 34 | /** 35 | * @param Type2SourceCloneCandidate $type2SourceCloneCandidate 36 | * @param Type2SourceCloneCandidate[] $newType2SourceCloneCandidates 37 | * @return Type2SourceCloneCandidate[] 38 | * @throws CollectionCannotBeEmpty 39 | */ 40 | private function addToExistingType2SourceCloneCandidateOrCreateNewOne( 41 | Type2SourceCloneCandidate $type2SourceCloneCandidate, 42 | array $newType2SourceCloneCandidates, 43 | ): array { 44 | $identity = $type2SourceCloneCandidate->identity(); 45 | 46 | $newNormalizedMethods = $type2SourceCloneCandidate->getMethodsCollection()->getAll(); 47 | if (!array_key_exists($identity, $newType2SourceCloneCandidates)) { 48 | $newType2SourceCloneCandidates[$identity] = Type2SourceCloneCandidate::create( 49 | $type2SourceCloneCandidate->getTokenSequence(), 50 | MethodsCollection::create(...$newNormalizedMethods), 51 | ); 52 | 53 | return $newType2SourceCloneCandidates; 54 | } 55 | 56 | foreach ($newNormalizedMethods as $newNormalizedMethod) { 57 | $newType2SourceCloneCandidates[$identity]->getMethodsCollection()->add($newNormalizedMethod); 58 | } 59 | 60 | return $newType2SourceCloneCandidates; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Merge/Type3SourceCloneCandidatesMerger.php: -------------------------------------------------------------------------------- 1 | $groups 15 | * @return Type3SourceCloneCandidate[] 16 | * 17 | * @throws CollectionCannotBeEmpty 18 | */ 19 | public function merge(array $groups): array 20 | { 21 | $mergedType3SourceCloneCandidates = []; 22 | foreach ($groups as $type3SourceCloneCandidates) { 23 | $tokenSequences = []; 24 | $methods = []; 25 | foreach ($type3SourceCloneCandidates as $type3SourceCloneCandidate) { 26 | $tokenSequences = array_merge( 27 | $tokenSequences, 28 | $type3SourceCloneCandidate->getTokenSequences() 29 | ); 30 | $methods = array_merge($methods, $type3SourceCloneCandidate->getMethodsCollection()->getAll()); 31 | } 32 | 33 | $mergedType3SourceCloneCandidates[] = Type3SourceCloneCandidate::create( 34 | $tokenSequences, 35 | MethodsCollection::create(...$methods) 36 | ); 37 | } 38 | 39 | return $mergedType3SourceCloneCandidates; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Model/CodePosition/CodePosition.php: -------------------------------------------------------------------------------- 1 | line; 23 | } 24 | 25 | public function getFilePos(): int 26 | { 27 | return $this->filePos; 28 | } 29 | 30 | /** @return array{'line': int, 'filePos': int} */ 31 | public function jsonSerialize(): array 32 | { 33 | return [ 34 | 'line' => $this->getLine(), 35 | 'filePos' => $this->getFilePos(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Model/CodePosition/CodePositionRange.php: -------------------------------------------------------------------------------- 1 | start; 23 | } 24 | 25 | public function getEnd(): CodePosition 26 | { 27 | return $this->end; 28 | } 29 | 30 | public function countOfLines(): int 31 | { 32 | return $this->getEnd()->getLine() - $this->getStart()->getLine(); 33 | } 34 | 35 | /** @return array{'start': CodePosition, 'end': CodePosition, 'countOfLines': int} */ 36 | public function jsonSerialize(): array 37 | { 38 | return [ 39 | 'start' => $this->getStart(), 40 | 'end' => $this->getEnd(), 41 | 'countOfLines' => $this->countOfLines(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/FilepathMethods/FilepathMethods.php: -------------------------------------------------------------------------------- 1 | filepath; 27 | } 28 | 29 | /** @return Function_[]|ClassMethod[] */ 30 | public function getMethods(): array 31 | { 32 | return $this->methods; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Model/Identity.php: -------------------------------------------------------------------------------- 1 | methodSignature; 36 | } 37 | 38 | public function getName(): string 39 | { 40 | return $this->name; 41 | } 42 | 43 | public function getFilepath(): string 44 | { 45 | return $this->filepath; 46 | } 47 | 48 | public function getCodePositionRange(): CodePositionRange 49 | { 50 | return $this->codePositionRange; 51 | } 52 | 53 | public function getContent(): string 54 | { 55 | return $this->content; 56 | } 57 | 58 | public function identity(): string 59 | { 60 | return $this->getFilepath() 61 | . '_' 62 | . $this->getName() 63 | . '_' 64 | . $this->getCodePositionRange()->getStart()->getLine() 65 | . $this->getCodePositionRange()->getStart()->getFilePos(); 66 | } 67 | 68 | /** @return array{'filepath': string, 'name': string, 'codePositionRange': CodePositionRange} */ 69 | public function jsonSerialize(): array 70 | { 71 | return [ 72 | 'filepath' => $this->getFilepath(), 73 | 'name' => $this->getName(), 74 | 'codePositionRange' => $this->getCodePositionRange(), 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Model/Method/MethodSignature.php: -------------------------------------------------------------------------------- 1 | paramTypes; 42 | } 43 | 44 | /** @return int[] */ 45 | public function getParamsOrder(): array 46 | { 47 | return $this->paramsOrder; 48 | } 49 | 50 | public function getReturnType(): string 51 | { 52 | return $this->returnType; 53 | } 54 | 55 | public function getHash(): string 56 | { 57 | if ($this->hash === '') { 58 | $this->hash = join('-', $this->getParamTypes()) . '_' . $this->getReturnType(); 59 | } 60 | 61 | return $this->hash; 62 | } 63 | 64 | /** @return array{'paramTypes': string[], "returnType": string} */ 65 | public function jsonSerialize(): array 66 | { 67 | return [ 68 | 'paramTypes' => $this->getParamTypes(), 69 | 'returnType' => $this->getReturnType(), 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Model/Method/MethodSignatureGroup.php: -------------------------------------------------------------------------------- 1 | methodSignature; 25 | } 26 | 27 | public function getMethodsCollection(): MethodsCollection 28 | { 29 | return $this->methodsCollection; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Model/Method/MethodTokenSequence.php: -------------------------------------------------------------------------------- 1 | method; 29 | } 30 | 31 | public function getTokenSequence(): TokenSequence 32 | { 33 | return $this->tokenSequence; 34 | } 35 | 36 | public function identity(): string 37 | { 38 | return $this->identity; 39 | } 40 | 41 | public function groupID(): string 42 | { 43 | return $this->identity; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Model/RunResult/RunResultSet.php: -------------------------------------------------------------------------------- 1 | method; 30 | } 31 | 32 | public function getParamListSet(): ParamListSet 33 | { 34 | return $this->paramListSet; 35 | } 36 | 37 | /** @return MethodResult[] */ 38 | public function getMethodResults(): array 39 | { 40 | return $this->methodResults; 41 | } 42 | 43 | public function hash(): string 44 | { 45 | return $this->paramListSet->hash() 46 | . '=>' 47 | . join('&', array_map(fn (MethodResult $mr): string => serialize($mr), $this->methodResults)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Model/SourceClone/SourceClone.php: -------------------------------------------------------------------------------- 1 | type; 32 | } 33 | 34 | public function getMethodsCollection(): MethodsCollection 35 | { 36 | return $this->methodsCollection; 37 | } 38 | 39 | /** @return array{'type': string, "methods": Method[]} */ 40 | public function jsonSerialize(): array 41 | { 42 | return [ 43 | 'type' => $this->getType(), 44 | 'methods' => $this->getMethodsCollection()->getAll(), 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Model/SourceCloneCandidate/SourceCloneCandidate.php: -------------------------------------------------------------------------------- 1 | tokenSequence; 24 | } 25 | 26 | public function getMethodsCollection(): MethodsCollection 27 | { 28 | return $this->methodsCollection; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Model/SourceCloneCandidate/Type2SourceCloneCandidate.php: -------------------------------------------------------------------------------- 1 | tokenSequence; 26 | } 27 | 28 | public function getMethodsCollection(): MethodsCollection 29 | { 30 | return $this->methodsCollection; 31 | } 32 | 33 | public function identity(): string 34 | { 35 | return $this->getTokenSequence()->identity(); 36 | } 37 | 38 | public function groupID(): string 39 | { 40 | return $this->identity(); 41 | } 42 | 43 | public function __toString(): string 44 | { 45 | return $this->identity(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Model/SourceCloneCandidate/Type3SourceCloneCandidate.php: -------------------------------------------------------------------------------- 1 | tokenSequences; 28 | } 29 | 30 | public function getMethodsCollection(): MethodsCollection 31 | { 32 | return $this->methodsCollection; 33 | } 34 | 35 | public function identity(): string 36 | { 37 | return join('-', array_map(fn (TokenSequence $ts): string => $ts->identity(), $this->getTokenSequences())); 38 | } 39 | 40 | public function __toString(): string 41 | { 42 | return $this->identity(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/SourceCloneCandidate/Type4SourceCloneCandidate.php: -------------------------------------------------------------------------------- 1 | methodsCollection; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OutputFormatter/Model/CodePosition/CodePositionOutputFormatter.php: -------------------------------------------------------------------------------- 1 | getLine(), 16 | $codePosition->getFilePos() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/OutputFormatter/Model/CodePosition/CodePositionRangeOutputFormatter.php: -------------------------------------------------------------------------------- 1 | codePositionOutputFormatter->format($codePositionRange->getStart()), 20 | $this->codePositionOutputFormatter->format($codePositionRange->getEnd()), 21 | $codePositionRange->countOfLines() 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/OutputFormatter/Model/Method/MethodOutputFormatter.php: -------------------------------------------------------------------------------- 1 | getFilepath(), 21 | $method->getName(), 22 | $this->codePositionRangeOutputFormatter->format($method->getCodePositionRange()) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OutputFormatter/Model/Method/MethodSignatureOutputFormatter.php: -------------------------------------------------------------------------------- 1 | getParamsOrder(), $methodSignature->getParamTypes()); 14 | ksort($orderedParamTypes); 15 | 16 | return sprintf( 17 | '(%s): %s', 18 | join(', ', $orderedParamTypes), 19 | $methodSignature->getReturnType() 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OutputFormatter/Model/SourceClone/SourceCloneOutputFormatter.php: -------------------------------------------------------------------------------- 1 | getType(), 22 | join( 23 | "\n\t", 24 | array_map( 25 | fn (Method $m) => $this->methodOutputFormatter->format($m), 26 | $sourceClone->getMethodsCollection()->getAll() 27 | ) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Report/Converter/SourceClonesToArrayConverter.php: -------------------------------------------------------------------------------- 1 | $clones 18 | * 19 | * @return array}}> 20 | */ 21 | public function sourceClonesToArray(array $clones): array 22 | { 23 | return array_map(function (SourceClone $clone): array { 24 | return [ 25 | 'sourceClone' => [ 26 | 'methods' => array_map(function (Method $method): array { 27 | return [ 28 | 'method' => [ 29 | 'name' => $method->getName(), 30 | 'methodSignature' => 'function ' . $method->getName() . $this->methodSignatureOutput->format($method->getMethodSignature()), 31 | 'filepath' => \Safe\realpath($method->getFilepath()), 32 | 'codePositionRange' => [ 33 | 'start' => [ 34 | 'line' => $method->getCodePositionRange()->getStart()->getLine(), 35 | 'filePos' => $method->getCodePositionRange()->getStart()->getFilePos(), 36 | ], 37 | 'end' => [ 38 | 'line' => $method->getCodePositionRange()->getEnd()->getLine(), 39 | 'filePos' => $method->getCodePositionRange()->getEnd()->getFilePos(), 40 | ], 41 | 'countOfLines' => $method->getCodePositionRange()->countOfLines(), 42 | ], 43 | 'content' => $method->getContent(), 44 | ], 45 | ]; 46 | }, $clone->getMethodsCollection()->getAll()), 47 | ], 48 | ]; 49 | }, $clones); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Report/Formatter/CliReportFormatter.php: -------------------------------------------------------------------------------- 1 | getAll() as $type => $clones) { 16 | $this->addLine($type); 17 | $this->addLine('------'); 18 | $this->addLine(''); 19 | 20 | foreach ($clones as $clone) { 21 | $clone = $clone['sourceClone']; 22 | 23 | foreach ($clone['methods'] as $method) { 24 | $m = $method['method']; 25 | $this->addLine(' * ' . $m['filepath'] 26 | . ': ' . $m['name'] 27 | . ' (' . $m['codePositionRange']['start']['line'] 28 | . ' (position ' . $m['codePositionRange']['start']['filePos'] . ')' 29 | . ' - ' . $m['codePositionRange']['end']['line'] 30 | . ' (position ' . $m['codePositionRange']['end']['filePos'] . ')' 31 | . ' (' . $m['codePositionRange']['countOfLines'] . ' lines)' 32 | . ')'); 33 | } 34 | } 35 | } 36 | 37 | return $this->popReport(); 38 | } 39 | 40 | private function addLine(string $line): void 41 | { 42 | $this->cliReport .= $line . "\n"; 43 | } 44 | 45 | private function popReport(): string 46 | { 47 | $report = $this->cliReport; 48 | $this->cliReport = ''; 49 | 50 | return $report; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Report/Formatter/HtmlReportFormatter.php: -------------------------------------------------------------------------------- 1 | fileSystemLoaderFactory->create(__DIR__ . '/../../../templates/php-dry'); 29 | $twig = $this->environmentFactory->create($loader, ['cache' => '/tmp/twig_compilation_cache']); 30 | 31 | $template = $twig->load('php-dry.html.twig'); 32 | 33 | return $template->render(['output' => $report->getAll()]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Report/Formatter/JsonReportFormatter.php: -------------------------------------------------------------------------------- 1 | getAll()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Report/Formatter/ReportFormatter.php: -------------------------------------------------------------------------------- 1 | }}> $type1 11 | * @param array}}> $type2 12 | * @param array}}> $type3 13 | * @param array}}> $type4 14 | */ 15 | private function __construct( 16 | private array $type1, 17 | private array $type2, 18 | private array $type3, 19 | private array $type4 20 | ) 21 | { 22 | } 23 | 24 | /** 25 | * @param array}}> $type1 26 | * @param array}}> $type2 27 | * @param array}}> $type3 28 | * @param array}}> $type4 29 | */ 30 | public static function create(array $type1, array $type2, array $type3, array $type4): self 31 | { 32 | return new self($type1, $type2, $type3, $type4); 33 | } 34 | 35 | /** @return array}}>> */ 36 | public function getAll(): array 37 | { 38 | return [ 39 | SourceClone::TYPE_1 => $this->type1, 40 | SourceClone::TYPE_2 => $this->type2, 41 | SourceClone::TYPE_3 => $this->type3, 42 | SourceClone::TYPE_4 => $this->type4, 43 | ]; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Report/ReportBuilder.php: -------------------------------------------------------------------------------- 1 | $clones */ 15 | public function createReport(array $clones): Report 16 | { 17 | $sortedClones = [ 18 | SourceClone::TYPE_1 => [], 19 | SourceClone::TYPE_2 => [], 20 | SourceClone::TYPE_3 => [], 21 | SourceClone::TYPE_4 => [], 22 | ]; 23 | 24 | foreach ($clones as $clone) { 25 | $cloneType = $clone->getType(); 26 | $sortedClones[$cloneType][] = $clone; 27 | } 28 | 29 | return Report::create( 30 | $this->sourceClonesToArrayConverter 31 | ->sourceClonesToArray($sortedClones[SourceClone::TYPE_1]), 32 | $this->sourceClonesToArrayConverter 33 | ->sourceClonesToArray($sortedClones[SourceClone::TYPE_2]), 34 | $this->sourceClonesToArrayConverter 35 | ->sourceClonesToArray($sortedClones[SourceClone::TYPE_3]), 36 | $this->sourceClonesToArrayConverter 37 | ->sourceClonesToArray($sortedClones[SourceClone::TYPE_4]), 38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Report/Reporter.php: -------------------------------------------------------------------------------- 1 | $clones 37 | * 38 | * @throws FilesystemException 39 | * @throws JsonException 40 | * @throws LoaderError 41 | * @throws RuntimeError 42 | * @throws SyntaxError 43 | */ 44 | public function report(array $clones): void 45 | { 46 | $report = $this->reportBuilder->createReport($clones); 47 | 48 | $configuration = Configuration::instance(); 49 | 50 | if ($configuration->getReportConfiguration()->getHtml()) { 51 | $htmlReport = $this->htmlReportFormatter->format($report); 52 | $this->htmlReportReporter->report($htmlReport); 53 | } 54 | 55 | if ($configuration->getReportConfiguration()->getJson()) { 56 | $jsonReport = $this->jsonReportFormatter->format($report); 57 | $this->jsonReportReporter->report($jsonReport); 58 | } 59 | 60 | if ($configuration->getReportConfiguration()->getCli()) { 61 | $cliReport = $this->cliReportFormatter->format($report); 62 | $this->cliReportReporter->report($cliReport); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Report/Saver/CliReportReporter.php: -------------------------------------------------------------------------------- 1 | detectClonesCommandOutput->single($report); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Report/Saver/HtmlReportReporter.php: -------------------------------------------------------------------------------- 1 | getReportConfiguration()->getHtml(); 18 | 19 | $htmlDirectory = rtrim($htmlConfiguration->getDirectory()) . '/php-dry_html-report/'; 20 | $htmlFilepath = $htmlDirectory . 'php-dry.html'; 21 | 22 | if (!file_exists($htmlDirectory)) { 23 | \Safe\mkdir($htmlDirectory); 24 | } 25 | if (!file_exists($htmlDirectory . '/resources/')) { 26 | \Safe\mkdir($htmlDirectory . '/resources/'); 27 | } 28 | if (!file_exists($htmlDirectory . '/resources/icons/')) { 29 | \Safe\mkdir($htmlDirectory . '/resources/icons/'); 30 | } 31 | 32 | \Safe\copy(__DIR__ . '/../../../resources/icons/php-dry.svg', $htmlDirectory . '/resources/icons/php-dry.svg'); 33 | 34 | \Safe\file_put_contents($htmlFilepath, $report); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Report/Saver/JsonReportReporter.php: -------------------------------------------------------------------------------- 1 | getReportConfiguration()->getJson(); 18 | 19 | \Safe\file_put_contents($jsonConfiguration->getFilepath(), $report); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Report/Saver/ReportReporter.php: -------------------------------------------------------------------------------- 1 | arrayUtil->flatten($cloneGroups), 33 | fn (SourceClone $c): bool => !$this->cloneShouldBeIgnored($c, $configuration) 34 | ) 35 | ); 36 | 37 | return $nonIgnoredClones; 38 | } 39 | 40 | private function cloneShouldBeIgnored(SourceClone $clone, Configuration $configuration): bool 41 | { 42 | /** @var non-empty-array $tokenLengths */ 43 | $tokenLengths = array_map( 44 | fn (Method $m): int => $this->tokenSequenceFactory->createFromMethod($m)->length(), 45 | $clone->getMethodsCollection()->getAll() 46 | ); 47 | 48 | return max($tokenLengths) < $configuration->getMinTokenLength(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Service/PhpDocumentorRunner.php: -------------------------------------------------------------------------------- 1 | &1', 18 | $configuration->getPhpDocumentorExecutablePath(), 19 | $directory, 20 | $configuration->getPhpDocumentorReportPath(), 21 | self::TEMPLATE_PATH 22 | ); 23 | 24 | exec($command, $output, $resultCode); 25 | 26 | if ($resultCode !== 0) { 27 | throw PhpDocumentorFailed::create(join("\n", $output)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Service/PhpDryVersionService.php: -------------------------------------------------------------------------------- 1 | kernel->getProjectDir() . '/VERSION'; 21 | 22 | return trim($this->filesystem->readFile($versionFilepath)); 23 | } 24 | } -------------------------------------------------------------------------------- /src/ServiceFactory/CrawlerFactory.php: -------------------------------------------------------------------------------- 1 | $options */ 13 | public function create(LoaderInterface $loader, array $options = []): Environment 14 | { 15 | return new Environment($loader, $options); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ServiceFactory/FileSystemLoaderFactory.php: -------------------------------------------------------------------------------- 1 | $paths */ 12 | public function create(string|array $paths = [], string|null $rootPath = null): FilesystemLoader 13 | { 14 | return new FilesystemLoader($paths, $rootPath); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ServiceFactory/FinderFactory.php: -------------------------------------------------------------------------------- 1 | flatten($value, $level - 1)); 24 | } else { 25 | $result[] = $value; 26 | } 27 | } 28 | 29 | return $result; 30 | } 31 | 32 | /** 33 | * @param array $a 34 | * @param array $b 35 | */ 36 | public function arrayContainsOtherArray(array $a, array $b): bool 37 | { 38 | return count(array_intersect($a, $b)) === count($b); 39 | } 40 | 41 | /** 42 | * @param array> $array 43 | * 44 | * @return array> 45 | */ 46 | public function removeEntriesThatAreSubsetsOfOtherEntries(array $array): array 47 | { 48 | /** @var array> $uniqueArray */ 49 | $uniqueArray = $this->unique($array); 50 | 51 | if (count($uniqueArray) <= 1) { 52 | return array_values($uniqueArray); 53 | } 54 | 55 | $result = []; 56 | 57 | foreach ($uniqueArray as $i => $a) { 58 | foreach ($uniqueArray as $j => $b) { 59 | if ($i === $j) { 60 | continue; 61 | } 62 | 63 | if ($this->arrayContainsOtherArray($b, $a)) { 64 | break; 65 | } 66 | 67 | if (in_array($a, $result)) { 68 | break; 69 | } 70 | 71 | $result[] = $a; 72 | } 73 | } 74 | 75 | return $result; 76 | } 77 | 78 | /** 79 | * @param mixed[] $array 80 | * @return mixed[] 81 | */ 82 | public function unique(array $array): array 83 | { 84 | $array = $this->nestedSort($array); 85 | 86 | $unique = []; 87 | foreach ($array as $item) { 88 | if (!in_array($item, $unique)) { 89 | $unique[] = $item; 90 | } 91 | } 92 | 93 | return $unique; 94 | } 95 | 96 | /** 97 | * @param mixed[] $array 98 | * @return mixed[] 99 | */ 100 | private function nestedSort(array $array): array 101 | { 102 | $sorted = []; 103 | foreach ($array as $item) { 104 | if (!is_array($item)) { 105 | $sorted[] = $item; 106 | } else { 107 | $sorted[] = $this->nestedSort($item); 108 | } 109 | } 110 | 111 | sort($sorted); 112 | 113 | return $sorted; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Util/Subsequence/LongestCommonSubsequenceUtil.php: -------------------------------------------------------------------------------- 1 | lcs($a, $b) / (max(strlen($a), strlen($b)))) * 100); 12 | } 13 | 14 | private function lcs(string $a, string $b): int 15 | { 16 | $aLength = strlen($a); 17 | $bLength = strlen($b); 18 | 19 | if (empty($aLength) || empty($bLength)) { 20 | return 0; 21 | } 22 | 23 | $table = []; 24 | 25 | foreach ([0, 1] as $i) { 26 | for ($j = 0; $j <= $bLength; $j++) { 27 | $table[$i][$j] = 0; 28 | } 29 | } 30 | 31 | for ($i = 1; $i < $aLength + 1; $i++) { 32 | for ($j = 0; $j < $bLength + 1; $j++) { 33 | $table[0][$j] = $table[1][$j]; 34 | } 35 | 36 | for ($j = 1; $j < $bLength + 1; $j++) { 37 | $table[1][$j] = match (true) { 38 | $a[$i - 1] === $b[$j - 1] => $table[0][$j - 1] + 1, 39 | default => max( 40 | $table[0][$j], 41 | $table[1][$j - 1] 42 | ) 43 | }; 44 | } 45 | } 46 | 47 | return $table[1][$bLength]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Util/Subsequence/SimilarTextSubsequenceUtil.php: -------------------------------------------------------------------------------- 1 | $this->longestCommonSubsequenceUtil, 25 | self::STRATEGY_SIMILAR_TEXT => $this->similarTextSubsequenceUtil, 26 | default => throw SubsequenceUtilNotFound::create($strategy) 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Wrapper/PhpTokenWrapper.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | Clone {{ cloneIndex }} 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for instanceIndex, method in clone.sourceClone.methods %} 19 | {{ include('clone_instance_row.html.twig', {type: type, cloneIndex: cloneIndex, instanceIndex: instanceIndex + 1, method: method}) }} 20 | {% endfor %} 21 | 22 |
#FileLinesContent
23 |
24 | -------------------------------------------------------------------------------- /templates/php-dry/clone_instance_row.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {{ instanceIndex }} 3 | {{ method.method.filepath }}
4 | {{ method.method.codePositionRange.start.line }}:{{ method.method.codePositionRange.start.filePos }} - {{ method.method.codePositionRange.end.line }}:{{ method.method.codePositionRange.end.filePos }} 5 | 6 | {{ method.method.codePositionRange.countOfLines }} 7 | 8 |
9 |
10 |

17 |
19 |
{{ method.method.content }}
20 |
21 |
22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /templates/php-dry/nav.html.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/php-dry/php-dry.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 11 | php-dry: Report 12 | 13 | 16 | 17 | 18 |
19 |
20 |
The logo of php-dry: A T-shirt on a clothesline rope.
22 |

php-dry: Report

23 |
24 | {{ include('nav.html.twig') }} 25 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /templates/php-dry/tabpanel.html.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Functional/Command/php-dry.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ../../testdata/clone-detection-testdata-with-native-types/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Functional/Command/with-DTOs/generated/reports/php-dry_html-report/resources/icons/php-dry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/Functional/Command/with-native-types/generated/reports/php-dry_html-report/resources/icons/php-dry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/Functional/Command/with-native-types/generated/reports/resources/icons/php-dry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/Functional/Service/PhpDocumentorRunnerTest.php: -------------------------------------------------------------------------------- 1 | createMock(Configuration::class); 14 | $configuration->method('getPhpDocumentorExecutablePath')->willReturn('tools/phpDocumentor.phar'); 15 | $configuration->method('getPhpDocumentorReportPath')->willReturn(__DIR__ . '/phpDocumentorReport'); 16 | 17 | Configuration::setInstance($configuration); 18 | 19 | (new PhpDocumentorRunner())->run(__DIR__ . '/../../testdata/clone-detection-testdata-with-native-types'); 20 | 21 | self::assertXmlFileEqualsXmlFile(__DIR__ . '/expected_phpDocumentor_structure.xml', __DIR__ . '/phpDocumentorReport/structure.xml'); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Unit/CloneDetection/Type1CloneDetectorTest.php: -------------------------------------------------------------------------------- 1 | createMock(MethodsCollection::class))]; 18 | 19 | $expected = $clones; 20 | 21 | $cloneDetector = $this->createMock(CloneDetector::class); 22 | $cloneDetector->method('detect')->willReturn($clones); 23 | 24 | self::assertSame($expected, (new Type1CloneDetector($cloneDetector))->detect([])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/CloneDetection/Type2CloneDetectorTest.php: -------------------------------------------------------------------------------- 1 | createMock(MethodsCollection::class))]; 18 | 19 | $expected = $clones; 20 | 21 | $cloneDetector = $this->createMock(CloneDetector::class); 22 | $cloneDetector->method('detect')->willReturn($clones); 23 | 24 | self::assertSame($expected, (new Type2CloneDetector($cloneDetector))->detect([])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/CloneDetection/Type3CloneDetectorTest.php: -------------------------------------------------------------------------------- 1 | createMock(MethodsCollection::class))]; 18 | 19 | $expected = $clones; 20 | 21 | $cloneDetector = $this->createMock(CloneDetector::class); 22 | $cloneDetector->method('detect')->willReturn($clones); 23 | 24 | self::assertSame($expected, (new Type3CloneDetector($cloneDetector))->detect([])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/Compare/MethodSignatureComparerTest.php: -------------------------------------------------------------------------------- 1 | areEqual($a, $b)); 16 | } 17 | 18 | public function areEqualProvider(): array 19 | { 20 | return [ 21 | 'non equal param types -> not equal' => [ 22 | \App\Model\Method\MethodSignature::create(['int'], [0], 'string'), 23 | \App\Model\Method\MethodSignature::create(['string'], [0], 'string'), 24 | 'expected' => false, 25 | ], 26 | 'non equal return type -> not equal' => [ 27 | \App\Model\Method\MethodSignature::create(['int'], [0], 'string'), 28 | \App\Model\Method\MethodSignature::create(['int'], [0], 'int'), 29 | 'expected' => false, 30 | ], 31 | 'everything equal -> equal' => [ 32 | \App\Model\Method\MethodSignature::create(['int'], [0], 'string'), 33 | \App\Model\Method\MethodSignature::create(['int'], [0], 'string'), 34 | 'expected' => true, 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/Configuration/ConfigurationFactoryTest.php: -------------------------------------------------------------------------------- 1 | createConfigurationFromXmlFile($xmlFilepath)); 16 | } 17 | 18 | public function createConfigurationFromXmlProvider(): \Generator 19 | { 20 | yield 'php-dry-01.xml' => [ 21 | 'expected' => Configuration::create( 22 | [ 23 | __DIR__ . '/../../testdata/src' 24 | ], 25 | false, 26 | 50, 27 | 80, 28 | false, 29 | 10, 30 | false, 31 | '/var/phpDocumentorReport', 32 | __DIR__ . '/../../testdata/tools/phpDocumentor.phar', 33 | '/cache', 34 | ReportConfiguration::create( 35 | ReportConfiguration\Cli::create(), 36 | ReportConfiguration\Html::create(__DIR__ . '/../../testdata/reports'), 37 | ReportConfiguration\Json::create(__DIR__ . '/../../testdata/reports/php-dry.json'), 38 | ) 39 | ), 40 | 'xmlFilepath' => __DIR__ . '/../../testdata/php-dry-01.xml' 41 | ]; 42 | 43 | yield 'php-dry-02.xml' => [ 44 | 'expected' => Configuration::create( 45 | [ 46 | '/var/www', 47 | '/bar/foo' 48 | ], 49 | true, 50 | 60, 51 | 75, 52 | true, 53 | 15, 54 | true, 55 | __DIR__ . '/../../testdata/report_of_phpdoc', 56 | __DIR__ . '/../../testdata/bla_foo', 57 | '/tmp/', 58 | ReportConfiguration::create( 59 | ReportConfiguration\Cli::create(), 60 | null, 61 | ReportConfiguration\Json::create(__DIR__ . '/../../testdata/foo/bar/report.json'), 62 | ) 63 | ), 64 | 'xmlFilepath' => __DIR__ . '/../../testdata/php-dry-02.xml' 65 | ]; 66 | } 67 | } -------------------------------------------------------------------------------- /tests/Unit/Exception/CollectionCannotBeEmptyTest.php: -------------------------------------------------------------------------------- 1 | getMessage()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/Exception/NoParamRequestForParamTypeTest.php: -------------------------------------------------------------------------------- 1 | getMessage() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unit/Exception/SubsequenceUtilNotFoundTest.php: -------------------------------------------------------------------------------- 1 | getMessage() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unit/Factory/CodePosition/CodePositionFactoryTest.php: -------------------------------------------------------------------------------- 1 | byStartClassMethodOrFunction($function)); 19 | } 20 | 21 | public function byStartClassMethodOrFunctionProvider(): array 22 | { 23 | return [ 24 | 'ClassMethod (#1)' => [ 25 | 'expected' => CodePosition::create(10, 0), 26 | 'function' => $this->mockClassMethod([10, 0], [20, 5]), 27 | ], 28 | 'ClassMethod (#2)' => [ 29 | 'expected' => CodePosition::create(99, 12), 30 | 'function' => $this->mockClassMethod([99, 12], [750, 50]), 31 | ], 32 | 'Function (#1)' => [ 33 | 'expected' => CodePosition::create(10, 0), 34 | 'function' => $this->mockFunction([10, 0], [20, 5]), 35 | ], 36 | 'Function (#2)' => [ 37 | 'expected' => CodePosition::create(99, 12), 38 | 'function' => $this->mockFunction([99, 12], [750, 50]), 39 | ], 40 | ]; 41 | } 42 | 43 | /** @dataProvider byEndClassMethodOrFunctionProvider */ 44 | public function testByEndClassMethodOrFunction(CodePosition $expected, ClassMethod|Function_ $function): void 45 | { 46 | self::assertEquals($expected, (new CodePositionFactory())->byEndClassMethodOrFunction($function)); 47 | } 48 | 49 | public function byEndClassMethodOrFunctionProvider(): array 50 | { 51 | return [ 52 | 'ClassMethod (#1)' => [ 53 | 'expected' => CodePosition::create(20, 5), 54 | 'function' => $this->mockClassMethod([10, 0], [20, 5]), 55 | ], 56 | 'ClassMethod (#2)' => [ 57 | 'expected' => CodePosition::create(750, 50), 58 | 'function' => $this->mockClassMethod([99, 12], [750, 50]), 59 | ], 60 | 'Function (#1)' => [ 61 | 'expected' => CodePosition::create(20, 5), 62 | 'function' => $this->mockFunction([10, 0], [20, 5]), 63 | ], 64 | 'Function (#2)' => [ 65 | 'expected' => CodePosition::create(750, 50), 66 | 'function' => $this->mockFunction([99, 12], [750, 50]), 67 | ], 68 | ]; 69 | } 70 | 71 | private function mockClassMethod(array $start, array $end): ClassMethod 72 | { 73 | $classMethod = $this->createMock(ClassMethod::class); 74 | $classMethod->method('getStartLine')->willReturn($start[0]); 75 | $classMethod->method('getStartFilePos')->willReturn($start[1]); 76 | $classMethod->method('getEndLine')->willReturn($end[0]); 77 | $classMethod->method('getEndFilePos')->willReturn($end[1]); 78 | 79 | return $classMethod; 80 | } 81 | 82 | private function mockFunction(array $start, array $end): Function_ 83 | { 84 | $function = $this->createMock(Function_::class); 85 | $function->method('getStartLine')->willReturn($start[0]); 86 | $function->method('getStartFilePos')->willReturn($start[1]); 87 | $function->method('getEndLine')->willReturn($end[0]); 88 | $function->method('getEndFilePos')->willReturn($end[1]); 89 | 90 | return $function; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Unit/Factory/CodePosition/CodePositionRangeFactoryTest.php: -------------------------------------------------------------------------------- 1 | createMock(CodePositionFactory::class); 20 | $codePositionFactory->method('byStartClassMethodOrFunction')->willReturn($start); 21 | $codePositionFactory->method('byEndClassMethodOrFunction')->willReturn($end); 22 | 23 | $function = $this->createMock(Function_::class); 24 | 25 | self::assertEquals($expected, (new CodePositionRangeFactory($codePositionFactory))->byClassMethodOrFunction($function)); 26 | } 27 | 28 | public function byClassMethodOrFunctionProvider(): array 29 | { 30 | $start = CodePosition::create(10, 50); 31 | $end = CodePosition::create(100, 7); 32 | 33 | return [ 34 | [ 35 | 'expected' => CodePositionRange::create($start, $end), 36 | 'start' => $start, 37 | 'end' => $end, 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Factory/Collection/MethodsCollectionFactoryTest.php: -------------------------------------------------------------------------------- 1 | fromMethodTokenSequence($hasMethods)); 21 | } 22 | 23 | public function fromHasMethodsProvider(): \Generator 24 | { 25 | $method1 = $this->mockMethod('method1'); 26 | $method2 = $this->mockMethod('method2'); 27 | $method3 = $this->mockMethod('method3'); 28 | $method4 = $this->mockMethod('method4'); 29 | 30 | $hasMethod1 = $this->mockMethodTokenSequence($method1); 31 | $hasMethod2 = $this->mockMethodTokenSequence($method2); 32 | $hasMethod3 = $this->mockMethodTokenSequence($method3); 33 | $hasMethod4 = $this->mockMethodTokenSequence($method4); 34 | 35 | yield [ 36 | 'expected' => MethodsCollection::create($method1, $method2, $method3, $method4), 37 | 'hasMethods' => [ 38 | $hasMethod1, 39 | $hasMethod2, 40 | $hasMethod3, 41 | $hasMethod4, 42 | ], 43 | ]; 44 | } 45 | 46 | private function mockMethod(string $name): Method 47 | { 48 | return Method::create( 49 | $this->createMock(MethodSignature::class), 50 | $name, 51 | '', 52 | $this->createMock(CodePositionRange::class), 53 | '', 54 | ); 55 | } 56 | 57 | private function mockMethodTokenSequence(Method $method): MethodTokenSequence 58 | { 59 | $methodTokenSequence = $this->createMock(MethodTokenSequence::class); 60 | $methodTokenSequence->method('getMethod')->willReturn($method); 61 | 62 | return $methodTokenSequence; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Unit/Factory/SourceCloneCandidate/Type2SourceCloneCandidateFactoryTest.php: -------------------------------------------------------------------------------- 1 | createMock(Type2SourceCloneCandidatesMerger::class); 26 | $type2SourceCloneCandidatesMerger->method('merge')->willReturnArgument(0); 27 | 28 | self::assertEquals( 29 | $expected, 30 | (new Type2SourceCloneCandidateFactory($tokenSequenceNormalizer, $type2SourceCloneCandidatesMerger)) 31 | ->createMultiple($sourceCloneCandidates) 32 | ); 33 | } 34 | 35 | public function createMultipleProvider(): \Generator 36 | { 37 | $tokenSequences = [ 38 | TokenSequence::create([$this->createMock(\PhpToken::class)]), 39 | TokenSequence::create([$this->createMock(\PhpToken::class), $this->createMock(\PhpToken::class)]), 40 | ]; 41 | $methodsCollections = [ 42 | MethodsCollection::create($this->createMock(Method::class)), 43 | MethodsCollection::create( 44 | $this->createMock(Method::class), 45 | $this->createMock(Method::class) 46 | ), 47 | ]; 48 | 49 | $type1SourceCloneCandidates = [ 50 | Type1SourceCloneCandidate::create( 51 | $tokenSequences[0], 52 | $methodsCollections[0] 53 | ), 54 | Type1SourceCloneCandidate::create( 55 | $tokenSequences[1], 56 | $methodsCollections[1] 57 | ), 58 | ]; 59 | 60 | $normalizedTokenSequences = [ 61 | TokenSequence::create([]), 62 | TokenSequence::create([$this->createMock(\PhpToken::class)]), 63 | ]; 64 | 65 | $expected = [ 66 | Type2SourceCloneCandidate::create( 67 | $normalizedTokenSequences[0], 68 | $methodsCollections[0] 69 | ), 70 | Type2SourceCloneCandidate::create( 71 | $normalizedTokenSequences[1], 72 | $methodsCollections[1] 73 | ), 74 | ]; 75 | 76 | $tokenSequenceNormalizer = $this->createMock(TokenSequenceNormalizer::class); 77 | $tokenSequenceNormalizer->method('normalizeLevel2')->willReturnOnConsecutiveCalls(...$normalizedTokenSequences); 78 | 79 | yield [ 80 | $expected, 81 | $tokenSequenceNormalizer, 82 | $type1SourceCloneCandidates, 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/Factory/TokenSequenceFactoryTest.php: -------------------------------------------------------------------------------- 1 | createMock(Method::class); 20 | $method->method('getContent')->willReturn(''); 21 | self::assertEquals($expected, (new TokenSequenceFactory($phpTokenWrapper))->createFromMethod($method)); 22 | } 23 | 24 | public function createFromMethodProvider(): Generator 25 | { 26 | $tokens = []; 27 | $expected = TokenSequence::create($tokens); 28 | $phpTokenWrapper = $this->createMock(PhpTokenWrapper::class); 29 | $phpTokenWrapper->method('tokenize')->willReturn($tokens); 30 | yield [$expected, $phpTokenWrapper]; 31 | 32 | $tokens = [ 33 | $this->createMock(\PhpToken::class), 34 | $this->createMock(\PhpToken::class), 35 | ]; 36 | $expected = TokenSequence::create($tokens); 37 | $phpTokenWrapper = $this->createMock(PhpTokenWrapper::class); 38 | $phpTokenWrapper->method('tokenize')->willReturn($tokens); 39 | yield [$expected, $phpTokenWrapper]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Grouper/MethodTokenSequencesByTokenSequencesGrouperTest.php: -------------------------------------------------------------------------------- 1 | createMock(Grouper::class); 16 | $identityGrouper->method('groupByGroupID')->willReturn(['grouped']); 17 | 18 | self::assertSame(['grouped'], (new MethodTokenSequencesByTokenSequencesGrouper($identityGrouper))->group([])); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Merge/Type2SourceCloneCandidateMergerTest.php: -------------------------------------------------------------------------------- 1 | merge($type2SourceCloneCandidates)); 20 | } 21 | 22 | public function mergeProvider(): \Generator 23 | { 24 | yield 'empty' => [ 25 | 'expected' => [], 26 | 'type2SourceCloneCandidates' => [], 27 | ]; 28 | 29 | $tokenSequence1 = TokenSequence::create([ 30 | new \PhpToken(T_VARIABLE, '$x1'), 31 | ]); 32 | $tokenSequence2 = TokenSequence::create([ 33 | new \PhpToken(T_LNUMBER, '1'), 34 | ]); 35 | $method1 = $this->createMock(Method::class); 36 | $method2 = $this->createMock(Method::class); 37 | $method3 = $this->createMock(Method::class); 38 | $method4 = $this->createMock(Method::class); 39 | $method5 = $this->createMock(Method::class); 40 | $method6 = $this->createMock(Method::class); 41 | 42 | yield 'not empty' => [ 43 | 'expected' => [ 44 | '$x1' => Type2SourceCloneCandidate::create( 45 | $tokenSequence1, 46 | MethodsCollection::create($method1, $method2, $method4, $method5) 47 | ), 48 | '1' => Type2SourceCloneCandidate::create( 49 | $tokenSequence2, 50 | MethodsCollection::create($method3, $method6) 51 | ), 52 | ], 53 | 'type2SourceCloneCandidates' => [ 54 | Type2SourceCloneCandidate::create( 55 | $tokenSequence1, 56 | MethodsCollection::create($method1, $method2) 57 | ), 58 | Type2SourceCloneCandidate::create( 59 | $tokenSequence2, 60 | MethodsCollection::create($method3) 61 | ), 62 | Type2SourceCloneCandidate::create( 63 | $tokenSequence1, 64 | MethodsCollection::create($method4, $method5) 65 | ), 66 | Type2SourceCloneCandidate::create( 67 | $tokenSequence2, 68 | MethodsCollection::create($method6) 69 | ), 70 | ], 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Unit/Merge/Type3SourceCloneCandidateMergerTest.php: -------------------------------------------------------------------------------- 1 | merge($groups)); 20 | } 21 | 22 | public function mergeProvider(): array 23 | { 24 | $tokenSequences = [ 25 | $this->createMock(TokenSequence::class), 26 | $this->createMock(TokenSequence::class), 27 | $this->createMock(TokenSequence::class), 28 | $this->createMock(TokenSequence::class), 29 | $this->createMock(TokenSequence::class), 30 | ]; 31 | 32 | $methods = [ 33 | $this->createMock(Method::class), 34 | $this->createMock(Method::class), 35 | $this->createMock(Method::class), 36 | $this->createMock(Method::class), 37 | $this->createMock(Method::class), 38 | ]; 39 | 40 | $groups = [ 41 | [ 42 | Type3SourceCloneCandidate::create( 43 | [$tokenSequences[0], $tokenSequences[1]], 44 | MethodsCollection::create($methods[0], $methods[1]) 45 | ), 46 | Type3SourceCloneCandidate::create( 47 | [$tokenSequences[2]], 48 | MethodsCollection::create($methods[2]) 49 | ), 50 | ], 51 | [ 52 | Type3SourceCloneCandidate::create( 53 | [$tokenSequences[3], $tokenSequences[4]], 54 | MethodsCollection::create($methods[3], $methods[4]) 55 | ), 56 | ], 57 | ]; 58 | 59 | $expected = [ 60 | Type3SourceCloneCandidate::create( 61 | [$tokenSequences[0], $tokenSequences[1], $tokenSequences[2]], 62 | MethodsCollection::create($methods[0], $methods[1], $methods[2]) 63 | ), 64 | Type3SourceCloneCandidate::create( 65 | [$tokenSequences[3], $tokenSequences[4]], 66 | MethodsCollection::create($methods[3], $methods[4]) 67 | ), 68 | ]; 69 | 70 | return [ 71 | [ 72 | 'expected' => $expected, 73 | 'groups' => $groups, 74 | ] 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Unit/Model/CodePosition/CodePositionTest.php: -------------------------------------------------------------------------------- 1 | getLine()); 16 | } 17 | 18 | public function getLineProvider(): array 19 | { 20 | return [ 21 | [ 22 | 'expected' => 10, 23 | CodePosition::create(10, 15), 24 | ], 25 | [ 26 | 'expected' => 17, 27 | CodePosition::create(17, 15), 28 | ], 29 | ]; 30 | } 31 | 32 | /** @dataProvider getFilePosProvider */ 33 | public function testGetFilePos(int $expected, CodePosition $codePosition): void 34 | { 35 | self::assertSame($expected, $codePosition->getFilePos()); 36 | } 37 | 38 | public function getFilePosProvider(): array 39 | { 40 | return [ 41 | [ 42 | 'expected' => 15, 43 | CodePosition::create(10, 15), 44 | ], 45 | [ 46 | 'expected' => 29, 47 | CodePosition::create(10, 29), 48 | ], 49 | ]; 50 | } 51 | 52 | /** @dataProvider jsonSerializeProvider */ 53 | public function testJsonSerialize(string $expected, CodePosition $codePosition): void 54 | { 55 | self::assertJsonStringEqualsJsonString($expected, \Safe\json_encode($codePosition)); 56 | } 57 | 58 | public function jsonSerializeProvider(): array 59 | { 60 | return [ 61 | [ 62 | 'expected' => \Safe\json_encode(['line' => 10, 'filePos' => 15]), 63 | CodePosition::create(10, 15), 64 | ], 65 | [ 66 | 'expected' => \Safe\json_encode(['line' => 999, 'filePos' => 29]), 67 | CodePosition::create(999, 29), 68 | ], 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Unit/Model/FilepathMethods/FilepathMethodsTest.php: -------------------------------------------------------------------------------- 1 | getFilepath()); 18 | } 19 | 20 | public function getFilepathProvider(): \Generator 21 | { 22 | $filepath = '/var/www/foo.php'; 23 | yield [$filepath, FilepathMethods::create($filepath, [])]; 24 | 25 | $filepath = '/foo/bar/bla/Test.php'; 26 | yield [$filepath, FilepathMethods::create($filepath, [])]; 27 | } 28 | 29 | /** @dataProvider getMethodsProvider */ 30 | public function testGetMethods(array $expected, FilepathMethods $filepathMethods): void 31 | { 32 | self::assertSame($expected, $filepathMethods->getMethods()); 33 | } 34 | 35 | public function getMethodsProvider(): \Generator 36 | { 37 | $methods = []; 38 | yield 'no methods' => [$methods, FilepathMethods::create('', $methods)]; 39 | 40 | $methods = [ 41 | $this->createMock(ClassMethod::class), 42 | $this->createMock(ClassMethod::class), 43 | $this->createMock(ClassMethod::class), 44 | ]; 45 | yield 'classMethods' => [$methods, FilepathMethods::create('', $methods)]; 46 | 47 | $methods = [ 48 | $this->createMock(Function_::class), 49 | $this->createMock(Function_::class), 50 | $this->createMock(Function_::class), 51 | ]; 52 | yield 'functions' => [$methods, FilepathMethods::create('', $methods)]; 53 | 54 | $methods = [ 55 | $this->createMock(Function_::class), 56 | $this->createMock(ClassMethod::class), 57 | $this->createMock(ClassMethod::class), 58 | $this->createMock(Function_::class), 59 | ]; 60 | yield 'mixed classMethods and functions' => [$methods, FilepathMethods::create('', $methods)]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Unit/Model/Method/MethodSignatureTest.php: -------------------------------------------------------------------------------- 1 | getParamTypes()); 17 | } 18 | 19 | public function getParamTypesProvider(): Generator 20 | { 21 | $paramTypes = []; 22 | yield [$paramTypes, MethodSignature::create($paramTypes, [], '')]; 23 | 24 | $paramTypes = ['string']; 25 | yield [$paramTypes, MethodSignature::create($paramTypes, [0], '')]; 26 | 27 | $paramTypes = ['int', 'string']; 28 | yield [$paramTypes, MethodSignature::create($paramTypes, [0, 1], '')]; 29 | } 30 | 31 | /** @dataProvider getReturnTypeProvider */ 32 | public function testGetReturnType(string $expected, MethodSignature $methodSignature): void 33 | { 34 | self::assertSame($expected, $methodSignature->getReturnType()); 35 | } 36 | 37 | public function getReturnTypeProvider(): Generator 38 | { 39 | $returnType = 'int'; 40 | yield [$returnType, MethodSignature::create([], [], $returnType)]; 41 | 42 | $returnType = 'string'; 43 | yield [$returnType, MethodSignature::create([], [], $returnType)]; 44 | 45 | $returnType = '?int'; 46 | yield [$returnType, MethodSignature::create([], [], $returnType)]; 47 | } 48 | 49 | /** @dataProvider jsonSerializeProvider */ 50 | public function testJsonSerialize(string $expected, MethodSignature $methodSignature): void 51 | { 52 | self::assertJsonStringEqualsJsonString($expected, \Safe\json_encode($methodSignature)); 53 | } 54 | 55 | public function jsonSerializeProvider(): Generator 56 | { 57 | yield [ 58 | 'expected' => \Safe\json_encode([ 59 | 'paramTypes' => [], 60 | 'returnType' => 'int', 61 | ]), 62 | MethodSignature::create([], [], 'int') 63 | ]; 64 | 65 | yield [ 66 | 'expected' => \Safe\json_encode([ 67 | 'paramTypes' => [], 68 | 'returnType' => '?array', 69 | ]), 70 | MethodSignature::create([], [], '?array') 71 | ]; 72 | 73 | yield [ 74 | 'expected' => \Safe\json_encode([ 75 | 'paramTypes' => ['int'], 76 | 'returnType' => 'int', 77 | ]), 78 | MethodSignature::create(['int'], [0], 'int') 79 | ]; 80 | 81 | yield [ 82 | 'expected' => \Safe\json_encode([ 83 | 'paramTypes' => ['int', 'string'], 84 | 'returnType' => 'int', 85 | ]), 86 | MethodSignature::create(['int', 'string'], [0, 1], 'int') 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Unit/Model/Method/MethodTokenSequenceTest.php: -------------------------------------------------------------------------------- 1 | createMock(Method::class); 17 | self::assertSame( 18 | $method, 19 | MethodTokenSequence::create( 20 | $method, 21 | $this->createMock(TokenSequence::class), 22 | '' 23 | )->getMethod() 24 | ); 25 | } 26 | 27 | public function testGetTokenSequence(): void 28 | { 29 | $tokenSequence = $this->createMock(TokenSequence::class); 30 | self::assertSame( 31 | $tokenSequence, 32 | MethodTokenSequence::create( 33 | $this->createMock(Method::class), 34 | $tokenSequence, 35 | '' 36 | )->getTokenSequence() 37 | ); 38 | } 39 | 40 | public function testIdentity(): void 41 | { 42 | self::assertSame( 43 | 'method token sequence identity', 44 | MethodTokenSequence::create( 45 | $this->createMock(Method::class), 46 | $this->createMock(TokenSequence::class), 47 | 'method token sequence identity' 48 | )->identity() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Model/SourceClone/SourceCloneTest.php: -------------------------------------------------------------------------------- 1 | createMock(MethodsCollection::class))->getType() 21 | ); 22 | } 23 | 24 | public function createProvider(): array 25 | { 26 | return [ 27 | SourceClone::TYPE_1 => [SourceClone::TYPE_1], 28 | SourceClone::TYPE_2 => [SourceClone::TYPE_2], 29 | SourceClone::TYPE_3 => [SourceClone::TYPE_3], 30 | SourceClone::TYPE_4 => [SourceClone::TYPE_4], 31 | ]; 32 | } 33 | 34 | /** @dataProvider getMethodsCollectionProvider */ 35 | public function testGetMethodsCollection(MethodsCollection $expected, SourceClone $sourceClone): void 36 | { 37 | self::assertSame($expected, $sourceClone->getMethodsCollection()); 38 | } 39 | 40 | public function getMethodsCollectionProvider(): Generator 41 | { 42 | $methodsCollection = $this->createMock(MethodsCollection::class); 43 | yield [$methodsCollection, SourceClone::create(SourceClone::TYPE_1, $methodsCollection)]; 44 | } 45 | 46 | private function mockJsonSerializableMethod(array $asJson): Method 47 | { 48 | $method = $this->createMock(Method::class); 49 | $method->method('jsonSerialize')->willReturn($asJson); 50 | 51 | return $method; 52 | } 53 | 54 | public function testJsonSerialize(): void 55 | { 56 | $methodsCollection = $this->createMock(MethodsCollection::class); 57 | $methodsCollection->method('getAll')->willReturn([ 58 | $this->mockJsonSerializableMethod(['name' => 'firstMethod']), 59 | $this->mockJsonSerializableMethod(['name' => 'secondMethod']), 60 | ]); 61 | self::assertJsonStringEqualsJsonString( 62 | \Safe\json_encode([ 63 | 'type' => 'TYPE_1', 64 | 'methods' => [ 65 | [ 66 | 'name' => 'firstMethod', 67 | ], 68 | [ 69 | 'name' => 'secondMethod', 70 | ], 71 | ], 72 | ]), 73 | \Safe\json_encode(SourceClone::create(SourceClone::TYPE_1, $methodsCollection)) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Unit/Model/SourceCloneCandidate/Type1SourceCloneCandidateTest.php: -------------------------------------------------------------------------------- 1 | createMock(TokenSequence::class); 17 | self::assertSame($tokenSequence, Type1SourceCloneCandidate::create( 18 | $tokenSequence, 19 | $this->createMock(MethodsCollection::class) 20 | )->getTokenSequence()); 21 | } 22 | 23 | public function testGetMethodsCollection(): void 24 | { 25 | $methodsCollection = $this->createMock(MethodsCollection::class); 26 | self::assertSame($methodsCollection, Type1SourceCloneCandidate::create( 27 | $this->createMock(TokenSequence::class), 28 | $methodsCollection, 29 | )->getMethodsCollection()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Model/SourceCloneCandidate/Type2SourceCloneCandidateTest.php: -------------------------------------------------------------------------------- 1 | createMock(TokenSequence::class); 17 | self::assertSame($tokenSequence, Type2SourceCloneCandidate::create( 18 | $tokenSequence, 19 | $this->createMock(MethodsCollection::class) 20 | )->getTokenSequence()); 21 | } 22 | 23 | public function testGetMethodsCollection(): void 24 | { 25 | $methodsCollection = $this->createMock(MethodsCollection::class); 26 | self::assertSame($methodsCollection, Type2SourceCloneCandidate::create( 27 | $this->createMock(TokenSequence::class), 28 | $methodsCollection, 29 | )->getMethodsCollection()); 30 | } 31 | 32 | public function testIdentity(): void 33 | { 34 | $tokenSequence = $this->createMock(TokenSequence::class); 35 | $tokenSequence->method('identity')->willReturn('tokenSequenceIdentity'); 36 | self::assertSame('tokenSequenceIdentity', Type2SourceCloneCandidate::create( 37 | $tokenSequence, 38 | $this->createMock(MethodsCollection::class) 39 | )->identity()); 40 | } 41 | 42 | public function testGroupID(): void 43 | { 44 | $tokenSequence = $this->createMock(TokenSequence::class); 45 | $tokenSequence->method('identity')->willReturn('tokenSequenceIdentity'); 46 | self::assertSame('tokenSequenceIdentity', Type2SourceCloneCandidate::create( 47 | $tokenSequence, 48 | $this->createMock(MethodsCollection::class) 49 | )->groupID()); 50 | } 51 | 52 | public function testToString(): void 53 | { 54 | $tokenSequence = $this->createMock(TokenSequence::class); 55 | $tokenSequence->method('identity')->willReturn('tokenSequenceIdentity'); 56 | self::assertSame('tokenSequenceIdentity', Type2SourceCloneCandidate::create( 57 | $tokenSequence, 58 | $this->createMock(MethodsCollection::class) 59 | )->__toString()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/Model/SourceCloneCandidate/Type3SourceCloneCandidateTest.php: -------------------------------------------------------------------------------- 1 | createMock(TokenSequence::class), 18 | $this->createMock(TokenSequence::class), 19 | $this->createMock(TokenSequence::class), 20 | ]; 21 | self::assertSame($tokenSequences, Type3SourceCloneCandidate::create( 22 | $tokenSequences, 23 | $this->createMock(MethodsCollection::class) 24 | )->getTokenSequences()); 25 | } 26 | 27 | public function testGetMethodsCollection(): void 28 | { 29 | $methodsCollection = $this->createMock(MethodsCollection::class); 30 | self::assertSame($methodsCollection, Type3SourceCloneCandidate::create( 31 | [$this->createMock(TokenSequence::class)], 32 | $methodsCollection, 33 | )->getMethodsCollection()); 34 | } 35 | 36 | public function testIdentity(): void 37 | { 38 | $tokenSequences = [ 39 | $this->mockTokenSequenceWithIdentity('a'), 40 | $this->mockTokenSequenceWithIdentity('b'), 41 | $this->mockTokenSequenceWithIdentity('c'), 42 | ]; 43 | self::assertSame('a-b-c', Type3SourceCloneCandidate::create( 44 | $tokenSequences, 45 | $this->createMock(MethodsCollection::class) 46 | )->identity()); 47 | } 48 | 49 | private function mockTokenSequenceWithIdentity(string $identity): TokenSequence 50 | { 51 | $tokenSequence = $this->createMock(TokenSequence::class); 52 | $tokenSequence->method('identity')->willReturn($identity); 53 | 54 | return $tokenSequence; 55 | } 56 | 57 | public function testToString(): void 58 | { 59 | $tokenSequences = [ 60 | $this->mockTokenSequenceWithIdentity('a'), 61 | $this->mockTokenSequenceWithIdentity('b'), 62 | $this->mockTokenSequenceWithIdentity('c'), 63 | ]; 64 | self::assertSame('a-b-c', Type3SourceCloneCandidate::create( 65 | $tokenSequences, 66 | $this->createMock(MethodsCollection::class) 67 | )->__toString()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Unit/OutputFormatter/Model/CodePosition/CodePositionOutputFormatterTest.php: -------------------------------------------------------------------------------- 1 | format($codePosition)); 19 | } 20 | 21 | public function formatProvider(): array 22 | { 23 | return [ 24 | [ 25 | 'expected' => '10 (position 15)', 26 | CodePosition::create(10, 15), 27 | ], 28 | [ 29 | 'expected' => '999 (position 29)', 30 | CodePosition::create(999, 29), 31 | ], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/OutputFormatter/Model/CodePosition/CodePositionRangeOutputFormatterTest.php: -------------------------------------------------------------------------------- 1 | format($codePositionRange)); 23 | } 24 | 25 | public function formatProvider(): \Generator 26 | { 27 | $start = CodePosition::create(10, 15); 28 | $end = CodePosition::create(11, 15); 29 | yield [ 30 | 'expected' => '10 (position 15) - 11 (position 15) (1 lines)', 31 | 'codePositionRange' => CodePositionRange::create($start, $end), 32 | ]; 33 | 34 | $start = CodePosition::create(700, 16); 35 | $end = CodePosition::create(1100, 15); 36 | yield [ 37 | 'expected' => '700 (position 16) - 1100 (position 15) (400 lines)', 38 | 'codePositionRange' => CodePositionRange::create($start, $end), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/OutputFormatter/Model/Method/MethodOutputFormatterTest.php: -------------------------------------------------------------------------------- 1 | format($method)); 28 | } 29 | 30 | public function formatProvider(): \Generator 31 | { 32 | $codePositionRange = CodePositionRange::create( 33 | CodePosition::create(1, 10), 34 | CodePosition::create(20, 17) 35 | ); 36 | yield [ 37 | 'expected' => '/var/www/foo.php: foobar (1 (position 10) - 20 (position 17) (19 lines))', 38 | 'method' => Method::create( 39 | $this->createMock(MethodSignature::class), 40 | 'foobar', 41 | '/var/www/foo.php', 42 | $codePositionRange, 43 | '', 44 | ), 45 | ]; 46 | 47 | yield [ 48 | 'expected' => '/fp/bar.php: barfoo (1 (position 10) - 20 (position 17) (19 lines))', 49 | 'method' => Method::create( 50 | $this->createMock(MethodSignature::class), 51 | 'barfoo', 52 | '/fp/bar.php', 53 | $codePositionRange, 54 | '', 55 | ), 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/OutputFormatter/Model/Method/MethodSignatureOutputFormatterTest.php: -------------------------------------------------------------------------------- 1 | format($method)); 19 | } 20 | 21 | public function formatProvider(): \Generator 22 | { 23 | yield [ 24 | '(): int', 25 | MethodSignature::create([], [], 'int') 26 | ]; 27 | 28 | yield [ 29 | '(): ?array', 30 | MethodSignature::create([], [], '?array') 31 | ]; 32 | 33 | yield [ 34 | '(int): int', 35 | MethodSignature::create(['int'], [0], 'int') 36 | ]; 37 | 38 | yield [ 39 | '(int, string): int', 40 | MethodSignature::create(['int', 'string'], [0, 1], 'int') 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/OutputFormatter/Model/SourceClone/SourceCloneOutputFormatterTest.php: -------------------------------------------------------------------------------- 1 | createMock(MethodsCollection::class); 19 | $methodsCollection->method('getAll')->willReturn([ 20 | $this->createMock(Method::class), 21 | $this->createMock(Method::class), 22 | ]); 23 | 24 | $methodOutput = $this->createMock(MethodOutputFormatter::class); 25 | $methodOutput->method('format')->willReturnOnConsecutiveCalls( 26 | 'firstMethod', 27 | 'secondMethod' 28 | ); 29 | 30 | $sourceCloneOutputFormatter = new SourceCloneOutputFormatter( 31 | $methodOutput 32 | ); 33 | 34 | self::assertSame( 35 | "CLONE: Type: TYPE_1, Methods: \n\tfirstMethod\n\tsecondMethod", 36 | $sourceCloneOutputFormatter->format(SourceClone::create(SourceClone::TYPE_1, $methodsCollection)) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Unit/Service/FindMethodsInPathsServiceTest.php: -------------------------------------------------------------------------------- 1 | createMock(Configuration::class); 22 | $configuration 23 | ->method('getPhpDocumentorReportPath') 24 | ->willReturn(__DIR__ . '/../../testdata/phpDocumentor/small_report'); 25 | Configuration::setInstance($configuration); 26 | 27 | $filesystem = $this->createMock(Filesystem::class); 28 | $filesystem->method('readFilePart')->willReturnArgument(0); 29 | 30 | $phpDocumentorRunner = $this->createMock(PhpDocumentorRunner::class); 31 | $phpDocumentorRunner->method('run'); 32 | 33 | $projectDirectory = '/var/www/'; 34 | $methods = (new FindMethodsInPathsService($filesystem, $phpDocumentorRunner))->findAll($projectDirectory); 35 | 36 | $expected = [ 37 | Method::create( 38 | MethodSignature::create( 39 | ['array', 'int'], 40 | [0, 1], 41 | 'array' 42 | ), 43 | 'foo', 44 | '/var/www/01_A.php', 45 | CodePositionRange::create( 46 | CodePosition::create(10, 143), 47 | CodePosition::create(18, 324), 48 | ), 49 | '/var/www/01_A.php', 50 | ), 51 | Method::create( 52 | MethodSignature::create( 53 | ['array', 'int'], 54 | [0, 1], 55 | 'array|int>' 56 | ), 57 | 'bar', 58 | '/var/www/02_B.php', 59 | CodePositionRange::create( 60 | CodePosition::create(10, 159), 61 | CodePosition::create(13, 239), 62 | ), 63 | '/var/www/02_B.php', 64 | ), 65 | ]; 66 | 67 | self::assertEqualsCanonicalizing($expected, $methods); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Unit/Util/Subsequence/LongestCommonSubsequenceUtilTest.php: -------------------------------------------------------------------------------- 1 | percentageOfSimilarText($a, $b)); 16 | } 17 | 18 | public function percentageOfSimilarTextProvider(): array 19 | { 20 | return [ 21 | 'abc <-> abc' => [ 22 | 'expected' => 100, 23 | 'a' => 'abc', 24 | 'b' => 'abc', 25 | ], 26 | 'abc <-> def' => [ 27 | 'expected' => 0, 28 | 'a' => 'abc', 29 | 'b' => 'def', 30 | ], 31 | 'abc <-> ab' => [ 32 | 'expected' => 67, 33 | 'a' => 'abc', 34 | 'b' => 'ab', 35 | ], 36 | 'abc <-> cba' => [ 37 | 'expected' => 33, 38 | 'a' => 'abc', 39 | 'b' => 'cba', 40 | ], 41 | 'abc <-> a_b_c' => [ 42 | 'expected' => 60, 43 | 'a' => 'abc', 44 | 'b' => 'a_b_c', 45 | ], 46 | 'abc <-> empty' => [ 47 | 'expected' => 0, 48 | 'a' => 'abc', 49 | 'b' => '', 50 | ], 51 | 'empty <-> abc' => [ 52 | 'expected' => 0, 53 | 'a' => '', 54 | 'b' => 'abc', 55 | ], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Util/Subsequence/SimilarTextSubsequenceUtilTest.php: -------------------------------------------------------------------------------- 1 | percentageOfSimilarText($a, $b)); 16 | } 17 | 18 | public function percentageOfSimilarTextProvider(): array 19 | { 20 | return [ 21 | 'abc <-> abc' => [ 22 | 'expected' => 100, 23 | 'a' => 'abc', 24 | 'b' => 'abc', 25 | ], 26 | 'abc <-> def' => [ 27 | 'expected' => 0, 28 | 'a' => 'abc', 29 | 'b' => 'def', 30 | ], 31 | 'abc <-> ab' => [ 32 | 'expected' => 67, 33 | 'a' => 'abc', 34 | 'b' => 'ab', 35 | ], 36 | 'abc <-> cba' => [ 37 | 'expected' => 33, 38 | 'a' => 'abc', 39 | 'b' => 'cba', 40 | ], 41 | 'abc <-> a_b_c' => [ 42 | 'expected' => 60, 43 | 'a' => 'abc', 44 | 'b' => 'a_b_c', 45 | ], 46 | 'abc <-> empty' => [ 47 | 'expected' => 0, 48 | 'a' => 'abc', 49 | 'b' => '', 50 | ], 51 | 'empty <-> abc' => [ 52 | 'expected' => 0, 53 | 'a' => '', 54 | 'b' => 'abc', 55 | ], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Util/Subsequence/SubsequenceUtilPickerTest.php: -------------------------------------------------------------------------------- 1 | pick($strategy)); 20 | } 21 | 22 | public function pickProvider(): array 23 | { 24 | $longestCommonSubsequenceUtil = $this->createMock(LongestCommonSubsequenceUtil::class); 25 | $similarTextSubsequenceUtil = $this->createMock(SimilarTextSubsequenceUtil::class); 26 | 27 | $subsequenceUtilPicker = new SubsequenceUtilPicker( 28 | $longestCommonSubsequenceUtil, 29 | $similarTextSubsequenceUtil 30 | ); 31 | 32 | return [ 33 | 'LCS' => [ 34 | 'expected' => $longestCommonSubsequenceUtil, 35 | 'subsequenceUtilPicker' => $subsequenceUtilPicker, 36 | 'strategy' => SubsequenceUtilPicker::STRATEGY_LCS, 37 | ], 38 | 'similar_text' => [ 39 | 'expected' => $similarTextSubsequenceUtil, 40 | 'subsequenceUtilPicker' => $subsequenceUtilPicker, 41 | 'strategy' => SubsequenceUtilPicker::STRATEGY_SIMILAR_TEXT, 42 | ], 43 | ]; 44 | } 45 | 46 | public function testPickThrows(): void 47 | { 48 | self::expectException(SubsequenceUtilNotFound::class); 49 | 50 | (new SubsequenceUtilPicker( 51 | $this->createMock(LongestCommonSubsequenceUtil::class), 52 | $this->createMock(SimilarTextSubsequenceUtil::class) 53 | ))->pick('not-existing'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Wrapper/PhpTokenWrapperTest.php: -------------------------------------------------------------------------------- 1 | tokenize($code)); 17 | } 18 | 19 | public function tokenizeProvider(): array 20 | { 21 | return [ 22 | ['bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /tests/generated/reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoVie/php-dry/dcfc57d12b1f7ed7794d1fca4a66778dbf69a151/tests/generated/reports/.gitkeep -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/01_A.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = 100; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/02_B.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array|int> 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | return [$p1, $p2]; 13 | } 14 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/03_A_Exact_Copy.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = 100; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/04_A_Additional_Whitespaces.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = 100 ; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | 18 | return $r; 19 | } 20 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/05_A_Additional_Comments.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | /* 13 | * Some actions are following: 14 | */ 15 | // first we do this 16 | $r = []; 17 | $f = 100; 18 | // then in the loop 19 | foreach ($p1 as $i) { 20 | // we do this 21 | $r[] = $i * $p2; 22 | } 23 | // and after that, we do this 24 | return $r; 25 | } 26 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/06_A_Changed_Layout.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array { 11 | $r = []; $f = 100; 12 | foreach ($p1 as $i) { $r[] = $i * $p2; } 13 | return $r; 14 | } 15 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/07_A_Changed_Variable_Names.php: -------------------------------------------------------------------------------- 1 | $first 7 | * 8 | * @return array 9 | */ 10 | function foo(array $first, int $second): array 11 | { 12 | $x = []; 13 | $k = 100; 14 | foreach ($first as $y) { 15 | $x[] = $y * $second; 16 | } 17 | return $x; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/08_A_Changed_Method_Names.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function bar(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = 100; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/09_A_Changed_Literals.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = -20; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/10_A_Additional_Statements.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | $f = 100; 14 | foreach ($p1 as $i) { 15 | print($i * $p2 . "\n"); 16 | $r[] = $i * $p2; 17 | } 18 | return $r; 19 | } 20 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/11_A_Removed_Statements.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $r = []; 13 | foreach ($p1 as $i) { 14 | $r[] = $i * $p2; 15 | } 16 | return $r; 17 | } 18 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/12_A_Changed_Statement_Order.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | $f = 100; 13 | $r = []; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/13_A_Changed_Syntax.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(array $p1, int $p2): array 11 | { 12 | return array_map( 13 | fn($i) => $i * $p2, 14 | $p1 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/testdata/clone-detection-testdata-with-native-types/14_A_Changed_Param_Order.php: -------------------------------------------------------------------------------- 1 | $p1 7 | * 8 | * @return array 9 | */ 10 | function foo(int $p2, array $p1): array 11 | { 12 | $r = []; 13 | $f = 100; 14 | foreach ($p1 as $i) { 15 | $r[] = $i * $p2; 16 | } 17 | return $r; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/testdata/file/file.txt: -------------------------------------------------------------------------------- 1 | file -------------------------------------------------------------------------------- /tests/testdata/php-dry-01.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | src 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/testdata/php-dry-02.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | /var/www 16 | /bar/foo 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/testdata/template/generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoVie/php-dry/dcfc57d12b1f7ed7794d1fca4a66778dbf69a151/tests/testdata/template/generated/.gitkeep -------------------------------------------------------------------------------- /tests/testdata/template/run_class_method_template.php: -------------------------------------------------------------------------------- 1 | '%PHP_FILE%':'%CLASS_NAME%':'%FUNCTION_NAME%':'%PARAMS%' -------------------------------------------------------------------------------- /tests/testdata/template/run_free_method_template.php: -------------------------------------------------------------------------------- 1 | '%PHP_FILE%':'%FUNCTION_NAME%':'%PARAMS%' -------------------------------------------------------------------------------- /tools/phpDocumentor.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeoVie/php-dry/dcfc57d12b1f7ed7794d1fca4a66778dbf69a151/tools/phpDocumentor.phar -------------------------------------------------------------------------------- /xsd/php-dry.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | --------------------------------------------------------------------------------