├── .dockerignore ├── .semver ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── phpmetrics ├── composer.json ├── config-example.json ├── config.yml ├── docker └── releasing │ └── Dockerfile ├── 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 │ │ │ ├── Php5NodeTraverser.php │ │ │ ├── Php7NodeTraverser.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 │ │ │ ├── MetricClassNameGenerator.php │ │ │ └── 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 /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !releases/phpmetrics.phar 3 | -------------------------------------------------------------------------------- /.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:7.1 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 | "require": { 34 | "php": ">=5.5", 35 | "ext-dom": "*", 36 | "ext-tokenizer": "*", 37 | "nikic/php-parser": "^3|^4" 38 | }, 39 | "require-dev": { 40 | "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14", 41 | "sebastian/comparator": ">=1.2.3", 42 | "squizlabs/php_codesniffer": "^3.5", 43 | "symfony/dom-crawler": "^3.0 || ^4.0 || ^5.0" 44 | }, 45 | "bin": [ 46 | "bin/phpmetrics" 47 | ], 48 | "config": { 49 | "sort-packages": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/releasing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer/composer:2-bin AS composer 2 | 3 | # please do not use alpine here: We need a debian based image for .deb distribution 4 | FROM php:7.4-cli-bullseye 5 | 6 | # Installing ruby, semver and required dependencies 7 | RUN apt update \ 8 | && apt install -y ruby ruby-dev make build-essential git gnupg2 debhelper \ 9 | && gem install semver \ 10 | && echo "phar.readonly=0" > /usr/local/etc/php/conf.d/phar.ini 11 | 12 | RUN git config --global --add safe.directory /app 13 | 14 | # Installing composer 15 | ENV COMPOSER_ALLOW_SUPERUSER=1 16 | ENV PATH="${PATH}:/root/.composer/vendor/bin" 17 | COPY --from=composer /composer /usr/bin/composer 18 | -------------------------------------------------------------------------------- /src/Hal/Application/Analyze.php: -------------------------------------------------------------------------------- 1 | output = $output; 60 | $this->config = $config; 61 | $this->issuer = $issuer; 62 | } 63 | 64 | /** 65 | * Runs analyze 66 | */ 67 | public function run($files) 68 | { 69 | // config 70 | ini_set('xdebug.max_nesting_level', 3000); 71 | 72 | $metrics = new Metrics(); 73 | 74 | // traverse all 75 | $whenToStop = function () { 76 | return true; 77 | }; 78 | 79 | // prepare parser 80 | $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); 81 | $traverser = new NodeTraverser(false, $whenToStop); 82 | $traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver()); 83 | $traverser->addVisitor(new ClassEnumVisitor($metrics)); 84 | $traverser->addVisitor(new CyclomaticComplexityVisitor($metrics)); 85 | $traverser->addVisitor(new ExternalsVisitor($metrics)); 86 | $traverser->addVisitor(new LcomVisitor($metrics)); 87 | $traverser->addVisitor(new HalsteadVisitor($metrics)); 88 | $traverser->addVisitor(new LengthVisitor($metrics)); 89 | $traverser->addVisitor(new CyclomaticComplexityVisitor($metrics)); 90 | $traverser->addVisitor(new MaintainabilityIndexVisitor($metrics)); 91 | $traverser->addVisitor(new KanDefectVisitor($metrics)); 92 | $traverser->addVisitor(new SystemComplexityVisitor($metrics)); 93 | $traverser->addVisitor(new PackageCollectingVisitor($metrics)); 94 | 95 | // create a new progress bar (50 units) 96 | $progress = new ProgressBar($this->output, count($files)); 97 | $progress->start(); 98 | 99 | foreach ($files as $file) { 100 | $progress->advance(); 101 | $code = file_get_contents($file); 102 | $this->issuer->set('filename', $file); 103 | try { 104 | $stmts = $parser->parse($code); 105 | $this->issuer->set('statements', $stmts); 106 | $traverser->traverse($stmts); 107 | } catch (Error $e) { 108 | $this->output->writeln(sprintf('Cannot parse %s', $file)); 109 | } 110 | $this->issuer->clear('filename'); 111 | $this->issuer->clear('statements'); 112 | } 113 | 114 | $progress->clear(); 115 | 116 | $this->output->write('Executing system analyzes...'); 117 | 118 | // 119 | // System analyses 120 | (new PageRank())->calculate($metrics); 121 | (new Coupling())->calculate($metrics); 122 | (new DepthOfInheritanceTree())->calculate($metrics); 123 | 124 | // 125 | // Package analyses 126 | (new PackageDependencies())->calculate($metrics); 127 | (new PackageAbstraction())->calculate($metrics); 128 | (new PackageInstability())->calculate($metrics); 129 | (new PackageDistance())->calculate($metrics); 130 | 131 | // 132 | // File analyses 133 | (new GitChanges($this->config, $files))->calculate($metrics); 134 | 135 | // 136 | // Unit test 137 | (new UnitTesting($this->config, $files))->calculate($metrics); 138 | 139 | $this->output->clearln(); 140 | 141 | // 142 | // Composer 143 | $this->output->write('Executing composer analyzes, requesting https://packagist.org...'); 144 | (new Composer($this->config, $files))->calculate($metrics); 145 | 146 | $this->output->clearln(); 147 | 148 | return $metrics; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Hal/Application/Application.php: -------------------------------------------------------------------------------- 1 | enable(); 31 | 32 | // config 33 | $config = (new Parser())->parse($argv); 34 | 35 | // Help 36 | if ($config->has('help')) { 37 | $output->writeln((new Validator())->help()); 38 | exit(0); 39 | } 40 | 41 | // Metrics list 42 | if ($config->has('metrics')) { 43 | $output->writeln((new Validator())->metrics()); 44 | exit(0); 45 | } 46 | 47 | // Version 48 | if ($config->has('version')) { 49 | $output->writeln(sprintf("PhpMetrics %s \nby Jean-François Lépine \n", 50 | getVersion())); 51 | exit(0); 52 | } 53 | 54 | try { 55 | (new Validator())->validate($config); 56 | } catch (ConfigException $e) { 57 | $output->writeln(sprintf("\n%s\n", $e->getMessage())); 58 | $output->writeln((new Validator())->help()); 59 | exit(1); 60 | } 61 | 62 | if ($config->has('quiet')) { 63 | $output->setQuietMode(true); 64 | } 65 | 66 | // find files 67 | $finder = new Finder($config->get('extensions'), $config->get('exclude')); 68 | $files = $finder->fetch($config->get('files')); 69 | 70 | // analyze 71 | try { 72 | $metrics = (new Analyze($config, $output, $issuer))->run($files); 73 | } catch (ConfigException $e) { 74 | $output->writeln(sprintf('%s', $e->getMessage())); 75 | exit(1); 76 | } 77 | 78 | // search 79 | $searches = $config->get('searches'); 80 | $searcher = new PatternSearcher(); 81 | $foundSearch = new SearchMetric('searches'); 82 | foreach ($searches->all() as $search) { 83 | $foundSearch->set($search->getName(), $searcher->executes($search, $metrics)); 84 | } 85 | $metrics->attach($foundSearch); 86 | 87 | // violations 88 | (new ViolationParser($config, $output))->apply($metrics); 89 | 90 | // report 91 | try { 92 | (new Report\Cli\Reporter($config, $output))->generate($metrics); 93 | (new Report\Cli\SearchReporter($config, $output))->generate($metrics); 94 | (new Report\Html\Reporter($config, $output))->generate($metrics); 95 | (new Report\Csv\Reporter($config, $output))->generate($metrics); 96 | (new Report\Json\Reporter($config, $output))->generate($metrics); 97 | (new Report\Json\SummaryReporter($config, $output))->generate($metrics); 98 | (new Report\Violations\Xml\Reporter($config, $output))->generate($metrics); 99 | } catch (Exception $e) { 100 | $output->writeln(sprintf('Cannot generate report: %s', $e->getMessage())); 101 | $output->writeln(''); 102 | exit(1); 103 | } 104 | 105 | // exit status 106 | $shouldExitDueToCriticalViolationsCount = 0; 107 | foreach ($metrics->all() as $metric) { 108 | foreach ($metric->get('violations') as $violation) { 109 | if (Violation::CRITICAL === $violation->getLevel()) { 110 | $shouldExitDueToCriticalViolationsCount++; 111 | } 112 | } 113 | } 114 | if (!empty($shouldExitDueToCriticalViolationsCount)) { 115 | $output->writeln(''); 116 | $output->writeln(sprintf('[ERR] Failed du to %d critical violations', 117 | $shouldExitDueToCriticalViolationsCount)); 118 | $output->writeln(''); 119 | exit(1); 120 | } 121 | 122 | // end 123 | $output->writeln(''); 124 | $output->writeln('Done'); 125 | $output->writeln(''); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /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) { 22 | if (preg_match('!\-\-config=(.*)!', $arg, $matches)) { 23 | $fileReader = ConfigFileReaderFactory::createFromFileName($matches[1]); 24 | $fileReader->read($config); 25 | unset($argv[$k]); 26 | } 27 | } 28 | 29 | // arguments with options 30 | foreach ($argv as $k => $arg) { 31 | if (preg_match('!\-\-([\w\-]+)=(.*)!', $arg, $matches)) { 32 | list(, $parameter, $value) = $matches; 33 | $config->set($parameter, trim($value, ' "\'')); 34 | unset($argv[$k]); 35 | } 36 | } 37 | 38 | // arguments without options 39 | foreach ($argv as $k => $arg) { 40 | if (preg_match('!\-\-([\w\-]+)$!', $arg, $matches)) { 41 | list(, $parameter) = $matches; 42 | $config->set($parameter, true); 43 | unset($argv[$k]); 44 | } 45 | } 46 | 47 | // last argument 48 | $files = array_pop($argv); 49 | if ($files && !preg_match('!^\-\-!', $files)) { 50 | $config->set('files', explode(',', $files)); 51 | } 52 | 53 | return $config; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Hal/Application/Config/Validator.php: -------------------------------------------------------------------------------- 1 | has('files')) { 24 | throw new ConfigException('Directory to parse is missing or incorrect'); 25 | } 26 | foreach ($config->get('files') as $dir) { 27 | if (!file_exists($dir)) { 28 | throw new ConfigException(sprintf('Directory %s does not exist', $dir)); 29 | } 30 | } 31 | 32 | // extensions 33 | if (!$config->has('extensions')) { 34 | $config->set('extensions', 'php,inc'); 35 | } 36 | $config->set('extensions', explode(',', $config->get('extensions'))); 37 | 38 | // excluded directories 39 | if (!$config->has('exclude')) { 40 | $config->set('exclude', 41 | 'vendor,test,Test,tests,Tests,testing,Testing,bower_components,node_modules,cache,spec'); 42 | } 43 | 44 | // retro-compatibility with excludes as string in config files 45 | if (is_array($config->get('exclude'))) { 46 | $config->set('exclude', implode(',', $config->get('exclude'))); 47 | } 48 | $config->set('exclude', array_filter(explode(',', $config->get('exclude')))); 49 | 50 | // groups by regex 51 | if (!$config->has('groups')) { 52 | $config->set('groups', []); 53 | } 54 | $groupsRaw = $config->get('groups'); 55 | 56 | $groups = array_map(static function (array $groupRaw) { 57 | return new Group($groupRaw['name'], $groupRaw['match']); 58 | }, $groupsRaw); 59 | $config->set('groups', $groups); 60 | 61 | if (!$config->has('composer')) { 62 | $config->set('composer', true); 63 | } 64 | $config->set('composer', filter_var($config->get('composer'), FILTER_VALIDATE_BOOLEAN)); 65 | 66 | // Search 67 | $validator = new SearchesValidator(); 68 | if (null === $config->get('searches')) { 69 | $config->set('searches', new Searches()); 70 | } 71 | $validator->validates($config->get('searches')); 72 | 73 | // parameters with values 74 | $keys = ['report-html', 'report-csv', 'report-violation', 'report-json', 'report-summary-json', 'extensions', 'config']; 75 | foreach ($keys as $key) { 76 | $value = $config->get($key); 77 | if ($config->has($key) && empty($value) || true === $value) { 78 | throw new ConfigException(sprintf('%s option requires a value', $key)); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * @return string 85 | */ 86 | public function help() 87 | { 88 | return << 92 | 93 | Required: 94 | 95 | List of directories to parse, separated by a comma (,) 96 | 97 | Optional: 98 | 99 | --config= Use a file for configuration. File can be a JSON, YAML or INI file. 100 | --exclude= List of directories to exclude, separated by a comma (,) 101 | --extensions= List of extensions to parse, separated by a comma (,) 102 | --metrics Display list of available metrics 103 | --report-html= Folder where report HTML will be generated 104 | --report-csv= File where report CSV will be generated 105 | --report-json= File where report Json will be generated 106 | --report-summary-json= File where the summary report Json will be generated 107 | --report-violations= File where XML violations report will be generated 108 | --git[=] Perform analyses based on Git History (default binary path: "git") 109 | --junit[=] Evaluates metrics according to JUnit logs 110 | --quiet Quiet mode 111 | --version Display current version 112 | 113 | Examples: 114 | 115 | phpmetrics --report-html="./report" ./src 116 | 117 | Analyze the "./src" directory and generate a HTML report on the "./report" folder 118 | 119 | 120 | phpmetrics --report-violations="./build/violations.xml" ./src,./lib 121 | 122 | Analyze the "./src" and "./lib" directories, and generate the "./build/violations.xml" file. This file could 123 | be read by any Continuous Integration Platform, and follows the "PMD Violation" standards. 124 | 125 | EOT; 126 | } 127 | 128 | public function metrics() 129 | { 130 | $help = <<getDefinitions(); 137 | foreach ($definitions as $key => $description) { 138 | $help .= sprintf("\n %s%s", str_pad($key, 40, ' ', STR_PAD_RIGHT), $description); 139 | } 140 | 141 | $help .= PHP_EOL; 142 | return $help; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/NodeTraverser.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | 9 | namespace Hal\Component\Ast; 10 | 11 | if (PHP_VERSION_ID >= 70000) { 12 | class_alias(Php7NodeTraverser::class, __NAMESPACE__ . '\\ActualNodeTraverser'); 13 | } else { 14 | class_alias(Php5NodeTraverser::class, __NAMESPACE__ . '\\ActualNodeTraverser'); 15 | } 16 | 17 | /** 18 | * Empty class to refer the good ActualNodeTraverser depending on the PHP version. 19 | * This class must be hard-coded and not directly used as an alias because composer can not handle class-aliases when 20 | * flag --classmap-authoritative is set. 21 | * @see https://github.com/phpmetrics/PhpMetrics/issues/373 22 | */ 23 | /** @noinspection PhpUndefinedClassInspection */ 24 | class NodeTraverser extends ActualNodeTraverser 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/Php5NodeTraverser.php: -------------------------------------------------------------------------------- 1 | traverser = new Traverser($this, $stopCondition); 20 | } 21 | 22 | public function traverseNode(Node $node) 23 | { 24 | return parent::traverseNode($node); 25 | } 26 | 27 | protected function traverseArray(array $nodes) 28 | { 29 | return $this->traverser->traverseArray($nodes, $this->visitors); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/Php7NodeTraverser.php: -------------------------------------------------------------------------------- 1 | traverser = new Traverser($this, $stopCondition); 21 | } 22 | 23 | public function traverseNode(Node $node): Node 24 | { 25 | return parent::traverseNode($node); 26 | } 27 | 28 | protected function traverseArray(array $nodes): array 29 | { 30 | return $this->traverser->traverseArray($nodes, $this->visitors); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Hal/Component/Ast/Traverser.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 | } else if (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 | /** 23 | * Follow symlinks 24 | */ 25 | const FOLLOW_SYMLINKS = RecursiveDirectoryIterator::FOLLOW_SYMLINKS; 26 | 27 | /** 28 | * Extensions to match 29 | * 30 | * @var array 31 | */ 32 | private $extensions = []; 33 | 34 | /** 35 | * Subdirectories to exclude 36 | * 37 | * @var array 38 | */ 39 | private $excludedDirs = []; 40 | 41 | /** 42 | * @param string[] $extensions regex of file extensions to include 43 | * @param string[] $excludedDirs regex of directories to exclude 44 | */ 45 | public function __construct(array $extensions = ['php'], array $excludedDirs = []) 46 | { 47 | $this->extensions = $extensions; 48 | $this->excludedDirs = $excludedDirs; 49 | } 50 | 51 | /** 52 | * Find files in path 53 | * 54 | * @param string[] $paths 55 | * @return array 56 | */ 57 | public function fetch(array $paths) 58 | { 59 | $files = []; 60 | foreach ($paths as $path) { 61 | if (is_dir($path)) { 62 | $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 63 | $directory = new RecursiveDirectoryIterator($path); 64 | $iterator = new RecursiveIteratorIterator($directory); 65 | 66 | $filterRegex = sprintf( 67 | '`^%s%s%s$`', 68 | preg_quote($path, '`'), 69 | !empty($this->excludedDirs) ? '((?!' . implode('|', array_map('preg_quote', $this->excludedDirs)) . ').)+' : '.+', 70 | '\.(' . implode('|', $this->extensions) . ')' 71 | ); 72 | 73 | $filteredIterator = new RegexIterator( 74 | $iterator, 75 | $filterRegex, 76 | \RecursiveRegexIterator::GET_MATCH 77 | ); 78 | 79 | foreach ($filteredIterator as $file) { 80 | $files[] = $file[0]; 81 | } 82 | } elseif (is_file($path)) { 83 | $files[] = $path; 84 | } 85 | } 86 | return $files; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /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 | 82 | EOT; 83 | 84 | $log = << 103 | Details 104 | ``` 105 | $trace 106 | 107 | 108 | $debug 109 | ``` 110 | 111 | 112 | EOT; 113 | 114 | $this->output->write($message); 115 | 116 | $this->log($logfile, $log); 117 | $this->terminate(1); 118 | } 119 | 120 | /** 121 | * @return $this 122 | */ 123 | public function enable() 124 | { 125 | set_error_handler([$this, 'onError']); 126 | return $this; 127 | } 128 | 129 | /** 130 | * @return $this 131 | */ 132 | public function disable() 133 | { 134 | restore_error_handler(); 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param $status 140 | */ 141 | protected function terminate($status) 142 | { 143 | exit($status); 144 | } 145 | 146 | /** 147 | * @param $log 148 | * @return $this 149 | */ 150 | protected function log($logfile, $log) 151 | { 152 | if (is_writable(getcwd())) { 153 | file_put_contents($logfile, $log); 154 | } else { 155 | $this->output->write($log); 156 | } 157 | return $this; 158 | } 159 | 160 | /** 161 | * @param $debugKey 162 | * @param $value 163 | * @return $this 164 | */ 165 | public function set($debugKey, $value) 166 | { 167 | $this->debug[$debugKey] = $value; 168 | return $this; 169 | } 170 | 171 | /** 172 | * @param $debugKey 173 | * @return $this 174 | */ 175 | public function clear($debugKey) 176 | { 177 | unset($this->debug[$debugKey]); 178 | return $this; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /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 | /** 19 | * @var Output 20 | */ 21 | private $output; 22 | 23 | /** 24 | * @var int 25 | */ 26 | private $max; 27 | 28 | /** 29 | * @var int 30 | */ 31 | private $current = 0; 32 | 33 | /** 34 | * @param Output $output 35 | * @param int $max 36 | */ 37 | public function __construct(Output $output, $max) 38 | { 39 | $this->output = $output; 40 | $this->max = $max; 41 | } 42 | 43 | /** 44 | * Start progress bar 45 | */ 46 | public function start() 47 | { 48 | $this->current = 0; 49 | } 50 | 51 | /** 52 | * Advance progress bar 53 | */ 54 | public function advance() 55 | { 56 | $this->current++; 57 | 58 | if ($this->output->hasAnsi()) { 59 | $percent = round($this->current / $this->max * 100); 60 | $this->output->write("\x0D"); 61 | $this->output->write("\x1B[2K"); 62 | $this->output->write(sprintf('... %s%% ...', $percent)); 63 | } else { 64 | $this->output->write('.'); 65 | } 66 | } 67 | 68 | /** 69 | * Clear console 70 | */ 71 | public function clear() 72 | { 73 | if ($this->output->hasAnsi()) { 74 | $this->output->write("\x0D"); 75 | $this->output->write("\x1B[2K"); 76 | $this->output->clearln(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function hasAnsi() 58 | { 59 | return false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | { 14 | } 15 | -------------------------------------------------------------------------------- /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 | /** 16 | * @var mixed 17 | */ 18 | private $data; 19 | 20 | /** 21 | * @var string 22 | */ 23 | private $key; 24 | 25 | /** 26 | * @var Edge[] 27 | */ 28 | private $edges = []; 29 | 30 | /** 31 | * @var bool 32 | */ 33 | public $visited = false; 34 | 35 | /** 36 | * @var bool 37 | */ 38 | public $cyclic = false; 39 | 40 | /** 41 | * @param string $key 42 | * @param mixed $data 43 | */ 44 | public function __construct($key, $data = null) 45 | { 46 | $this->key = $key; 47 | $this->data = $data; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getKey() 54 | { 55 | return $this->key; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function getAdjacents() 62 | { 63 | $adjacents = []; 64 | foreach ($this->edges as $edge) { 65 | if ($edge->getFrom()->getKey() != $this->getKey()) { 66 | $adjacents[$edge->getFrom()->getKey()] = $edge->getFrom(); 67 | } 68 | if ($edge->getTo()->getKey() != $this->getKey()) { 69 | $adjacents[$edge->getTo()->getKey()] = $edge->getTo(); 70 | } 71 | } 72 | return $adjacents; 73 | } 74 | 75 | /** 76 | * @return Edge[] 77 | */ 78 | public function getEdges() 79 | { 80 | return $this->edges; 81 | } 82 | 83 | /** 84 | * @param Edge $edge 85 | * @return $this 86 | */ 87 | public function addEdge(Edge $edge) 88 | { 89 | array_push($this->edges, $edge); 90 | return $this; 91 | } 92 | 93 | /** 94 | * @return mixed 95 | */ 96 | public function getData() 97 | { 98 | return $this->data; 99 | } 100 | 101 | /** 102 | * @param mixed $data 103 | * @return Node 104 | */ 105 | public function setData($data) 106 | { 107 | $this->data = $data; 108 | return $this; 109 | } 110 | 111 | /** 112 | * @return string Unique id for this node independent of class name or node type 113 | */ 114 | public function getUniqueId() 115 | { 116 | return spl_object_hash($this); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /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; 19 | $this->set('name', $name); 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getName() 26 | { 27 | return $this->name; 28 | } 29 | 30 | /** 31 | * @param $key 32 | * @param $value 33 | * @return $this 34 | */ 35 | public function set($key, $value) 36 | { 37 | $this->bag[$key] = $value; 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param $key 43 | * @return bool 44 | */ 45 | public function has($key) 46 | { 47 | return isset($this->bag[$key]); 48 | } 49 | 50 | /** 51 | * @param $key 52 | * @return null 53 | */ 54 | public function get($key) 55 | { 56 | return $this->has($key) ? $this->bag[$key] : null; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function all() 63 | { 64 | return $this->bag; 65 | } 66 | 67 | /** 68 | * @param array $array 69 | * @return $this 70 | */ 71 | public function fromArray(array $array) 72 | { 73 | foreach ($array as $key => $value) { 74 | $this->set($key, $value); 75 | } 76 | return $this; 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | #[\ReturnTypeWillChange] 83 | public function jsonSerialize() 84 | { 85 | return array_merge($this->all(), ['_type' => get_class($this)]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Hal/Metric/ClassMetric.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 26 | } 27 | 28 | 29 | public function leaveNode(Node $node) 30 | { 31 | if ($node instanceof Stmt\Class_ 32 | || $node instanceof Stmt\Interface_ 33 | || $node instanceof Stmt\Trait_ 34 | ) { 35 | if ($node instanceof Stmt\Interface_) { 36 | $class = new InterfaceMetric($node->namespacedName->toString()); 37 | $class->set('interface', true); 38 | $class->set('abstract', true); 39 | } else { 40 | $name = (string)(isset($node->namespacedName) ? $node->namespacedName : 'anonymous@' . spl_object_hash($node)); 41 | $class = new ClassMetric($name); 42 | $class->set('interface', false); 43 | $class->set('abstract', $node instanceof Stmt\Trait_ || $node->isAbstract()); 44 | $class->set('final', !$node instanceof Stmt\Trait_ && $node->isFinal()); 45 | } 46 | 47 | $methods = []; 48 | 49 | $methodsPublic = $methodsPrivate = $nbGetters = $nbSetters = 0; 50 | $roleDetector = new RoleOfMethodDetector(); 51 | foreach ($node->stmts as $stmt) { 52 | if ($stmt instanceof Stmt\ClassMethod) { 53 | $function = new FunctionMetric((string)$stmt->name); 54 | 55 | $role = $roleDetector->detects($stmt); 56 | $function->set('role', $role); 57 | switch ($role) { 58 | case 'getter': 59 | $nbGetters++; 60 | break; 61 | case 'setter': 62 | $nbSetters++; 63 | break; 64 | } 65 | 66 | if (null === $role) { 67 | if ($stmt->isPublic()) { 68 | $methodsPublic++; 69 | $function->set('public', true); 70 | $function->set('private', false); 71 | } 72 | 73 | if ($stmt->isPrivate() || $stmt->isProtected()) { 74 | $methodsPrivate++; 75 | $function->set('public', false); 76 | $function->set('private', true); 77 | } 78 | } 79 | 80 | array_push($methods, $function); 81 | } 82 | } 83 | 84 | $class->set('methods', $methods); 85 | $class->set('nbMethodsIncludingGettersSetters', count($methods)); 86 | $class->set('nbMethods', count($methods) - ($nbGetters + $nbSetters)); 87 | $class->set('nbMethodsPrivate', $methodsPrivate); 88 | $class->set('nbMethodsPublic', $methodsPublic); 89 | $class->set('nbMethodsGetter', $nbGetters); 90 | $class->set('nbMethodsSetters', $nbSetters); 91 | 92 | $this->metrics->attach($class); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Complexity/KanDefectVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function leaveNode(Node $node) 35 | { 36 | if ($node instanceof Stmt\Class_ 37 | || $node instanceof Stmt\Interface_ 38 | || $node instanceof Stmt\Trait_ 39 | ) { 40 | $class = $this->metrics->get(MetricClassNameGenerator::getName($node)); 41 | 42 | $select = $while = $if = 0; 43 | 44 | iterate_over_node($node, function ($node) use (&$while, &$select, &$if) { 45 | switch (true) { 46 | case $node instanceof Stmt\Do_: 47 | case $node instanceof Stmt\Foreach_: 48 | case $node instanceof Stmt\While_: 49 | $while++; 50 | break; 51 | case $node instanceof Stmt\If_: 52 | $if++; 53 | break; 54 | case $node instanceof Stmt\Switch_: 55 | $select++; 56 | break; 57 | } 58 | }); 59 | 60 | $defect = 0.15 + 0.23 * $while + 0.22 * $select + 0.07 * $if; 61 | $class->set('kanDefect', round($defect, 2)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Component/MaintainabilityIndexVisitor.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class MaintainabilityIndexVisitor extends NodeVisitorAbstract 29 | { 30 | 31 | /** 32 | * @var Metrics 33 | */ 34 | private $metrics; 35 | 36 | /** 37 | * @param Metrics $metrics 38 | */ 39 | public function __construct(Metrics $metrics) 40 | { 41 | $this->metrics = $metrics; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function leaveNode(Node $node) 48 | { 49 | if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { 50 | $name = (string)(isset($node->namespacedName) ? $node->namespacedName : 'anonymous@' . spl_object_hash($node)); 51 | $classOrFunction = $this->metrics->get($name); 52 | 53 | if (null === $lloc = $classOrFunction->get('lloc')) { 54 | throw new \LogicException('please enable length (lloc) visitor first'); 55 | } 56 | if (null === $cloc = $classOrFunction->get('cloc')) { 57 | throw new \LogicException('please enable length (cloc) visitor first'); 58 | } 59 | if (null === $loc = $classOrFunction->get('loc')) { 60 | throw new \LogicException('please enable length (loc) visitor first'); 61 | } 62 | if (null === $ccn = $classOrFunction->get('ccn')) { 63 | throw new \LogicException('please enable McCabe visitor first'); 64 | } 65 | if (null === $volume = $classOrFunction->get('volume')) { 66 | throw new \LogicException('please enable Halstead visitor first'); 67 | } 68 | 69 | // maintainability index without comment 70 | $MIwoC = max( 71 | (171 72 | - (5.2 * \log($volume)) 73 | - (0.23 * $ccn) 74 | - (16.2 * \log($lloc)) 75 | ) * 100 / 171, 76 | 0 77 | ); 78 | if (is_infinite($MIwoC)) { 79 | $MIwoC = 171; 80 | } 81 | 82 | // comment weight 83 | if ($loc > 0) { 84 | $CM = $cloc / $loc; 85 | $commentWeight = 50 * sin(sqrt(2.4 * $CM)); 86 | } 87 | 88 | // maintainability index 89 | $mi = $MIwoC + $commentWeight; 90 | 91 | // save result 92 | $classOrFunction 93 | ->set('mi', round($mi, 2)) 94 | ->set('mIwoC', round($MIwoC, 2)) 95 | ->set('commentWeight', round($commentWeight, 2)); 96 | $this->metrics->attach($classOrFunction); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { 40 | // we build a graph of internal dependencies in class 41 | $graph = new GraphDeduplicated(); 42 | $class = $this->metrics->get(MetricClassNameGenerator::getName($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 | 20 | */ 21 | class SystemComplexityVisitor extends NodeVisitorAbstract 22 | { 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 ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { 43 | $class = $this->metrics->get(MetricClassNameGenerator::getName($node)); 44 | 45 | $sy = $dc = $sc = []; 46 | 47 | foreach ($node->stmts as $stmt) { 48 | if ($stmt instanceof Stmt\ClassMethod) { 49 | // number of returns and calls 50 | $output = 0; 51 | $fanout = []; 52 | 53 | $parentNode = $node; 54 | iterate_over_node($node, function ($node) use (&$output, &$fanout, $parentNode) { 55 | switch (true) { 56 | case $node instanceof Stmt\Return_: 57 | $output++; 58 | break; 59 | case $node instanceof Node\Expr\StaticCall: 60 | $class = getNameOfNode($node->class); 61 | if ('static' === $class || 'self' === $class) { 62 | $class = getNameOfNode($parentNode); 63 | } 64 | $fanout[] = $class . '::' . getNameOfNode($node->name) . '()'; 65 | break; 66 | case $node instanceof Node\Expr\MethodCall: 67 | $class = getNameOfNode($node->var); 68 | if ('this' === $class) { 69 | $class = getNameOfNode($parentNode); 70 | } 71 | $fanout[] = $class . '->' . getNameOfNode($node->name) . '()'; 72 | break; 73 | } 74 | }); 75 | 76 | $fanout = count(array_unique($fanout)); 77 | $v = count($stmt->params) + $output; 78 | $ldc = $v / ($fanout + 1); 79 | $lsc = pow($fanout, 2); 80 | $sy[] = $ldc + $lsc; 81 | $dc[] = $ldc; 82 | $sc[] = $lsc; 83 | } 84 | } 85 | 86 | // average for class 87 | $class 88 | ->set('relativeStructuralComplexity', empty($sc) ? 0 : round(array_sum($sc) / count($sc), 2)) 89 | ->set('relativeDataComplexity', empty($dc) ? 0 : round(array_sum($dc) / count($dc), 2)) 90 | ->set('relativeSystemComplexity', empty($sy) ? 0 : round(array_sum($sy) / count($sy), 2)) 91 | ->set('totalStructuralComplexity', round(array_sum($sc), 2)) 92 | ->set('totalDataComplexity', round(array_sum($dc), 2)) 93 | ->set('totalSystemComplexity', round(array_sum($dc) + array_sum($sc), 2)); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Hal/Metric/Class_/Text/LengthVisitor.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 28 | } 29 | 30 | /** 31 | * @inheritdoc 32 | */ 33 | public function leaveNode(Node $node) 34 | { 35 | if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Function_ || $node instanceof Stmt\Trait_) { 36 | if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) { 37 | $name = (string)(isset($node->namespacedName) ? $node->namespacedName : 'anonymous@' . spl_object_hash($node)); 38 | $classOrFunction = $this->metrics->get($name); 39 | } else { 40 | $classOrFunction = new FunctionMetric((string)$node->name); 41 | $this->metrics->attach($classOrFunction); 42 | } 43 | 44 | $prettyPrinter = new PrettyPrinter\Standard(); 45 | $code = $prettyPrinter->prettyPrintFile([$node]); 46 | 47 | // count all lines 48 | $loc = count(preg_split('/\r\n|\r|\n/', $code)) - 1; 49 | 50 | // count and remove multi lines comments 51 | $cloc = 0; 52 | if (preg_match_all('!/\*.*?\*/!s', $code, $matches)) { 53 | foreach ($matches[0] as $match) { 54 | $cloc += max(1, count(preg_split('/\r\n|\r|\n/', $match))); 55 | } 56 | } 57 | $code = preg_replace('!/\*.*?\*/!s', '', $code); 58 | 59 | // count and remove single line comments 60 | $code = preg_replace_callback('!(\'[^\']*\'|"[^"]*")|((?:#|\/\/).*$)!m', function (array $matches) use (&$cloc) { 61 | if (isset($matches[2])) { 62 | $cloc += 1; 63 | } 64 | return $matches[1]; 65 | }, $code, -1); 66 | 67 | // count and remove empty lines 68 | $code = trim(preg_replace('!(^\s*[\r\n])!sm', '', $code)); 69 | $lloc = count(preg_split('/\r\n|\r|\n/', $code)); 70 | 71 | // save result 72 | $classOrFunction 73 | ->set('cloc', $cloc) 74 | ->set('loc', $loc) 75 | ->set('lloc', $lloc); 76 | $this->metrics->attach($classOrFunction); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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/Helper/MetricClassNameGenerator.php: -------------------------------------------------------------------------------- 1 | isAnonymous()) ? 20 | 'anonymous@' . spl_object_hash($node) : 21 | $node->namespacedName->toString(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Hal/Metric/InterfaceMetric.php: -------------------------------------------------------------------------------- 1 | data[$metric->getName()] = $metric; 23 | return $this; 24 | } 25 | 26 | /** 27 | * @param $key 28 | * @return Metric|null 29 | */ 30 | public function get($key) 31 | { 32 | return $this->has($key) ? $this->data[$key] : null; 33 | } 34 | 35 | /** 36 | * @param $key 37 | * @return bool 38 | */ 39 | public function has($key) 40 | { 41 | return isset($this->data[$key]); 42 | } 43 | 44 | /** 45 | * @return Metric[] 46 | */ 47 | public function all() 48 | { 49 | return $this->data; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | #[\ReturnTypeWillChange] 56 | public function jsonSerialize() 57 | { 58 | return $this->all(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 = isset($node->namespacedName) ? $node->namespacedName : 'anonymous@' . spl_object_hash($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) { 12 | if ($each instanceof PackageMetric && $each->getAbstraction() !== null && $each->getInstability() !== null) { 13 | $each->setNormalizedDistance(abs($each->getAbstraction() + $each->getInstability() - 1)); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 isset($instabilitiesByPackage[$packageName]) ? $instabilitiesByPackage[$packageName] : null; 33 | }, $eachPackage->getOutgoingPackageDependencies()); 34 | $dependentInstabilities = array_combine( 35 | $eachPackage->getOutgoingPackageDependencies(), 36 | $dependentInstabilities 37 | ); 38 | $dependentInstabilities = array_filter($dependentInstabilities, 'is_float'); 39 | $eachPackage->setDependentInstabilities($dependentInstabilities); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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 | } 57 | 58 | public function allForStructures() 59 | { 60 | return array_keys($this->definitionsForStructures); 61 | } 62 | 63 | public function getDefinitions() 64 | { 65 | return $this->definitionsForStructures; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Hal/Metric/SearchMetric.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Coupling 16 | { 17 | 18 | /** 19 | * @param Metrics $metrics 20 | */ 21 | public function calculate(Metrics $metrics) 22 | { 23 | // build a graph of relations 24 | $graph = new GraphDeduplicated(); 25 | 26 | foreach ($metrics->all() as $metric) { 27 | if (!$metric instanceof ClassMetric) { 28 | continue; 29 | } 30 | 31 | if (!$graph->has($metric->get('name'))) { 32 | $graph->insert(new Node($metric->get('name'))); 33 | } 34 | $from = $graph->get($metric->get('name')); 35 | 36 | foreach ($metric->get('externals') as $external) { 37 | if (!$graph->has($external)) { 38 | $graph->insert(new Node($external)); 39 | } 40 | 41 | $to = $graph->get($external); 42 | 43 | $graph->addEdge($from, $to); 44 | } 45 | } 46 | 47 | // analyze relations 48 | foreach ($metrics->all() as $metric) { 49 | if (!$metric instanceof ClassMetric) { 50 | continue; 51 | } 52 | $efferent = $afferent = 0; 53 | 54 | $node = $graph->get($metric->get('name')); 55 | foreach ($node->getEdges() as $edge) { 56 | if ($edge->getTo()->getKey() == $node->getKey()) { 57 | // affects 58 | $afferent++; 59 | } 60 | 61 | if ($edge->getFrom()->getKey() == $node->getKey()) { 62 | // receive effects 63 | $efferent++; 64 | } 65 | } 66 | 67 | $instability = 0; 68 | if ($efferent + $afferent > 0) { 69 | $instability = $efferent / ($afferent + $efferent); 70 | } 71 | $metric 72 | ->set('afferentCoupling', $afferent) 73 | ->set('efferentCoupling', $efferent) 74 | ->set('instability', round($instability, 2)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Coupling/DepthOfInheritanceTree.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DepthOfInheritanceTree 19 | { 20 | 21 | /** 22 | * @param Metrics $metrics 23 | */ 24 | public function calculate(Metrics $metrics) 25 | { 26 | $projectMetric = new ProjectMetric('tree'); 27 | 28 | // building graph with parents / childs relations only 29 | $graph = new GraphDeduplicated(); 30 | 31 | foreach ($metrics->all() as $metric) { 32 | if (!$metric instanceof ClassMetric) { 33 | continue; 34 | } 35 | 36 | if (!$graph->has($metric->get('name'))) { 37 | $graph->insert(new Node($metric->get('name'))); 38 | } 39 | 40 | $to = $graph->get($metric->get('name')); 41 | 42 | foreach ($metric->get('parents') as $parent) { 43 | if (!$graph->has($parent)) { 44 | $graph->insert(new Node($parent)); 45 | } 46 | 47 | $from = $graph->get($parent); 48 | 49 | $graph->addEdge($from, $to); 50 | } 51 | } 52 | 53 | $size = new SizeOfTree($graph); 54 | $averageHeight = $size->getAverageHeightOfGraph(); 55 | 56 | $projectMetric->set('depthOfInheritanceTree', $averageHeight); 57 | $metrics->attach($projectMetric); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Coupling/PageRank.php: -------------------------------------------------------------------------------- 1 | all() as $metric) { 22 | if (!$metric instanceof ClassMetric) { 23 | continue; 24 | } 25 | 26 | $links[$metric->get('name')] = $metric->get('externals'); 27 | } 28 | 29 | $ranks = $this->calculatePageRank($links); 30 | 31 | // save in the metrics object 32 | foreach ($ranks as $name => $rank) { 33 | $metrics->get($name)->set('pageRank', round($rank, 2)); 34 | } 35 | } 36 | 37 | /** 38 | * @see http://phpir.com/pagerank-in-php/ 39 | * @param $linkGraph 40 | * @param float $dampingFactor 41 | * @return array 42 | */ 43 | private function calculatePageRank($linkGraph, $dampingFactor = 0.15) 44 | { 45 | $pageRank = []; 46 | $tempRank = []; 47 | $nodeCount = count($linkGraph); 48 | 49 | // initialise the PR as 1/n 50 | foreach ($linkGraph as $node => $outbound) { 51 | $pageRank[$node] = 1 / $nodeCount; 52 | $tempRank[$node] = 0; 53 | } 54 | 55 | $change = 1; 56 | $i = 0; 57 | while ($change > 0.00005 && $i < 100) { 58 | $change = 0; 59 | $i++; 60 | 61 | // distribute the PR of each page 62 | foreach ($linkGraph as $node => $outbound) { 63 | $outboundCount = count($outbound); 64 | foreach ($outbound as $link) { 65 | // case of unversionned dependency 66 | if (!isset($tempRank[$link])) { 67 | $tempRank[$link] = 0; 68 | } 69 | $tempRank[$link] += $pageRank[$node] / $outboundCount; 70 | } 71 | } 72 | 73 | $total = 0; 74 | // calculate the new PR using the damping factor 75 | foreach ($linkGraph as $node => $outbound) { 76 | $tempRank[$node] = ($dampingFactor / $nodeCount) 77 | + (1 - $dampingFactor) * $tempRank[$node]; 78 | $change += abs($pageRank[$node] - $tempRank[$node]); 79 | $pageRank[$node] = $tempRank[$node]; 80 | $tempRank[$node] = 0; 81 | $total += $pageRank[$node]; 82 | } 83 | 84 | // Normalise the page ranks so it's all a proportion 0-1 85 | foreach ($pageRank as $node => $score) { 86 | $pageRank[$node] /= $total; 87 | } 88 | } 89 | 90 | return $pageRank; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Hal/Metric/System/Packages/Composer/Composer.php: -------------------------------------------------------------------------------- 1 | config = $config; 28 | } 29 | 30 | /** 31 | * @param Metrics $metrics 32 | * @throws ConfigException 33 | */ 34 | public function calculate(Metrics $metrics) 35 | { 36 | if ($this->config->has('composer') && false === $this->config->get('composer')) { 37 | return; 38 | } 39 | 40 | $projectMetric = new ProjectMetric('composer'); 41 | $projectMetric->set('packages', []); 42 | $metrics->attach($projectMetric); 43 | $packages = []; 44 | $rawRequirements = $this->getComposerJsonRequirements(); 45 | $rawInstalled = $this->getComposerLockInstalled(\array_keys($rawRequirements)); 46 | 47 | $packagist = new Packagist(); 48 | foreach ($rawRequirements as $requirement => $version) { 49 | $installed = isset($rawInstalled[$requirement]) ? $rawInstalled[$requirement] : null; 50 | $package = $packagist->get($requirement); 51 | 52 | $package->installed = $installed; 53 | $package->required = $version; 54 | $package->name = $requirement; 55 | // Manage case where the package is not hosted on packagist (private repository) so we can't know the status 56 | if ($installed === null || $package->latest === null) { 57 | $package->status = 'unknown'; 58 | } else { 59 | $package->status = version_compare($installed, $package->latest, '<') ? 'outdated' : 'latest'; 60 | } 61 | $packages[$requirement] = $package; 62 | } 63 | 64 | // exclude extensions 65 | $packages = array_filter($packages, function ($package) { 66 | return !preg_match('!(^php$|^ext\-)!', $package->name); 67 | }); 68 | 69 | $projectMetric->set('packages', $packages); 70 | $projectMetric->set('packages-installed', $rawInstalled); 71 | } 72 | 73 | /** 74 | * Returns the requirements defined in the composer(-dist)?.json file. 75 | * @return array 76 | */ 77 | protected function getComposerJsonRequirements() 78 | { 79 | $rawRequirements = [[]]; 80 | 81 | // find composer.json files 82 | $finder = new Finder(['json'], $this->config->get('exclude')); 83 | 84 | // include root dir by default 85 | $files = array_merge($this->config->get('files'), ['./']); 86 | $files = $finder->fetch($files); 87 | 88 | foreach ($files as $filename) { 89 | if (!\preg_match('/composer(-dist)?\.json/', $filename)) { 90 | continue; 91 | } 92 | $composerJson = (object)\json_decode(\file_get_contents($filename)); 93 | 94 | if (!isset($composerJson->require)) { 95 | continue; 96 | } 97 | 98 | $rawRequirements[] = (array)$composerJson->require; 99 | } 100 | 101 | return \call_user_func_array('array_merge', $rawRequirements); 102 | } 103 | 104 | /** 105 | * Returns the installed packages from the composer.lock file. 106 | * @param array $rootPackageRequirements List of requirements to match installed packages only with requirements. 107 | * @return array 108 | */ 109 | protected function getComposerLockInstalled($rootPackageRequirements) 110 | { 111 | $rawInstalled = [[]]; 112 | 113 | // Find composer.lock file 114 | $finder = new Finder(['lock'], $this->config->get('exclude')); 115 | 116 | // include root dir by default 117 | $files = array_merge($this->config->get('files'), ['./']); 118 | $files = $finder->fetch($files); 119 | 120 | // List all composer.lock found in the project. 121 | foreach ($files as $filename) { 122 | if (false === \strpos($filename, 'composer.lock')) { 123 | continue; 124 | } 125 | $composerLockJson = (object)\json_decode(\file_get_contents($filename)); 126 | 127 | if (!isset($composerLockJson->packages)) { 128 | continue; 129 | } 130 | 131 | $installed = []; 132 | foreach ($composerLockJson->packages as $package) { 133 | if (!\in_array($package->name, $rootPackageRequirements, true)) { 134 | continue; 135 | } 136 | 137 | $installed[$package->name] = \preg_replace('#[^.\d]#', '', $package->version); 138 | } 139 | 140 | $rawInstalled[] = $installed; 141 | } 142 | 143 | return \call_user_func_array('array_merge', $rawInstalled); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /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; 29 | $this->output = $output; 30 | } 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-csv'); 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 CSV report in ' . $logFile); 45 | } 46 | 47 | $availables = (new Registry())->allForStructures(); 48 | $hwnd = fopen($logFile, 'w'); 49 | fputcsv($hwnd, $availables); 50 | 51 | foreach ($metrics->all() as $metric) { 52 | if (!$metric instanceof ClassMetric) { 53 | continue; 54 | } 55 | $row = []; 56 | foreach ($availables as $key) { 57 | $data = $metric->get($key); 58 | if (is_array($data) || !is_scalar($data)) { 59 | $data = 'N/A'; 60 | } 61 | 62 | array_push($row, $data); 63 | } 64 | fputcsv($hwnd, $row); 65 | } 66 | 67 | fclose($hwnd); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /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; 20 | return $this; 21 | } 22 | 23 | /** 24 | * @param $name 25 | * @return Search|null 26 | */ 27 | public function get($name) 28 | { 29 | return $this->has($name) ? $this->searches[$name] : null; 30 | } 31 | 32 | /** 33 | * @param $name 34 | * @return bool 35 | */ 36 | public function has($name) 37 | { 38 | return isset($this->searches[$name]); 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function all() 45 | { 46 | return $this->searches; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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; 31 | 32 | if ($metric->get('wmc') > 50) { 33 | $metric->get('violations')->add($this); 34 | } 35 | } 36 | 37 | public function getLevel() 38 | { 39 | return Violation::ERROR; 40 | } 41 | 42 | public function getDescription() 43 | { 44 | return <<metric->get('ccn')}) 48 | * Component uses {$this->metric->get('number_operators')} operators 49 | 50 | Maybe you should delegate some code to other objects. 51 | EOT; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Hal/Violation/Class_/TooComplexMethodCode.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 33 | 34 | if ($metric->get('ccnMethodMax') > 10) { 35 | $metric->get('violations')->add($this); 36 | return; 37 | } 38 | } 39 | 40 | public function getLevel() 41 | { 42 | return Violation::ERROR; 43 | } 44 | 45 | public function getDescription() 46 | { 47 | return <<metric->get('ccnMethodMax')}) 51 | 52 | Maybe you should delegate some code to other objects or split complex method. 53 | EOT; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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); 16 | } 17 | 18 | public function apply(Metric $metric) 19 | { 20 | if ($metric->has('was-not-expected') && $metric->get('was-not-expected')) { 21 | $this->concernedSearches = array_unique( 22 | array_merge( 23 | $this->concernedSearches, 24 | $metric->get('was-not-expected-by') 25 | ) 26 | ); 27 | $metric->get('violations')->add($this); 28 | } 29 | } 30 | 31 | public function getLevel() 32 | { 33 | return Violation::CRITICAL; 34 | } 35 | 36 | public function getDescription() 37 | { 38 | return 'According configuration, this component is not expected to be found in the code.'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Hal/Violation/Violation.php: -------------------------------------------------------------------------------- 1 | all() as $metric) { 32 | $metric->set('violations', new Violations); 33 | 34 | foreach ($violations as $violation) { 35 | $violation->apply($metric); 36 | } 37 | } 38 | 39 | return $this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Hal/Violation/Violations.php: -------------------------------------------------------------------------------- 1 | data); 21 | } 22 | 23 | /** 24 | * @param Violation $violation 25 | */ 26 | public function add(Violation $violation) 27 | { 28 | $this->data[] = clone $violation; 29 | } 30 | 31 | /** 32 | * @return int 33 | */ 34 | #[\ReturnTypeWillChange] 35 | public function count() 36 | { 37 | return count($this->data); 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function __toString() 44 | { 45 | $string = ''; 46 | foreach ($this->data as $violation) { 47 | $string .= $violation->getName() . ','; 48 | } 49 | return $string; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/favicon.ico -------------------------------------------------------------------------------- /templates/html_report/fonts/material-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/fonts/material-icons.ttf -------------------------------------------------------------------------------- /templates/html_report/fonts/roboto-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/fonts/roboto-bold.ttf -------------------------------------------------------------------------------- /templates/html_report/fonts/roboto-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/fonts/roboto-light.ttf -------------------------------------------------------------------------------- /templates/html_report/images/logo-git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/images/logo-git.png -------------------------------------------------------------------------------- /templates/html_report/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/templates/html_report/images/logo.png -------------------------------------------------------------------------------- /templates/html_report/images/phpmetrics-maintenability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpmetrics/PhpMetrics/c43217cd7783bbd54d0b8c1dd43f697bc36ef79d/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 | --------------------------------------------------------------------------------