Skip to content

Commit 228cc02

Browse files
committed
Initial version of lib
0 parents  commit 228cc02

File tree

6 files changed

+356
-0
lines changed

6 files changed

+356
-0
lines changed

.gitignore

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Symfony template
3+
# Cache and logs (Symfony2)
4+
/app/cache/*
5+
/app/logs/*
6+
!app/cache/.gitkeep
7+
!app/logs/.gitkeep
8+
9+
# Email spool folder
10+
/app/spool/*
11+
12+
# Cache, session files and logs (Symfony3)
13+
/var/cache/*
14+
/var/logs/*
15+
/var/sessions/*
16+
!var/cache/.gitkeep
17+
!var/logs/.gitkeep
18+
!var/sessions/.gitkeep
19+
20+
# Parameters
21+
/app/config/parameters.yml
22+
/app/config/parameters.ini
23+
24+
# Managed by Composer
25+
/app/bootstrap.php.cache
26+
/var/bootstrap.php.cache
27+
/bin/*
28+
!bin/console
29+
!bin/symfony_requirements
30+
31+
# Assets and user uploads
32+
/web/bundles/
33+
/web/uploads/
34+
35+
# PHPUnit
36+
/app/phpunit.xml
37+
/phpunit.xml
38+
39+
# Build data
40+
/build/
41+
42+
# Backup entities generated with doctrine:generate:entities command
43+
**/Entity/*~
44+
45+
# Embedded web-server pid file
46+
/.web-server-pid
47+
### Composer template
48+
composer.phar
49+
/vendor/
50+
51+
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
52+
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
53+
# composer.lock
54+
### JetBrains template
55+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
56+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
57+
58+
# User-specific stuff:
59+
.idea/**/workspace.xml
60+
.idea/**/tasks.xml
61+
.idea/dictionaries
62+
63+
# Sensitive or high-churn files:
64+
.idea/**/dataSources/
65+
.idea/**/dataSources.ids
66+
.idea/**/dataSources.xml
67+
.idea/**/dataSources.local.xml
68+
.idea/**/sqlDataSources.xml
69+
.idea/**/dynamic.xml
70+
.idea/**/uiDesigner.xml
71+
72+
# Gradle:
73+
.idea/**/gradle.xml
74+
.idea/**/libraries
75+
76+
# CMake
77+
cmake-build-debug/
78+
79+
# Mongo Explorer plugin:
80+
.idea/**/mongoSettings.xml
81+
82+
## File-based project format:
83+
*.iws
84+
85+
## Plugin-specific files:
86+
87+
# IntelliJ
88+
out/
89+
90+
# mpeltonen/sbt-idea plugin
91+
.idea_modules/
92+
93+
# JIRA plugin
94+
atlassian-ide-plugin.xml
95+
96+
# Cursive Clojure plugin
97+
.idea/replstate.xml
98+
99+
# Crashlytics plugin (for Android Studio and IntelliJ)
100+
com_crashlytics_export_strings.xml
101+
crashlytics.properties
102+
crashlytics-build.properties
103+
fabric.properties
104+
105+
106+
zsh_history
107+
composer.lock
108+
.idea
109+
110+
docker-compose.yml

composer.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "bronek/code-complexity",
3+
"description": "Simple tool to measure code cyclomatic complexity as described on https://phpmd.org/rules/codesize.html",
4+
"type": "project",
5+
"require": {
6+
"nikic/php-parser": "^3.1",
7+
"symfony/console": "^3.3"
8+
},
9+
"require-dev": {
10+
"phpspec/phpspec": "^4.0",
11+
"phpunit/phpunit": "^6.3"
12+
},
13+
"autoload": {
14+
"psr-4": {
15+
"Bronek\\CodeComplexity\\": "src/"
16+
}
17+
},
18+
"autoload-dev": {
19+
"psr-4": {
20+
"spec\\Bronek\\CodeComplexity\\": "spec/",
21+
"tests\\Bronek\\CodeComplexity\\": "tests/"
22+
}
23+
},
24+
"license": "MIT",
25+
"authors": [
26+
{
27+
"name": "Bronisław Białek",
28+
"email": "[email protected]"
29+
}
30+
]
31+
}

src/CalculatorNodeVisitor.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bronek\CodeComplexity;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
8+
final class CalculatorNodeVisitor extends NodeVisitorAbstract
9+
{
10+
private $complexity = 0;
11+
12+
private const STMTS = [
13+
Node\Stmt\If_::class,
14+
Node\Stmt\Case_::class,
15+
Node\Stmt\Function_::class,
16+
Node\Stmt\ClassMethod::class,
17+
Node\Stmt\Catch_::class,
18+
Node\Stmt\For_::class,
19+
Node\Stmt\Foreach_::class,
20+
Node\Stmt\While_::class,
21+
Node\Stmt\ElseIf_::class,
22+
Node\Expr\BinaryOp\LogicalXor::class,
23+
Node\Expr\BinaryOp\BooleanAnd::class,
24+
Node\Expr\BinaryOp\BooleanOr::class,
25+
];
26+
27+
public function enterNode(Node $node): void
28+
{
29+
if (in_array(get_class($node), self::STMTS, true)) {
30+
++$this->complexity;
31+
}
32+
}
33+
34+
public function calculatedComplexity(): int
35+
{
36+
return $this->complexity;
37+
}
38+
}

src/CodeComplexity.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Bronek\CodeComplexity;
4+
5+
interface CodeComplexity
6+
{
7+
/**
8+
* Calculates complexity of given code
9+
* Code must start with php open tag (<?php)
10+
*/
11+
public function calculate(string $code): int;
12+
}

src/CodeComplexityCalculator.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bronek\CodeComplexity;
4+
5+
use PhpParser\NodeTraverser;
6+
use PhpParser\Parser;
7+
use PhpParser\ParserFactory;
8+
9+
final class CodeComplexityCalculator implements CodeComplexity
10+
{
11+
/** @var Parser */
12+
private $parser;
13+
14+
public function __construct(Parser $parser = null)
15+
{
16+
$this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
17+
}
18+
19+
public function calculate(string $code): int
20+
{
21+
$nodeVisitor = new CalculatorNodeVisitor();
22+
23+
$traverser = new NodeTraverser();
24+
$traverser->addVisitor($nodeVisitor);
25+
$traverser->traverse($this->parser->parse($code));
26+
27+
return $nodeVisitor->calculatedComplexity();
28+
}
29+
}

tests/ComplexityTest.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace tests\Bronek\CodeComplexity;
4+
5+
use Bronek\CodeComplexity\CodeComplexityCalculator;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class ComplexityTest extends TestCase
9+
{
10+
private static function assertCodeComplexity(int $complexity, string $code): void
11+
{
12+
$calculator = new CodeComplexityCalculator();
13+
14+
self::assertEquals($complexity, $calculator->calculate($code), 'Complexity of code');
15+
}
16+
17+
/** @test */
18+
function calculating_code_complexity()
19+
{
20+
self::assertCodeComplexity(
21+
0,
22+
<<<EOT
23+
<?php
24+
class x {}
25+
EOT
26+
);
27+
28+
self::assertCodeComplexity(
29+
1,
30+
<<<EOT
31+
<?php
32+
class x {
33+
function text() {}
34+
}
35+
EOT
36+
);
37+
38+
self::assertCodeComplexity(
39+
8,
40+
<<<EOT
41+
<?php
42+
class CyclomaticComplexityNumber
43+
{
44+
public function example( \$x, \$y )
45+
{
46+
if ( \$x > 23 || \$y < 42 )
47+
{
48+
for ( \$i = \$x; \$i >= \$x && \$i <= \$y; ++\$i )
49+
{
50+
}
51+
}
52+
else
53+
{
54+
switch ( \$x + \$y )
55+
{
56+
case 1:
57+
break;
58+
case 2:
59+
break;
60+
default:
61+
break;
62+
}
63+
}
64+
}
65+
}
66+
EOT
67+
);
68+
69+
self::assertCodeComplexity(
70+
16,
71+
<<<EOT
72+
<?php
73+
// ...
74+
function _countCalls(PHP_Depend_Code_AbstractCallable \$callable)
75+
{
76+
\$callT = array(
77+
\PDepend\Source\Tokenizer\Tokens::T_STRING,
78+
\PDepend\Source\Tokenizer\Tokens::T_VARIABLE
79+
);
80+
\$chainT = array(
81+
\PDepend\Source\Tokenizer\Tokens::T_DOUBLE_COLON,
82+
\PDepend\Source\Tokenizer\Tokens::T_OBJECT_OPERATOR,
83+
);
84+
85+
\$called = array();
86+
87+
\$tokens = \$callable->getTokens();
88+
\$count = count(\$tokens);
89+
for (\$i = 0; \$i < \$count; ++\$i) {
90+
// break on function body open
91+
if (\$tokens[\$i]->type === \PDepend\Source\Tokenizer\Tokens::T_CURLY_BRACE_OPEN) {
92+
break;
93+
}
94+
}
95+
96+
for (; \$i < \$count; ++\$i) {
97+
// Skip non parenthesis tokens
98+
if (\$tokens[\$i]->type !== \PDepend\Source\Tokenizer\Tokens::T_PARENTHESIS_OPEN) {
99+
continue;
100+
}
101+
// Skip first token
102+
if (!isset(\$tokens[\$i - 1]) || !in_array(\$tokens[\$i - 1]->type, \$callT)) {
103+
continue;
104+
}
105+
// Count if no other token exists
106+
if (!isset(\$tokens[\$i - 2]) && !isset(\$called[\$tokens[\$i - 1]->image])) {
107+
\$called[\$tokens[\$i - 1]->image] = true;
108+
++\$this->_calls;
109+
continue;
110+
} else if (in_array(\$tokens[\$i - 2]->type, \$chainT)) {
111+
\$identifier = \$tokens[\$i - 2]->image . \$tokens[\$i - 1]->image;
112+
for (\$j = \$i - 3; \$j >= 0; --\$j) {
113+
if (!in_array(\$tokens[\$j]->type, \$callT)
114+
&& !in_array(\$tokens[\$j]->type, \$chainT)
115+
) {
116+
break;
117+
}
118+
\$identifier = \$tokens[\$j]->image . \$identifier;
119+
}
120+
121+
if (!isset(\$called[\$identifier])) {
122+
\$called[\$identifier] = true;
123+
++\$this->_calls;
124+
}
125+
} else if (\$tokens[\$i - 2]->type !== \PDepend\Source\Tokenizer\Tokens::T_NEW
126+
&& !isset(\$called[\$tokens[\$i - 1]->image])
127+
) {
128+
\$called[\$tokens[\$i - 1]->image] = true;
129+
++\$this->_calls;
130+
}
131+
}
132+
}
133+
EOT
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)