Skip to content

Commit

Permalink
Merge pull request #353 from UFOMelkor/metric/ccn
Browse files Browse the repository at this point in the history
Change the calculation of cyclomatic complexity
  • Loading branch information
UFOMelkor committed Jul 9, 2018
2 parents d54d5b1 + f5f5c49 commit 0086da0
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 55 deletions.
73 changes: 40 additions & 33 deletions src/Hal/Metric/Class_/Complexity/CyclomaticComplexityVisitor.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?php
namespace Hal\Metric\Class_\Complexity;

use Hal\Component\Reflected\Method;
use Hal\Metric\Helper\MetricClassNameGenerator;
use Hal\Metric\Metrics;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\NodeVisitorAbstract;

/**
* Calculate cyclomatic complexity number
* Calculate cyclomatic complexity number and weighted method count.
*
* The cyclomatic complexity (CC) is a measure of control structure complexity of a function or procedure.
* We can calculate ccn in two ways (we choose the second):
*
* 1. Cyclomatic complexity (CC) = E - N + 2P
Expand All @@ -19,40 +19,46 @@
* E = number of edges (transfers of control)
* N = number of nodes (sequential group of statements containing only one transfer of control)
*
* 2. CC = Number of each decision point
* 2. CC = Number of each decision point
*
* The weighted method count (WMC) is count of methods parameterized by a algorithm to compute the weight of a method.
* Given a weight metric w and methods m it can be computed as
*
* sum m(w') over (w' in w)
*
* Possible algorithms are:
*
* - Cyclomatic Complexity
* - Lines of Code
* - 1 (unweighted WMC)
*
* This visitor provides two metrics, the maximal CC of all methods from one class (currently stored as ccnMethodMax)
* and the WMC using the CC as weight metric (currently stored as ccn).
*
* @see https://en.wikipedia.org/wiki/Cyclomatic_complexity
* @see http://www.literateprogramming.com/mccabe.pdf
* @see https://www.pitt.edu/~ckemerer/CK%20research%20papers/MetricForOOD_ChidamberKemerer94.pdf
*/
class CyclomaticComplexityVisitor extends NodeVisitorAbstract
{

/**
* @var Metrics
*/
/** @var Metrics */
private $metrics;

/**
* ClassEnumVisitor constructor.
* @param Metrics $metrics
*/
public function __construct(Metrics $metrics)
{
$this->metrics = $metrics;
}

/**
* @inheritdoc
*/
public function leaveNode(Node $node)
{
if ($node instanceof Stmt\Class_
|| $node instanceof Stmt\Interface_
|| $node instanceof Stmt\Trait_
) {

$class = $this->metrics->get(MetricClassNameGenerator::getName($node));

$ccn = 1;
$ccnByMethod = array();
$ccn = 0;
$ccnByMethod = [0]; // default maxMethodCcn if no methods are available

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Stmt\ClassMethod) {
Expand All @@ -62,9 +68,9 @@ public function leaveNode(Node $node)
$ccn = 0;

foreach (get_object_vars($node) as $name => $member) {
foreach (is_array($member) ? $member : [$member] as $member_item) {
if ($member_item instanceof Node) {
$ccn += $cb($member_item);
foreach (is_array($member) ? $member : [$member] as $memberItem) {
if ($memberItem instanceof Node) {
$ccn += $cb($memberItem);
}
}
}
Expand All @@ -78,35 +84,36 @@ public function leaveNode(Node $node)
case $node instanceof Stmt\Do_:
case $node instanceof Node\Expr\BinaryOp\LogicalAnd:
case $node instanceof Node\Expr\BinaryOp\LogicalOr:
case $node instanceof Node\Expr\BinaryOp\LogicalXor:
case $node instanceof Node\Expr\BinaryOp\BooleanAnd:
case $node instanceof Node\Expr\BinaryOp\BooleanOr:
case $node instanceof Node\Expr\BinaryOp\Spaceship:
case $node instanceof Stmt\Case_: // include default
case $node instanceof Stmt\Catch_:
case $node instanceof Stmt\Continue_:
$ccn++;
break;
case $node instanceof Node\Expr\Ternary:
case $node instanceof Node\Expr\BinaryOp\Coalesce:
$ccn = $ccn + 2;
$ccn++;
break;
case $node instanceof Stmt\Case_: // include default
if ($node->cond !== null) { // exclude default
$ccn++;
}
break;
case $node instanceof Node\Expr\BinaryOp\Spaceship:
$ccn += 2;
break;

}
return $ccn;
};

$methodCcn = $cb($stmt);
$methodCcn = $cb($stmt) + 1; // each method by default is CCN 1 even if it's empty

$ccn += $methodCcn;
$ccnByMethod[] = $methodCcn + 1; // each method by default is CCN 1 even if it's empty
$ccnByMethod[] = $methodCcn;
}
}

$class->set('ccn', $ccn);

$class->set('ccnMethodMax', 0);
if (count($ccnByMethod)) {
$class->set('ccnMethodMax', max($ccnByMethod));
}
$class->set('ccnMethodMax', max($ccnByMethod));
}
}
}
48 changes: 26 additions & 22 deletions tests/Metric/Class_/Complexity/CyclomaticComplexityVisitorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,43 @@

use Hal\Metric\Class_\ClassEnumVisitor;
use Hal\Metric\Class_\Complexity\CyclomaticComplexityVisitor;
use Hal\Metric\Class_\Complexity\McCabeVisitor;
use Hal\Metric\Metrics;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;

class CyclomaticComplexityVisitorTest extends \PHPUnit_Framework_TestCase {


class CyclomaticComplexityVisitorTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider provideExamplesForClasses
* @dataProvider provideExamplesForWmc
*/
public function testCyclomaticComplexityOfClassesIsWellCalculated($example, $classname, $expectedCcn)
public function testWeightedMethodCountOfClassesIsWellCalculated($example, $classname, $expectedWmc)
{
$metrics = new Metrics();

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver());
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor(new ClassEnumVisitor($metrics));
$traverser->addVisitor(new CyclomaticComplexityVisitor($metrics));

$code = file_get_contents($example);
$stmts = $parser->parse($code);
$traverser->traverse($stmts);

$this->assertSame($expectedCcn, $metrics->get($classname)->get('ccn'));
$this->assertSame($expectedWmc, $metrics->get($classname)->get('ccn'));
}

/**
* @dataProvider provideExamplesForMethods
* @dataProvider provideExamplesForMaxCc
*/
public function testCyclomaticComplexityOfMethodsIsWellCalculated($example, $classname, $expectedCcnMethodMax)
public function testMaximalCyclomaticComplexityOfMethodsIsWellCalculated($example, $classname, $expectedCcnMethodMax)
{
$metrics = new Metrics();

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver());
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor(new ClassEnumVisitor($metrics));
$traverser->addVisitor(new CyclomaticComplexityVisitor($metrics));

Expand All @@ -50,21 +50,25 @@ public function testCyclomaticComplexityOfMethodsIsWellCalculated($example, $cla
$this->assertSame($expectedCcnMethodMax, $metrics->get($classname)->get('ccnMethodMax'));
}

public function provideExamplesForClasses()
public static function provideExamplesForWmc()
{
return [
[ __DIR__.'/../../examples/cyclomatic1.php', 'A', 8],
[ __DIR__.'/../../examples/cyclomatic1.php', 'B', 5],
[ __DIR__.'/../../examples/cyclomatic_anon.php', 'Foo\C', 1],
'A' => [__DIR__ . '/../../examples/cyclomatic1.php', 'A', 10],
'B' => [__DIR__ . '/../../examples/cyclomatic1.php', 'B', 4],
'Foo\\C' => [__DIR__ . '/../../examples/cyclomatic_anon.php', 'Foo\\C', 1],
'SwitchCase' => [__DIR__ . '/../../examples/cyclomatic_full.php', 'SwitchCase', 4],
'IfElseif' => [__DIR__ . '/../../examples/cyclomatic_full.php', 'IfElseif', 7],
'Loops' => [__DIR__ . '/../../examples/cyclomatic_full.php', 'Loops', 5],
'CatchIt' => [__DIR__ . '/../../examples/cyclomatic_full.php', 'CatchIt', 3],
'Logical' => [__DIR__ . '/../../examples/cyclomatic_full.php', 'Logical', 11],
];
}

public function provideExamplesForMethods()
public static function provideExamplesForMaxCc()
{
return [
[ __DIR__.'/../../examples/cyclomatic1.php', 'A', 6],
[ __DIR__.'/../../examples/cyclomatic1.php', 'B', 5],
[__DIR__ . '/../../examples/cyclomatic1.php', 'A', 6],
[__DIR__ . '/../../examples/cyclomatic1.php', 'B', 4],
];
}

}
}
72 changes: 72 additions & 0 deletions tests/Metric/examples/cyclomatic_full.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
class SwitchCase // ccn2: 4
{
function __invoke()
{
switch ('abc') {
case 'abc':
case 'def':
case 'hij':
break;
default:
}
}
}

class IfElseif // ccn2: 7
{
function __invoke()
{
if (true) {
if (true) {
} elseif (true) {
} else {
}
} elseif (true) {
if (false) {
}
}

if (true) {
}
}
}

class Loops // ccn2: 5
{
function __invoke()
{
while (true) {
do {
} while (false);
}
foreach (array() as $each) {
for ($i = 0; $i < 0; ++$i) {
}
}
}
}

class CatchIt // ccn2: 3
{
function __invoke()
{
try {
} catch (Exception $e) {
} catch (Throwable $e) {
} finally {
}
}
}

class Logical // ccn2: 11
{
function __invoke()
{
$a = (true || false) and (false && true) or (true xor false);
$b = $a ? 1 : 2;
$c = $b ?: 0;
$d = $b ?? $c;
$e = $b <=> $d;
}
}

0 comments on commit 0086da0

Please sign in to comment.