├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpcloc └── src ├── Analyzers ├── ClocAnalyzer.php └── DuplicateAnalyzer.php └── Commands ├── ClocCommand.php └── DuplicateCommand.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | temp 3 | composer.lock 4 | .DS_STORE 5 | generate-phar.php 6 | phpcloc.phar 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sohel Amin 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PHPCloc 3 | :rocket: Cloc & duplicate code checker written in PHP 4 | 5 | ## Requirements 6 | PHP >= 5.5.9 7 | 8 | ## Installation 9 | ### Manual 10 | ``` 11 | $ wget https://github.com/appzcoder/phpcloc/releases/download/v0.0.2/phpcloc.phar -O phpcloc 12 | // or 13 | $ curl -L https://github.com/appzcoder/phpcloc/releases/download/v0.0.2/phpcloc.phar -o phpcloc 14 | ``` 15 | Then 16 | ``` 17 | $ sudo chmod a+x phpcloc 18 | $ sudo mv phpcloc /usr/local/bin/phpcloc 19 | ``` 20 | 21 | ### Composer 22 | ``` 23 | $ composer global require appzcoder/phpcloc 24 | ``` 25 | 26 | ## Usage 27 | ### Cloc 28 | ``` 29 | $ phpcloc cloc . 30 | ``` 31 | cloc 32 | 33 | ### Duplicate code checker 34 | ``` 35 | $ phpcloc duplicate . --ext=php 36 | ``` 37 | duplicate 38 | 39 | #### Available Commands 40 | ``` 41 | $ phpcloc cloc directory --ext=php,js --exclude=vendor,node_modules 42 | ``` 43 | 44 | ``` 45 | $ phpcloc duplicate directory --ext=php --exclude=vendor 46 | ``` 47 | 48 | ## Todo 49 | - Improve algorithm complexity 50 | - Testing 51 | 52 | ## Author 53 | 54 | [Sohel Amin](http://sohelamin.com) 55 | 56 | ## License 57 | 58 | This project is licensed under the MIT License - see the [License File](LICENSE) for details 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appzcoder/phpcloc", 3 | "license": "MIT", 4 | "description": "Cloc & duplicate code checker tool.", 5 | "keywords": [ 6 | "phpcloc", 7 | "cloc", 8 | "count lines of code", 9 | "duplicate code checker" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Sohel Amin", 14 | "email": "sohelamincse@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.5.9", 19 | "symfony/console": "^3.4 || ^4.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Appzcoder\\PHPCloc\\": "src/" 24 | } 25 | }, 26 | "bin": [ 27 | "phpcloc" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /phpcloc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | Sohel Amin '; 21 | } 22 | } 23 | 24 | $application = new Application(); 25 | 26 | $application->add(new ClocCommand()); 27 | $application->add(new DuplicateCommand()); 28 | 29 | $application->run(); 30 | -------------------------------------------------------------------------------- /src/Analyzers/ClocAnalyzer.php: -------------------------------------------------------------------------------- 1 | 'ActionScript', 29 | 'ada' => 'Ada', 30 | 'adb' => 'Ada', 31 | 'ads' => 'Ada', 32 | 'Ant' => 'Ant', 33 | 'adoc' => 'AsciiDoc', 34 | 'asciidoc' => 'AsciiDoc', 35 | 'asm' => 'Assembly', 36 | 'S' => 'Assembly', 37 | 's' => 'Assembly', 38 | 'awk' => 'Awk', 39 | 'bat' => 'Batch', 40 | 'btm' => 'Batch', 41 | 'bb' => 'BitBake', 42 | 'cbl' => 'COBOL', 43 | 'cmd' => 'Batch', 44 | 'bash' => 'BASH', 45 | 'sh' => 'Bourne Shell', 46 | 'c' => 'C', 47 | 'carp' => 'Carp', 48 | 'csh' => 'C Shell', 49 | 'ec' => 'C', 50 | 'erl' => 'Erlang', 51 | 'hrl' => 'Erlang', 52 | 'pgc' => 'C', 53 | 'capnp' => 'Cap\'n Proto', 54 | 'chpl' => 'Chapel', 55 | 'cs' => 'C#', 56 | 'clj' => 'Clojure', 57 | 'coffee' => 'CoffeeScript', 58 | 'cfm' => 'ColdFusion', 59 | 'cfc' => 'ColdFusion CFScript', 60 | 'cmake' => 'CMake', 61 | 'cc' => 'C++', 62 | 'cpp' => 'C++', 63 | 'cxx' => 'C++', 64 | 'pcc' => 'C++', 65 | 'c++' => 'C++', 66 | 'cr' => 'Crystal', 67 | 'css' => 'CSS', 68 | 'cu' => 'CUDA', 69 | 'd' => 'D', 70 | 'dart' => 'Dart', 71 | 'dtrace' => 'DTrace', 72 | 'dts' => 'Device Tree', 73 | 'dtsi' => 'Device Tree', 74 | 'e' => 'Eiffel', 75 | 'elm' => 'Elm', 76 | 'el' => 'LISP', 77 | 'exp' => 'Expect', 78 | 'ex' => 'Elixir', 79 | 'exs' => 'Elixir', 80 | 'feature' => 'Gherkin', 81 | 'fish' => 'Fish', 82 | 'fr' => 'Frege', 83 | 'fst' => 'F*', 84 | 'F#' => 'F#', 85 | 'GLSL' => 'GLSL', 86 | 'vs' => 'GLSL', 87 | 'shader' => 'HLSL', 88 | 'cg' => 'HLSL', 89 | 'cginc' => 'HLSL', 90 | 'hlsl' => 'HLSL', 91 | 'lean' => 'Lean', 92 | 'hlean' => 'Lean', 93 | 'lgt' => 'Logtalk', 94 | 'lisp' => 'LISP', 95 | 'lsp' => 'LISP', 96 | 'lua' => 'Lua', 97 | 'ls' => 'LiveScript', 98 | 'sc' => 'LISP', 99 | 'f' => 'FORTRAN Legacy', 100 | 'f77' => 'FORTRAN Legacy', 101 | 'for' => 'FORTRAN Legacy', 102 | 'ftn' => 'FORTRAN Legacy', 103 | 'pfo' => 'FORTRAN Legacy', 104 | 'f90' => 'FORTRAN Modern', 105 | 'f95' => 'FORTRAN Modern', 106 | 'f03' => 'FORTRAN Modern', 107 | 'f08' => 'FORTRAN Modern', 108 | 'go' => 'Go', 109 | 'groovy' => 'Groovy', 110 | 'gradle' => 'Groovy', 111 | 'h' => 'C Header', 112 | 'hs' => 'Haskell', 113 | 'hpp' => 'C++ Header', 114 | 'hh' => 'C++ Header', 115 | 'html' => 'HTML', 116 | 'hx' => 'Haxe', 117 | 'hxx' => 'C++ Header', 118 | 'idr' => 'Idris', 119 | 'il' => 'SKILL', 120 | 'io' => 'Io', 121 | 'ipynb' => 'Jupyter Notebook', 122 | 'jai' => 'JAI', 123 | 'java' => 'Java', 124 | 'js' => 'JavaScript', 125 | 'jl' => 'Julia', 126 | 'json' => 'JSON', 127 | 'jsx' => 'JSX', 128 | 'kt' => 'Kotlin', 129 | 'lds' => 'LD Script', 130 | 'less' => 'LESS', 131 | 'Objective-C' => 'Objective-C', 132 | 'Matlab' => 'MATLAB', 133 | 'Mercury' => 'Mercury', 134 | 'md' => 'Markdown', 135 | 'markdown' => 'Markdown', 136 | 'nix' => 'Nix', 137 | 'nsi' => 'NSIS', 138 | 'nsh' => 'NSIS', 139 | 'nu' => 'Nu', 140 | 'ML' => 'OCaml', 141 | 'ml' => 'OCaml', 142 | 'mli' => 'OCaml', 143 | 'mll' => 'OCaml', 144 | 'mly' => 'OCaml', 145 | 'mm' => 'Objective-C++', 146 | 'maven' => 'Maven', 147 | 'makefile' => 'Makefile', 148 | 'mustache' => 'Mustache', 149 | 'm4' => 'M4', 150 | 'l' => 'lex', 151 | 'nim' => 'Nim', 152 | 'php' => 'PHP', 153 | 'pas' => 'Pascal', 154 | 'PL' => 'Perl', 155 | 'pl' => 'Perl', 156 | 'pm' => 'Perl', 157 | 'plan9sh' => 'Plan9 Shell', 158 | 'pony' => 'Pony', 159 | 'ps1' => 'PowerShell', 160 | 'text' => 'Plain Text', 161 | 'txt' => 'Plain Text', 162 | 'polly' => 'Polly', 163 | 'proto' => 'Protocol Buffers', 164 | 'py' => 'Python', 165 | 'pxd' => 'Cython', 166 | 'pyx' => 'Cython', 167 | 'r' => 'R', 168 | 'R' => 'R', 169 | 'raml' => 'RAML', 170 | 'Rebol' => 'Rebol', 171 | 'red' => 'Red', 172 | 'Rmd' => 'RMarkdown', 173 | 'rake' => 'Ruby', 174 | 'rb' => 'Ruby', 175 | 'rkt' => 'Racket', 176 | 'rhtml' => 'Ruby HTML', 177 | 'rs' => 'Rust', 178 | 'rst' => 'ReStructuredText', 179 | 'sass' => 'Sass', 180 | 'scala' => 'Scala', 181 | 'scss' => 'Sass', 182 | 'scm' => 'Scheme', 183 | 'sed' => 'sed', 184 | 'stan' => 'Stan', 185 | 'sml' => 'Standard ML', 186 | 'sol' => 'Solidity', 187 | 'sql' => 'SQL', 188 | 'swift' => 'Swift', 189 | 't' => 'Terra', 190 | 'tex' => 'TeX', 191 | 'thy' => 'Isabelle', 192 | 'tla' => 'TLA', 193 | 'sty' => 'TeX', 194 | 'tcl' => 'Tcl/Tk', 195 | 'toml' => 'TOML', 196 | 'ts' => 'TypeScript', 197 | 'mat' => 'Unity-Prefab', 198 | 'prefab' => 'Unity-Prefab', 199 | 'Coq' => 'Coq', 200 | 'vala' => 'Vala', 201 | 'Verilog' => 'Verilog', 202 | 'csproj' => 'MSBuild script', 203 | 'vcproj' => 'MSBuild script', 204 | 'vim' => 'VimL', 205 | 'xml' => 'XML', 206 | 'XML' => 'XML', 207 | 'xsd' => 'XSD', 208 | 'xsl' => 'XSLT', 209 | 'xslt' => 'XSLT', 210 | 'wxs' => 'WiX', 211 | 'yaml' => 'YAML', 212 | 'yml' => 'YAML', 213 | 'y' => 'Yacc', 214 | 'zep' => 'Zephir', 215 | 'zsh' => 'Zsh', 216 | ]; 217 | 218 | /** 219 | * Constructor method. 220 | * 221 | * @param string $path 222 | * @param string $ext 223 | * @param string $exclude 224 | */ 225 | public function __construct($path, $ext, $exclude) 226 | { 227 | $directoryIterator = new RecursiveDirectoryIterator($path); 228 | $filterIterator = new RecursiveCallbackFilterIterator( 229 | $directoryIterator, 230 | function ($current, $key, $iterator) use ($ext, $exclude) { 231 | // Allow recursion 232 | if ($iterator->hasChildren() && !in_array($current->getFilename(), explode(',', $exclude))) { 233 | return true; 234 | } elseif ($current->isFile() && (in_array($current->getExtension(), explode(',', $ext)) || !$ext)) { 235 | return true; 236 | } else { 237 | return false; 238 | } 239 | } 240 | ); 241 | $files = new RecursiveIteratorIterator($filterIterator); 242 | 243 | foreach ($files as $file) { 244 | if ($file->isFile()) { 245 | $this->processLines($file->openFile()); 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * Process the lines from a file. 252 | * 253 | * @param SplFileObject $file 254 | * 255 | * @return void 256 | */ 257 | protected function processLines(SplFileObject $file) 258 | { 259 | $extension = $file->getExtension(); 260 | 261 | $totalLines = 0; 262 | $totalBlankLines = 0; 263 | $totalComments = 0; 264 | $isMultilines = false; 265 | while ($file->valid()) { 266 | $currentLine = $file->fgets(); 267 | $trimLine = trim($currentLine); 268 | 269 | // Ignoring the last new line 270 | if ($file->eof() && empty($trimLine)) { 271 | break; 272 | } 273 | 274 | $totalLines ++; 275 | if (empty($trimLine)) { 276 | $totalBlankLines ++; 277 | } 278 | 279 | // Detecting comments 280 | if (strpos($trimLine, '//') === 0 281 | || strpos($trimLine, '#') === 0) { 282 | $totalComments ++; 283 | } 284 | 285 | // Detecting multilines comments 286 | if (strpos($trimLine, '/*') === 0) { 287 | $isMultilines = true; 288 | } 289 | if ($isMultilines) { 290 | $totalComments ++; 291 | } 292 | if (strpos($trimLine, '*/') === 0) { 293 | $isMultilines = false; 294 | } 295 | } 296 | 297 | if (!isset($this->extensions[$extension])) { 298 | return; 299 | } 300 | 301 | $this->setStats($extension, [ 302 | 'language' => $this->extensions[$extension], 303 | 'files' => 1, 304 | 'blank' => $totalBlankLines, 305 | 'comment' => $totalComments, 306 | 'code' => $totalLines - ($totalBlankLines + $totalComments), 307 | ]); 308 | } 309 | 310 | /** 311 | * Set or increment the stat. 312 | * 313 | * @param string $extension 314 | * @param array $stat 315 | * 316 | * @return void 317 | */ 318 | protected function setStats($extension, $stat) 319 | { 320 | if (isset($this->stats[$extension])) { 321 | $this->stats[$extension]['files'] += $stat['files']; 322 | $this->stats[$extension]['blank'] += $stat['blank']; 323 | $this->stats[$extension]['comment'] += $stat['comment']; 324 | $this->stats[$extension]['code'] += $stat['code']; 325 | } else { 326 | $this->stats[$extension] = $stat; 327 | } 328 | } 329 | 330 | /** 331 | * Return the stats. 332 | * 333 | * @return array 334 | */ 335 | public function stats() 336 | { 337 | return $this->stats; 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/Analyzers/DuplicateAnalyzer.php: -------------------------------------------------------------------------------- 1 | hasChildren() && !in_array($current->getFilename(), explode(',', $exclude))) { 38 | return true; 39 | } 40 | 41 | if ($current->isFile() && in_array($current->getExtension(), explode(',', $ext))) { 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | ); 48 | $files = new RecursiveIteratorIterator($filterIterator); 49 | 50 | foreach ($files as $file) { 51 | if ($file->isFile()) { 52 | $this->processLines($file->openFile()); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Process the lines from a file. 59 | * 60 | * @param SplFileObject $file 61 | * 62 | * @return void 63 | */ 64 | protected function processLines(SplFileObject $file) 65 | { 66 | $filename = $file->getPathname(); 67 | 68 | $lines = []; 69 | $duplicates = []; 70 | foreach ($file as $line) { 71 | $trimLine = trim($line); 72 | $lineNo = ($file->key() + 1); 73 | 74 | if (isset($lines[$trimLine])) { 75 | $foundLineNo = $lines[$trimLine]; 76 | if (!in_array($foundLineNo, $duplicates)) { 77 | $duplicates[] = $foundLineNo; 78 | } 79 | 80 | $duplicates[] = $lineNo; 81 | } else if (strlen($trimLine) > 3) { 82 | // Non duplicate first line 83 | $lines[$trimLine] = $lineNo; 84 | } 85 | } 86 | 87 | $totalDuplicates = count($duplicates); 88 | if ($totalDuplicates > 0) { 89 | sort($duplicates); 90 | $duplicatesStr = ''; 91 | foreach (array_chunk($duplicates, 10) as $chunk) { 92 | $duplicatesStr .= implode(', ', $chunk) . PHP_EOL; 93 | } 94 | 95 | $this->stats[$filename] = [ 96 | 'file' => $filename, 97 | 'duplicate' => $totalDuplicates, 98 | 'line_no' => $duplicatesStr, 99 | ]; 100 | } 101 | } 102 | 103 | /** 104 | * Return the stats. 105 | * 106 | * @return array 107 | */ 108 | public function stats() 109 | { 110 | return $this->stats; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Commands/ClocCommand.php: -------------------------------------------------------------------------------- 1 | setName('cloc') 28 | ->setDescription('Count the total lines of code.') 29 | ->addArgument('path', InputArgument::REQUIRED, 'The path to scan.') 30 | ->addOption( 31 | 'ext', 32 | null, 33 | InputOption::VALUE_REQUIRED, 34 | 'Which extension are you looking for?' 35 | ) 36 | ->addOption( 37 | 'exclude', 38 | null, 39 | InputOption::VALUE_REQUIRED, 40 | 'Dir(s) to exclude. eg. --exclude=vendor,node_modules' 41 | ) 42 | ; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function execute(InputInterface $input, OutputInterface $output) 49 | { 50 | $path = $input->getArgument('path'); 51 | $ext = $input->getOption('ext'); 52 | $exclude = $input->getOption('exclude'); 53 | 54 | $stats = array_values((new ClocAnalyzer($path, $ext, $exclude))->stats()); 55 | if (count($stats) === 0) { 56 | $output->writeln('No files found.'); 57 | return; 58 | } 59 | 60 | array_push( 61 | $stats, 62 | new TableSeparator(), 63 | [ 64 | 'Total', 65 | array_reduce($stats, function ($carry, $item) { 66 | return $carry + $item['files']; 67 | }), 68 | array_reduce($stats, function ($carry, $item) { 69 | return $carry + $item['blank']; 70 | }), 71 | array_reduce($stats, function ($carry, $item) { 72 | return $carry + $item['comment']; 73 | }), 74 | array_reduce($stats, function ($carry, $item) { 75 | return $carry + $item['code']; 76 | }), 77 | ] 78 | ); 79 | 80 | $table = new Table($output); 81 | $table 82 | ->setHeaders(['Language', 'files', 'blank', 'comment', 'code']) 83 | ->setRows($stats) 84 | ; 85 | $table->setColumnWidths([20, 10, 10, 10, 10]); 86 | $table->render(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Commands/DuplicateCommand.php: -------------------------------------------------------------------------------- 1 | setName('duplicate') 29 | ->setDescription('Check code duplicates.') 30 | ->addArgument('path', InputArgument::REQUIRED, 'The path to scan.') 31 | ->addOption( 32 | 'ext', 33 | null, 34 | InputOption::VALUE_REQUIRED, 35 | 'Which extension are you looking for?', 36 | 'php' 37 | ) 38 | ->addOption( 39 | 'exclude', 40 | null, 41 | InputOption::VALUE_REQUIRED, 42 | 'Dir(s) to exclude. eg. --exclude=vendor,node_modules' 43 | ) 44 | ; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function execute(InputInterface $input, OutputInterface $output) 51 | { 52 | $path = $input->getArgument('path'); 53 | $ext = $input->getOption('ext'); 54 | $exclude = $input->getOption('exclude'); 55 | 56 | $stats = (new DuplicateAnalyzer($path, $ext, $exclude))->stats(); 57 | if (count($stats) === 0) { 58 | $output->writeln('No files found.'); 59 | return; 60 | } 61 | 62 | array_push( 63 | $stats, 64 | new TableSeparator(), 65 | [ 66 | 'Total', 67 | new TableCell( 68 | array_reduce($stats, function ($carry, $item) { 69 | return $carry + $item['duplicate']; 70 | }), 71 | ['colspan' => 2] 72 | ), 73 | ] 74 | ); 75 | 76 | $table = new Table($output); 77 | $table 78 | ->setHeaders(['File', 'Duplicate', 'In Line(s)']) 79 | ->setRows($stats) 80 | ; 81 | $table->render(); 82 | } 83 | } 84 | --------------------------------------------------------------------------------