apstats.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #!/usr/bin/env php
  2. <?php
  3. /*
  4. * Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * \file dev/tools/apstats.php
  21. * \brief Script to report Advanced Statistics on a coding project
  22. */
  23. $sapi_type = php_sapi_name();
  24. $script_file = basename(__FILE__);
  25. $path = dirname(__FILE__) . '/';
  26. // Test si mode batch
  27. $sapi_type = php_sapi_name();
  28. if (substr($sapi_type, 0, 3) == 'cgi') {
  29. echo "Error: You are using PHP for CGI. To execute " . $script_file . " from command line, you must use PHP for CLI mode.\n";
  30. exit();
  31. }
  32. error_reporting(E_ALL & ~E_DEPRECATED);
  33. define('PRODUCT', "apstats");
  34. define('VERSION', "1.0");
  35. $phpstanlevel = 2;
  36. print '***** '.constant('PRODUCT').' - '.constant('VERSION').' *****'."\n";
  37. if (empty($argv[1])) {
  38. print 'You must run this tool being into the root of the project.'."\n";
  39. print 'Usage: '.constant('PRODUCT').'.php pathto/outputfile.html [--dir-scc=pathtoscc] [--dir-phpstan=pathtophpstan]'."\n";
  40. print 'Example: '.constant('PRODUCT').'.php documents/apstats/index.html --dir-scc=/snap/bin --dir-phpstan=~/git/phpstan/htdocs/includes/bin';
  41. exit(0);
  42. }
  43. $outputpath = $argv[1];
  44. $outputdir = dirname($outputpath);
  45. $outputfile = basename($outputpath);
  46. if (!is_dir($outputdir)) {
  47. print 'Error: dir '.$outputdir.' does not exists or is not writable'."\n";
  48. exit(1);
  49. }
  50. $dirscc = '';
  51. $dirphpstan = '';
  52. $i = 0;
  53. while ($i < $argc) {
  54. $reg = array();
  55. if (preg_match('/--dir-scc=(.*)$/', $argv[$i], $reg)) {
  56. $dirscc = $reg[1];
  57. }
  58. if (preg_match('/--dir-phpstan=(.*)$/', $argv[$i], $reg)) {
  59. $dirphpstan = $reg[1];
  60. }
  61. $i++;
  62. }
  63. $timestart = time();
  64. // Count lines of code of Dolibarr itself
  65. /*
  66. $commandcheck = 'cloc . --exclude-dir=includes --exclude-dir=custom --ignore-whitespace --vcs=git';
  67. $resexec = shell_exec($commandcheck);
  68. $resexec = (int) (empty($resexec) ? 0 : trim($resexec));
  69. // Count lines of code of external dependencies
  70. $commandcheck = 'cloc htdocs/includes --ignore-whitespace --vcs=git';
  71. $resexec = shell_exec($commandcheck);
  72. $resexec = (int) (empty($resexec) ? 0 : trim($resexec));
  73. */
  74. // Count lines of code of application
  75. $commandcheck = ($dirscc ? $dirscc.'/' : '').'scc . --exclude-dir=htdocs/includes,htdocs/custom,htdocs/theme/common/fontawesome-5,htdocs/theme/common/octicons';
  76. print 'Execute SCC to count lines of code in project: '.$commandcheck."\n";
  77. $output_arrproj = array();
  78. $resexecproj = 0;
  79. exec($commandcheck, $output_arrproj, $resexecproj);
  80. // Count lines of code of dependencies
  81. $commandcheck = ($dirscc ? $dirscc.'/' : '').'scc htdocs/includes htdocs/theme/common/fontawesome-5 htdocs/theme/common/octicons';
  82. print 'Execute SCC to count lines of code in dependencies: '.$commandcheck."\n";
  83. $output_arrdep = array();
  84. $resexecdep = 0;
  85. exec($commandcheck, $output_arrdep, $resexecdep);
  86. // Get technical debt
  87. $commandcheck = ($dirphpstan ? $dirphpstan.'/' : '').'phpstan --level='.$phpstanlevel.' -v analyze -a build/phpstan/bootstrap.php --memory-limit 5G --error-format=github';
  88. print 'Execute PHPStan to get the technical debt: '.$commandcheck."\n";
  89. $output_arrtd = array();
  90. $resexectd = 0;
  91. exec($commandcheck, $output_arrtd, $resexectd);
  92. $arrayoflineofcode = array();
  93. $arraycocomo = array();
  94. $arrayofmetrics = array(
  95. 'proj' => array('Bytes' => 0, 'Files' => 0, 'Lines' => 0, 'Blanks' => 0, 'Comments' => 0, 'Code' => 0, 'Complexity' => 0),
  96. 'dep' => array('Bytes' => 0, 'Files' => 0, 'Lines' => 0, 'Blanks' => 0, 'Comments' => 0, 'Code' => 0, 'Complexity' => 0)
  97. );
  98. // Analyse $output_arrproj
  99. foreach (array('proj', 'dep') as $source) {
  100. print 'Analyze SCC result for lines of code for '.$source."\n";
  101. if ($source == 'proj') {
  102. $output_arr = &$output_arrproj;
  103. } elseif ($source == 'dep') {
  104. $output_arr = &$output_arrdep;
  105. } else {
  106. print 'Bad value for $source';
  107. die();
  108. }
  109. foreach ($output_arr as $line) {
  110. if (preg_match('/^(───|Language|Total)/', $line)) {
  111. continue;
  112. }
  113. //print $line."<br>\n";
  114. if (preg_match('/^Estimated Cost.*\$(.*)/i', $line, $reg)) {
  115. $arraycocomo[$source]['currency'] = preg_replace('/[^\d\.]/', '', str_replace(array(',', ' '), array('', ''), $reg[1]));
  116. }
  117. if (preg_match('/^Estimated Schedule Effort.*\s([\d\s,]+)/i', $line, $reg)) {
  118. $arraycocomo[$source]['effort'] = str_replace(array(',', ' '), array('.', ''), $reg[1]);
  119. }
  120. if (preg_match('/^Estimated People.*\s([\d\s,]+)/i', $line, $reg)) {
  121. $arraycocomo[$source]['people'] = str_replace(array(',', ' '), array('.', ''), $reg[1]);
  122. }
  123. if (preg_match('/^Processed\s(\d+)\s/i', $line, $reg)) {
  124. $arrayofmetrics[$source]['Bytes'] = $reg[1];
  125. }
  126. if (preg_match('/^(.*)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/', $line, $reg)) {
  127. $arrayoflineofcode[$source][$reg[1]]['Files'] = $reg[2];
  128. $arrayoflineofcode[$source][$reg[1]]['Lines'] = $reg[3];
  129. $arrayoflineofcode[$source][$reg[1]]['Blanks'] = $reg[4];
  130. $arrayoflineofcode[$source][$reg[1]]['Comments'] = $reg[5];
  131. $arrayoflineofcode[$source][$reg[1]]['Code'] = $reg[6];
  132. $arrayoflineofcode[$source][$reg[1]]['Complexity'] = $reg[7];
  133. }
  134. }
  135. if (!empty($arrayoflineofcode[$source])) {
  136. foreach ($arrayoflineofcode[$source] as $key => $val) {
  137. $arrayofmetrics[$source]['Files'] += $val['Files'];
  138. $arrayofmetrics[$source]['Lines'] += $val['Lines'];
  139. $arrayofmetrics[$source]['Blanks'] += $val['Blanks'];
  140. $arrayofmetrics[$source]['Comments'] += $val['Comments'];
  141. $arrayofmetrics[$source]['Code'] += $val['Code'];
  142. $arrayofmetrics[$source]['Complexity'] += $val['Complexity'];
  143. }
  144. }
  145. }
  146. $timeend = time();
  147. /*
  148. * View
  149. */
  150. $html = '<html>'."\n";
  151. $html .= '<meta charset="utf-8">'."\n";
  152. $html .= '<meta http-equiv="refresh" content="300">'."\n";
  153. $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">'."\n";
  154. $html .= '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css" integrity="sha512-q3eWabyZPc1XTCmF+8/LuE1ozpg5xxn7iO89yfSOd5/oKvyqLngoNGsx8jq92Y8eXJ/IRxQbEC+FGSYxtk2oiw==" crossorigin="anonymous" referrerpolicy="no-referrer" />'."\n";
  155. $html .= '<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js" integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>'."\n";
  156. $html .= '
  157. <style>
  158. body {
  159. margin: 10px;
  160. margin-left: 50px;
  161. margin-right: 50px;
  162. }
  163. h1 {
  164. font-size: 1.5em;
  165. font-weight: bold;
  166. padding-top: 5px;
  167. padding-bottom: 5px;
  168. margin-top: 5px;
  169. margin-bottom: 5px;
  170. }
  171. header {
  172. text-align: center;
  173. }
  174. header, section.chapter {
  175. margin-top: 10px;
  176. margin-bottom: 10px;
  177. padding: 10px;
  178. }
  179. table {
  180. border-collapse: collapse;
  181. }
  182. th,td {
  183. padding-top: 5px;
  184. padding-bottom: 5px;
  185. padding-left: 10px;
  186. padding-right: 10px;
  187. }
  188. .left {
  189. text-align: left;
  190. }
  191. .right {
  192. text-align: right;
  193. }
  194. .nowrap {
  195. white-space: nowrap;
  196. }
  197. .opacitymedium {
  198. opacity: 0.5;
  199. }
  200. .centpercent {
  201. width: 100%;
  202. }
  203. .hidden {
  204. display: none;
  205. }
  206. .trgroup {
  207. background-color: #EEE;
  208. }
  209. .seedetail {
  210. color: #000088;
  211. cursor: pointer;
  212. }
  213. .box {
  214. padding: 20px;
  215. font-size: 1.2em;
  216. margin-top: 10px;
  217. margin-bottom: 10px;
  218. width: 200px;
  219. }
  220. .box.inline-box {
  221. display: inline-block;
  222. text-align: center;
  223. margin-left: 10px;
  224. }
  225. .back1 {
  226. background-color: #884466;
  227. color: #FFF;
  228. }
  229. .back2 {
  230. background-color: #664488;
  231. color: #FFF;
  232. }
  233. div.fiche>form>div.div-table-responsive {
  234. min-height: 392px;
  235. }
  236. div.fiche>form>div.div-table-responsive, div.fiche>form>div.div-table-responsive-no-min {
  237. overflow-x: auto;
  238. }
  239. .div-table-responsive {
  240. line-height: 120%;
  241. }
  242. .div-table-responsive, .div-table-responsive-no-min {
  243. overflow-x: auto;
  244. min-height: 0.01%;
  245. }
  246. /* Force values for small screen 767 */
  247. @media only screen and (max-width: 767px)
  248. {
  249. body {
  250. margin: 5px;
  251. margin-left: 5px;
  252. margin-right: 5px;
  253. }
  254. }
  255. </style>'."\n";
  256. $html .= '<body>'."\n";
  257. $html .= '<header>'."\n";
  258. $html .= '<h1>Advanced Project Statistics</h1>'."\n";
  259. $currentDate = date("Y-m-d H:i:s"); // Format: Year-Month-Day Hour:Minute:Second
  260. $html .= '<span class="opacitymedium">Generated on '.$currentDate.' in '.($timeend - $timestart).' seconds</span>'."\n";
  261. $html .= '</header>'."\n";
  262. $html .= '<section class="chapter" id="linesofcode">'."\n";
  263. $html .= '<h2>Lines of code</h2>'."\n";
  264. $html .= '<div class="div-table-responsive">'."\n";
  265. $html .= '<table class="centpercent">';
  266. $html .= '<tr class="loc">';
  267. $html .= '<th class="left">Language</th>';
  268. $html .= '<th class="right">Bytes</th>';
  269. $html .= '<th class="right">Files</th>';
  270. $html .= '<th class="right">Lines</th>';
  271. $html .= '<th class="right">Blanks</th>';
  272. $html .= '<th class="right">Comments</th>';
  273. $html .= '<th class="right">Code</th>';
  274. //$html .= '<td class="right">'.$val['Complexity'].'</td>';
  275. $html .= '</tr>';
  276. foreach (array('proj', 'dep') as $source) {
  277. $html .= '<tr class="trgroup" id="source'.$source.'">';
  278. if ($source == 'proj') {
  279. $html .= '<td>All files without dependencies';
  280. } elseif ($source == 'dep') {
  281. $html .= '<td>All files of dependencies only';
  282. }
  283. $html .= ' &nbsp; &nbsp; <span class="seedetail" data-source="'.$source.'">(See detail per file type...)</span>';
  284. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Bytes']).'</td>';
  285. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Files']).'</td>';
  286. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Lines']).'</td>';
  287. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Blanks']).'</td>';
  288. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Comments']).'</td>';
  289. $html .= '<td class="right">'.formatNumber($arrayofmetrics[$source]['Code']).'</td>';
  290. $html .= '<td></td>';
  291. $html .= '</tr>';
  292. if (!empty($arrayoflineofcode[$source])) {
  293. foreach ($arrayoflineofcode[$source] as $key => $val) {
  294. $html .= '<tr class="loc hidden source'.$source.' language'.str_replace(' ', '', $key).'">';
  295. $html .= '<td>'.$key.'</td>';
  296. $html .= '<td class="right"></td>';
  297. $html .= '<td class="right nowrap">'.(empty($val['Files']) ? '' : formatNumber($val['Files'])).'</td>';
  298. $html .= '<td class="right nowrap">'.(empty($val['Lines']) ? '' : formatNumber($val['Lines'])).'</td>';
  299. $html .= '<td class="right nowrap">'.(empty($val['Blanks']) ? '' : formatNumber($val['Blanks'])).'</td>';
  300. $html .= '<td class="right nowrap">'.(empty($val['Comments']) ? '' : formatNumber($val['Comments'])).'</td>';
  301. $html .= '<td class="right nowrap">'.(empty($val['Code']) ? '' : formatNumber($val['Code'])).'</td>';
  302. //$html .= '<td class="right">'.(empty($val['Complexity']) ? '' : $val['Complexity']).'</td>';
  303. $html .= '<td class="nowrap">TODO graph here...</td>';
  304. $html .= '</tr>';
  305. }
  306. }
  307. }
  308. $html .= '<tr class="trgroup">';
  309. $html .= '<td class="left">Total</td>';
  310. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Bytes'] + $arrayofmetrics['dep']['Bytes']).'</td>';
  311. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Files'] + $arrayofmetrics['dep']['Files']).'</td>';
  312. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Lines'] + $arrayofmetrics['dep']['Lines']).'</td>';
  313. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Blanks'] + $arrayofmetrics['dep']['Blanks']).'</td>';
  314. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Comments'] + $arrayofmetrics['dep']['Comments']).'</td>';
  315. $html .= '<td class="right nowrap">'.formatNumber($arrayofmetrics['proj']['Code'] + $arrayofmetrics['dep']['Code']).'</td>';
  316. //$html .= '<td>'.$arrayofmetrics['Complexity'].'</td>';
  317. $html .= '<td></td>';
  318. $html .= '</tr>';
  319. $html .= '</table>';
  320. $html .= '</div>';
  321. $html .= '</section>'."\n";
  322. $html .= '<section class="chapter" id="projectvalue">'."\n";
  323. $html .= '<h2>Project value</h2><br>'."\n";
  324. $html .= '<div class="box inline-box back1">';
  325. $html .= 'COCOMO (Basic organic model) value:<br>';
  326. $html .= '<b>$'.formatNumber((empty($arraycocomo['proj']['currency']) ? 0 : $arraycocomo['proj']['currency']) + (empty($arraycocomo['dep']['currency']) ? 0 : $arraycocomo['dep']['currency']), 2).'</b>';
  327. $html .= '</div>';
  328. $html .= '<div class="box inline-box back2">';
  329. $html .= 'COCOMO (Basic organic model) effort<br>';
  330. $html .= '<b>'.formatNumber($arraycocomo['proj']['people'] * $arraycocomo['proj']['effort'] + $arraycocomo['dep']['people'] * $arraycocomo['dep']['effort']);
  331. $html .= ' monthes people</b><br>';
  332. $html .= '</section>'."\n";
  333. $tmp = '';
  334. $nblines = 0;
  335. foreach ($output_arrtd as $line) {
  336. $reg = array();
  337. //print $line."\n";
  338. preg_match('/^::error file=(.*),line=(\d+),col=(\d+)::(.*)$/', $line, $reg);
  339. if (!empty($reg[1])) {
  340. $tmp .= '<tr><td>'.$reg[1].'</td><td>'.$reg[2].'</td><td>'.$reg[4].'</td></tr>'."\n";
  341. $nblines++;
  342. }
  343. }
  344. $html .= '<section class="chapter" id="technicaldebt">'."\n";
  345. $html .= '<h2>Technical debt <span class="opacitymedium">(PHPStan level '.$phpstanlevel.' -> '.$nblines.' warnings)</span></h2><br>'."\n";
  346. $html .= '<div class="div-table-responsive">'."\n";
  347. $html .= '<table class="list_technical_debt">'."\n";
  348. $html .= '<tr><td>File</td><td>Line</td><td>Type</td></tr>'."\n";
  349. $html .= $tmp;
  350. $html .= '</table>';
  351. $html .= '</div>';
  352. $html .= '</section>'."\n";
  353. $html .= '
  354. <script>
  355. $(document).ready(function() {
  356. $( ".seedetail" ).on( "click", function() {
  357. var source = $(this).attr("data-source");
  358. console.log("Click on "+source);
  359. jQuery(".source"+source).toggle();
  360. } );
  361. });
  362. </script>
  363. ';
  364. $html .= '</body>';
  365. $html .= '</html>';
  366. $fh = fopen($outputpath, 'w');
  367. if ($fh) {
  368. fwrite($fh, $html);
  369. fclose($fh);
  370. print 'Generation of output file '.$outputfile.' done.'."\n";
  371. } else {
  372. print 'Failed to open '.$outputfile.' for ouput.'."\n";
  373. }
  374. /**
  375. * function to format a number
  376. *
  377. * @param string|int $number Number to format
  378. * @param int $nbdec Number of decimal digits
  379. * @return string Formated string
  380. */
  381. function formatNumber($number, $nbdec = 0)
  382. {
  383. return number_format($number, 0, '.', ' ');
  384. }