├── .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 |
32 |
33 | ### Duplicate code checker
34 | ```
35 | $ phpcloc duplicate . --ext=php
36 | ```
37 |
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 |
--------------------------------------------------------------------------------