├── .dockerignore ├── .php-cs-fixer.php ├── .semver ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── phpmetrics ├── composer.json ├── config-example.json ├── config.yml ├── craft.yml ├── rector.php ├── src ├── Hal │ ├── Application │ │ ├── Analyze.php │ │ ├── Application.php │ │ └── Config │ │ │ ├── Config.php │ │ │ ├── ConfigException.php │ │ │ ├── File │ │ │ ├── ConfigFileReaderFactory.php │ │ │ ├── ConfigFileReaderIni.php │ │ │ ├── ConfigFileReaderInterface.php │ │ │ ├── ConfigFileReaderJson.php │ │ │ └── ConfigFileReaderYaml.php │ │ │ ├── Parser.php │ │ │ └── Validator.php │ ├── Component │ │ ├── Ast │ │ │ ├── NodeTraverser.php │ │ │ ├── NodeTyper.php │ │ │ ├── ParserFactoryBridge.php │ │ │ ├── ParserTraverserVisitorsAssigner.php │ │ │ ├── Php5NodeTraverser.php │ │ │ ├── Php7NodeTraverser.php │ │ │ ├── Php8NodeTraverser.php │ │ │ └── Traverser.php │ │ ├── File │ │ │ └── Finder.php │ │ ├── Issue │ │ │ └── Issuer.php │ │ ├── Output │ │ │ ├── CliOutput.php │ │ │ ├── Output.php │ │ │ ├── ProgressBar.php │ │ │ └── TestOutput.php │ │ └── Tree │ │ │ ├── Edge.php │ │ │ ├── Graph.php │ │ │ ├── GraphDeduplicated.php │ │ │ ├── GraphException.php │ │ │ ├── HashMap.php │ │ │ ├── Node.php │ │ │ └── Operator │ │ │ ├── CycleDetector.php │ │ │ └── SizeOfTree.php │ ├── Metric │ │ ├── BagTrait.php │ │ ├── ClassMetric.php │ │ ├── Class_ │ │ │ ├── ClassEnumVisitor.php │ │ │ ├── Complexity │ │ │ │ ├── CyclomaticComplexityVisitor.php │ │ │ │ └── KanDefectVisitor.php │ │ │ ├── Component │ │ │ │ └── MaintainabilityIndexVisitor.php │ │ │ ├── Coupling │ │ │ │ └── ExternalsVisitor.php │ │ │ ├── Structural │ │ │ │ ├── LcomVisitor.php │ │ │ │ └── SystemComplexityVisitor.php │ │ │ └── Text │ │ │ │ ├── HalsteadVisitor.php │ │ │ │ └── LengthVisitor.php │ │ ├── Consolidated.php │ │ ├── FileMetric.php │ │ ├── FunctionMetric.php │ │ ├── Group │ │ │ └── Group.php │ │ ├── Helper │ │ │ └── RoleOfMethodDetector.php │ │ ├── InterfaceMetric.php │ │ ├── Metric.php │ │ ├── Metrics.php │ │ ├── Package │ │ │ ├── PackageAbstraction.php │ │ │ ├── PackageCollectingVisitor.php │ │ │ ├── PackageDependencies.php │ │ │ ├── PackageDistance.php │ │ │ └── PackageInstability.php │ │ ├── PackageMetric.php │ │ ├── ProjectMetric.php │ │ ├── Registry.php │ │ ├── SearchMetric.php │ │ └── System │ │ │ ├── Changes │ │ │ └── GitChanges.php │ │ │ ├── Coupling │ │ │ ├── Coupling.php │ │ │ ├── DepthOfInheritanceTree.php │ │ │ └── PageRank.php │ │ │ ├── Packages │ │ │ └── Composer │ │ │ │ ├── Composer.php │ │ │ │ └── Packagist.php │ │ │ └── UnitTesting │ │ │ └── UnitTesting.php │ ├── Report │ │ ├── Cli │ │ │ ├── Reporter.php │ │ │ ├── SearchReporter.php │ │ │ └── SummaryWriter.php │ │ ├── Csv │ │ │ └── Reporter.php │ │ ├── Html │ │ │ └── Reporter.php │ │ ├── Json │ │ │ ├── Reporter.php │ │ │ ├── SummaryReporter.php │ │ │ └── SummaryWriter.php │ │ ├── SummaryProvider.php │ │ └── Violations │ │ │ └── Xml │ │ │ └── Reporter.php │ ├── Search │ │ ├── PatternSearcher.php │ │ ├── Search.php │ │ ├── Searches.php │ │ ├── SearchesFactory.php │ │ └── SearchesValidator.php │ └── Violation │ │ ├── Class_ │ │ ├── Blob.php │ │ ├── ProbablyBugged.php │ │ ├── TooComplexClassCode.php │ │ ├── TooComplexMethodCode.php │ │ ├── TooDependent.php │ │ └── TooLong.php │ │ ├── Package │ │ ├── StableAbstractionsPrinciple.php │ │ └── StableDependenciesPrinciple.php │ │ ├── Search │ │ └── SearchShouldNotBeFoundPrinciple.php │ │ ├── Violation.php │ │ ├── ViolationParser.php │ │ └── Violations.php └── functions.php ├── templates └── html_report │ ├── _footer.php │ ├── _header.php │ ├── all.php │ ├── complexity.php │ ├── composer.php │ ├── coupling.php │ ├── css │ ├── clusterize.css │ ├── material-icons.css │ ├── milligram.min.css │ ├── milligram.min.css.map │ ├── normalize.css │ ├── roboto.css │ └── style.css │ ├── favicon.ico │ ├── fonts │ ├── material-icons.ttf │ ├── roboto-bold.ttf │ └── roboto-light.ttf │ ├── git.php │ ├── images │ ├── logo-git.png │ ├── logo.png │ └── phpmetrics-maintenability.png │ ├── index.php │ ├── js │ ├── FileSaver.min.js │ ├── FileSaver.min.js.map │ ├── clusterize.min.js │ ├── d3.hexbin.v0.js │ ├── d3.v3.js │ ├── functions.js │ ├── graph-licenses.js │ ├── graph-maintainability.js │ └── sort-table.min.js │ ├── junit.php │ ├── loc.php │ ├── oop.php │ ├── package_relations.php │ ├── packages.php │ ├── panel.php │ ├── relations.php │ └── violations.php └── tooling ├── README.md └── composer.json /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !releases/phpmetrics.phar 3 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 5 | ; 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | '@PER-CS' => true, 10 | '@PHP82Migration' => true, 11 | ]) 12 | ->setFinder($finder) 13 | ->setUnsupportedPhpVersionAllowed(true) 14 | ; 15 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 2 3 | :minor: 8 4 | :patch: 2 5 | :special: '' 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.8.2] - 2023-02-?? 8 | 9 | ### Fixed 10 | - Fixed errors in HTML template. (thanks @Hikingyo and @gemal) 11 | - Improved README.md contents. (thanks @kudashevs) 12 | - Fixed junit parameter in JSON configuration file 13 | - Minor removals of unnecessary source code 14 | - Remove wrong artefacts causing download issues 15 | - Add favicon to HTML rendered pages (thanks @gemal) 16 | - Add version to CSS and script to counter cache (thanks @gemal) 17 | 18 | ## [2.8.1] - 2022-03-24 19 | 20 | ### Fixed 21 | - Fixed issue with relative pat when using YAML configuration. 22 | 23 | ## [2.8.0] - 2022-03-23 24 | 25 | ### New features 26 | - Allow to search for patterns of code. 27 | - Possibility to add custom violation rules via configuration. 28 | - Allow to use YAML for configuration 29 | - Add `--metrics` option to display documentation about some metrics calculated and used by PhpMetrics. 30 | - Exclude getters and setters from the CCN (cyclomatic complexity number) and LCoM (lack in cohesion of method) calculations 31 | - Add `composer` option to enable or disable the composer packages analysis 32 | - Add `--report-summary-json` option to report a summarized information from the calculated metrics. 33 | 34 | ### Fixed 35 | - Fixed issue with some columns in HTML reports 36 | 37 | ## [2.7.4] - 2020-06-30 38 | 39 | ### Fixed 40 | - Fixed compatibility issue where PHP 5 was no longer available on Debian systems (#434) 41 | - Fixed issue with display of charts in groups (#429, #433) 42 | 43 | ## [2.7.3] - 2020-06-27 44 | 45 | ### Fixed 46 | - Fixed missing `composer.json` files when located in the root directory. 47 | 48 | ## [2.7.2] - 2020-06-27 49 | 50 | ### Fixed 51 | - Fixed path of violations HTML templates. 52 | 53 | ## [2.7.1] - 2020-06-27 54 | 55 | ### Fixed 56 | - Fixed error due to permission on generation of HTML report (#429) 57 | - Fixed analysis on composer packages wrongly reported outdated when latest version is used. (#431) 58 | 59 | ## [2.7.0] - 2020-06-26 60 | 61 | ### New features 62 | - Way to group analysis by layer 63 | 64 | ### Fixed 65 | - Improved UI 66 | 67 | ## [2.6.2] - 2020-04-02 68 | 69 | ### Fixed 70 | - Improved UI 71 | 72 | ## [2.6.1] - 2020-04-02 73 | 74 | ### Fixed 75 | - Fixed undefined constant PROJECT_DIR (#426) 76 | 77 | ## [2.6.0] - 2020-03-28 78 | 79 | ### New features 80 | - Way to download report 81 | - Way to download chart 82 | - Resolve PHP7 getters / setters (#405) 83 | - Add metrics description file 84 | - Add a carousel in the main HTML report page to display both graph at the same time 85 | 86 | ### Fixed 87 | - Explicitly define the class \Hal\Component\Ast\NodeTraverser to make PhpMetrics work using composer --classmap-authoritative. (#402) 88 | - Ensure the packagist license is an array, so they can be displayed. (#404) 89 | - Fix warning "Division by zero" when no package is defined. (#401) 90 | 91 | ### Misc 92 | - Move templates out of src 93 | - Remove folders from phpcs 94 | 95 | ## [2.5.0] - 2019-12-11 96 | 97 | ### Changed 98 | - Test the codebase against PHP 7.3 and 7.4 99 | 100 | ### Fixed 101 | - Skip `self` and `parent` from external dependencies of dependency graph (#370) thanks to (@lencse) 102 | - Don't leave notice when array is small in percentile function of loc report (#372) thanks to (@lencse) 103 | 104 | ## [2.4.1] - 2017-07-10 105 | 106 | ### Fixed 107 | - Fix parsing errors with PHP < 7 (#360, #361) 108 | - Remain CCN for backward compatibility (#359, #362) 109 | 110 | ### Deprecated 111 | - CCN by classes is deprecated and will be removed in the next major release (#359, #362) 112 | 113 | ## [2.4.0] - 2017-07-09 114 | 115 | ### Added 116 | - Added package metrics (#283) 117 | 118 | ### Changed 119 | - Enhanced composer package comparison (#337, #342, thanks @juliendufresne) 120 | - Better PHP 7 support (#335, #334, #336 thanks @carusogabriel) 121 | - Support nikic/php-parser:^4 (#345, #347) 122 | 123 | ### Fixed 124 | - Refine Cyclomatic Complexity Metric (#343, #344, #353, #357, #358, thanks @fabianbadoi) 125 | - Improved composer package version comparison (#337, thanks @juliendufresne) 126 | - Resolved root path exclusion conflict (#355, thanks @fabianbadoi) 127 | - Fixed getter and setter detection with types (#335, #336, thanks @jakagacic) 128 | - Fixed documentation URL (#321, thanks @ottaviano) 129 | - Fix non unique block ids in HTML output (#356, thanks @dumith-erange) 130 | - Fix rounding of metrics (#339, thanks @ssfinney) 131 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.4 2 | 3 | MAINTAINER "niconoe-" 4 | 5 | COPY releases/phpmetrics.phar /usr/local/bin/phpmetrics 6 | 7 | RUN chmod +x /usr/local/bin/phpmetrics \ 8 | # Install git to be able to use option "--git". 9 | && apt-get update && apt-get install -y git \ 10 | && rm -rf /var/cache/apk/* /var/tmp/* /tmp/* 11 | 12 | VOLUME ["/app"] 13 | WORKDIR /app 14 | 15 | ENTRYPOINT ["phpmetrics"] 16 | CMD ["--version"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jean-François Lépine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhpMetrics 2 | 3 | 4 | [![License](https://poser.pugx.org/phpmetrics/phpmetrics/license.svg)](https://packagist.org/packages/phpmetrics/phpmetrics) 5 | [![Latest Stable Version](https://poser.pugx.org/phpmetrics/phpmetrics/v/stable.svg)](https://packagist.org/packages/phpmetrics/phpmetrics) 6 | [![Slack](https://img.shields.io/badge/slack/phpmetrics-yellow.svg?logo=slack)](https://join.slack.com/t/phpmetrics/shared_invite/enQtODU3MjQ4ODAxOTM5LWRhOGFhODMxN2JmMDRmOGVjNGQ0ZjNjNzVlNDIwNzQ2MWQ2YzgxYmRlNmM5NzIzZjlhYTFjZjZhYzAyMjM0YmE) 7 | 8 | 9 | 10 | ![Standard report](https://phpmetrics.github.io/website/assets/preview.png) 11 | 12 | 13 | PhpMetrics 14 | 15 | PhpMetrics provides metrics about PHP project and classes, with beautiful and readable HTML report. 16 | 17 | [Documentation](https://phpmetrics.github.io/website/) | [Twitter](https://twitter.com/Halleck45) | [Contributing](https://github.com/phpmetrics/PhpMetrics/blob/master/doc/contributing.md) 18 | 19 |

20 | 21 | 22 | ## Quick start 23 | 24 | Follow the [quick start guide](https://phpmetrics.github.io/website/getting-started/) to get started. 25 | ```bash 26 | # install the package as a dev dependency 27 | composer require phpmetrics/phpmetrics --dev 28 | 29 | # run PHPMetrics to analyze a folder and generate a report 30 | php ./vendor/bin/phpmetrics --report-html=myreport 31 | ``` 32 | 33 | Then open the generated `./myreport/index.html` file in your browser. 34 | 35 | > You can use a [configuration file](https://phpmetrics.github.io/website/configuration/) to customize 36 | > the report, add options, configure rules for Continuous Integration, etc. 37 | 38 | ## Metrics 39 | 40 | You'll find detailed list of metrics in [documentation](https://phpmetrics.github.io/website/metrics/), or 41 | running `php ./vendor/bin/phpmetrics --metrics` 42 | 43 | ## Author 44 | 45 | + Jean-François Lépine <[@Halleck45](https://twitter.com/Halleck45)> 46 | 47 | ## License 48 | 49 | See the [LICENSE](LICENSE) file. 50 | 51 | ## Contributing 52 | 53 | See the [CONTRIBUTING](doc/contributing.md) file. 54 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | v2. | :white_check_mark: | 10 | | v1.x | :x: | 11 | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please [open an issue](https://github.com/phpmetrics/PhpMetrics/issues) with the details to reproduce the exploit. 16 | 17 | Thanks for your help ! 18 | -------------------------------------------------------------------------------- /bin/phpmetrics: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run($argv); 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpmetrics/phpmetrics", 3 | "replace": { 4 | "halleck45/phpmetrics": "*", 5 | "halleck45/php-metrics": "*" 6 | }, 7 | "description": "Static analyzer tool for PHP : Coupling, Cyclomatic complexity, Maintainability Index, Halstead's metrics... and more !", 8 | "license": "MIT", 9 | "keywords": [ 10 | "quality", 11 | "analysis", 12 | "testing", 13 | "qa" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Jean-François Lépine", 18 | "email": "lepinejeanfrancois@yahoo.fr", 19 | "homepage": "http://www.lepine.pro", 20 | "role": "Copyright Holder" 21 | } 22 | ], 23 | "homepage": "http://www.phpmetrics.org", 24 | "support": { 25 | "issues": "https://github.com/PhpMetrics/PhpMetrics/issues" 26 | }, 27 | "autoload": { 28 | "psr-0": { 29 | "Hal\\": "./src/" 30 | }, 31 | "files": ["./src/functions.php"] 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Test\\Hal\\": "tests/" 36 | } 37 | }, 38 | "require": { 39 | "ext-dom": "*", 40 | "ext-tokenizer": "*", 41 | "nikic/php-parser": "^3|^4|^5" 42 | }, 43 | "bin": [ 44 | "bin/phpmetrics" 45 | ], 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "require-dev": { 50 | "phpunit/phpunit": "*" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "includes": [ 3 | "Controller" 4 | ], 5 | "excludes": [ 6 | "tests" 7 | ], 8 | "report": { 9 | "html": "/tmp/report/", 10 | "csv": "/tmp/report.csv", 11 | "json": "/tmp/report.json", 12 | "violations": "/tmp/violations.xml" 13 | }, 14 | "groups": [ 15 | { 16 | "name": "Component", 17 | "match": "!component!i" 18 | }, 19 | { 20 | "name": "Reporters", 21 | "match": "!Report!" 22 | } 23 | ], 24 | "searches": { 25 | "Repository which uses Service": { 26 | "type": "class", 27 | "instanceOf": [ 28 | "App\\MyRepository" 29 | ], 30 | "nameMatches": ".*Repository.*", 31 | "usesClasses": [ 32 | ".*Service" 33 | ], 34 | "failIfFound": true 35 | }, 36 | "Class with too complex code": { 37 | "type": "class", 38 | "ccn": ">=3", 39 | "failIfFound": true 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | composer: false 3 | includes: 4 | - "src" 5 | excludes: 6 | extensions: 7 | - php 8 | - php8 9 | report: 10 | html: "/tmp/report/" 11 | csv: "/tmp/report.csv" 12 | json: "/tmp/report.json" 13 | violations: "/tmp/violations.xml" 14 | groups: 15 | - name: Component 16 | match: "!component!i" 17 | - name: Metric 18 | match: "!metric!i" 19 | plugins: 20 | git: 21 | binary: git 22 | junit: 23 | file: "/tmp/junit.xml" 24 | -------------------------------------------------------------------------------- /craft.yml: -------------------------------------------------------------------------------- 1 | php-version: 8.4 2 | extensions: "apcu,phar,curl,dom,fileinfo,filter,intl,mbstring,mysqlnd,openssl,tokenizer,zlib" 3 | sapi: micro 4 | build-options: 5 | with-upx-pack: true 6 | prefer-pre-built: true 7 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 7 | __DIR__ . '/src', 8 | __DIR__ . '/tests', 9 | ]) 10 | ->withSets([ 11 | \Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_110, 12 | ]) 13 | ->withRules([ 14 | \Rector\TypeDeclaration\Rector\Class_\AddTestsVoidReturnTypeWhereNoReturnRector::class, 15 | \Rector\PHPUnit\CodeQuality\Rector\Class_\AddParentSetupCallOnSetupRector::class, 16 | \Rector\PHPUnit\PHPUnit60\Rector\ClassMethod\ExceptionAnnotationRector::class, 17 | \Rector\PHPUnit\PHPUnit100\Rector\StmtsAwareInterface\WithConsecutiveRector::class, 18 | \Rector\PHPUnit\PHPUnit100\Rector\Class_\StaticDataProviderClassMethodRector::class, 19 | ]); 20 | -------------------------------------------------------------------------------- /src/Hal/Application/Analyze.php: -------------------------------------------------------------------------------- 1 | output = $output; 63 | $this->config = $config; 64 | $this->issuer = $issuer; 65 | } 66 | 67 | /** 68 | * Runs analyze 69 | */ 70 | public function run($files) 71 | { 72 | // config 73 | ini_set('xdebug.max_nesting_level', 3000); 74 | 75 | $metrics = new Metrics(); 76 | 77 | // traverse all 78 | $whenToStop = function () { 79 | return true; 80 | }; 81 | 82 | // prepare parser 83 | $parser = (new ParserFactoryBridge())->create(); 84 | $traverser = new NodeTraverser(); 85 | 86 | (new ParserTraverserVisitorsAssigner())->assign( 87 | $traverser, 88 | [ 89 | new NameResolver(), 90 | new ClassEnumVisitor($metrics), 91 | new CyclomaticComplexityVisitor($metrics), 92 | new ExternalsVisitor($metrics), 93 | new LcomVisitor($metrics), 94 | new HalsteadVisitor($metrics), 95 | new LengthVisitor($metrics), 96 | new CyclomaticComplexityVisitor($metrics), 97 | new MaintainabilityIndexVisitor($metrics), 98 | new KanDefectVisitor($metrics), 99 | new SystemComplexityVisitor($metrics), 100 | new PackageCollectingVisitor($metrics) 101 | ] 102 | ); 103 | 104 | // create a new progress bar (50 units) 105 | $progress = new ProgressBar($this->output, count($files)); 106 | $progress->start(); 107 | 108 | foreach ($files as $file) { 109 | $progress->advance(); 110 | $code = file_get_contents($file); 111 | $this->issuer->set('filename', $file); 112 | try { 113 | $stmts = $parser->parse($code); 114 | $this->issuer->set('statements', $stmts); 115 | $traverser->traverse($stmts); 116 | } catch (Error $e) { 117 | $this->output->writeln(sprintf('Cannot parse %s', $file)); 118 | } 119 | $this->issuer->clear('filename'); 120 | $this->issuer->clear('statements'); 121 | } 122 | 123 | $progress->clear(); 124 | 125 | $this->output->write('Executing system analyzes...'); 126 | 127 | // 128 | // System analyses 129 | (new PageRank())->calculate($metrics); 130 | (new Coupling())->calculate($metrics); 131 | (new DepthOfInheritanceTree())->calculate($metrics); 132 | 133 | // 134 | // Package analyses 135 | (new PackageDependencies())->calculate($metrics); 136 | (new PackageAbstraction())->calculate($metrics); 137 | (new PackageInstability())->calculate($metrics); 138 | (new PackageDistance())->calculate($metrics); 139 | 140 | // 141 | // File analyses 142 | (new GitChanges($this->config, $files))->calculate($metrics); 143 | 144 | // 145 | // Unit test 146 | (new UnitTesting($this->config, $files))->calculate($metrics); 147 | 148 | $this->output->clearln(); 149 | 150 | // 151 | // Composer 152 | $this->output->write('Executing composer analyzes, requesting https://packagist.org...'); 153 | (new Composer($this->config, $files))->calculate($metrics); 154 | 155 | $this->output->clearln(); 156 | 157 | return $metrics; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Hal/Application/Application.php: -------------------------------------------------------------------------------- 1 | enable(); 30 | 31 | // config 32 | $config = (new Parser())->parse($argv); 33 | 34 | // Help 35 | if ($config->has('help')) { 36 | $output->writeln((new Validator())->help()); 37 | exit(0); 38 | } 39 | 40 | // Metrics list 41 | if ($config->has('metrics')) { 42 | $output->writeln((new Validator())->metrics()); 43 | exit(0); 44 | } 45 | 46 | // Version 47 | if ($config->has('version')) { 48 | $output->writeln(sprintf( 49 | "PhpMetrics %s \nby Jean-François Lépine \n", 50 | getVersion() 51 | )); 52 | exit(0); 53 | } 54 | 55 | try { 56 | (new Validator())->validate($config); 57 | } catch (ConfigException $e) { 58 | $output->writeln(sprintf("\n%s\n", $e->getMessage())); 59 | $output->writeln((new Validator())->help()); 60 | exit(1); 61 | } 62 | 63 | if ($config->has('quiet')) { 64 | $output->setQuietMode(true); 65 | } 66 | 67 | // find files 68 | $finder = new Finder($config->get('extensions'), $config->get('exclude')); 69 | $files = $finder->fetch($config->get('files')); 70 | 71 | // analyze 72 | try { 73 | $metrics = (new Analyze($config, $output, $issuer))->run($files); 74 | } catch (ConfigException $e) { 75 | $output->writeln(sprintf('%s', $e->getMessage())); 76 | exit(1); 77 | } 78 | 79 | // search 80 | $searches = $config->get('searches'); 81 | $searcher = new PatternSearcher(); 82 | $foundSearch = new SearchMetric('searches'); 83 | foreach ($searches->all() as $search) { 84 | $foundSearch->set($search->getName(), $searcher->executes($search, $metrics)); 85 | } 86 | $metrics->attach($foundSearch); 87 | 88 | // violations 89 | (new ViolationParser($config, $output))->apply($metrics); 90 | 91 | // report 92 | try { 93 | (new Report\Cli\Reporter($config, $output))->generate($metrics); 94 | (new Report\Cli\SearchReporter($config, $output))->generate($metrics); 95 | (new Report\Html\Reporter($config, $output))->generate($metrics); 96 | (new Report\Csv\Reporter($config, $output))->generate($metrics); 97 | (new Report\Json\Reporter($config, $output))->generate($metrics); 98 | (new Report\Json\SummaryReporter($config, $output))->generate($metrics); 99 | (new Report\Violations\Xml\Reporter($config, $output))->generate($metrics); 100 | } catch (Exception $e) { 101 | $output->writeln(sprintf('Cannot generate report: %s', $e->getMessage())); 102 | $output->writeln(''); 103 | exit(1); 104 | } 105 | 106 | // exit status 107 | $shouldExitDueToCriticalViolationsCount = 0; 108 | foreach ($metrics->all() as $metric) { 109 | foreach ($metric->get('violations') as $violation) { 110 | if (Violation::CRITICAL === $violation->getLevel()) { 111 | $shouldExitDueToCriticalViolationsCount++; 112 | } 113 | } 114 | } 115 | if (!empty($shouldExitDueToCriticalViolationsCount)) { 116 | $output->writeln(''); 117 | $output->writeln(sprintf( 118 | '[ERR] Failed du to %d critical violations', 119 | $shouldExitDueToCriticalViolationsCount 120 | )); 121 | $output->writeln(''); 122 | exit(1); 123 | } 124 | 125 | // end 126 | $output->writeln(''); 127 | $output->writeln('Done'); 128 | $output->writeln(''); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/Config.php: -------------------------------------------------------------------------------- 1 | bag[$key] = $value; 20 | return $this; 21 | } 22 | 23 | /** 24 | * @param $key 25 | * @return bool 26 | */ 27 | public function has($key) 28 | { 29 | return isset($this->bag[$key]); 30 | } 31 | 32 | /** 33 | * @param $key 34 | * @return null 35 | */ 36 | public function get($key) 37 | { 38 | return $this->has($key) ? $this->bag[$key] : null; 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function all() 45 | { 46 | return $this->bag; 47 | } 48 | 49 | /** 50 | * @param array $array 51 | * @return $this 52 | */ 53 | public function fromArray(array $array) 54 | { 55 | foreach ($array as $key => $value) { 56 | $this->set($key, $value); 57 | } 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/ConfigException.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 20 | } 21 | 22 | /** 23 | * @param Config $config 24 | * 25 | * @return void 26 | */ 27 | public function read(Config $config) 28 | { 29 | $options = parse_ini_file($this->filename); 30 | 31 | if (false === $options) { 32 | throw new \InvalidArgumentException("Cannot parse configuration file '{$this->filename}'"); 33 | } 34 | 35 | foreach ($options as $name => $value) { 36 | $config->set($name, $value); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/File/ConfigFileReaderInterface.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 22 | } 23 | 24 | /** 25 | * @param Config $config 26 | * 27 | * @return void 28 | */ 29 | public function read(Config $config) 30 | { 31 | $jsonText = file_get_contents($this->filename); 32 | 33 | if (false === $jsonText) { 34 | throw new InvalidArgumentException("Cannot read configuration file '{$this->filename}'"); 35 | } 36 | 37 | $jsonData = json_decode($jsonText, true); 38 | 39 | $this->parseJson($jsonData, $config); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | private function resolvePath($path) 46 | { 47 | if (DIRECTORY_SEPARATOR !== $path[0]) { 48 | $path = dirname($this->filename) . DIRECTORY_SEPARATOR . $path; 49 | } 50 | 51 | return $path; 52 | } 53 | 54 | protected function parseJson($jsonData, Config $config) 55 | { 56 | if (false === $jsonData || null === $jsonData) { 57 | throw new InvalidArgumentException("Bad config file '{$this->filename}'"); 58 | } 59 | 60 | if (isset($jsonData['includes'])) { 61 | $includes = $jsonData['includes']; 62 | $files = []; 63 | // with config file, includes are relative to config file 64 | foreach ($includes as $include) { 65 | $include = $this->resolvePath($include); 66 | $files[] = $include; 67 | } 68 | $config->set('files', $files); 69 | } 70 | 71 | if (isset($jsonData['groups'])) { 72 | $config->set('groups', $jsonData['groups']); 73 | } 74 | 75 | if (isset($jsonData['extensions'])) { 76 | $config->set('extensions', implode(',', $jsonData['extensions'])); 77 | } 78 | 79 | // Composer 80 | if (array_key_exists('composer', $jsonData)) { 81 | $config->set('composer', (bool) $jsonData['composer']); 82 | } 83 | 84 | // Search 85 | if (!isset($jsonData['searches'])) { 86 | $jsonData['searches'] = []; 87 | } 88 | $factory = new SearchesFactory(); 89 | $config->set('searches', $factory->factory($jsonData['searches'])); 90 | 91 | if (isset($jsonData['excludes'])) { 92 | // retro-compatibility 93 | // "exclude" is a string 94 | // excludes is an array 95 | $config->set('exclude', implode(',', $jsonData['excludes'])); 96 | } else { 97 | if (isset($jsonData['exclude'])) { 98 | $config->set('exclude', $jsonData['exclude']); 99 | } 100 | } 101 | 102 | if (isset($jsonData['plugins']['git']['binary'])) { 103 | $config->set('git', $jsonData['plugins']['git']['binary']); 104 | } 105 | 106 | // backward compatibility with typo in documentation 107 | // see https://github.com/phpmetrics/PhpMetrics/issues/441 108 | // file -> report 109 | if (isset($jsonData['plugins']['junit']['file'])) { 110 | $jsonData['plugins']['junit']['report'] = $jsonData['plugins']['junit']['file']; 111 | } 112 | 113 | if (isset($jsonData['plugins']['junit']['report'])) { 114 | $config->set('junit', $jsonData['plugins']['junit']['report']); 115 | } 116 | 117 | // reports 118 | if (isset($jsonData['report']) && is_array($jsonData['report'])) { 119 | foreach ($jsonData['report'] as $reportType => $path) { 120 | $path = $this->resolvePath($path); 121 | $config->set('report-' . $reportType, $path); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/File/ConfigFileReaderYaml.php: -------------------------------------------------------------------------------- 1 | filename); 19 | 20 | $this->parseJson($json, $config); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/Parser.php: -------------------------------------------------------------------------------- 1 | $arg) { 23 | if (preg_match('!\-\-config=(.*)!', $arg, $matches)) { 24 | $fileReader = ConfigFileReaderFactory::createFromFileName($matches[1]); 25 | $fileReader->read($config); 26 | unset($argv[$k]); 27 | } 28 | } 29 | 30 | // arguments with options 31 | foreach ($argv as $k => $arg) { 32 | if (preg_match('!\-\-([\w\-]+)=(.*)!', $arg, $matches)) { 33 | list(, $parameter, $value) = $matches; 34 | $config->set($parameter, trim($value, ' "\'')); 35 | unset($argv[$k]); 36 | } 37 | } 38 | 39 | // arguments without options 40 | foreach ($argv as $k => $arg) { 41 | if (preg_match('!\-\-([\w\-]+)$!', $arg, $matches)) { 42 | list(, $parameter) = $matches; 43 | $config->set($parameter, true); 44 | unset($argv[$k]); 45 | } 46 | } 47 | 48 | // last argument 49 | $files = array_pop($argv); 50 | if ($files && !preg_match('!^\-\-!', $files)) { 51 | $config->set('files', explode(',', $files)); 52 | } 53 | 54 | return $config; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/NodeTraverser.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Ast; 11 | 12 | if (PHP_VERSION_ID >= 80000) { 13 | class_alias(Php8NodeTraverser::class, __NAMESPACE__ . '\\ActualNodeTraverser'); 14 | }elseif (PHP_VERSION_ID >= 70000) { 15 | class_alias(Php7NodeTraverser::class, __NAMESPACE__ . '\\ActualNodeTraverser'); 16 | } else { 17 | class_alias(Php5NodeTraverser::class, __NAMESPACE__ . '\\ActualNodeTraverser'); 18 | } 19 | 20 | /** 21 | * Empty class to refer the good ActualNodeTraverser depending on the PHP version. 22 | * This class must be hard-coded and not directly used as an alias because composer can not handle class-aliases when 23 | * flag --classmap-authoritative is set. 24 | * @see https://github.com/phpmetrics/PhpMetrics/issues/373 25 | */ 26 | /** @noinspection PhpUndefinedClassInspection */ 27 | class NodeTraverser extends ActualNodeTraverser {} 28 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/NodeTyper.php: -------------------------------------------------------------------------------- 1 | create($kind); 17 | } 18 | 19 | if ($kind !== null) { 20 | return (new \PhpParser\ParserFactory())->createForVersion($kind); 21 | } 22 | 23 | return (new \PhpParser\ParserFactory())->createForNewestSupportedVersion(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/ParserTraverserVisitorsAssigner.php: -------------------------------------------------------------------------------- 1 | = v5, visitors are traversed in LIFO order. 15 | // With nikic/php-parser < v5, visitors are traversed in FIFO order. 16 | 17 | if (! method_exists('PhpParser\ParserFactory', 'create')) { 18 | $visitors = array_reverse($visitors); 19 | } 20 | 21 | foreach ($visitors as $visitor) { 22 | $traverser->addVisitor($visitor); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/Php5NodeTraverser.php: -------------------------------------------------------------------------------- 1 | traverser = new Traverser($this, $stopCondition); 21 | } 22 | 23 | public function traverseNode(Node $node) 24 | { 25 | return parent::traverseNode($node); 26 | } 27 | 28 | protected function traverseArray(array $nodes) 29 | { 30 | return $this->traverser->traverseArray($nodes, $this->visitors); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/Php7NodeTraverser.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Ast; 11 | 12 | use PhpParser\Node; 13 | use PhpParser\NodeTraverser as Mother; 14 | use PhpParser\NodeVisitor; 15 | 16 | /** 17 | * @author Jean-François Lépine 18 | * @internal 19 | */ 20 | class Traverser 21 | { 22 | /** 23 | * @var callable 24 | */ 25 | protected $stopCondition; 26 | 27 | /** @var Mother */ 28 | private $traverser; 29 | 30 | /** 31 | * @param Mother $traverser 32 | * @param callable|null $stopCondition 33 | */ 34 | public function __construct(Mother $traverser, $stopCondition = null) 35 | { 36 | if (null === $stopCondition) { 37 | $stopCondition = function ($node) { 38 | if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Interface_) { 39 | return false; 40 | } 41 | 42 | return true; 43 | }; 44 | } 45 | 46 | $this->stopCondition = $stopCondition; 47 | $this->traverser = $traverser; 48 | } 49 | 50 | /** 51 | * @param array $nodes 52 | * @param NodeVisitor[] $visitors 53 | * @return array 54 | */ 55 | public function traverseArray(array $nodes, array $visitors) 56 | { 57 | $doNodes = []; 58 | 59 | foreach ($nodes as $i => &$node) { 60 | if (is_array($node)) { 61 | $node = $this->traverseArray($node, $visitors); 62 | } elseif ($node instanceof Node) { 63 | $traverseChildren = call_user_func($this->stopCondition, $node); 64 | 65 | foreach ($visitors as $visitor) { 66 | $return = $visitor->enterNode($node); 67 | if (Mother::DONT_TRAVERSE_CHILDREN === $return) { 68 | $traverseChildren = false; 69 | } elseif (null !== $return) { 70 | $node = $return; 71 | } 72 | } 73 | 74 | if ($traverseChildren) { 75 | $node = $this->traverser->traverseNode($node); 76 | } 77 | 78 | foreach ($visitors as $visitor) { 79 | $return = $visitor->leaveNode($node); 80 | 81 | if (Mother::REMOVE_NODE === $return) { 82 | $doNodes[] = [$i, []]; 83 | break; 84 | } elseif (is_array($return)) { 85 | $doNodes[] = [$i, $return]; 86 | break; 87 | } elseif (null !== $return) { 88 | $node = $return; 89 | } 90 | } 91 | } 92 | } 93 | 94 | if (!empty($doNodes)) { 95 | while (list($i, $replace) = array_pop($doNodes)) { 96 | array_splice($nodes, $i, 1, $replace); 97 | } 98 | } 99 | 100 | return $nodes; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Hal/Component/File/Finder.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\File; 11 | 12 | use RecursiveDirectoryIterator; 13 | use RecursiveIteratorIterator; 14 | use RegexIterator; 15 | 16 | /** 17 | * @author Jean-François Lépine 18 | */ 19 | class Finder 20 | { 21 | /** 22 | * Follow symlinks 23 | */ 24 | const FOLLOW_SYMLINKS = RecursiveDirectoryIterator::FOLLOW_SYMLINKS; 25 | 26 | /** 27 | * Extensions to match 28 | * 29 | * @var array 30 | */ 31 | private $extensions = []; 32 | 33 | /** 34 | * Subdirectories to exclude 35 | * 36 | * @var array 37 | */ 38 | private $excludedDirs = []; 39 | 40 | /** 41 | * @param string[] $extensions regex of file extensions to include 42 | * @param string[] $excludedDirs regex of directories to exclude 43 | */ 44 | public function __construct(array $extensions = ['php'], array $excludedDirs = []) 45 | { 46 | $this->extensions = $extensions; 47 | $this->excludedDirs = $excludedDirs; 48 | } 49 | 50 | /** 51 | * Find files in path 52 | * 53 | * @param string[] $paths 54 | * @return array 55 | */ 56 | public function fetch(array $paths) 57 | { 58 | $files = []; 59 | foreach ($paths as $path) { 60 | if (is_dir($path)) { 61 | $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 62 | $directory = new RecursiveDirectoryIterator($path); 63 | $iterator = new RecursiveIteratorIterator($directory); 64 | 65 | $filterRegex = sprintf( 66 | '`^%s%s%s$`', 67 | preg_quote($path, '`'), 68 | !empty($this->excludedDirs) ? '((?!' . implode('|', array_map('preg_quote', $this->excludedDirs)) . ').)+' : '.+', 69 | '\.(' . implode('|', $this->extensions) . ')' 70 | ); 71 | 72 | $filteredIterator = new RegexIterator( 73 | $iterator, 74 | $filterRegex, 75 | \RecursiveRegexIterator::GET_MATCH 76 | ); 77 | 78 | foreach ($filteredIterator as $file) { 79 | $files[] = $file[0]; 80 | } 81 | } elseif (is_file($path)) { 82 | $files[] = $path; 83 | } 84 | } 85 | return $files; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Hal/Component/Issue/Issuer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Issue; 11 | 12 | use Hal\Component\Output\Output; 13 | use PhpParser\Node; 14 | use PhpParser\PrettyPrinter\Standard; 15 | 16 | /** 17 | * @package Hal\Component\Issue 18 | */ 19 | class Issuer 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private $debug = []; 25 | 26 | /** 27 | * @var Output 28 | */ 29 | private $output; 30 | 31 | /** 32 | * @param Output $output 33 | */ 34 | public function __construct(Output $output) 35 | { 36 | $this->output = $output; 37 | } 38 | 39 | /** 40 | * @param $errno 41 | * @param $errstr 42 | * @param $errfile 43 | * @param $errline 44 | * @throws \ErrorException 45 | */ 46 | public function onError($errno, $errstr, $errfile, $errline) 47 | { 48 | if (error_reporting() == 0) { 49 | return; 50 | } 51 | $php = PHP_VERSION; 52 | $os = php_uname(); 53 | $phpmetrics = getVersion(); 54 | $traces = debug_backtrace(0, 10); 55 | $trace = ''; 56 | foreach ($traces as $c) { 57 | if (isset($c['file'])) { 58 | $trace .= sprintf("+ %s (line %d)\n", $c['file'], $c['line']); 59 | } 60 | } 61 | 62 | $debug = ''; 63 | foreach ($this->debug as $key => $value) { 64 | if ($value instanceof Node || is_array($value)) { 65 | $value = (new Standard())->prettyPrint($value); 66 | } 67 | 68 | $debug .= sprintf("%s: %s\n", $key, $value); 69 | } 70 | 71 | $logfile = './phpmetrics-error.log'; 72 | 73 | $message = <<We're sorry : an unexpected error occured. 76 | 77 | Can you help us ? Please open a new issue at https://github.com/phpmetrics/PhpMetrics/issues/new, and copy-paste the content 78 | of this file: $logfile ? 79 | 80 | Thanks for your help :) 81 | EOT; 82 | 83 | $log = << 102 | Details 103 | ``` 104 | $trace 105 | 106 | 107 | $debug 108 | ``` 109 | 110 | EOT; 111 | 112 | $this->output->write($message); 113 | 114 | $this->log($logfile, $log); 115 | $this->terminate(1); 116 | } 117 | 118 | /** 119 | * @return $this 120 | */ 121 | public function enable() 122 | { 123 | set_error_handler([$this, 'onError']); 124 | return $this; 125 | } 126 | 127 | /** 128 | * @return $this 129 | */ 130 | public function disable() 131 | { 132 | restore_error_handler(); 133 | return $this; 134 | } 135 | 136 | /** 137 | * @param $status 138 | */ 139 | protected function terminate($status) 140 | { 141 | exit($status); 142 | } 143 | 144 | /** 145 | * @param $log 146 | * @return $this 147 | */ 148 | protected function log($logfile, $log) 149 | { 150 | if (is_writable(getcwd())) { 151 | file_put_contents($logfile, $log); 152 | } else { 153 | $this->output->write($log); 154 | } 155 | return $this; 156 | } 157 | 158 | /** 159 | * @param $debugKey 160 | * @param $value 161 | * @return $this 162 | */ 163 | public function set($debugKey, $value) 164 | { 165 | $this->debug[$debugKey] = $value; 166 | return $this; 167 | } 168 | 169 | /** 170 | * @param $debugKey 171 | * @return $this 172 | */ 173 | public function clear($debugKey) 174 | { 175 | unset($this->debug[$debugKey]); 176 | return $this; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Hal/Component/Output/CliOutput.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Output; 11 | 12 | /** 13 | * @package Hal\Component\Issue 14 | */ 15 | class CliOutput implements Output 16 | { 17 | /** 18 | * @var bool 19 | */ 20 | private $quietMode = false; 21 | 22 | /** 23 | * @param string $message 24 | * @return $this 25 | */ 26 | public function writeln($message) 27 | { 28 | $this->write(PHP_EOL . $message); 29 | return $this; 30 | } 31 | 32 | /** 33 | * @param string $message 34 | * @return $this 35 | */ 36 | public function write($message) 37 | { 38 | if (preg_match_all('!<([a-z]+)(?:=[^}]+)?>(.*?)!', $message, $matches, PREG_SET_ORDER)) { 39 | list(, $type, $message) = $matches[0]; 40 | $color = ''; 41 | switch ($type) { 42 | case 'error': 43 | $color = "\033[31m"; 44 | break; 45 | case 'warning': 46 | $color = "\033[33m"; 47 | break; 48 | case 'success': 49 | $color = "\033[32m"; 50 | break; 51 | case 'info': 52 | $color = "\033[34m"; 53 | break; 54 | } 55 | 56 | $message = $color . $message . "\033[0m"; 57 | } 58 | 59 | $this->quietMode || file_put_contents('php://stdout', $message); 60 | return $this; 61 | } 62 | 63 | /** 64 | * @param string $message 65 | * @return $this 66 | */ 67 | public function err($message) 68 | { 69 | file_put_contents('php://stderr', $message); 70 | return $this; 71 | } 72 | 73 | public function clearln() 74 | { 75 | $this->writeln("\x0D"); 76 | $this->writeln("\x1B[2K"); 77 | return $this; 78 | } 79 | 80 | /** 81 | * @param bool $quietMode 82 | * @return $this 83 | */ 84 | public function setQuietMode($quietMode) 85 | { 86 | $this->quietMode = $quietMode; 87 | return $this; 88 | } 89 | 90 | /** 91 | * @inheritDoc 92 | */ 93 | public function hasAnsi() 94 | { 95 | if (DIRECTORY_SEPARATOR === '\\') { 96 | return 97 | 0 >= version_compare( 98 | '10.0.10586', 99 | PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD 100 | ) 101 | || false !== getenv('ANSICON') 102 | || 'ON' === getenv('ConEmuANSI') 103 | || 'xterm' === getenv('TERM'); 104 | } 105 | 106 | return function_exists('posix_isatty') && @posix_isatty(STDOUT); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Hal/Component/Output/Output.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Output; 11 | 12 | /** 13 | * @package Hal\Component\Output 14 | */ 15 | interface Output 16 | { 17 | /** 18 | * @param string $message 19 | * @return $this 20 | */ 21 | public function writeln($message); 22 | 23 | /** 24 | * @param string $message 25 | * @return $this 26 | */ 27 | public function write($message); 28 | 29 | /** 30 | * @param string $message 31 | * @return $this 32 | */ 33 | public function err($message); 34 | 35 | /** 36 | * @return $this 37 | */ 38 | public function clearln(); 39 | 40 | /** 41 | * Detects ANSI support 42 | * 43 | * @return bool 44 | */ 45 | public function hasAnsi(); 46 | } 47 | -------------------------------------------------------------------------------- /src/Hal/Component/Output/ProgressBar.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Output; 11 | 12 | /** 13 | * @package Hal\Component\Output 14 | */ 15 | class ProgressBar 16 | { 17 | /** 18 | * @var Output 19 | */ 20 | private $output; 21 | 22 | /** 23 | * @var int 24 | */ 25 | private $max; 26 | 27 | /** 28 | * @var int 29 | */ 30 | private $current = 0; 31 | 32 | /** 33 | * @param Output $output 34 | * @param int $max 35 | */ 36 | public function __construct(Output $output, $max) 37 | { 38 | $this->output = $output; 39 | $this->max = $max; 40 | } 41 | 42 | /** 43 | * Start progress bar 44 | */ 45 | public function start() 46 | { 47 | $this->current = 0; 48 | } 49 | 50 | /** 51 | * Advance progress bar 52 | */ 53 | public function advance() 54 | { 55 | $this->current++; 56 | 57 | if ($this->output->hasAnsi()) { 58 | $percent = round($this->current / $this->max * 100); 59 | $this->output->write("\x0D"); 60 | $this->output->write("\x1B[2K"); 61 | $this->output->write(sprintf('... %s%% ...', $percent)); 62 | } else { 63 | $this->output->write('.'); 64 | } 65 | } 66 | 67 | /** 68 | * Clear console 69 | */ 70 | public function clear() 71 | { 72 | if ($this->output->hasAnsi()) { 73 | $this->output->write("\x0D"); 74 | $this->output->write("\x1B[2K"); 75 | $this->output->clearln(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Hal/Component/Output/TestOutput.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Output; 11 | 12 | /** 13 | * @package Hal\Component\Issue 14 | */ 15 | class TestOutput implements Output 16 | { 17 | public $output; 18 | public $err; 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | public function writeln($message) 24 | { 25 | $this->write(PHP_EOL . $message); 26 | return $this; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function write($message) 33 | { 34 | $this->output .= $message; 35 | return $this; 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function err($message) 42 | { 43 | $this->err .= $message; 44 | return $this; 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function clearln() {} 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function hasAnsi() 56 | { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/Edge.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree; 11 | 12 | class Edge 13 | { 14 | /** 15 | * @var Node 16 | */ 17 | private $from; 18 | 19 | /** 20 | * @var Node 21 | */ 22 | private $to; 23 | 24 | /** 25 | * @var boolean 26 | */ 27 | public $cyclic = false; 28 | 29 | /** 30 | * @param Node $from 31 | * @param Node $to 32 | */ 33 | public function __construct(Node $from, Node $to) 34 | { 35 | $this->from = $from; 36 | $this->to = $to; 37 | } 38 | 39 | /** 40 | * @return Node 41 | */ 42 | public function getFrom() 43 | { 44 | return $this->from; 45 | } 46 | 47 | /** 48 | * @return Node 49 | */ 50 | public function getTo() 51 | { 52 | return $this->to; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function asString() 59 | { 60 | return sprintf('%s -> %s', $this->from->getKey(), $this->to->getKey()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/Graph.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree; 11 | 12 | class Graph implements \Countable 13 | { 14 | /** 15 | * @var Node[] 16 | */ 17 | private $data = []; 18 | 19 | /** 20 | * @var Edge[] 21 | */ 22 | private $edges = []; 23 | 24 | /** 25 | * @param Node $node 26 | * @return $this 27 | */ 28 | public function insert(Node $node) 29 | { 30 | if ($this->has($node->getKey())) { 31 | throw new GraphException(sprintf('node %s is already present', $node->getKey())); 32 | } 33 | $this->data[$node->getKey()] = $node; 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param Node $from 39 | * @param Node $to 40 | * @return $this 41 | */ 42 | public function addEdge(Node $from, Node $to) 43 | { 44 | if (!$this->has($from->getKey())) { 45 | throw new GraphException('from is not is in the graph'); 46 | } 47 | if (!$this->has($to->getKey())) { 48 | throw new GraphException('to is not is in the graph'); 49 | } 50 | 51 | $edge = new Edge($from, $to); 52 | $from->addEdge($edge); 53 | $to->addEdge($edge); 54 | array_push($this->edges, $edge); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function asString() 63 | { 64 | $string = ''; 65 | foreach ($this->all() as $node) { 66 | $string .= sprintf("%s;\n", $node->getKey()); 67 | } 68 | foreach ($this->getEdges() as $edge) { 69 | $string .= sprintf("%s;\n", $edge->asString()); 70 | } 71 | return $string; 72 | } 73 | 74 | /** 75 | * @return Edge[] 76 | */ 77 | public function getEdges() 78 | { 79 | return $this->edges; 80 | } 81 | 82 | /** 83 | * @param $key 84 | * @return Node|null 85 | */ 86 | public function get($key) 87 | { 88 | return $this->has($key) ? $this->data[$key] : null; 89 | } 90 | 91 | /** 92 | * @param $key 93 | * @return bool 94 | */ 95 | public function has($key) 96 | { 97 | return isset($this->data[$key]); 98 | } 99 | 100 | /** 101 | * @return int 102 | */ 103 | #[\ReturnTypeWillChange] 104 | public function count() 105 | { 106 | return count($this->data); 107 | } 108 | 109 | /** 110 | * @return Node[] 111 | */ 112 | public function all() 113 | { 114 | return $this->data; 115 | } 116 | 117 | /** 118 | * @return $this 119 | */ 120 | public function resetVisits() 121 | { 122 | foreach ($this->all() as $node) { 123 | $node->visited = false; 124 | } 125 | return $this; 126 | } 127 | 128 | /** 129 | * Get the list of all root nodes 130 | * 131 | * we can have array of roots : graph can be a "forest" 132 | * 133 | * @return array 134 | */ 135 | public function getRootNodes() 136 | { 137 | $roots = []; 138 | foreach ($this->all() as $node) { 139 | $isRoot = true; 140 | 141 | foreach ($node->getEdges() as $edge) { 142 | if ($edge->getTo() == $node) { 143 | $isRoot = false; 144 | } 145 | } 146 | 147 | if ($isRoot) { 148 | array_push($roots, $node); 149 | } 150 | } 151 | 152 | return $roots; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/GraphDeduplicated.php: -------------------------------------------------------------------------------- 1 | getUniqueId() . '->' . $to->getUniqueId(); 21 | 22 | if (isset($this->edgesMap[$key])) { 23 | return $this; 24 | } 25 | 26 | $this->edgesMap[$key] = true; 27 | 28 | return parent::addEdge($from, $to); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/GraphException.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree; 11 | 12 | class GraphException extends \LogicException {} 13 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/HashMap.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree; 11 | 12 | class HashMap implements \Countable, \IteratorAggregate 13 | { 14 | /** 15 | * @var array 16 | */ 17 | private $nodes = []; 18 | 19 | /** 20 | * @param Node $node 21 | * @return $this 22 | */ 23 | public function attach(Node $node) 24 | { 25 | $this->nodes[$node->getKey()] = $node; 26 | return $this; 27 | } 28 | 29 | /** 30 | * @param $key 31 | * @return Node 32 | */ 33 | public function get($key) 34 | { 35 | return $this->has($key) ? $this->nodes[$key] : null; 36 | } 37 | 38 | /** 39 | * @param $key 40 | * @return bool 41 | */ 42 | public function has($key) 43 | { 44 | return isset($this->nodes[$key]); 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function count() 51 | { 52 | return count($this->nodes); 53 | } 54 | 55 | /** 56 | * @return \ArrayIterator 57 | */ 58 | public function getIterator() 59 | { 60 | return new \ArrayIterator($this->nodes); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/Node.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree; 11 | 12 | class Node 13 | { 14 | /** 15 | * @var mixed 16 | */ 17 | private $data; 18 | 19 | /** 20 | * @var string 21 | */ 22 | private $key; 23 | 24 | /** 25 | * @var Edge[] 26 | */ 27 | private $edges = []; 28 | 29 | /** 30 | * @var bool 31 | */ 32 | public $visited = false; 33 | 34 | /** 35 | * @var bool 36 | */ 37 | public $cyclic = false; 38 | 39 | /** 40 | * @param string $key 41 | * @param mixed $data 42 | */ 43 | public function __construct($key, $data = null) 44 | { 45 | $this->key = $key; 46 | $this->data = $data; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getKey() 53 | { 54 | return $this->key; 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function getAdjacents() 61 | { 62 | $adjacents = []; 63 | foreach ($this->edges as $edge) { 64 | if ($edge->getFrom()->getKey() != $this->getKey()) { 65 | $adjacents[$edge->getFrom()->getKey()] = $edge->getFrom(); 66 | } 67 | if ($edge->getTo()->getKey() != $this->getKey()) { 68 | $adjacents[$edge->getTo()->getKey()] = $edge->getTo(); 69 | } 70 | } 71 | return $adjacents; 72 | } 73 | 74 | /** 75 | * @return Edge[] 76 | */ 77 | public function getEdges() 78 | { 79 | return $this->edges; 80 | } 81 | 82 | /** 83 | * @param Edge $edge 84 | * @return $this 85 | */ 86 | public function addEdge(Edge $edge) 87 | { 88 | array_push($this->edges, $edge); 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return mixed 94 | */ 95 | public function getData() 96 | { 97 | return $this->data; 98 | } 99 | 100 | /** 101 | * @param mixed $data 102 | * @return Node 103 | */ 104 | public function setData($data) 105 | { 106 | $this->data = $data; 107 | return $this; 108 | } 109 | 110 | /** 111 | * @return string Unique id for this node independent of class name or node type 112 | */ 113 | public function getUniqueId() 114 | { 115 | return spl_object_hash($this); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/Operator/CycleDetector.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree\Operator; 11 | 12 | use Hal\Component\Tree\Graph; 13 | use Hal\Component\Tree\Node; 14 | 15 | /** 16 | * @package Hal\Component\Tree\Util 17 | * @see http://www.geeksforgeeks.org/detect-cycle-in-a-graph/ 18 | */ 19 | class CycleDetector 20 | { 21 | /** 22 | * Check if graph contains cycle 23 | * 24 | * Each node in cycle is flagged with the "cyclic" attribute 25 | * 26 | * @param Graph $graph 27 | * @return bool 28 | */ 29 | public function isCyclic(Graph $graph) 30 | { 31 | // prepare stack 32 | $recursionStack = []; 33 | foreach ($graph->all() as $node) { 34 | $recursionStack[$node->getKey()] = false; 35 | } 36 | 37 | // start analysis 38 | $isCyclic = false; 39 | foreach ($graph->getEdges() as $edge) { 40 | if ($r = $this->detectCycle($edge->getFrom(), $recursionStack)) { 41 | $edge->cyclic = true; 42 | $isCyclic = true; 43 | } 44 | 45 | $recursionStack[$node->getKey()] = false; 46 | } 47 | 48 | $graph->resetVisits(); 49 | 50 | return $isCyclic; 51 | } 52 | 53 | /** 54 | * @param Node $node 55 | * @param $recursionStack 56 | * @return bool 57 | */ 58 | private function detectCycle(Node $node, &$recursionStack) 59 | { 60 | if (!$node->visited) { 61 | // mark the current node as visited and part of recursion stack 62 | $recursionStack[$node->getKey()] = true; 63 | $node->visited = true; 64 | 65 | // recur for all the vertices adjacent to this vertex 66 | foreach ($node->getEdges() as $edge) { 67 | if ($edge->getTo() === $node) { 68 | continue; 69 | } 70 | 71 | if (!$edge->getTo()->visited && $this->detectCycle($edge->getTo(), $recursionStack)) { 72 | $edge->cyclic = $edge->getTo()->cyclic = true; 73 | return true; 74 | } elseif ($recursionStack[$edge->getTo()->getKey()]) { 75 | $edge->cyclic = $edge->getTo()->cyclic = true; 76 | return true; 77 | } 78 | } 79 | } 80 | // remove the vertex from recursion stack 81 | $recursionStack[$node->getKey()] = false; 82 | return false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Hal/Component/Tree/Operator/SizeOfTree.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Hal\Component\Tree\Operator; 11 | 12 | use Hal\Component\Tree\Graph; 13 | use Hal\Component\Tree\Node; 14 | 15 | class SizeOfTree 16 | { 17 | /** 18 | * @var Graph 19 | */ 20 | private $graph; 21 | 22 | /** 23 | * @param Graph $graph 24 | */ 25 | public function __construct(Graph $graph) 26 | { 27 | if ((new CycleDetector())->isCyclic($graph)) { 28 | throw new \LogicException('Cannot get size informations of cyclic graph'); 29 | } 30 | 31 | $this->graph = $graph; 32 | } 33 | 34 | /** 35 | * Get depth of node 36 | * 37 | * @param Node $node 38 | * @return int 39 | */ 40 | public function getDepthOfNode(Node $node) 41 | { 42 | $edges = $node->getEdges(); 43 | 44 | if (0 === count($edges)) { 45 | return 0; 46 | } 47 | 48 | // our tree is not binary : interface can have more than one parent 49 | $max = 0; 50 | foreach ($edges as $edge) { 51 | if ($edge->getFrom() == $node) { 52 | continue; 53 | } 54 | 55 | $n = 1 + $this->getDepthOfNode($edge->getFrom()); 56 | if ($n > $max) { 57 | $max = $n; 58 | } 59 | } 60 | 61 | return $max; 62 | } 63 | 64 | /** 65 | * Get depth of node 66 | * 67 | * @param Node $node 68 | * @return int 69 | */ 70 | public function getNumberOfChilds(Node $node, $uniqs = false) 71 | { 72 | $edges = $node->getEdges(); 73 | 74 | if (0 === count($edges)) { 75 | return 0; 76 | } 77 | 78 | // our tree is not binary : interface can have more than one parent 79 | $max = 0; 80 | $n = 0; 81 | 82 | foreach ($edges as $edge) { 83 | if ($edge->getTo() == $node) { 84 | continue; 85 | } 86 | 87 | if (true == $uniqs && $edge->getTo()->visited) { 88 | continue; 89 | } 90 | $edge->getTo()->visited = true; 91 | 92 | $n += 1 + $this->getNumberOfChilds($edge->getTo(), $uniqs); 93 | 94 | $edge->getTo()->visited = false; 95 | 96 | if ($n > $max) { 97 | $max = $n; 98 | } 99 | } 100 | 101 | return $max; 102 | } 103 | 104 | /** 105 | * Get average height of graph 106 | * 107 | * @return float 108 | */ 109 | public function getAverageHeightOfGraph() 110 | { 111 | $ns = []; 112 | foreach ($this->graph->getRootNodes() as $node) { 113 | array_push($ns, $this->getLongestBranch($node)); 114 | } 115 | return round(array_sum($ns) / max(1, count($ns)), 2); 116 | } 117 | 118 | /** 119 | * @param Node $node 120 | * @return int 121 | */ 122 | public function getLongestBranch(Node $node) 123 | { 124 | $max = 1; 125 | foreach ($node->getEdges() as $edge) { 126 | if ($node == $edge->getTo()) { 127 | // only descendants 128 | continue; 129 | } 130 | 131 | $n = 1 + $this->getLongestBranch($edge->getTo()); 132 | 133 | if ($n > $max) { 134 | $max = $n; 135 | } 136 | } 137 | 138 | return $max; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Hal/Metric/BagTrait.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->set('name', $name); 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getName() 27 | { 28 | return $this->name; 29 | } 30 | 31 | /** 32 | * @param $key 33 | * @param $value 34 | * @return $this 35 | */ 36 | public function set($key, $value) 37 | { 38 | $this->bag[$key] = $value; 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param $key 44 | * @return bool 45 | */ 46 | public function has($key) 47 | { 48 | return isset($this->bag[$key]); 49 | } 50 | 51 | /** 52 | * @param $key 53 | * @return null 54 | */ 55 | public function get($key) 56 | { 57 | return $this->has($key) ? $this->bag[$key] : null; 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function all() 64 | { 65 | return $this->bag; 66 | } 67 | 68 | /** 69 | * @param array $array 70 | * @return $this 71 | */ 72 | public function fromArray(array $array) 73 | { 74 | foreach ($array as $key => $value) { 75 | $this->set($key, $value); 76 | } 77 | return $this; 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | #[\ReturnTypeWillChange] 84 | public function jsonSerialize() 85 | { 86 | return array_merge($this->all(), ['_type' => get_class($this)]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Hal/Metric/ClassMetric.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 28 | } 29 | 30 | 31 | public function leaveNode(Node $node) 32 | { 33 | if ( 34 | NodeTyper::isOrganizedStructure($node) 35 | ) { 36 | if ($node instanceof Stmt\Interface_) { 37 | $class = new InterfaceMetric($node->namespacedName->toString()); 38 | $class->set('interface', true); 39 | $class->set('abstract', true); 40 | } else { 41 | $name = getNameOfNode($node); 42 | $class = new ClassMetric($name); 43 | $class->set('interface', false); 44 | 45 | $isAbstract = false; 46 | if ($node instanceof Stmt\Class_) { 47 | $isAbstract = $node->isAbstract(); 48 | } elseif ($node instanceof Stmt\Trait_) { 49 | $isAbstract = true; // Traits are always abstract 50 | } 51 | 52 | $isFinal = false; 53 | if ($node instanceof Stmt\Class_) { 54 | $isFinal = $node->isFinal(); 55 | } elseif ($node instanceof Stmt\Trait_) { 56 | $isFinal = false; // Traits cannot be final 57 | } 58 | 59 | $class->set('abstract', $isAbstract); 60 | $class->set('final', $isFinal); 61 | } 62 | 63 | $methods = []; 64 | 65 | $methodsPublic = $methodsPrivate = $nbGetters = $nbSetters = 0; 66 | $roleDetector = new RoleOfMethodDetector(); 67 | foreach ($node->stmts as $stmt) { 68 | if ($stmt instanceof Stmt\ClassMethod) { 69 | $function = new FunctionMetric((string)$stmt->name); 70 | 71 | $role = $roleDetector->detects($stmt); 72 | $function->set('role', $role); 73 | switch ($role) { 74 | case 'getter': 75 | $nbGetters++; 76 | break; 77 | case 'setter': 78 | $nbSetters++; 79 | break; 80 | } 81 | 82 | if (null === $role) { 83 | if ($stmt->isPublic()) { 84 | $methodsPublic++; 85 | $function->set('public', true); 86 | $function->set('private', false); 87 | } 88 | 89 | if ($stmt->isPrivate() || $stmt->isProtected()) { 90 | $methodsPrivate++; 91 | $function->set('public', false); 92 | $function->set('private', true); 93 | } 94 | } 95 | 96 | array_push($methods, $function); 97 | } 98 | } 99 | 100 | $class->set('methods', $methods); 101 | $class->set('nbMethodsIncludingGettersSetters', count($methods)); 102 | $class->set('nbMethods', count($methods) - ($nbGetters + $nbSetters)); 103 | $class->set('nbMethodsPrivate', $methodsPrivate); 104 | $class->set('nbMethodsPublic', $methodsPublic); 105 | $class->set('nbMethodsGetter', $nbGetters); 106 | $class->set('nbMethodsSetters', $nbSetters); 107 | 108 | $this->metrics->attach($class); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Complexity/KanDefectVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function leaveNode(Node $node) 36 | { 37 | if (NodeTyper::isOrganizedStructure($node)) { 38 | $class = $this->metrics->get(getNameOfNode($node)); 39 | 40 | $select = $while = $if = 0; 41 | 42 | iterate_over_node($node, function ($node) use (&$while, &$select, &$if) { 43 | switch (true) { 44 | case $node instanceof Stmt\Do_: 45 | case $node instanceof Stmt\Foreach_: 46 | case $node instanceof Stmt\While_: 47 | $while++; 48 | break; 49 | case $node instanceof Stmt\If_: 50 | $if++; 51 | break; 52 | case $node instanceof Stmt\Switch_: 53 | $select++; 54 | break; 55 | } 56 | }); 57 | 58 | $defect = 0.15 + 0.23 * $while + 0.22 * $select + 0.07 * $if; 59 | $class->set('kanDefect', round($defect, 2)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Component/MaintainabilityIndexVisitor.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class MaintainabilityIndexVisitor extends NodeVisitorAbstract 31 | { 32 | /** 33 | * @var Metrics 34 | */ 35 | private $metrics; 36 | 37 | /** 38 | * @param Metrics $metrics 39 | */ 40 | public function __construct(Metrics $metrics) 41 | { 42 | $this->metrics = $metrics; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function leaveNode(Node $node) 49 | { 50 | if (NodeTyper::isOrganizedLogicalClassStructure($node)) { 51 | $name = getNameOfNode($node); 52 | $classOrFunction = $this->metrics->get($name); 53 | 54 | if(null === $classOrFunction) { 55 | throw new \LogicException('class or function ' . $name . ' not found in metrics'); 56 | } 57 | 58 | if (null === $lloc = $classOrFunction->get('lloc')) { 59 | throw new \LogicException('please enable length (lloc) visitor first'); 60 | } 61 | if (null === $cloc = $classOrFunction->get('cloc')) { 62 | throw new \LogicException('please enable length (cloc) visitor first'); 63 | } 64 | if (null === $loc = $classOrFunction->get('loc')) { 65 | throw new \LogicException('please enable length (loc) visitor first'); 66 | } 67 | if (null === $ccn = $classOrFunction->get('ccn')) { 68 | throw new \LogicException('please enable McCabe visitor first'); 69 | } 70 | if (null === $volume = $classOrFunction->get('volume')) { 71 | throw new \LogicException('please enable Halstead visitor first'); 72 | } 73 | 74 | // maintainability index without comment 75 | $MIwoC = max( 76 | ( 77 | 171 78 | - (5.2 * \log($volume)) 79 | - (0.23 * $ccn) 80 | - (16.2 * \log($lloc)) 81 | ) * 100 / 171, 82 | 0 83 | ); 84 | if (is_infinite($MIwoC)) { 85 | $MIwoC = 171; 86 | } 87 | 88 | // comment weight 89 | if ($loc > 0) { 90 | $CM = $cloc / $loc; 91 | $commentWeight = 50 * sin(sqrt(2.4 * $CM)); 92 | } 93 | 94 | // maintainability index 95 | $mi = $MIwoC + $commentWeight; 96 | 97 | // save result 98 | $classOrFunction 99 | ->set('mi', round($mi, 2)) 100 | ->set('mIwoC', round($MIwoC, 2)) 101 | ->set('commentWeight', round($commentWeight, 2)); 102 | $this->metrics->attach($classOrFunction); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Structural/LcomVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | public function leaveNode(Node $node) 38 | { 39 | if (NodeTyper::isOrganizedLogicalClassStructure($node)) { 40 | // we build a graph of internal dependencies in class 41 | $graph = new GraphDeduplicated(); 42 | $class = $this->metrics->get(getNameOfNode($node)); 43 | 44 | $roleDetector = new RoleOfMethodDetector(); 45 | 46 | foreach ($node->stmts as $stmt) { 47 | if ($stmt instanceof Stmt\ClassMethod) { 48 | 49 | $role = $roleDetector->detects($stmt); 50 | if (in_array($role, ['getter', 'setter'])) { 51 | // We don't want to increase the LCOM for getters and setters, 52 | continue; 53 | } 54 | 55 | if (!$graph->has($stmt->name . '()')) { 56 | $graph->insert(new TreeNode($stmt->name . '()')); 57 | } 58 | $from = $graph->get($stmt->name . '()'); 59 | 60 | \iterate_over_node($stmt, function ($node) use ($from, &$graph) { 61 | if ($node instanceof Node\Expr\PropertyFetch && isset($node->var->name) && $node->var->name == 'this') { 62 | $name = getNameOfNode($node); 63 | // use of attribute $this->xxx; 64 | if (!$graph->has($name)) { 65 | $graph->insert(new TreeNode($name)); 66 | } 67 | $to = $graph->get($name); 68 | $graph->addEdge($from, $to); 69 | return; 70 | } 71 | 72 | if ($node instanceof Node\Expr\MethodCall) { 73 | if (!$node->var instanceof Node\Expr\New_ && isset($node->var->name) && getNameOfNode($node->var) === 'this') { 74 | // use of method call $this->xxx(); 75 | // use of attribute $this->xxx; 76 | $name = getNameOfNode($node->name) . '()'; 77 | if (!$graph->has($name)) { 78 | $graph->insert(new TreeNode($name)); 79 | } 80 | $to = $graph->get($name); 81 | $graph->addEdge($from, $to); 82 | return; 83 | } 84 | } 85 | }); 86 | } 87 | } 88 | 89 | // we count paths 90 | $paths = 0; 91 | foreach ($graph->all() as $node) { 92 | $paths += $this->traverse($node); 93 | } 94 | 95 | $class->set('lcom', $paths); 96 | } 97 | } 98 | 99 | /** 100 | * Traverse node, and return 1 if node has not been visited yet 101 | * 102 | * @param TreeNode $node 103 | * @return int 104 | */ 105 | private function traverse(TreeNode $node) 106 | { 107 | if ($node->visited) { 108 | return 0; 109 | } 110 | $node->visited = true; 111 | 112 | foreach ($node->getAdjacents() as $adjacent) { 113 | $this->traverse($adjacent); 114 | } 115 | 116 | return 1; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Structural/SystemComplexityVisitor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SystemComplexityVisitor extends NodeVisitorAbstract 23 | { 24 | /** 25 | * @var Metrics 26 | */ 27 | private $metrics; 28 | 29 | /** 30 | * @param Metrics $metrics 31 | */ 32 | public function __construct(Metrics $metrics) 33 | { 34 | $this->metrics = $metrics; 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function leaveNode(Node $node) 41 | { 42 | if (NodeTyper::isOrganizedLogicalClassStructure($node)) { 43 | $class = $this->metrics->get(getNameOfNode($node)); 44 | if (null === $class) { 45 | throw new \RuntimeException('Class metric not found for ' . getNameOfNode($node)); 46 | } 47 | 48 | $sy = $dc = $sc = []; 49 | 50 | foreach ($node->stmts as $stmt) { 51 | if ($stmt instanceof Stmt\ClassMethod) { 52 | // number of returns and calls 53 | $output = 0; 54 | $fanout = []; 55 | 56 | $parentNode = $node; 57 | iterate_over_node($node, function ($node) use (&$output, &$fanout, $parentNode) { 58 | switch (true) { 59 | case $node instanceof Stmt\Return_: 60 | $output++; 61 | break; 62 | case $node instanceof Node\Expr\StaticCall: 63 | $class = getNameOfNode($node->class); 64 | if ('static' === $class || 'self' === $class) { 65 | $class = getNameOfNode($parentNode); 66 | } 67 | $fanout[] = $class . '::' . getNameOfNode($node->name) . '()'; 68 | break; 69 | case $node instanceof Node\Expr\MethodCall: 70 | $class = getNameOfNode($node->var); 71 | if ('this' === $class) { 72 | $class = getNameOfNode($parentNode); 73 | } 74 | $fanout[] = $class . '->' . getNameOfNode($node->name) . '()'; 75 | break; 76 | } 77 | }); 78 | 79 | $fanout = count(array_unique($fanout)); 80 | $v = count($stmt->params) + $output; 81 | $ldc = $v / ($fanout + 1); 82 | $lsc = pow($fanout, 2); 83 | $sy[] = $ldc + $lsc; 84 | $dc[] = $ldc; 85 | $sc[] = $lsc; 86 | } 87 | } 88 | 89 | // average for class 90 | $class 91 | ->set('relativeStructuralComplexity', empty($sc) ? 0 : round(array_sum($sc) / count($sc), 2)) 92 | ->set('relativeDataComplexity', empty($dc) ? 0 : round(array_sum($dc) / count($dc), 2)) 93 | ->set('relativeSystemComplexity', empty($sy) ? 0 : round(array_sum($sy) / count($sy), 2)) 94 | ->set('totalStructuralComplexity', round(array_sum($sc), 2)) 95 | ->set('totalDataComplexity', round(array_sum($dc), 2)) 96 | ->set('totalSystemComplexity', round(array_sum($dc) + array_sum($sc), 2)); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Text/LengthVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function leaveNode(Node $node) 35 | { 36 | if ( 37 | NodeTyper::isOrganizedLogicalClassStructure($node) 38 | || $node instanceof Stmt\Function_ 39 | ) { 40 | if ( 41 | NodeTyper::isOrganizedLogicalClassStructure($node) 42 | ) { 43 | $name = getNameOfNode($node); 44 | $classOrFunction = $this->metrics->get($name); 45 | } else { 46 | $classOrFunction = new FunctionMetric((string) $node->name); 47 | $this->metrics->attach($classOrFunction); 48 | } 49 | 50 | $prettyPrinter = new PrettyPrinter\Standard(); 51 | $code = $prettyPrinter->prettyPrintFile([$node]); 52 | 53 | // count all lines 54 | $loc = count(preg_split('/\r\n|\r|\n/', $code)) - 1; 55 | 56 | // count and remove multi lines comments 57 | $cloc = 0; 58 | if (preg_match_all('!/\*.*?\*/!s', $code, $matches)) { 59 | foreach ($matches[0] as $match) { 60 | $cloc += max(1, count(preg_split('/\r\n|\r|\n/', $match))); 61 | } 62 | } 63 | $code = preg_replace('!/\*.*?\*/!s', '', $code); 64 | 65 | // count and remove single line comments 66 | $code = preg_replace_callback('!(\'[^\']*\'|"[^"]*")|((?:#|\/\/).*$)!m', function (array $matches) use (&$cloc) { 67 | if (isset($matches[2])) { 68 | $cloc += 1; 69 | } 70 | return $matches[1]; 71 | }, $code, -1); 72 | 73 | // count and remove empty lines 74 | $code = trim(preg_replace('!(^\s*[\r\n])!sm', '', $code)); 75 | $lloc = count(preg_split('/\r\n|\r|\n/', $code)); 76 | 77 | // save result 78 | $classOrFunction 79 | ->set('cloc', $cloc) 80 | ->set('loc', $loc) 81 | ->set('lloc', $lloc); 82 | $this->metrics->attach($classOrFunction); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Hal/Metric/FileMetric.php: -------------------------------------------------------------------------------- 1 | name = $name; 32 | $this->regex = $regex; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getName() 39 | { 40 | return $this->name; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getRegex() 47 | { 48 | return $this->regex; 49 | } 50 | 51 | /** 52 | * @param Metrics $metrics 53 | * @return Metrics 54 | */ 55 | public function reduceMetrics(Metrics $metrics) 56 | { 57 | $all = $metrics->all(); 58 | $matched = new Metrics(); 59 | 60 | foreach ($all as $metric) { 61 | if (!preg_match($this->regex, $metric->getName())) { 62 | continue; 63 | } 64 | 65 | $matched->attach($metric); 66 | } 67 | 68 | return $matched; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Hal/Metric/InterfaceMetric.php: -------------------------------------------------------------------------------- 1 | data[$metric->getName()] = $metric; 22 | return $this; 23 | } 24 | 25 | /** 26 | * @param $key 27 | * @return Metric|null 28 | */ 29 | public function get($key) 30 | { 31 | return $this->has($key) ? $this->data[$key] : null; 32 | } 33 | 34 | /** 35 | * @param $key 36 | * @return bool 37 | */ 38 | public function has($key) 39 | { 40 | return isset($this->data[$key]); 41 | } 42 | 43 | /** 44 | * @return Metric[] 45 | */ 46 | public function all() 47 | { 48 | return $this->data; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | #[\ReturnTypeWillChange] 55 | public function jsonSerialize() 56 | { 57 | return $this->all(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Hal/Metric/Package/PackageAbstraction.php: -------------------------------------------------------------------------------- 1 | all() as $eachPackage) { 14 | if (! $eachPackage instanceof PackageMetric) { 15 | continue; 16 | } 17 | $abstractClassCount = 0; 18 | $classCount = count($eachPackage->getClasses()); 19 | foreach ($eachPackage->getClasses() as $eachClassName) { 20 | $eachClass = $metrics->get($eachClassName); 21 | $abstractClassCount += $eachClass->get('abstract'); 22 | } 23 | $eachPackage->setAbstraction($abstractClassCount / $classCount); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Hal/Metric/Package/PackageCollectingVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 25 | } 26 | 27 | public function enterNode(Node $node) 28 | { 29 | if ($node instanceof Namespace_) { 30 | $this->namespace = (string) $node->name; 31 | } 32 | } 33 | 34 | public function leaveNode(Node $node) 35 | { 36 | if ($node instanceof Class_ || $node instanceof Interface_ || $node instanceof Trait_) { 37 | $package = $this->namespace; 38 | 39 | $docComment = $node->getDocComment(); 40 | $docBlockText = $docComment ? $docComment->getText() : ''; 41 | if (preg_match('/^\s*\* @package (.*)/m', $docBlockText, $matches)) { 42 | $package = $matches[1]; 43 | } 44 | if (preg_match('/^\s*\* @subpackage (.*)/m', $docBlockText, $matches)) { 45 | $package = $package . '\\' . $matches[1]; 46 | } 47 | 48 | $packageName = $package . '\\'; 49 | if (! $packageMetric = $this->metrics->get($packageName)) { 50 | $packageMetric = new PackageMetric($packageName); 51 | $this->metrics->attach($packageMetric); 52 | } 53 | /* @var PackageMetric $packageMetric */ 54 | $elementName = getNameOfNode($node); 55 | $elementName = (string) $elementName; 56 | $packageMetric->addClass($elementName); 57 | 58 | $this->metrics->get($elementName)->set('package', $packageName); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Hal/Metric/Package/PackageDependencies.php: -------------------------------------------------------------------------------- 1 | all(), function (Metric $metric) { 16 | return $metric instanceof ClassMetric || $metric instanceof InterfaceMetric; 17 | }); 18 | 19 | foreach ($classes as $each) { 20 | $this->increaseDependencies($each, $metrics); 21 | } 22 | } 23 | 24 | /** 25 | * @param ClassMetric|InterfaceMetric|Metric $class 26 | * @param Metrics $metrics 27 | */ 28 | private function increaseDependencies(Metric $class, Metrics $metrics) 29 | { 30 | if (! $class->has('package') || ! $class->has('externals')) { 31 | return; 32 | } 33 | $incomingPackage = $metrics->get($class->get('package')); /* @var $incomingPackage PackageMetric */ 34 | foreach ($class->get('externals') as $outgoingClassName) { 35 | // same package? 36 | if (in_array($outgoingClassName, $incomingPackage->getClasses())) { 37 | continue; 38 | } 39 | $outgoingPackageName = $this->getPackageOfClass($outgoingClassName, $metrics); 40 | $incomingPackage->addOutgoingClassDependency($outgoingClassName, $outgoingPackageName); 41 | $outgoingPackage = $metrics->get($outgoingPackageName); 42 | 43 | if ($outgoingPackage instanceof PackageMetric) { 44 | $outgoingPackage->addIncomingClassDependency($class->getName(), $incomingPackage->getName()); 45 | } 46 | } 47 | } 48 | 49 | private function getPackageOfClass($className, Metrics $metrics) 50 | { 51 | if ($metrics->has($className) && $metrics->get($className)->has('package')) { 52 | return $metrics->get($className)->get('package'); 53 | } 54 | if (strpos($className, '\\') === false) { 55 | return '\\'; 56 | } 57 | $parts = explode('\\', $className); 58 | array_pop($parts); 59 | return implode('\\', $parts) . '\\'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Hal/Metric/Package/PackageDistance.php: -------------------------------------------------------------------------------- 1 | all() as $each) { 13 | if ($each instanceof PackageMetric && $each->getAbstraction() !== null && $each->getInstability() !== null) { 14 | $each->setNormalizedDistance(abs($each->getAbstraction() + $each->getInstability() - 1)); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Hal/Metric/Package/PackageInstability.php: -------------------------------------------------------------------------------- 1 | all(), function ($metric) { 14 | return $metric instanceof PackageMetric; 15 | }); 16 | 17 | // Calculate instability 18 | $instabilitiesByPackage = []; 19 | foreach ($packages as $eachPackage) { 20 | $afferentCoupling = count($eachPackage->getIncomingClassDependencies()); 21 | $efferentCoupling = count($eachPackage->getOutgoingClassDependencies()); 22 | if ($afferentCoupling + $efferentCoupling !== 0) { 23 | $eachPackage->setInstability( 24 | $efferentCoupling / ($afferentCoupling + $efferentCoupling) 25 | ); 26 | $instabilitiesByPackage[$eachPackage->getName()] = $eachPackage->getInstability(); 27 | } 28 | } 29 | // Set depending instabilities 30 | foreach ($packages as $eachPackage) { 31 | $dependentInstabilities = array_map(function ($packageName) use ($instabilitiesByPackage) { 32 | //return $instabilitiesByPackage[$packageName] ?? null; 33 | return isset($instabilitiesByPackage[$packageName]) ? $instabilitiesByPackage[$packageName] : null; 34 | }, $eachPackage->getOutgoingPackageDependencies()); 35 | $dependentInstabilities = array_combine( 36 | $eachPackage->getOutgoingPackageDependencies(), 37 | $dependentInstabilities 38 | ); 39 | $dependentInstabilities = array_filter($dependentInstabilities, 'is_float'); 40 | $eachPackage->setDependentInstabilities($dependentInstabilities); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Hal/Metric/PackageMetric.php: -------------------------------------------------------------------------------- 1 | has('classes') ? $this->get('classes') : []; 15 | } 16 | 17 | /** @param string $name */ 18 | public function addClass($name) 19 | { 20 | $elements = $this->get('classes'); 21 | $elements[] = (string) $name; 22 | $this->set('classes', $elements); 23 | } 24 | 25 | /** @param float $abstraction */ 26 | public function setAbstraction($abstraction) 27 | { 28 | if ($abstraction !== null) { 29 | $abstraction = (float) $abstraction; 30 | } 31 | $this->set('abstraction', $abstraction); 32 | } 33 | 34 | /** @return float|null */ 35 | public function getAbstraction() 36 | { 37 | return $this->get('abstraction'); 38 | } 39 | 40 | /** @param float $instability */ 41 | public function setInstability($instability) 42 | { 43 | if ($instability !== null) { 44 | $instability = (float) $instability; 45 | } 46 | $this->set('instability', $instability); 47 | } 48 | 49 | /** @return float|null */ 50 | public function getInstability() 51 | { 52 | return $this->get('instability'); 53 | } 54 | 55 | /** 56 | * @param string $className 57 | * @param string $packageName 58 | */ 59 | public function addOutgoingClassDependency($className, $packageName) 60 | { 61 | if ($packageName === $this->getName()) { 62 | return; 63 | } 64 | $classDependencies = $this->getOutgoingClassDependencies(); 65 | $packageDependencies = $this->getOutgoingPackageDependencies(); 66 | if (! in_array($className, $classDependencies)) { 67 | $classDependencies[] = $className; 68 | $this->set('outgoing_class_dependencies', $classDependencies); 69 | } 70 | if (! in_array($packageName, $packageDependencies)) { 71 | $packageDependencies[] = $packageName; 72 | $this->set('outgoing_package_dependencies', $packageDependencies); 73 | } 74 | } 75 | 76 | /** @return string[] */ 77 | public function getOutgoingClassDependencies() 78 | { 79 | return $this->has('outgoing_class_dependencies') ? $this->get('outgoing_class_dependencies') : []; 80 | } 81 | 82 | /** @return string[] */ 83 | public function getOutgoingPackageDependencies() 84 | { 85 | return $this->has('outgoing_package_dependencies') ? $this->get('outgoing_package_dependencies') : []; 86 | } 87 | 88 | /** 89 | * @param string $className 90 | * @param string $packageName 91 | */ 92 | public function addIncomingClassDependency($className, $packageName) 93 | { 94 | if ($packageName === $this->getName()) { 95 | return; 96 | } 97 | $classDependencies = $this->getIncomingClassDependencies(); 98 | $packageDependencies = $this->getIncomingPackageDependencies(); 99 | if (! in_array($className, $classDependencies)) { 100 | $classDependencies[] = $className; 101 | $this->set('incoming_class_dependencies', $classDependencies); 102 | } 103 | if (! in_array($packageName, $packageDependencies)) { 104 | $packageDependencies[] = $packageName; 105 | $this->set('incoming_package_dependencies', $packageDependencies); 106 | } 107 | } 108 | 109 | /** @return string[] */ 110 | public function getIncomingClassDependencies() 111 | { 112 | return $this->has('incoming_class_dependencies') ? $this->get('incoming_class_dependencies') : []; 113 | } 114 | 115 | /** @return string[] */ 116 | public function getIncomingPackageDependencies() 117 | { 118 | return $this->has('incoming_package_dependencies') ? $this->get('incoming_package_dependencies') : []; 119 | } 120 | 121 | /** @param float $normalizedDistance */ 122 | public function setNormalizedDistance($normalizedDistance) 123 | { 124 | $this->set('distance', $normalizedDistance / sqrt(2.0)); 125 | $this->set('normalized_distance', $normalizedDistance); 126 | } 127 | 128 | /** @return float|null */ 129 | public function getDistance() 130 | { 131 | return $this->get('distance'); 132 | } 133 | 134 | /** @return float|null */ 135 | public function getNormalizedDistance() 136 | { 137 | return $this->get('normalized_distance'); 138 | } 139 | 140 | /** @param float[] $instabilities */ 141 | public function setDependentInstabilities(array $instabilities) 142 | { 143 | $this->set('dependent_instabilities', $instabilities); 144 | } 145 | 146 | /** @return float[] */ 147 | public function getDependentInstabilities() 148 | { 149 | return $this->has('dependent_instabilities') ? $this->get('dependent_instabilities') : []; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Hal/Metric/ProjectMetric.php: -------------------------------------------------------------------------------- 1 | 'Name of component', 9 | 'length' => "Halstead'e program length", 10 | 'vocabulary' => "Halstead's vocabulary", 11 | 'volume' => "Halstead's program volume", 12 | 'difficulty' => "Halstead's difficulty", 13 | 'effort' => "Halstead's effort", 14 | 'level' => "Halstead's program level", 15 | 'bugs' => "Halstead's estimation of number of bugs", 16 | 'time' => "Halstead's estimated time to program", 17 | 'intelligentContent' => "Halstead's program level", 18 | 'number_operators' => 'Number of operators', 19 | 'number_operands' => 'Number of operands', 20 | 'number_operators_unique' => 'Number of unique operators', 21 | 'number_operands_unique' => 'Number of unique operands', 22 | 'ccn' => "Cyclomatic complexity", 23 | 'ccnMethodMax' => 'Max Cyclomatic complexity for method', 24 | 'kanDefect' => "Kan's defects", 25 | 'mi' => "Maintainability Index", 26 | 'mIwoC' => "Maintainability Index without comments", 27 | 'commentWeight' => "Weight of comments", 28 | 'externals' => "List of external dependencies", 29 | 'parents' => "List of parent classes", 30 | 'lcom' => "Lack of cohesion of methods", 31 | 'relativeStructuralComplexity' => "Relative structural complexity", 32 | 'relativeDataComplexity' => "Relative data complexity", 33 | 'relativeSystemComplexity' => "Relative system complexity", 34 | 'totalStructuralComplexity' => "Total structural complexity", 35 | 'totalDataComplexity' => "Total data complexity", 36 | 'totalSystemComplexity' => "Total system complexity", 37 | 'cloc' => "Comment Lines of Code", 38 | 'loc' => "Lines of Code", 39 | 'lloc' => "Logical Lines of Code", 40 | 'methods' => "List of methods", 41 | 'nbMethodsIncludingGettersSetters' => "Number of methods including getters and setters", 42 | 'nbMethods' => "Number of methods excluding getters and setters", 43 | 'nbMethodsPrivate' => "Number of private methods", 44 | 'nbMethodsPublic' => "Number of public methods", 45 | 'nbMethodsGetter' => "Number of getters", 46 | 'nbMethodsSetters' => "Number of setters", 47 | 'afferentCoupling' => "Afferent coupling", 48 | 'efferentCoupling' => "Efferent coupling", 49 | 'instability' => "Package Instability", 50 | 'depthOfInheritanceTree' => "Depth of inheritance tree", 51 | 'pageRank' => "PageRank for component", 52 | ]; 53 | 54 | public function allForProject() {} 55 | 56 | public function allForStructures() 57 | { 58 | return array_keys($this->definitionsForStructures); 59 | } 60 | 61 | public function getDefinitions() 62 | { 63 | return $this->definitionsForStructures; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Hal/Metric/SearchMetric.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Coupling 16 | { 17 | /** 18 | * @param Metrics $metrics 19 | */ 20 | public function calculate(Metrics $metrics) 21 | { 22 | // build a graph of relations 23 | $graph = new GraphDeduplicated(); 24 | 25 | foreach ($metrics->all() as $metric) { 26 | if (!$metric instanceof ClassMetric) { 27 | continue; 28 | } 29 | 30 | if (!$graph->has($metric->get('name'))) { 31 | $graph->insert(new Node($metric->get('name'))); 32 | } 33 | $from = $graph->get($metric->get('name')); 34 | 35 | foreach ($metric->get('externals') as $external) { 36 | if (!$graph->has($external)) { 37 | $graph->insert(new Node($external)); 38 | } 39 | 40 | $to = $graph->get($external); 41 | 42 | $graph->addEdge($from, $to); 43 | } 44 | } 45 | 46 | // analyze relations 47 | foreach ($metrics->all() as $metric) { 48 | if (!$metric instanceof ClassMetric) { 49 | continue; 50 | } 51 | $efferent = $afferent = 0; 52 | 53 | $node = $graph->get($metric->get('name')); 54 | foreach ($node->getEdges() as $edge) { 55 | if ($edge->getTo()->getKey() == $node->getKey()) { 56 | // affects 57 | $afferent++; 58 | } 59 | 60 | if ($edge->getFrom()->getKey() == $node->getKey()) { 61 | // receive effects 62 | $efferent++; 63 | } 64 | } 65 | 66 | $instability = 0; 67 | if ($efferent + $afferent > 0) { 68 | $instability = $efferent / ($afferent + $efferent); 69 | } 70 | $metric 71 | ->set('afferentCoupling', $afferent) 72 | ->set('efferentCoupling', $efferent) 73 | ->set('instability', round($instability, 2)); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Coupling/DepthOfInheritanceTree.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DepthOfInheritanceTree 19 | { 20 | /** 21 | * @param Metrics $metrics 22 | */ 23 | public function calculate(Metrics $metrics) 24 | { 25 | $projectMetric = new ProjectMetric('tree'); 26 | 27 | // building graph with parents / childs relations only 28 | $graph = new GraphDeduplicated(); 29 | 30 | foreach ($metrics->all() as $metric) { 31 | if (!$metric instanceof ClassMetric) { 32 | continue; 33 | } 34 | 35 | if (!$graph->has($metric->get('name'))) { 36 | $graph->insert(new Node($metric->get('name'))); 37 | } 38 | 39 | $to = $graph->get($metric->get('name')); 40 | 41 | foreach ($metric->get('parents') as $parent) { 42 | if (!$graph->has($parent)) { 43 | $graph->insert(new Node($parent)); 44 | } 45 | 46 | $from = $graph->get($parent); 47 | 48 | $graph->addEdge($from, $to); 49 | } 50 | } 51 | 52 | $size = new SizeOfTree($graph); 53 | $averageHeight = $size->getAverageHeightOfGraph(); 54 | 55 | $projectMetric->set('depthOfInheritanceTree', $averageHeight); 56 | $metrics->attach($projectMetric); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Coupling/PageRank.php: -------------------------------------------------------------------------------- 1 | all() as $metric) { 21 | if (!$metric instanceof ClassMetric) { 22 | continue; 23 | } 24 | 25 | $links[$metric->get('name')] = $metric->get('externals'); 26 | } 27 | 28 | $ranks = $this->calculatePageRank($links); 29 | 30 | // save in the metrics object 31 | foreach ($ranks as $name => $rank) { 32 | $metrics->get($name)->set('pageRank', round($rank, 2)); 33 | } 34 | } 35 | 36 | /** 37 | * @see http://phpir.com/pagerank-in-php/ 38 | * @param $linkGraph 39 | * @param float $dampingFactor 40 | * @return array 41 | */ 42 | private function calculatePageRank($linkGraph, $dampingFactor = 0.15) 43 | { 44 | $pageRank = []; 45 | $tempRank = []; 46 | $nodeCount = count($linkGraph); 47 | 48 | // initialise the PR as 1/n 49 | foreach ($linkGraph as $node => $outbound) { 50 | $pageRank[$node] = 1 / $nodeCount; 51 | $tempRank[$node] = 0; 52 | } 53 | 54 | $change = 1; 55 | $i = 0; 56 | while ($change > 0.00005 && $i < 100) { 57 | $change = 0; 58 | $i++; 59 | 60 | // distribute the PR of each page 61 | foreach ($linkGraph as $node => $outbound) { 62 | $outboundCount = count($outbound); 63 | foreach ($outbound as $link) { 64 | // case of unversionned dependency 65 | if (!isset($tempRank[$link])) { 66 | $tempRank[$link] = 0; 67 | } 68 | $tempRank[$link] += $pageRank[$node] / $outboundCount; 69 | } 70 | } 71 | 72 | $total = 0; 73 | // calculate the new PR using the damping factor 74 | foreach ($linkGraph as $node => $outbound) { 75 | $tempRank[$node] = ($dampingFactor / $nodeCount) 76 | + (1 - $dampingFactor) * $tempRank[$node]; 77 | $change += abs($pageRank[$node] - $tempRank[$node]); 78 | $pageRank[$node] = $tempRank[$node]; 79 | $tempRank[$node] = 0; 80 | $total += $pageRank[$node]; 81 | } 82 | 83 | // Normalise the page ranks so it's all a proportion 0-1 84 | foreach ($pageRank as $node => $score) { 85 | $pageRank[$node] /= $total; 86 | } 87 | } 88 | 89 | return $pageRank; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Packages/Composer/Composer.php: -------------------------------------------------------------------------------- 1 | config = $config; 27 | } 28 | 29 | /** 30 | * @param Metrics $metrics 31 | * @throws ConfigException 32 | */ 33 | public function calculate(Metrics $metrics) 34 | { 35 | if ($this->config->has('composer') && false === $this->config->get('composer')) { 36 | return; 37 | } 38 | 39 | $projectMetric = new ProjectMetric('composer'); 40 | $projectMetric->set('packages', []); 41 | $metrics->attach($projectMetric); 42 | $packages = []; 43 | $rawRequirements = $this->getComposerJsonRequirements(); 44 | $rawInstalled = $this->getComposerLockInstalled(\array_keys($rawRequirements)); 45 | 46 | $packagist = new Packagist(); 47 | foreach ($rawRequirements as $requirement => $version) { 48 | $installed = isset($rawInstalled[$requirement]) ? $rawInstalled[$requirement] : null; 49 | $package = $packagist->get($requirement); 50 | 51 | $package->installed = $installed; 52 | $package->required = $version; 53 | $package->name = $requirement; 54 | // Manage case where the package is not hosted on packagist (private repository) so we can't know the status 55 | if ($installed === null || $package->latest === null) { 56 | $package->status = 'unknown'; 57 | } else { 58 | $package->status = version_compare($installed, $package->latest, '<') ? 'outdated' : 'latest'; 59 | } 60 | $packages[$requirement] = $package; 61 | } 62 | 63 | // exclude extensions 64 | $packages = array_filter($packages, function ($package) { 65 | return !preg_match('!(^php$|^ext\-)!', $package->name); 66 | }); 67 | 68 | $projectMetric->set('packages', $packages); 69 | $projectMetric->set('packages-installed', $rawInstalled); 70 | } 71 | 72 | /** 73 | * Returns the requirements defined in the composer(-dist)?.json file. 74 | * @return array 75 | */ 76 | protected function getComposerJsonRequirements() 77 | { 78 | $rawRequirements = [[]]; 79 | 80 | // find composer.json files 81 | $exclude = $this->config->has('exclude') ? $this->config->get('exclude') : []; 82 | $finder = new Finder(['json'], $exclude); 83 | 84 | // include root dir by default 85 | $files = $this->config->has('files') ? $this->config->get('files') : []; 86 | $files = array_merge($files, ['./']); 87 | $files = $finder->fetch($files); 88 | 89 | foreach ($files as $filename) { 90 | if (!\preg_match('/composer(-dist)?\.json/', $filename)) { 91 | continue; 92 | } 93 | $composerJson = (object) \json_decode(\file_get_contents($filename)); 94 | 95 | if (!isset($composerJson->require)) { 96 | continue; 97 | } 98 | 99 | $rawRequirements[] = (array) $composerJson->require; 100 | } 101 | 102 | return \call_user_func_array('array_merge', $rawRequirements); 103 | } 104 | 105 | /** 106 | * Returns the installed packages from the composer.lock file. 107 | * @param array $rootPackageRequirements List of requirements to match installed packages only with requirements. 108 | * @return array 109 | */ 110 | protected function getComposerLockInstalled($rootPackageRequirements) 111 | { 112 | $rawInstalled = [[]]; 113 | 114 | // Find composer.lock file 115 | $exclude = $this->config->has('exclude') ? $this->config->get('exclude') : []; 116 | $finder = new Finder(['lock'], $exclude); 117 | 118 | // include root dir by default 119 | $files = $this->config->has('files') ? $this->config->get('files') : []; 120 | $files = array_merge($files, ['./']); 121 | $files = $finder->fetch($files); 122 | 123 | // List all composer.lock found in the project. 124 | foreach ($files as $filename) { 125 | if (false === \strpos($filename, 'composer.lock')) { 126 | continue; 127 | } 128 | $composerLockJson = (object) \json_decode(\file_get_contents($filename)); 129 | 130 | if (!isset($composerLockJson->packages)) { 131 | continue; 132 | } 133 | 134 | $installed = []; 135 | foreach ($composerLockJson->packages as $package) { 136 | if (!\in_array($package->name, $rootPackageRequirements, true)) { 137 | continue; 138 | } 139 | 140 | $installed[$package->name] = \preg_replace('#[^.\d]#', '', $package->version); 141 | } 142 | 143 | $rawInstalled[] = $installed; 144 | } 145 | 146 | return \call_user_func_array('array_merge', $rawInstalled); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Packages/Composer/Packagist.php: -------------------------------------------------------------------------------- 1 | latest = null; 18 | $response->license = []; 19 | $response->homepage = null; 20 | $response->description = null; 21 | $response->time = null; 22 | $response->zip = null; 23 | $response->compare = null; 24 | $response->type = 'unknown'; 25 | $response->github_stars = 0; 26 | $response->github_watchers = 0; 27 | $response->github_forks = 0; 28 | $response->github_open_issues = 0; 29 | $response->download_total = 0; 30 | $response->download_monthly = 0; 31 | $response->download_daily = 0; 32 | $response->favers = 0; 33 | 34 | if (!preg_match('/\w+\/\w+/', $package)) { 35 | return $response; 36 | } 37 | list($user, $name) = explode('/', $package); 38 | $uri = sprintf('https://packagist.org/packages/%s/%s.json', $user, $name); 39 | $json = $this->getURIContentAsJson($uri); 40 | 41 | if (!isset($json->package) || !is_object($json->package)) { 42 | return $response; 43 | } 44 | 45 | $response->type = $json->package->type; 46 | $response->description = $json->package->description; 47 | $response->type = $json->package->type; 48 | $response->github_stars = $json->package->github_stars; 49 | $response->github_watchers = $json->package->github_watchers; 50 | $response->github_forks = $json->package->github_forks; 51 | $response->github_open_issues = $json->package->github_open_issues; 52 | $response->download_total = $json->package->downloads->total; 53 | $response->download_monthly = $json->package->downloads->monthly; 54 | $response->download_daily = $json->package->downloads->daily; 55 | $response->favers = $json->package->favers; 56 | 57 | // get latest version 58 | $latest = '0.0.0'; 59 | foreach ((array) $json->package->versions as $version => $datas) { 60 | if ($version[0] === 'v') { 61 | $version = substr($version, 1); 62 | } 63 | if (!preg_match('#^[\.\d]+$#', $version)) { 64 | continue; 65 | } 66 | if ($compare = version_compare($version, $latest) == 1) { 67 | $latest = $version; 68 | $response->name = $package; 69 | $response->latest = $version; 70 | $response->license = (array) $datas->license; 71 | $response->homepage = $datas->homepage; 72 | $response->time = $datas->time; 73 | $response->zip = $datas->dist->url; 74 | $response->compare = $compare; 75 | } 76 | } 77 | 78 | return $response; 79 | } 80 | 81 | /** 82 | * Download the given URI and decode it as JSON. 83 | * 84 | * @param string $uri 85 | * 86 | * @return mixed 87 | */ 88 | private function getURIContentAsJson($uri) 89 | { 90 | // Get the environment variable. 91 | $httpsProxy = getenv('https_proxy'); 92 | $context = null; 93 | if ('' !== $httpsProxy) { 94 | // Create the context. 95 | $context = stream_context_create( 96 | [ 97 | 'http' => [ 98 | 'proxy' => str_replace(['http://', 'https://'], 'tcp://', $httpsProxy), 99 | 'request_fulluri' => true, 100 | ], 101 | ] 102 | ); 103 | } 104 | 105 | return json_decode(@file_get_contents($uri, false, $context)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Hal/Report/Cli/Reporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 29 | $this->output = $output; 30 | } 31 | 32 | public function generate(Metrics $metrics) 33 | { 34 | if ($this->config->has('quiet')) { 35 | return; 36 | } 37 | 38 | $this->output->write( 39 | (new SummaryWriter($metrics, new Consolidated($metrics), $this->config))->getReport() 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Hal/Report/Cli/SearchReporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 33 | $this->output = $output; 34 | } 35 | 36 | /** 37 | * @param Metrics $metrics 38 | */ 39 | public function generate(Metrics $metrics) 40 | { 41 | /** @var SearchMetric $searches */ 42 | $searches = $metrics->get('searches'); 43 | if (empty($searches)) { 44 | return; 45 | } 46 | 47 | foreach ($searches->all() as $name => $search) { 48 | 49 | if (!is_array($search)) { 50 | continue; 51 | } 52 | 53 | $this->displayCliReport($name, $search); 54 | } 55 | } 56 | 57 | private function displayCliReport($searchName, array $foundSearch) 58 | { 59 | $title = sprintf( 60 | 'Found %d occurrences for search "%s"', 61 | sizeof($foundSearch), 62 | $searchName 63 | ); 64 | 65 | $config = $this->config->get('searches')->get($searchName)->getConfig(); 66 | if (!empty($foundSearch) && !empty($config->failIfFound) && true === $config->failIfFound) { 67 | $title = sprintf( 68 | '[ERR] Found %d occurrences for search "%s"', 69 | sizeof($foundSearch), 70 | $searchName 71 | ); 72 | } 73 | 74 | $sampleToDisplay = 5; 75 | $this->output->writeln($title); 76 | 77 | $parts = array_slice($foundSearch, 0, $sampleToDisplay); 78 | foreach ($parts as $part) { 79 | $this->output->writeln(sprintf('- %s', $part->getName())); 80 | } 81 | 82 | if (sizeof($foundSearch) > $sampleToDisplay) { 83 | $this->output->writeln(sprintf('... and %d more', sizeof($foundSearch) - $sampleToDisplay)); 84 | } 85 | 86 | $this->output->writeln(PHP_EOL); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Hal/Report/Cli/SummaryWriter.php: -------------------------------------------------------------------------------- 1 | sum->loc} 14 | Logical lines of code {$this->sum->lloc} 15 | Comment lines of code {$this->sum->cloc} 16 | Average volume {$this->avg->volume} 17 | Average comment weight {$this->avg->commentWeight} 18 | Average intelligent content {$this->avg->commentWeight} 19 | Logical lines of code by class {$this->locByClass} 20 | Logical lines of code by method {$this->locByMethod} 21 | Object oriented programming 22 | Classes {$this->sum->nbClasses} 23 | Interface {$this->sum->nbInterfaces} 24 | Methods {$this->sum->nbMethods} 25 | Methods by class {$this->methodsByClass} 26 | Lack of cohesion of methods {$this->avg->lcom} 27 | 28 | Coupling 29 | Average afferent coupling {$this->avg->afferentCoupling} 30 | Average efferent coupling {$this->avg->efferentCoupling} 31 | Average instability {$this->avg->instability} 32 | Depth of Inheritance Tree {$this->treeInheritenceDepth} 33 | 34 | Package 35 | Packages {$this->sum->nbPackages} 36 | Average classes per package {$this->avg->classesPerPackage} 37 | Average distance {$this->avg->distance} 38 | Average incoming class dependencies {$this->avg->incomingCDep} 39 | Average outgoing class dependencies {$this->avg->outgoingCDep} 40 | Average incoming package dependencies {$this->avg->incomingPDep} 41 | Average outgoing package dependencies {$this->avg->outgoingPDep} 42 | Complexity 43 | Average Cyclomatic complexity by class {$this->avg->ccn} 44 | Average Weighted method count by class {$this->avg->wmc} 45 | Average Relative system complexity {$this->avg->relativeSystemComplexity} 46 | Average Difficulty {$this->avg->difficulty} 47 | 48 | Bugs 49 | Average bugs by class {$this->avg->bugs} 50 | Average defects by class (Kan) {$this->avg->kanDefect} 51 | Violations 52 | Critical {$this->sum->violations->critical} 53 | Error {$this->sum->violations->error} 54 | Warning {$this->sum->violations->warning} 55 | Information {$this->sum->violations->information} 56 | EOT; 57 | 58 | // git 59 | if ($this->config->has('git')) { 60 | $commits = []; 61 | foreach ($this->consolidated->getFiles() as $name => $file) { 62 | $commits[$name] = $file['gitChanges']; 63 | } 64 | arsort($commits); 65 | $commits = array_slice($commits, 0, 10); 66 | 67 | $out .= "\nTop 10 committed files"; 68 | foreach ($commits as $file => $nb) { 69 | $out .= sprintf("\n %d %s", $nb, $file); 70 | } 71 | if (0 === count($commits)) { 72 | $out .= "\n NA"; 73 | } 74 | $out .= "\n"; 75 | } 76 | 77 | // Junit 78 | if ($this->config->has('junit')) { 79 | $out .= <<metrics->get('unitTesting')->get('nbSuites')} 83 | Classes called by tests {$this->metrics->get('unitTesting')->get('nbCoveredClasses')} 84 | Classes called by tests (percent) {$this->metrics->get('unitTesting')->get('percentCoveredClasses')} % 85 | EOT; 86 | } 87 | 88 | $out .= "\n\n"; 89 | 90 | return $out; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Hal/Report/Csv/Reporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | $this->output = $output; 31 | } 32 | 33 | 34 | public function generate(Metrics $metrics) 35 | { 36 | if ($this->config->has('quiet')) { 37 | return; 38 | } 39 | 40 | $logFile = $this->config->get('report-csv'); 41 | if (!$logFile) { 42 | return; 43 | } 44 | if (!file_exists(dirname($logFile)) || !is_writable(dirname($logFile))) { 45 | throw new \RuntimeException('You don\'t have permissions to write CSV report in ' . $logFile); 46 | } 47 | 48 | $availables = (new Registry())->allForStructures(); 49 | $hwnd = fopen($logFile, 'w'); 50 | fputcsv($hwnd, $availables); 51 | 52 | foreach ($metrics->all() as $metric) { 53 | if (!$metric instanceof ClassMetric) { 54 | continue; 55 | } 56 | $row = []; 57 | foreach ($availables as $key) { 58 | $data = $metric->get($key); 59 | if (is_array($data) || !is_scalar($data)) { 60 | $data = 'N/A'; 61 | } 62 | 63 | array_push($row, $data); 64 | } 65 | fputcsv($hwnd, $row); 66 | } 67 | 68 | fclose($hwnd); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Hal/Report/Json/Reporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 28 | $this->output = $output; 29 | } 30 | 31 | 32 | public function generate(Metrics $metrics) 33 | { 34 | if ($this->config->has('quiet')) { 35 | return; 36 | } 37 | 38 | $logFile = $this->config->get('report-json'); 39 | if (!$logFile) { 40 | return; 41 | } 42 | if (!file_exists(dirname($logFile)) || !is_writable(dirname($logFile))) { 43 | throw new \RuntimeException('You don\'t have permissions to write JSON report in ' . $logFile); 44 | } 45 | 46 | file_put_contents($logFile, json_encode($metrics, JSON_PRETTY_PRINT)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Hal/Report/Json/SummaryReporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | $this->output = $output; 31 | } 32 | 33 | public function generate(Metrics $metrics) 34 | { 35 | if ($this->config->has('quiet')) { 36 | return; 37 | } 38 | 39 | $logFile = $this->config->get('report-summary-json'); 40 | if (!$logFile) { 41 | return; 42 | } 43 | if (!file_exists(dirname($logFile)) || !is_writable(dirname($logFile))) { 44 | throw new \RuntimeException('You don\'t have permissions to write JSON report in ' . $logFile); 45 | } 46 | 47 | $summaryWriter = new SummaryWriter($metrics, new Consolidated($metrics), $this->config); 48 | file_put_contents($logFile, json_encode($summaryWriter->getReport(), JSON_PRETTY_PRINT)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Hal/Report/Json/SummaryWriter.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'linesOfCode' => $this->sum->loc, 14 | 'logicalLinesOfCode' => $this->sum->lloc, 15 | 'commentLinesOfCode' => $this->sum->cloc, 16 | 'avgVolume' => $this->avg->volume, 17 | 'avgCommentWeight' => $this->avg->commentWeight, 18 | 'avgIntelligentContent' => $this->avg->commentWeight, 19 | 'logicalLinesByClass' => $this->locByClass, 20 | 'logicalLinesByMethod' => $this->locByMethod, 21 | ], 22 | 'OOP' => [ 23 | 'classes' => $this->sum->nbClasses, 24 | 'interface' => $this->sum->nbInterfaces, 25 | 'methods' => $this->sum->nbMethods, 26 | 'methodsByClass' => $this->methodsByClass, 27 | 'lackCohesionOfMethods' => $this->avg->lcom, 28 | ], 29 | 'Coupling' => [ 30 | 'avgAfferentCoupling' => $this->avg->afferentCoupling, 31 | 'avgEfferentCoupling' => $this->avg->efferentCoupling, 32 | 'avgInstability' => $this->avg->instability, 33 | 'inheritanceTreeDepth' => $this->treeInheritenceDepth, 34 | ], 35 | 'Package' => [ 36 | 'packages' => $this->sum->nbPackages, 37 | 'acgClassesPerPackage' => $this->avg->classesPerPackage, 38 | 'avgDistance' => $this->avg->distance, 39 | 'avgIncomingClassDependencies' => $this->avg->incomingCDep, 40 | 'avgOutgoingClassDependencies' => $this->avg->outgoingCDep, 41 | 'avgIncomingPackageDependencies' => $this->avg->incomingPDep, 42 | 'avgOutgoingPackageDependencies' => $this->avg->outgoingPDep, 43 | ], 44 | 'Complexity' => [ 45 | 'avgCyclomaticComplexityByClass' => $this->avg->ccn, 46 | 'avgWeightedMethodCountByClass' => $this->avg->wmc, 47 | 'avgRelativeSystemComplexity' => $this->avg->relativeSystemComplexity, 48 | 'avgDifficulty' => $this->avg->difficulty, 49 | ], 50 | 'Bugs' => [ 51 | 'avgBugsByClass' => $this->avg->bugs, 52 | 'avgDefectsByClass' => $this->avg->kanDefect, 53 | ], 54 | 'Violations' => [ 55 | 'critical' => $this->sum->violations->critical, 56 | 'error' => $this->sum->violations->error, 57 | 'warning' => $this->sum->violations->warning, 58 | 'information' => $this->sum->violations->information, 59 | ], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Hal/Report/SummaryProvider.php: -------------------------------------------------------------------------------- 1 | consolidated = $consolidated; 59 | $this->config = $config; 60 | $this->metrics = $metrics; 61 | $this->sum = $consolidated->getSum(); 62 | $this->avg = $consolidated->getAvg(); 63 | 64 | // grouping results 65 | if ($this->sum->nbClasses > 0) { 66 | $this->methodsByClass = round($this->sum->nbMethods / $this->sum->nbClasses, 2); 67 | $this->locByClass = round($this->sum->lloc / $this->sum->nbClasses); 68 | } 69 | if ($this->sum->nbMethods > 0) { 70 | $this->locByMethod = round($this->sum->lloc / $this->sum->nbMethods); 71 | } 72 | 73 | $this->treeInheritenceDepth = $metrics->get('tree')->get('depthOfInheritanceTree'); 74 | } 75 | 76 | abstract public function getReport(); 77 | } 78 | -------------------------------------------------------------------------------- /src/Hal/Report/Violations/Xml/Reporter.php: -------------------------------------------------------------------------------- 1 | config = $config; 29 | $this->output = $output; 30 | } 31 | 32 | 33 | public function generate(Metrics $metrics) 34 | { 35 | if (!class_exists('\DOMDocument')) { 36 | $this->output->writeln('The DOM extension is not available. Please install it if you want to use the Xml Violations report.'); 37 | return; 38 | } 39 | 40 | $logFile = $this->config->get('report-violations'); 41 | if (!$logFile) { 42 | return; 43 | } 44 | 45 | // map of levels 46 | $map = [ 47 | Violation::CRITICAL => 1, 48 | Violation::ERROR => 2, 49 | Violation::WARNING => 3, 50 | Violation::INFO => 4, 51 | ]; 52 | 53 | // root 54 | $xml = new \DOMDocument("1.0", "UTF-8"); 55 | $xml->formatOutput = true; 56 | $root = $xml->createElement("pmd"); 57 | $root->setAttribute('version', '@package_version@'); 58 | $root->setAttribute('timestamp', date('c')); 59 | 60 | foreach ($metrics->all() as $metric) { 61 | $violations = $metric->get('violations'); 62 | if (null === $violations || count($violations) == 0) { 63 | continue; 64 | } 65 | 66 | $node = $xml->createElement('file'); 67 | $node->setAttribute('name', $metric->get('name')); 68 | 69 | foreach ($violations as $violation) { 70 | $item = $xml->createElement('violation'); 71 | $item->setAttribute('beginline', 1); 72 | $item->setAttribute('rule', $violation->getName()); 73 | $item->setAttribute('ruleset', $violation->getName()); 74 | $item->setAttribute('externalInfoUrl', 'http://www.phpmetrics.org/documentation/index.html'); 75 | $item->setAttribute('priority', $map[$violation->getLevel()]); 76 | $item->nodeValue = $violation->getDescription(); 77 | $node->appendChild($item); 78 | } 79 | 80 | $root->appendChild($node); 81 | } 82 | 83 | $xml->appendChild($root); 84 | 85 | // save file 86 | file_exists(dirname($logFile)) || mkdir(dirname($logFile), 0755, true); 87 | file_put_contents($logFile, $xml->saveXML()); 88 | 89 | $this->output->writeln(sprintf('XML report generated in "%s"', $logFile)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Hal/Search/PatternSearcher.php: -------------------------------------------------------------------------------- 1 | all() as $metric) { 13 | if (!$search->matches($metric)) { 14 | continue; 15 | } 16 | 17 | $matched = []; 18 | if ($metric->has('matched-searches')) { 19 | $matched[] = $metric->get('matched-searches'); 20 | } 21 | $matched[] = $search->getName(); 22 | $metric->set('matched-searches', $matched); 23 | 24 | 25 | $found[] = $metric; 26 | } 27 | 28 | return $found; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Hal/Search/Searches.php: -------------------------------------------------------------------------------- 1 | searches[$search->getName()] = $search; 19 | return $this; 20 | } 21 | 22 | /** 23 | * @param $name 24 | * @return Search|null 25 | */ 26 | public function get($name) 27 | { 28 | return $this->has($name) ? $this->searches[$name] : null; 29 | } 30 | 31 | /** 32 | * @param $name 33 | * @return bool 34 | */ 35 | public function has($name) 36 | { 37 | return isset($this->searches[$name]); 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function all() 44 | { 45 | return $this->searches; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Hal/Search/SearchesFactory.php: -------------------------------------------------------------------------------- 1 | $search) { 16 | $searches->add(new Search($name, $search)); 17 | } 18 | 19 | return $searches; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Hal/Search/SearchesValidator.php: -------------------------------------------------------------------------------- 1 | all() as $search) { 13 | $config = $search->getConfig(); 14 | 15 | $allowedKeys = [ 16 | 'type', 17 | 'nameMatches', 18 | 'instanceOf', 19 | 'usesClasses', 20 | 'failIfFound', 21 | ]; 22 | $registry = new Registry(); 23 | $allowedKeys = array_merge($allowedKeys, $registry->allForStructures()); 24 | 25 | $diff = array_diff(array_keys((array) $config), $allowedKeys); 26 | if (count($diff) > 0) { 27 | throw new ConfigException( 28 | sprintf( 29 | 'Invalid config for search "%s". Allowed keys are {%s}', 30 | $search->getName(), 31 | implode(', ', $allowedKeys) 32 | ) 33 | ); 34 | } 35 | 36 | if (isset($config->type) && !in_array($config->type, ['class', 'interface'])) { 37 | throw new ConfigException('Invalid config for "type". Should be "class" or "interface"'); 38 | } 39 | 40 | if (isset($config->nameMatches) && !is_string($config->nameMatches)) { 41 | throw new ConfigException('Invalid config for "nameMatches". Should be a regex'); 42 | } 43 | 44 | if (isset($config->instanceOf) && !is_array($config->instanceOf)) { 45 | throw new ConfigException('Invalid config for "instanceOf". Should be an array of classnames'); 46 | } 47 | 48 | // usesMatches 49 | if (isset($config->usesClasses) && !is_array($config->usesClasses)) { 50 | throw new ConfigException('Invalid config for "usesClasses". Should be a, array of classnames or regexes matching classnames'); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/Blob.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 29 | 30 | $suspect = 0; 31 | if ($metric->get('nbMethodsPublic') >= 8) { 32 | $suspect++; 33 | } 34 | 35 | if ($metric->get('lcom') >= 3) { 36 | $suspect++; 37 | } 38 | 39 | if (count($metric->get('externals')) >= 8) { 40 | $suspect++; 41 | } 42 | 43 | if ($suspect >= 3) { 44 | $metric->get('violations')->add($this); 45 | } 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function getLevel() 52 | { 53 | return Violation::ERROR; 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function getDescription() 60 | { 61 | return <<metric->get('nbMethodsPublic')}, excluding getters and setters) 65 | * object has a high Lack of cohesion of methods (LCOM={$this->metric->get('lcom')}) 66 | * object knows everything (and use lot of external classes) 67 | 68 | Maybe you should reducing the number of methods splitting this object in many sub objects. 69 | EOT; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/ProbablyBugged.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 29 | 30 | $suspect = 0; 31 | if ($metric->get('bugs') >= .35) { 32 | $metric->get('violations')->add($this); 33 | return; 34 | } 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function getLevel() 41 | { 42 | return Violation::WARNING; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getDescription() 49 | { 50 | return <<metric->get('bugs')} bugs. 52 | 53 | * Calculation is based on number of operators, operands, cyclomatic complexity 54 | * See more details at https://en.wikipedia.org/wiki/Halstead_complexity_measures 55 | * {$this->metric->get('numberOfUnitTests')} testsuites has dependency to this class. 56 | 57 | Maybe you should check your unit tests for this class. 58 | EOT; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/TooComplexClassCode.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 32 | 33 | if ($metric->get('wmc') > 50) { 34 | $metric->get('violations')->add($this); 35 | } 36 | } 37 | 38 | public function getLevel() 39 | { 40 | return Violation::ERROR; 41 | } 42 | 43 | public function getDescription() 44 | { 45 | return <<metric->get('ccn')}) 49 | * Component uses {$this->metric->get('number_operators')} operators 50 | 51 | Maybe you should delegate some code to other objects. 52 | EOT; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/TooComplexMethodCode.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 34 | 35 | if ($metric->get('ccnMethodMax') > 10) { 36 | $metric->get('violations')->add($this); 37 | return; 38 | } 39 | } 40 | 41 | public function getLevel() 42 | { 43 | return Violation::ERROR; 44 | } 45 | 46 | public function getDescription() 47 | { 48 | return <<metric->get('ccnMethodMax')}) 52 | 53 | Maybe you should delegate some code to other objects or split complex method. 54 | EOT; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/TooDependent.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 29 | 30 | if ($metric->get('efferentCoupling') >= 20) { 31 | $metric->get('violations')->add($this); 32 | return; 33 | } 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | public function getLevel() 40 | { 41 | return Violation::INFO; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function getDescription() 48 | { 49 | return <<metric->get('efferentCoupling')}, so this class uses {$this->metric->get('efferentCoupling')} different external components. 53 | 54 | Maybe you should check why this class has lot of dependencies. 55 | EOT; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/TooLong.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 29 | 30 | if ($metric->get('lloc') >= 200) { 31 | $metric->get('violations')->add($this); 32 | } 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | public function getLevel() 39 | { 40 | return Violation::INFO; 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function getDescription() 47 | { 48 | return <<metric->get('lloc')} logical lines of code 52 | * Class has {$this->metric->get('loc')} lines of code 53 | 54 | Maybe your class should not exceed 200 lines of logical code 55 | EOT; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Hal/Violation/Package/StableAbstractionsPrinciple.php: -------------------------------------------------------------------------------- 1 | getDistance()) > sqrt(2) / 4) { 25 | $this->metric = $metric; 26 | $metric->get('violations')->add($this); 27 | } 28 | } 29 | 30 | public function getLevel() 31 | { 32 | return Violation::WARNING; 33 | } 34 | 35 | public function getDescription() 36 | { 37 | $violation = $this->metric->getDistance() > 0 38 | ? 'instable and abstract' 39 | : 'stable and concrete'; 40 | 41 | return <<getInstability(); 28 | $violatingInstabilities = array_filter( 29 | $metric->getDependentInstabilities(), 30 | function ($otherInstability) use ($instability) { 31 | return $otherInstability >= $instability; 32 | } 33 | ); 34 | if (count($violatingInstabilities) > 0) { 35 | $this->violatingInstabilities = $violatingInstabilities; 36 | $this->metric = $metric; 37 | $metric->get('violations')->add($this); 38 | } 39 | } 40 | 41 | public function getLevel() 42 | { 43 | return Violation::WARNING; 44 | } 45 | 46 | public function getDescription() 47 | { 48 | $count = count($this->violatingInstabilities); 49 | $thisInstability = round($this->metric->getInstability(), 3); 50 | $packages = implode("\n* ", array_map(function ($name, $instability) { 51 | $name = $name === '\\' ? 'global' : substr($name, 0, -1); 52 | $instability = round($instability, 3); 53 | return "$name ($instability)"; 54 | }, array_keys($this->violatingInstabilities), $this->violatingInstabilities)); 55 | return <<concernedSearches); 15 | } 16 | 17 | public function apply(Metric $metric) 18 | { 19 | if ($metric->has('was-not-expected') && $metric->get('was-not-expected')) { 20 | $this->concernedSearches = array_unique( 21 | array_merge( 22 | $this->concernedSearches, 23 | $metric->get('was-not-expected-by') 24 | ) 25 | ); 26 | $metric->get('violations')->add($this); 27 | } 28 | } 29 | 30 | public function getLevel() 31 | { 32 | return Violation::CRITICAL; 33 | } 34 | 35 | public function getDescription() 36 | { 37 | return 'According configuration, this component is not expected to be found in the code.'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Hal/Violation/Violation.php: -------------------------------------------------------------------------------- 1 | all() as $metric) { 31 | $metric->set('violations', new Violations()); 32 | 33 | foreach ($violations as $violation) { 34 | $violation->apply($metric); 35 | } 36 | } 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Hal/Violation/Violations.php: -------------------------------------------------------------------------------- 1 | data); 22 | } 23 | 24 | /** 25 | * @param Violation $violation 26 | */ 27 | public function add(Violation $violation) 28 | { 29 | $this->data[] = clone $violation; 30 | } 31 | 32 | /** 33 | * @return int 34 | */ 35 | #[\ReturnTypeWillChange] 36 | public function count() 37 | { 38 | return count($this->data); 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function __toString() 45 | { 46 | $string = ''; 47 | foreach ($this->data as $violation) { 48 | $string .= $violation->getName() . ','; 49 | } 50 | return $string; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /templates/html_report/_header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PhpMetrics report 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 33 |
34 | 35 | 47 | 48 | 49 |
50 |
51 |
52 | Created at 53 | , with PHPMetrics (Jean-François Lépine). 54 |
55 | 56 | groups) {?> 57 |
58 |
    59 |
  • All
  • 60 | groups as $group) { ?> 61 |
  • 62 | getName();?> 63 |
  • 64 | 65 |
66 |
67 | 68 | -------------------------------------------------------------------------------- /templates/html_report/all.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | $v) { ?> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | $v) { ?> 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /templates/html_report/complexity.php: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 |
6 |
7 |
Average weighted method count by class (CC)
8 |
9 | wmc; ?> 10 |
11 | getTrend('avg', 'wmc', true); ?> 12 |
13 |
14 |
15 |
16 |
Average cyclomatic complexity by class
17 |
18 | ccn; ?> 19 |
20 | getTrend('avg', 'ccn', true); ?> 21 |
22 |
23 |
24 |
25 |
Average relative System complexity
26 |
27 | relativeSystemComplexity; ?> 28 |
29 | getTrend('avg', 'relativeSystemComplexity', true); ?> 30 |
31 |
32 |
33 |
34 |
Average bugs by class(Halstead)
35 |
36 | bugs; ?> 37 |
38 | getTrend('avg', 'bugs', true); ?> 39 |
40 |
41 |
42 |
43 |
average defects by class (Kan)
44 |
45 | kanDefect; ?> 46 |
47 | getTrend('avg', 'kanDefect', true); ?> 48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | has('junit')) { ?> 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 82 | 83 | has('junit')) { ?> 84 | 85 | 86 | 87 | 88 |
ClassWMCClass cycl.Max method cycl.Relative system complexityRelative data complexityRelative structural complexityBugsDefectsUnit testsuites calling it
78 | > 79 | 80 | 81 |
89 |
90 |
91 |
92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /templates/html_report/coupling.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |

Coupling

7 | 8 |
9 | Afferent coupling (AC) is the number of classes affected by given class. 10 |
Efferent coupling (EC) is the number of classes from which given class receives 11 | effects. 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 |
ClassAfferent couplingEfferent couplingInstabilityClassRank
29 | > 30 | 31 | 32 |
38 | 39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /templates/html_report/css/clusterize.css: -------------------------------------------------------------------------------- 1 | /* max-height - the only parameter in this file that needs to be edited. 2 | * Change it to suit your needs. The rest is recommended to leave as is. 3 | */ 4 | .clusterize-scroll{ 5 | max-height: 200px; 6 | overflow: auto; 7 | } 8 | 9 | /** 10 | * Avoid vertical margins for extra tags 11 | * Necessary for correct calculations when rows have nonzero vertical margins 12 | */ 13 | .clusterize-extra-row{ 14 | margin-top: 0 !important; 15 | margin-bottom: 0 !important; 16 | } 17 | 18 | /* By default extra tag .clusterize-keep-parity added to keep parity of rows. 19 | * Useful when used :nth-child(even/odd) 20 | */ 21 | .clusterize-extra-row.clusterize-keep-parity{ 22 | display: none; 23 | } 24 | 25 | /* During initialization clusterize adds tabindex to force the browser to keep focus 26 | * on the scrolling list, see issue #11 27 | * Outline removes default browser's borders for focused elements. 28 | */ 29 | .clusterize-content{ 30 | outline: 0; 31 | } 32 | 33 | /* Centering message that appears when no data provided 34 | */ 35 | .clusterize-no-data td{ 36 | text-align: center; 37 | } -------------------------------------------------------------------------------- /templates/html_report/css/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(fonts/material-icons.ttf) format('truetype'); 6 | } 7 | 8 | .material-icons { 9 | font-family: 'Material Icons'; 10 | font-weight: normal; 11 | font-style: normal; 12 | font-size: 24px; 13 | line-height: 1; 14 | letter-spacing: normal; 15 | text-transform: none; 16 | display: inline-block; 17 | white-space: nowrap; 18 | word-wrap: normal; 19 | direction: ltr; 20 | } 21 | -------------------------------------------------------------------------------- /templates/html_report/css/roboto.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/roboto-light.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Roboto'; 9 | font-style: normal; 10 | font-weight: 700; 11 | src: local('Roboto Bold'), local('Roboto-Bold'), url(../fonts/roboto-bold.ttf) format('truetype'); 12 | } 13 | -------------------------------------------------------------------------------- /templates/html_report/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/favicon.ico -------------------------------------------------------------------------------- /templates/html_report/fonts/material-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/fonts/material-icons.ttf -------------------------------------------------------------------------------- /templates/html_report/fonts/roboto-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/fonts/roboto-bold.ttf -------------------------------------------------------------------------------- /templates/html_report/fonts/roboto-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/fonts/roboto-light.ttf -------------------------------------------------------------------------------- /templates/html_report/images/logo-git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/images/logo-git.png -------------------------------------------------------------------------------- /templates/html_report/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/images/logo.png -------------------------------------------------------------------------------- /templates/html_report/images/phpmetrics-maintenability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/a7b3b533469edf8413ff39aee32429cbaec9d1c4/templates/html_report/images/phpmetrics-maintenability.png -------------------------------------------------------------------------------- /templates/html_report/js/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"undefined"!=typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}});f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); 2 | 3 | //# sourceMappingURL=FileSaver.min.js.map -------------------------------------------------------------------------------- /templates/html_report/js/d3.hexbin.v0.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | d3.hexbin = function() { 4 | var width = 1, 5 | height = 1, 6 | r, 7 | x = d3_hexbinX, 8 | y = d3_hexbinY, 9 | dx, 10 | dy; 11 | 12 | function hexbin(points) { 13 | var binsById = {}; 14 | 15 | points.forEach(function(point, i) { 16 | var py = y.call(hexbin, point, i) / dy, pj = Math.round(py), 17 | px = x.call(hexbin, point, i) / dx - (pj & 1 ? .5 : 0), pi = Math.round(px), 18 | py1 = py - pj; 19 | 20 | if (Math.abs(py1) * 3 > 1) { 21 | var px1 = px - pi, 22 | pi2 = pi + (px < pi ? -1 : 1) / 2, 23 | pj2 = pj + (py < pj ? -1 : 1), 24 | px2 = px - pi2, 25 | py2 = py - pj2; 26 | if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; 27 | } 28 | 29 | var id = pi + "-" + pj, bin = binsById[id]; 30 | if (bin) bin.push(point); else { 31 | bin = binsById[id] = [point]; 32 | bin.i = pi; 33 | bin.j = pj; 34 | bin.x = (pi + (pj & 1 ? 1 / 2 : 0)) * dx; 35 | bin.y = pj * dy; 36 | } 37 | }); 38 | 39 | return d3.values(binsById); 40 | } 41 | 42 | function hexagon(radius) { 43 | var x0 = 0, y0 = 0; 44 | return d3_hexbinAngles.map(function(angle) { 45 | var x1 = Math.sin(angle) * radius, 46 | y1 = -Math.cos(angle) * radius, 47 | dx = x1 - x0, 48 | dy = y1 - y0; 49 | x0 = x1, y0 = y1; 50 | return [dx, dy]; 51 | }); 52 | } 53 | 54 | hexbin.x = function(_) { 55 | if (!arguments.length) return x; 56 | x = _; 57 | return hexbin; 58 | }; 59 | 60 | hexbin.y = function(_) { 61 | if (!arguments.length) return y; 62 | y = _; 63 | return hexbin; 64 | }; 65 | 66 | hexbin.hexagon = function(radius) { 67 | if (arguments.length < 1) radius = r; 68 | return "m" + hexagon(radius).join("l") + "z"; 69 | }; 70 | 71 | hexbin.centers = function() { 72 | var centers = []; 73 | for (var y = 0, odd = false, j = 0; y < height + r; y += dy, odd = !odd, ++j) { 74 | for (var x = odd ? dx / 2 : 0, i = 0; x < width + dx / 2; x += dx, ++i) { 75 | var center = [x, y]; 76 | center.i = i; 77 | center.j = j; 78 | centers.push(center); 79 | } 80 | } 81 | return centers; 82 | }; 83 | 84 | hexbin.mesh = function() { 85 | var fragment = hexagon(r).slice(0, 4).join("l"); 86 | return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join(""); 87 | }; 88 | 89 | hexbin.size = function(_) { 90 | if (!arguments.length) return [width, height]; 91 | width = +_[0], height = +_[1]; 92 | return hexbin; 93 | }; 94 | 95 | hexbin.radius = function(_) { 96 | if (!arguments.length) return r; 97 | r = +_; 98 | dx = r * 2 * Math.sin(Math.PI / 3); 99 | dy = r * 1.5; 100 | return hexbin; 101 | }; 102 | 103 | return hexbin.radius(1); 104 | }; 105 | 106 | var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3), 107 | d3_hexbinX = function(d) { return d[0]; }, 108 | d3_hexbinY = function(d) { return d[1]; }; 109 | 110 | })(); 111 | -------------------------------------------------------------------------------- /templates/html_report/js/functions.js: -------------------------------------------------------------------------------- 1 | function loadJSON(filename, callback) { 2 | 3 | var xobj = new XMLHttpRequest(); 4 | xobj.overrideMimeType("application/json"); 5 | xobj.open('GET', filename, true); 6 | xobj.onreadystatechange = function () { 7 | if (xobj.readyState == 4 && xobj.status == "200") { 8 | // Required use of an anonymous callback as .open will NOT return a value but simply returns undefined in asynchronous mode 9 | callback(xobj.responseText); 10 | } 11 | }; 12 | xobj.send(null); 13 | } 14 | 15 | 16 | function equalsHeightOf(node1, node2) { 17 | var w1 = node1.style.height; 18 | node2.style.height = w1 + 'px'; 19 | } 20 | 21 | function saveSvgAsImage(svg, name, width, height) { 22 | width = width || 600; 23 | height = height || 600; 24 | var img = new Image(), 25 | serializer = new XMLSerializer(), 26 | svgStr = serializer.serializeToString(svg); 27 | 28 | img.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr); 29 | var canvas = document.createElement("canvas"); 30 | document.body.appendChild(canvas); 31 | canvas.width = width; 32 | canvas.height = height; 33 | img.onload = function () { 34 | canvas.getContext("2d").drawImage(img,0,0, width, height); 35 | canvas.toBlob(function (blob) { 36 | saveAs(blob, name + ".png"); 37 | }); 38 | }; 39 | canvas.parentNode.removeChild(canvas); 40 | } 41 | -------------------------------------------------------------------------------- /templates/html_report/js/graph-licenses.js: -------------------------------------------------------------------------------- 1 | function chartLicenses(json) { 2 | 3 | var diameter = document.getElementById('svg-licenses').offsetWidth; 4 | 5 | var svg = d3.select('#svg-licenses').append('svg'), 6 | width = 300,//document.getElementById('svg-licenses').offsetWidth, 7 | height = 300,//document.getElementById('svg-licenses').offsetWidth, 8 | radius = Math.min(width, height) / 2; 9 | 10 | //var r = 300; // outer radius 11 | 12 | var color = d3.scale.ordinal() 13 | .range(["#BBDEFB", "#90CAF9", "#64B5F6", "#42A5F5", "#2196F3", "#1E88E5", "#1976D2", "#1565C0", "#0D47A1"]); 14 | 15 | svg 16 | .attr("width", width) 17 | .attr("height", height); 18 | 19 | var group = svg.append("g") 20 | .attr("transform", "translate(" + Math.ceil(width / 2) + ", " + Math.ceil(height / 2) + ")"); // set center of pie 21 | 22 | var arc = d3.svg.arc() 23 | .innerRadius(radius - 10) 24 | .outerRadius(0); 25 | 26 | var pie = d3.layout.pie() 27 | .value(function (d) { 28 | return d.value; 29 | }); 30 | 31 | var arcs = group.selectAll(".arc") 32 | .data(pie(json)) 33 | .enter() 34 | .append("g") 35 | .attr("class", "arc"); 36 | 37 | arcs.append("path") 38 | .attr("d", arc) // here the arc function works on every record d of data 39 | .attr("fill", function (d) { 40 | return color(d.data.value); 41 | }); 42 | 43 | arcs.append("text") 44 | .attr("transform", function (d) { 45 | return "translate(" + arc.centroid(d) + ")"; 46 | }) 47 | .attr("text-anchor", "middle") 48 | .attr('color', '#FFF') 49 | .text(function (d) { 50 | return d.data.name; 51 | }); 52 | } -------------------------------------------------------------------------------- /templates/html_report/js/graph-maintainability.js: -------------------------------------------------------------------------------- 1 | function chartMaintainability(withoutComment) { 2 | var chartId = 'svg-maintainability'; 3 | withoutComment = typeof (withoutComment) !== 'undefined' ? withoutComment : false; 4 | var diameter = document.getElementById(chartId).offsetWidth; 5 | 6 | var json = { 7 | name: 'chart', 8 | children: classes 9 | }; 10 | 11 | // if already loaded, removed previous node 12 | var previous = d3.select('#' + chartId).select('svg'); 13 | if (previous) { 14 | previous.remove(); 15 | } 16 | previous = d3.select('#' + chartId).select('button'); 17 | if (previous) { 18 | previous.remove(); 19 | } 20 | 21 | var svg = d3.select('#' + chartId).append('svg') 22 | .attr('width', diameter) 23 | .attr('height', diameter); 24 | 25 | var bubble = d3.layout.pack() 26 | .size([diameter, diameter]) 27 | .padding(3) 28 | .value(function (d) { 29 | return d.ccn; 30 | }); 31 | 32 | var nodes = bubble.nodes(json) 33 | .filter(function (d) { 34 | return !d.children; 35 | }); // filter out the outer bubble* 36 | 37 | var vis = svg.selectAll('circle') 38 | .data(nodes, function (d) { 39 | return d.name; 40 | }); 41 | 42 | vis.enter().append('circle') 43 | .attr('transform', function (d) { 44 | return 'translate(' + d.x + ',' + d.y + ')'; 45 | }) 46 | .attr('r', function (d) { 47 | return d.r; 48 | }) 49 | .style("fill", function (d) { 50 | if (true === withoutComment) { 51 | if (d.mIwoC > 65) { 52 | return '#8BC34A'; 53 | } else if (d.mIwoC > 53) { 54 | return '#FFC107'; 55 | } else { 56 | return '#F44336'; 57 | } 58 | } else { 59 | if (d.mi > 85) { 60 | return '#8BC34A'; 61 | } else if (d.mi > 69) { 62 | return '#FFC107'; 63 | } else { 64 | return '#F44336'; 65 | } 66 | } 67 | }) 68 | .on('mouseover', function (d) { 69 | var text = ''; 70 | if (true === withoutComment) { 71 | text = '' + d.name + '' 72 | + "
Cyclomatic Complexity : " + d.ccn 73 | + "
Maintainability Index (w/o comments): " + d.mIwoC; 74 | } else { 75 | text = '' + d.name + '' 76 | + "
Cyclomatic Complexity : " + d.ccn 77 | + "
Maintainability Index: " + d.mi; 78 | } 79 | d3.select('.tooltip').html(text); 80 | d3.select(".tooltip") 81 | .style("opacity", 1) 82 | .style("z-index", 1); 83 | }) 84 | .on('mousemove', function () { 85 | d3.select(".tooltip") 86 | .style("left", (d3.event.pageX + 5) + "px") 87 | .style("top", (d3.event.pageY + 5) + "px"); 88 | }) 89 | .on('mouseout', function () { 90 | d3.select(".tooltip") 91 | .style("opacity", 0) 92 | .style("z-index", -1); 93 | }); 94 | 95 | d3.select("body") 96 | .append("div") 97 | .attr("class", "tooltip") 98 | .style("opacity", 0); 99 | 100 | // button for saving image 101 | var button = d3.select('#' + chartId).append('button'); 102 | button 103 | .classed('btn-save-image', true) 104 | .text('download') 105 | .on('click', function () { 106 | var svg = d3.select('#' + chartId + ' svg')[0][0]; 107 | var nameImage = (withoutComment) 108 | ? 'PhpMetrics maintainability without comments / complexity' 109 | : 'PhpMetrics maintainability / complexity'; 110 | saveSvgAsImage(svg, nameImage, 1900, 1900); 111 | }); 112 | } 113 | 114 | function toggleChartMaintainability(item) { 115 | if (item.getAttribute('data-current') === 'with-comments') { 116 | item.setAttribute('data-current', 'without-comments'); 117 | item.innerHTML = '(without comments)'; 118 | } else { 119 | item.setAttribute('data-current', 'with-comments'); 120 | item.innerHTML = '(with comments)'; 121 | } 122 | 123 | chartMaintainability(item.getAttribute('data-current') !== 'with-comments') 124 | } 125 | -------------------------------------------------------------------------------- /templates/html_report/js/sort-table.min.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2006-2013 Tyler Uebele * Released under the MIT license. * latest at https://github.com/tyleruebele/sort-table * minified by Google Closure Compiler */ 2 | function sortTable(a,b,d){var c;sortTable.sortCol=-1;c=a.className.match(/js-sort-\d+/);null!=c&&(sortTable.sortCol=c[0].replace(/js-sort-/,""),a.className=a.className.replace(RegExp(" ?"+c[0]+"\\b"),""));"undefined"===typeof b&&(b=sortTable.sortCol);"undefined"!==typeof d?sortTable.sortDir=-1==d||"desc"==d?-1:1:(c=a.className.match(/js-sort-(a|de)sc/),sortTable.sortDir=null!=c&&sortTable.sortCol==b?"js-sort-asc"==c[0]?-1:1:1);a.className=a.className.replace(/ ?js-sort-(a|de)sc/g,"");a.className+= 3 | " js-sort-"+b;sortTable.sortCol=b;a.className+=" js-sort-"+(-1==sortTable.sortDir?"desc":"asc");bc?1:-1)}; 5 | sortTable.stripTags=function(a){return a.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi,"")};sortTable.date=function(a){return new Date(sortTable.stripTags(a.innerHTML))};sortTable.number=function(a){return Number(sortTable.stripTags(a.innerHTML).replace(/[^-\d.]/g,""))};sortTable.string=function(a){return sortTable.stripTags(a.innerHTML).toLowerCase()};sortTable.last=function(a){return sortTable.stripTags(a.innerHTML).split(" ").pop().toLowerCase()}; 6 | sortTable.input=function(a){for(var b=0;b 2 | 3 | 4 | 1) { 17 | $range = range(0.5, 1, .05); 18 | foreach ($range as $percentile) { 19 | $json[] = (object)[ 20 | 'lloc' => percentile($array, $percentile), 21 | 'percentile' => round($percentile * 100), 22 | ]; 23 | } 24 | } 25 | 26 | ?> 27 | 28 | 29 |
30 |
31 |
32 |

Percentile distribution of logical lines of code by class

33 |
34 |
Percentile
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |

Explore

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 |
ClassLLOCCLOCVolumeIntelligent contentComment Weight
60 | > 61 | 62 | 63 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 154 | -------------------------------------------------------------------------------- /templates/html_report/oop.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | 0) { 13 | $lcom = round(array_sum($lcom) / count($lcom), 2); 14 | } else { 15 | $lcom = 0; 16 | } 17 | ?> 18 | 19 | 20 |
21 |
22 |
23 |
classes getTrend('sum', 'nbClasses'); ?>
24 |
25 | nbClasses; ?> 26 | (nbClasses / count($classes) * 100) : '0'); ?> %) 27 |
28 |
29 |
30 |
31 |
32 |
interfaces getTrend('sum', 'nbInterfaces'); ?>
33 |
nbInterfaces; ?> 34 | (nbInterfaces / count($classes) * 100) : '0'); ?> %) 35 |
36 |
37 |
38 |
39 |
40 |
average LCOM getTrend('avg', 'lcom', true); ?>
41 |
42 |
43 |
44 |
45 |
46 |
logical lines of code by class
47 |
nbClasses ? round($sum->lloc / $sum->nbClasses) : '-'; ?>
48 |
49 |
50 |
51 |
52 |
logical lines of code by method
53 |
nbMethods ? round($sum->lloc / $sum->nbMethods) : '-'; ?>
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 |
ClassLCOMVolumeClass cycl.Max method cycl.BugsDifficulty
80 | > 81 | 82 | 83 |
88 |
89 |
90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tooling/README.md: -------------------------------------------------------------------------------- 1 | # Tooling 2 | 3 | This folder contains external dependencies used for building and testing the project only. 4 | 5 | It allows avoiding conflicts with the main project dependencies and to keep the project clean. 6 | -------------------------------------------------------------------------------- /tooling/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpmetrics/tooling", 3 | "license": "MIT", 4 | "autoload": { 5 | "psr-4": { 6 | "Phpmetrics\\Tooling\\": "src/" 7 | } 8 | }, 9 | "authors": [ 10 | { 11 | "name": "Jean-François Lépine" 12 | } 13 | ], 14 | "require-dev": { 15 | "rector/rector": "^2.1" 16 | } 17 | } 18 | --------------------------------------------------------------------------------