#!/usr/bin/env php * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file dev/tools/apstats.php * \brief Script to report Advanced Statistics on a coding PHP project */ $sapi_type = php_sapi_name(); $script_file = basename(__FILE__); $path = dirname(__FILE__) . '/'; // Test si mode batch $sapi_type = php_sapi_name(); if (substr($sapi_type, 0, 3) == 'cgi') { echo "Error: You are using PHP for CGI. To execute " . $script_file . " from command line, you must use PHP for CLI mode.\n"; exit(); } error_reporting(E_ALL & ~E_DEPRECATED); define('PRODUCT', "apstats"); define('VERSION', "1.0"); $phpstanlevel = 3; print '***** '.constant('PRODUCT').' - '.constant('VERSION').' *****'."\n"; if (empty($argv[1])) { print 'You must run this tool being into the root of the project.'."\n"; print 'Usage: '.constant('PRODUCT').'.php pathto/outputfile.html [--dir-scc=pathtoscc] [--dir-phpstan=pathtophpstan]'."\n"; print 'Example: '.constant('PRODUCT').'.php documents/apstats/index.html --dir-scc=/snap/bin --dir-phpstan=~/git/phpstan/htdocs/includes/bin'; exit(0); } $outputpath = $argv[1]; $outputdir = dirname($outputpath); $outputfile = basename($outputpath); if (!is_dir($outputdir)) { print 'Error: dir '.$outputdir.' does not exists or is not writable'."\n"; exit(1); } $dirscc = ''; $dirphpstan = ''; $i = 0; while ($i < $argc) { $reg = array(); if (preg_match('/--dir-scc=(.*)$/', $argv[$i], $reg)) { $dirscc = $reg[1]; } if (preg_match('/--dir-phpstan=(.*)$/', $argv[$i], $reg)) { $dirphpstan = $reg[1]; } $i++; } $timestart = time(); // Count lines of code of Dolibarr itself /* $commandcheck = 'cloc . --exclude-dir=includes --exclude-dir=custom --ignore-whitespace --vcs=git'; $resexec = shell_exec($commandcheck); $resexec = (int) (empty($resexec) ? 0 : trim($resexec)); // Count lines of code of external dependencies $commandcheck = 'cloc htdocs/includes --ignore-whitespace --vcs=git'; $resexec = shell_exec($commandcheck); $resexec = (int) (empty($resexec) ? 0 : trim($resexec)); */ // Retrieve the .git information $urlgit = 'https://github.com/Dolibarr/dolibarr/blob/develop/'; // Count lines of code of application $commandcheck = ($dirscc ? $dirscc.'/' : '').'scc . --exclude-dir=htdocs/includes,htdocs/custom,htdocs/theme/common/fontawesome-5,htdocs/theme/common/octicons'; print 'Execute SCC to count lines of code in project: '.$commandcheck."\n"; $output_arrproj = array(); $resexecproj = 0; exec($commandcheck, $output_arrproj, $resexecproj); // Count lines of code of dependencies $commandcheck = ($dirscc ? $dirscc.'/' : '').'scc htdocs/includes htdocs/theme/common/fontawesome-5 htdocs/theme/common/octicons'; print 'Execute SCC to count lines of code in dependencies: '.$commandcheck."\n"; $output_arrdep = array(); $resexecdep = 0; exec($commandcheck, $output_arrdep, $resexecdep); // Get technical debt $commandcheck = ($dirphpstan ? $dirphpstan.'/' : '').'phpstan --level='.$phpstanlevel.' -v analyze -a build/phpstan/bootstrap.php --memory-limit 5G --error-format=github'; print 'Execute PHPStan to get the technical debt: '.$commandcheck."\n"; $output_arrtd = array(); $resexectd = 0; exec($commandcheck, $output_arrtd, $resexectd); // Count lines of code of dependencies $commandcheck = "git log --shortstat --no-renames --no-merges --use-mailmap --pretty='format:%cI;%H;%aN;%ae;%ce'"; // --since= --until=... print 'Execute git log to count number of commits by day: '.$commandcheck."\n"; $output_arrglpu = array(); $resexecglpu = 0; //exec($commandcheck, $output_arrglpu, $resexecglpu); $arrayoflineofcode = array(); $arraycocomo = array(); $arrayofmetrics = array( 'proj' => array('Bytes' => 0, 'Files' => 0, 'Lines' => 0, 'Blanks' => 0, 'Comments' => 0, 'Code' => 0, 'Complexity' => 0), 'dep' => array('Bytes' => 0, 'Files' => 0, 'Lines' => 0, 'Blanks' => 0, 'Comments' => 0, 'Code' => 0, 'Complexity' => 0) ); // Analyse $output_arrproj foreach (array('proj', 'dep') as $source) { print 'Analyze SCC result for lines of code for '.$source."\n"; if ($source == 'proj') { $output_arr = &$output_arrproj; } elseif ($source == 'dep') { $output_arr = &$output_arrdep; } else { print 'Bad value for $source'; die(); } foreach ($output_arr as $line) { if (preg_match('/^(───|Language|Total)/', $line)) { continue; } //print $line."
\n"; if (preg_match('/^Estimated Cost.*\$(.*)/i', $line, $reg)) { $arraycocomo[$source]['currency'] = preg_replace('/[^\d\.]/', '', str_replace(array(',', ' '), array('', ''), $reg[1])); } if (preg_match('/^Estimated Schedule Effort.*\s([\d\s,]+)/i', $line, $reg)) { $arraycocomo[$source]['effort'] = str_replace(array(',', ' '), array('.', ''), $reg[1]); } if (preg_match('/^Estimated People.*\s([\d\s,]+)/i', $line, $reg)) { $arraycocomo[$source]['people'] = str_replace(array(',', ' '), array('.', ''), $reg[1]); } if (preg_match('/^Processed\s(\d+)\s/i', $line, $reg)) { $arrayofmetrics[$source]['Bytes'] = $reg[1]; } if (preg_match('/^(.*)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/', $line, $reg)) { $arrayoflineofcode[$source][$reg[1]]['Files'] = $reg[2]; $arrayoflineofcode[$source][$reg[1]]['Lines'] = $reg[3]; $arrayoflineofcode[$source][$reg[1]]['Blanks'] = $reg[4]; $arrayoflineofcode[$source][$reg[1]]['Comments'] = $reg[5]; $arrayoflineofcode[$source][$reg[1]]['Code'] = $reg[6]; $arrayoflineofcode[$source][$reg[1]]['Complexity'] = $reg[7]; } } if (!empty($arrayoflineofcode[$source])) { foreach ($arrayoflineofcode[$source] as $key => $val) { $arrayofmetrics[$source]['Files'] += $val['Files']; $arrayofmetrics[$source]['Lines'] += $val['Lines']; $arrayofmetrics[$source]['Blanks'] += $val['Blanks']; $arrayofmetrics[$source]['Comments'] += $val['Comments']; $arrayofmetrics[$source]['Code'] += $val['Code']; $arrayofmetrics[$source]['Complexity'] += $val['Complexity']; } } } // Search the max $arrayofmax = array('Lines'=>0); foreach (array('proj', 'dep') as $source) { foreach ($arrayoflineofcode[$source] as $val) { $arrayofmax['Lines'] = max($arrayofmax['Lines'], $val['Lines']); } } $timeend = time(); /* * View */ $html = ''."\n"; $html .= ''."\n"; $html .= ''."\n"; $html .= ''."\n"; $html .= ''."\n"; $html .= ''."\n"; $html .= ' '."\n"; $html .= ''."\n"; // Header $html .= '
'."\n"; $html .= '

Advanced Project Statistics

'."\n"; $currentDate = date("Y-m-d H:i:s"); // Format: Year-Month-Day Hour:Minute:Second $html .= 'Generated on '.$currentDate.' in '.($timeend - $timestart).' seconds'."\n"; $html .= '
'."\n"; // Lines of code $html .= '
'."\n"; $html .= '

Lines of code

'."\n"; $html .= '
'."\n"; $html .= '
'."\n"; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; //$html .= ''; $html .= ''; foreach (array('proj', 'dep') as $source) { $html .= ''; if ($source == 'proj') { $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; //$html .= ''; $html .= ''; if (!empty($arrayoflineofcode[$source])) { foreach ($arrayoflineofcode[$source] as $key => $val) { $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; //$html .= ''; /*$html .= ''; */ $html .= ''; } } } $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; //$html .= ''; //$html .= ''; $html .= ''; $html .= '
LanguageBytesFilesLinesBlanksCommentsCode'.$val['Complexity'].'
All files without dependencies'; } elseif ($source == 'dep') { $html .= 'All files of dependencies only'; } $html .= '    
See detail per file type...'; $html .= '
'.formatNumber($arrayofmetrics[$source]['Bytes']).''.formatNumber($arrayofmetrics[$source]['Files']).''.formatNumber($arrayofmetrics[$source]['Lines']).''.formatNumber($arrayofmetrics[$source]['Blanks']).''.formatNumber($arrayofmetrics[$source]['Comments']).''.formatNumber($arrayofmetrics[$source]['Code']).'
Total'.formatNumber($arrayofmetrics['proj']['Bytes'] + $arrayofmetrics['dep']['Bytes']).''.formatNumber($arrayofmetrics['proj']['Files'] + $arrayofmetrics['dep']['Files']).''.formatNumber($arrayofmetrics['proj']['Lines'] + $arrayofmetrics['dep']['Lines']).''.formatNumber($arrayofmetrics['proj']['Blanks'] + $arrayofmetrics['dep']['Blanks']).''.formatNumber($arrayofmetrics['proj']['Comments'] + $arrayofmetrics['dep']['Comments']).''.formatNumber($arrayofmetrics['proj']['Code'] + $arrayofmetrics['dep']['Code']).''.$arrayofmetrics['Complexity'].'
'; $html .= '
'; $html .= '
'; $html .= '
'."\n"; // Contributions $html .= '
'."\n"; $html .= '

Contributions

'."\n"; $html .= '
'."\n"; $html .= 'TODO...'; $html .= ''; $html .= '
'; $html .= '
'."\n"; // Contributors $html .= '
'."\n"; $html .= '

Contributors

'."\n"; $html .= '
'."\n"; $html .= 'TODO...'; $html .= '
'; $html .= '
'."\n"; // Project value $html .= '
'."\n"; $html .= '

Project value

'."\n"; $html .= '
'."\n"; $html .= '
'; $html .= 'COCOMO value
(Basic organic model)
'; $html .= '$'.formatNumber((empty($arraycocomo['proj']['currency']) ? 0 : $arraycocomo['proj']['currency']) + (empty($arraycocomo['dep']['currency']) ? 0 : $arraycocomo['dep']['currency']), 2).''; $html .= '
'; $html .= '
'; $html .= 'COCOMO effort
(Basic organic model)
'; $html .= ''.formatNumber($arraycocomo['proj']['people'] * $arraycocomo['proj']['effort'] + $arraycocomo['dep']['people'] * $arraycocomo['dep']['effort']); $html .= ' months people'; $html .= '
'; $html .= '
'; $html .= '
'."\n"; $tmp = ''; $nblines = 0; foreach ($output_arrtd as $line) { $reg = array(); //print $line."\n"; preg_match('/^::error file=(.*),line=(\d+),col=(\d+)::(.*)$/', $line, $reg); if (!empty($reg[1])) { if ($nblines < 20) { $tmp .= ''; } else { $tmp .= ''; } $tmp .= ''.$reg[1].''; $tmp .= ''; $tmp .= ''.$reg[2].''; $tmp .= ''; $tmp .= ''.$reg[4].''; $tmp .= ''."\n"; $nblines++; } } // Technical debt $html .= '
'."\n"; $html .= '

Technical debt (PHPStan level '.$phpstanlevel.' -> '.$nblines.' warnings)

'."\n"; $html .= '
'."\n"; $html .= '
'."\n"; $html .= ''."\n"; $html .= ''."\n"; $html .= $tmp; $html .= ''; $html .= '
FileLineType
Show all...
'; $html .= '
'; $html .= '
'; $html .= '
'."\n"; // JS code $html .= ' '; $html .= ''; $html .= ''; // Output report into a HTML file $fh = fopen($outputpath, 'w'); if ($fh) { fwrite($fh, $html); fclose($fh); print 'Generation of output file '.$outputfile.' done.'."\n"; } else { print 'Failed to open '.$outputfile.' for output.'."\n"; } /** * function to format a number * * @param string|int $number Number to format * @param int $nbdec Number of decimal digits * @return string Formatted string */ function formatNumber($number, $nbdec = 0) { return number_format($number, 0, '.', ' '); }