#!/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;
// Include Dolibarr environment
require_once $path.'../../htdocs/master.inc.php';
require_once $path.'../../htdocs/core/lib/files.lib.php';
require_once $path.'../../htdocs/core/lib/geturl.lib.php';
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|disabled] [--dir-phpstan=pathtophpstan|disabled]'."\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
if ($dirscc != 'disabled') {
$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
if ($dirphpstan != 'disabled') {
$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) {
if (!empty($arrayoflineofcode[$source])) {
foreach ($arrayoflineofcode[$source] as $val) {
$arrayofmax['Lines'] = max($arrayofmax['Lines'], $val['Lines']);
}
}
}
// Retrieve the .git information
$nbofdays = 90;
$delay = (3600 * 24 * $nbofdays);
$urlgit = 'https://api.github.com/search/issues?q=is:pr+repo:Dolibarr/dolibarr+created:>'.dol_print_date(dol_now() - $delay, "%Y-%m");
$arrayofalerts = array();
$arrayofalerts1 = $arrayofalerts2 = $arrayofalerts3 = array();
// Count lines of code of application
$newurl = $urlgit.'+CVE';
$result = getURLContent($newurl);
print 'Execute GET on github for '.$newurl."\n";
if ($result && $result['http_code'] == 200) {
$arrayofalerts1 = json_decode($result['content']);
foreach ($arrayofalerts1->items as $val) {
$tmpval = cleanVal($val);
if (preg_match('/CVE/i', $tmpval['title'])) {
$arrayofalerts[$tmpval['number']] = $tmpval;
}
}
} else {
print 'Error: failed to get github response';
exit(-1);
}
$newurl = $urlgit.'+yogosha';
$result = getURLContent($newurl);
print 'Execute GET on github for '.$newurl."\n";
if ($result && $result['http_code'] == 200) {
$arrayofalerts2 = json_decode($result['content']);
foreach ($arrayofalerts2->items as $val) {
$tmpval = cleanVal($val);
if (preg_match('/yogosha:/i', $tmpval['title'])) {
$arrayofalerts[$tmpval['number']] = $tmpval;
}
}
} else {
print 'Error: failed to get github response';
exit(-1);
}
$newurl = $urlgit.'+Sec:';
$result = getURLContent($newurl);
print 'Execute GET on github for '.$newurl."\n";
if ($result && $result['http_code'] == 200) {
$arrayofalerts3 = json_decode($result['content']);
foreach ($arrayofalerts3->items as $val) {
$tmpval = cleanVal($val);
if (preg_match('/Sec:/i', $tmpval['title'])) {
$arrayofalerts[$tmpval['number']] = $tmpval;
}
}
} else {
print 'Error: failed to get github response';
exit(-1);
}
$timeend = time();
$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 .= '| Language | ';
$html .= 'Bytes | ';
$html .= 'Files | ';
$html .= 'Lines | ';
$html .= ' | ';
$html .= 'Blanks | ';
$html .= 'Comments | ';
$html .= 'Code | ';
//$html .= ''.$val['Complexity'].' | ';
$html .= '
';
foreach (array('proj', 'dep') as $source) {
$html .= '';
if ($source == 'proj') {
$html .= '| All files without dependencies';
} elseif ($source == 'dep') {
$html .= ' | All files of dependencies only';
}
$html .= ' See detail per file type...';
$html .= ' | '.formatNumber($arrayofmetrics[$source]['Bytes']).' | ';
$html .= ''.formatNumber($arrayofmetrics[$source]['Files']).' | ';
$html .= ''.formatNumber($arrayofmetrics[$source]['Lines']).' | ';
$html .= ' | ';
$html .= ''.formatNumber($arrayofmetrics[$source]['Blanks']).' | ';
$html .= ''.formatNumber($arrayofmetrics[$source]['Comments']).' | ';
$html .= ''.formatNumber($arrayofmetrics[$source]['Code']).' | ';
//$html .= ' | ';
$html .= '
';
if (!empty($arrayoflineofcode[$source])) {
foreach ($arrayoflineofcode[$source] as $key => $val) {
$html .= '';
$html .= '| '.$key.' | ';
$html .= ' | ';
$html .= ''.(empty($val['Files']) ? '' : formatNumber($val['Files'])).' | ';
$html .= ''.(empty($val['Lines']) ? '' : formatNumber($val['Lines'])).' | ';
$html .= '';
$percent = $val['Lines'] / $arrayofmax['Lines'];
$widthbar = round(200 * $percent);
$html .= ' ';
$html .= ' | ';
$html .= ''.(empty($val['Blanks']) ? '' : formatNumber($val['Blanks'])).' | ';
$html .= ''.(empty($val['Comments']) ? '' : formatNumber($val['Comments'])).' | ';
$html .= ''.(empty($val['Code']) ? '' : formatNumber($val['Code'])).' | ';
//$html .= ''.(empty($val['Complexity']) ? '' : $val['Complexity']).' | ';
/*$html .= '';
$html .= '';
$html .= ' | ';
*/
$html .= '
';
}
}
}
$html .= '';
$html .= '| Total | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Bytes'] + $arrayofmetrics['dep']['Bytes']).' | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Files'] + $arrayofmetrics['dep']['Files']).' | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Lines'] + $arrayofmetrics['dep']['Lines']).' | ';
$html .= ' | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Blanks'] + $arrayofmetrics['dep']['Blanks']).' | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Comments'] + $arrayofmetrics['dep']['Comments']).' | ';
$html .= ''.formatNumber($arrayofmetrics['proj']['Code'] + $arrayofmetrics['dep']['Code']).' | ';
//$html .= ''.$arrayofmetrics['Complexity'].' | ';
//$html .= ' | ';
$html .= '
';
$html .= '
';
$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;
if (!empty($output_arrtd)) {
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++;
}
}
}
// Last security errors
$html .= ''."\n";
$html .= 'Last security alerts (last '.$nbofdays.' days)
'."\n";
$html .= ''."\n";
$html .= '
'."\n";
$html .= '
'."\n";
$html .= '| ID | Title | Date |
'."\n";
foreach ($arrayofalerts as $alert) {
$html .= '| #'.$alert['number'].' | ';
$html .= ''.$alert['title'].' | ';
$html .= $alert['created_at'].' | ';
$html .= '
';
}
$html .= '
';
$html .= '
';
$html .= '
';
$html .= '';
// Technical debt
$html .= ''."\n";
$html .= 'Technical debt (PHPStan level '.$phpstanlevel.' -> '.$nblines.' warnings)
'."\n";
$html .= ''."\n";
$html .= '
'."\n";
$html .= '
'."\n";
$html .= '| File | Line | Type |
'."\n";
$html .= $tmp;
$html .= '| Show all... |
';
$html .= '
';
$html .= '
';
$html .= '
';
$html .= ''."\n";
// JS code to allow to expand/collapse
$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, '.', ' ');
}
/**
* cleanVal
*
* @param array $val Array of a PR
* @return Array of a PR
*/
function cleanVal($val)
{
$tmpval = array();
$tmpval['url'] = $val->url;
$tmpval['number'] = $val->number;
$tmpval['title'] = $val->title;
$tmpval['created_at'] = $val->created_at;
$tmpval['updated_at'] = $val->updated_at;
return $tmpval;
}