Repository: bmitch/churn-php (php)

Issue:
CyclomaticComplexityAssessor - Improve by using something like `nikic/PHP-Parser`
The score is calculated by using regular expressions which leads to errors.

Example of a PHP file containing only a comment:
```php
<?php
// if ( if ( if (
```
In this example the score is 3 whereas it should be 1.

This kind of error could be avoided by using [PHP-Parser](https://github.com/nikic/PHP-Parser).



Discussion:
Thanks @villfa - please feel free to submit a PR if you like. Otherwise I will get to it when I can.
Perhaps we can use `nikic/PHP-Parser` to analyse the PHP file being assessed and not have to rely on regular expressions? Should be something interesting to work on.
Started a new branch `bmitchell-146` with a new test that has the above code provided by @villfa:

https://github.com/bmitch/churn-php/blob/bmitchell-146/tests/Unit/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessorTest.php#L89-L93

Can use that as a starting point for a new CyclomaticComplexityAssessor that would use `PHP-Parser` or something similar.
I think you should use php-ast instead of php-parser.
@emilv I've never used either package so I don't know the pros and cons of each. Why would you chose `php-ast`?
php-ast uses the native PHP abstract syntax tree. Both are maintained by nikic, a PHP core developer (who also introduced the formal parser into the PHP runtime!).

Nikic have written about the differences here: https://github.com/nikic/php-ast#differences-to-php-parser

But reading that list makes me doubt my own suggestion. Perhaps PHP-Parser is the right choice after all?
Thanks @emilv that link is a huge help. I will take a deeper look as soon as I have time.
I think using PHP-Parser is probably going to be the better choice. PHP-ast is a PHP extension. I think to keep this as accessible as possible it is better to use PHP-Parser because it doesn't require an extension to be installed.

Patch:
diff --git a/bin/CyclomaticComplexityAssessorRunner b/bin/CyclomaticComplexityAssessorRunner
--- a/bin/CyclomaticComplexityAssessorRunner
+++ b/bin/CyclomaticComplexityAssessorRunner
@@ -1,13 +1,16 @@
 #!/usr/bin/env php
 <?php
 
+declare(strict_types=1);
+
 require_once __DIR__ . '/bootstrap.php';
 
 use Churn\Assessor\CyclomaticComplexityAssessor;
 
 $file = $argv[1];
 $contents = \file_get_contents($file);
-if ($contents === false) {
+
+if (false === $contents) {
     echo 0;
     return;
 }
diff --git a/bin/app.php b/bin/app.php
--- a/bin/app.php
+++ b/bin/app.php
@@ -16,7 +16,7 @@
 
     return $version;
 })('bmitch/churn-php'));
-$application->add(new AssessComplexityCommand());
+$application->add(AssessComplexityCommand::newInstance());
 $application->add($run = RunCommand::newInstance());
 $application->setDefaultCommand($run->getName());
 
diff --git a/src/Assessor/CyclomaticComplexityAssessor.php b/src/Assessor/CyclomaticComplexityAssessor.php
--- a/src/Assessor/CyclomaticComplexityAssessor.php
+++ b/src/Assessor/CyclomaticComplexityAssessor.php
@@ -10,141 +10,78 @@
 class CyclomaticComplexityAssessor
 {
     /**
-     * The total cyclomatic complexity score.
-     *
-     * @var integer
+     * @var array<int, int>
      */
-    private $score = 0;
+    private $tokens = [
+        \T_CLASS => 1,
+        \T_INTERFACE => 1,
+        \T_TRAIT => 1,
+        \T_IF => 1,
+        \T_ELSEIF => 1,
+        \T_FOR => 1,
+        \T_FOREACH => 1,
+        \T_WHILE => 1,
+        \T_CASE => 1,
+        \T_CATCH => 1,
+        \T_BOOLEAN_AND => 1,
+        \T_LOGICAL_AND => 1,
+        \T_BOOLEAN_OR => 1,
+        \T_LOGICAL_OR => 1,
+        \T_COALESCE => 1,
+    ];
 
     /**
-     * Asses the files cyclomatic complexity.
-     *
-     * @param string $contents The contents of a PHP file.
+     * Class constructor.
      */
-    public function assess(string $contents): int
+    public function __construct()
     {
-        $this->score = 0;
-        $this->hasAtLeastOneMethod($contents);
-        $this->countTheIfStatements($contents);
-        $this->countTheElseIfStatements($contents);
-        $this->countTheWhileLoops($contents);
-        $this->countTheForLoops($contents);
-        $this->countTheCaseStatements($contents);
-        $this->countTheTernaryOperators($contents);
-        $this->countTheLogicalAnds($contents);
-        $this->countTheLogicalOrs($contents);
-
-        if (0 === $this->score) {
-            $this->score = 1;
-        }
-
-        return $this->score;
+        $this->init();
     }
 
     /**
-     * Does the class have at least one method?
+     * Assess the files cyclomatic complexity.
      *
-     * @param string $contents File contents.
+     * @param string $contents The contents of a PHP file.
      */
-    private function hasAtLeastOneMethod(string $contents): void
+    public function assess(string $contents): int
     {
-        \preg_match("/[ ]function[ ]/", $contents, $matches);
-
-        if (!isset($matches[0])) {
-            return;
+        $score = 0;
+        foreach (\token_get_all($contents) as $token) {
+            $score += $this->getComplexity($token[0]);
         }
 
-        $this->score++;
-    }
-
-    /**
-     * Count how many if statements there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheIfStatements(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/[ ]if[ ]{0,}\(/", $contents);
-    }
-
-    /**
-     * Count how many else if statements there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheElseIfStatements(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/else[ ]{0,}if[ ]{0,}\(/", $contents);
+        return \max(1, $score);
     }
 
     /**
-     * Count how many while loops there are.
-     *
-     * @param string $contents File contents.
+     * Add missing tokens depending on the PHP version.
      */
-    private function countTheWhileLoops(string $contents): void
+    private function init(): void
     {
-        $this->score += $this->howManyPatternMatches("/while[ ]{0,}\(/", $contents);
-    }
-
-    /**
-     * Count how many for loops there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheForLoops(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/[ ]for(each){0,1}[ ]{0,}\(/", $contents);
-    }
-
-    /**
-     * Count how many case statements there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheCaseStatements(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/[ ]case[ ]{1}(.*)\:/", $contents);
-    }
-
-    /**
-     * Count how many ternary operators there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheTernaryOperators(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/[ ]\?.*:.*;/", $contents);
-    }
-
-    /**
-     * Count how many '&&' there are.
-     *
-     * @param string $contents File contents.
-     */
-    private function countTheLogicalAnds(string $contents): void
-    {
-        $this->score += $this->howManyPatternMatches("/[ ]&&[ ]/", $contents);
+        $tokens = [
+            // Since PHP 7.4
+            'T_COALESCE_EQUAL',
+            // Since PHP 8.1
+            'T_ENUM',
+        ];
+        foreach ($tokens as $token) {
+            if (!\defined($token)) {
+                continue;
+            }
+
+            $this->tokens[(int) \constant($token)] = 1;
+        }
     }
 
     /**
-     * Count how many '||' there are.
-     *
-     * @param string $contents File contents.
+     * @param integer|string $code Code of a PHP token.
      */
-    private function countTheLogicalOrs(string $contents): void
+    private function getComplexity($code): int
     {
-        $this->score += $this->howManyPatternMatches("/[ ]\|\|[ ]/", $contents);
-    }
+        if ('?' === $code) {
+            return 1;
+        }
 
-    /**
-     * For the given $pattern on $string, how many matches are returned?
-     *
-     * @param string $pattern Regex pattern.
-     * @param string $string Any string.
-     */
-    private function howManyPatternMatches(string $pattern, string $string): int
-    {
-        return (int) \preg_match_all($pattern, $string);
+        return $this->tokens[$code] ?? 0;
     }
 }
diff --git a/src/Command/AssessComplexityCommand.php b/src/Command/AssessComplexityCommand.php
--- a/src/Command/AssessComplexityCommand.php
+++ b/src/Command/AssessComplexityCommand.php
@@ -15,6 +15,31 @@
  */
 class AssessComplexityCommand extends Command
 {
+    /**
+     * @var CyclomaticComplexityAssessor
+     */
+    private $assessor;
+
+    /**
+     * Class constructor.
+     *
+     * @param CyclomaticComplexityAssessor $assessor The class calculating the complexity.
+     */
+    public function __construct(CyclomaticComplexityAssessor $assessor)
+    {
+        parent::__construct();
+
+        $this->assessor = $assessor;
+    }
+
+    /**
+     * Returns a new instance of the command.
+     */
+    public static function newInstance(): self
+    {
+        return new self(new CyclomaticComplexityAssessor());
+    }
+
     /**
      * Configure the command
      */
@@ -44,8 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
             return 0;
         }
 
-        $assessor = new CyclomaticComplexityAssessor();
-        $output->writeln((string) $assessor->assess($contents));
+        $output->writeln((string) $this->assessor->assess($contents));
 
         return 0;
     }


Test patch:
diff --git a/tests/Integration/Command/AssessComplexityCommandTest.php b/tests/Integration/Command/AssessComplexityCommandTest.php
--- a/tests/Integration/Command/AssessComplexityCommandTest.php
+++ b/tests/Integration/Command/AssessComplexityCommandTest.php
@@ -17,7 +17,7 @@ class AssessComplexityCommandTest extends BaseTestCase
     protected function setUp()
     {
         $application = new Application('churn-php', 'test');
-        $application->add(new AssessComplexityCommand());
+        $application->add(AssessComplexityCommand::newInstance());
         $command = $application->find('assess-complexity');
         $this->commandTester = new CommandTester($command);
     }
diff --git a/tests/Unit/Assessor/Assets/ClassWithForLoop.inc b/tests/Unit/Assessor/Assets/ClassWithForLoop.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithForLoop.inc
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-class ClassWithWhileLoop
-{
-    public function foobar()
-    {
-        for ($n = 0; $n < $h; $n++) {
-            foobar();
-        }
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithIfElseIf.inc b/tests/Unit/Assessor/Assets/ClassWithIfElseIf.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithIfElseIf.inc
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-class ClassWithIfElseIf
-{
-    public function foobar()
-    {
-        if ($true) {
-            return true;
-        } elseif ($false) {
-            return false;
-        }
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithLogicalAnd.inc b/tests/Unit/Assessor/Assets/ClassWithLogicalAnd.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithLogicalAnd.inc
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-class ClassWithLogicalAnd
-{
-    public function foobar()
-    {
-        return ($a == $b && $c == $d);
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithLogicalOr.inc b/tests/Unit/Assessor/Assets/ClassWithLogicalOr.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithLogicalOr.inc
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-class ClassWithLogicalOr
-{
-    public function foobar()
-    {
-        return ($a == $b || $c == $d);
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithManyMethodsAndLotsOfBranches.inc b/tests/Unit/Assessor/Assets/ClassWithManyMethodsAndLotsOfBranches.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithManyMethodsAndLotsOfBranches.inc
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-class ClassWithWhileLoop
-{
-    public function foo()
-    {
-        for ($n = 0; $n < $h; $n++) {
-            foobar();
-        }
-    }
-
-    public function bar()
-    {
-        if ($true) {
-            return true;
-        } elseif ($false) {
-            return false;
-        }
-
-        if ($true) {
-            if ($true) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    public function baz()
-    {
-        if ($true) {
-            return true;
-        }
-
-        return false;
-    }
-
-    public function uggh()
-    {
-        switch ($z) {
-            case 1:
-                foobar();
-                break;
-            case 2:
-                foobar();
-                break;
-            case 3:
-                foobar();
-                break;
-            default:
-                foobar();
-                break;
-        }
-
-        while ($c == $d) {
-            foobar();
-        }
-    }
-}
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithNoForLoops.inc b/tests/Unit/Assessor/Assets/ClassWithNoForLoops.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithNoForLoops.inc
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class ClassWithNoForLoops
-{
-    public function forteen()
-    {
-        // For foreach
-        // This does not contain any for or foreach loops
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithOneEmptyMethod.inc b/tests/Unit/Assessor/Assets/ClassWithOneEmptyMethod.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithOneEmptyMethod.inc
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php
-
-class ClassWithOneEmptyMethod
-{
-    public function foobar()
-    {
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithOneMethodWithNestedIf.inc b/tests/Unit/Assessor/Assets/ClassWithOneMethodWithNestedIf.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithOneMethodWithNestedIf.inc
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-class ClassWithOneMethodWithNestedIf
-{
-    public function foobar()
-    {
-        if ($true) {
-            if ($true) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithOneMethodWithOneIf.inc b/tests/Unit/Assessor/Assets/ClassWithOneMethodWithOneIf.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithOneMethodWithOneIf.inc
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-class ClassWithOneMethodWithOneIf
-{
-    public function foobar()
-    {
-        if ($true) {
-            return true;
-        }
-
-        return false;
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithSwitchStatement.inc b/tests/Unit/Assessor/Assets/ClassWithSwitchStatement.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithSwitchStatement.inc
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-class ClassWithWhileLoop
-{
-    public function foobar()
-    {
-        switch ($z) {
-            case 1:
-                foobar();
-                break;
-            case 2:
-                foobar();
-                break;
-            case 3:
-                foobar();
-                break;
-            default:
-                foobar();
-                break;
-        }
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithTernary.inc b/tests/Unit/Assessor/Assets/ClassWithTernary.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithTernary.inc
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-class ClassWithTernary
-{
-    public function foobar()
-    {
-        $foo == 'bar' ? $baz++ : $zug;
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/ClassWithWhileLoop.inc b/tests/Unit/Assessor/Assets/ClassWithWhileLoop.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/ClassWithWhileLoop.inc
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-class ClassWithWhileLoop
-{
-    public function foobar()
-    {
-        while ($c == $d) {
-            foobar();
-        }
-    }
-}
diff --git a/tests/Unit/Assessor/Assets/EmptyClass.inc b/tests/Unit/Assessor/Assets/EmptyClass.inc
deleted file mode 100644
--- a/tests/Unit/Assessor/Assets/EmptyClass.inc
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class EmptyClass
-{
-
-}
diff --git a/tests/Unit/Assessor/CyclomaticComplexityAssessorTest.php b/tests/Unit/Assessor/CyclomaticComplexityAssessorTest.php
--- a/tests/Unit/Assessor/CyclomaticComplexityAssessorTest.php
+++ b/tests/Unit/Assessor/CyclomaticComplexityAssessorTest.php
@@ -9,84 +9,287 @@
 
 class CyclomaticComplexityAssessorTest extends BaseTestCase
 {
-    /** @test */
-    public function an_empty_class_should_have_a_complexity_of_one()
+    /**
+     * @dataProvider provide_assess
+     */
+    public function test_assess(int $expectedScore, string $code)
     {
-        $this->assertEquals(1, $this->assess('tests/Unit/Assessor/Assets/EmptyClass.inc'));
+        $assessor = new CyclomaticComplexityAssessor();
+
+        $this->assertEquals($expectedScore, $assessor->assess($code));
+    }
+
+    public function provide_assess(): iterable
+    {
+        yield 'an empty file' => [
+            1,
+            ''
+        ];
+
+        yield 'an empty class' => [
+            1,
+            <<<'EOC'
+<?php
+class EmptyClass
+{
+}
+EOC
+        ];
+
+        yield 'a class with one empty method' => [
+            1,
+            <<<'EOC'
+<?php
+class ClassWithOneEmptyMethod
+{
+    public function foobar()
+    {
+    }
+}
+EOC
+        ];
+
+        yield 'a class with a method containing one if statement' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithOneMethodWithOneIf
+{
+    public function foobar()
+    {
+        if ($true) {
+            return true;
+        }
+
+        return false;
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_one_empty_method_has_a_complexity_of_one()
+        yield 'a class with a method containing a nested if statement' => [
+            3,
+            <<<'EOC'
+<?php
+class ClassWithOneMethodWithNestedIf
+{
+    public function foobar()
     {
-        $this->assertEquals(1, $this->assess('tests/Unit/Assessor/Assets/ClassWithOneEmptyMethod.inc'));
+        if ($true) {
+            if ($true) {
+                return true;
+            }
+        }
+
+        return false;
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_method_containing_one_if_statement_has_a_complexity_of_two()
+        yield 'a class with a method containing an if else if statement' => [
+            3,
+            <<<'EOC'
+<?php
+class ClassWithIfElseIf
+{
+    public function foobar()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithOneMethodWithOneIf.inc'));
+        if ($true) {
+            return true;
+        } elseif ($false) {
+            return false;
+        }
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_method_containing_a_nested_if_statement_has_a_complexity_of_three()
+        yield 'a class with a method containing a while loop' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithWhileLoop
+{
+    public function foobar()
     {
-        $this->assertEquals(3, $this->assess('tests/Unit/Assessor/Assets/ClassWithOneMethodWithNestedIf.inc'));
+        while ($c == $d) {
+            foobar();
+        }
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_method_containing_an_if_else_if_statement_has_a_complexity_of_three()
+        yield 'a class with a method containing a for loop' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithForLoop
+{
+    public function foobar()
     {
-        $this->assertEquals(3, $this->assess('tests/Unit/Assessor/Assets/ClassWithIfElseIf.inc'));
+        for ($n = 0; $n < $h; $n++) {
+            foobar();
+        }
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_method_containing_a_while_loop_has_a_complexity_of_two()
+        yield 'a class with a method a switch statement with 3 cases' => [
+            4,
+            <<<'EOC'
+<?php
+class ClassWithSwitch
+{
+    public function foobar()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithWhileLoop.inc'));
+        switch ($z) {
+            case 1:
+                foobar();
+                break;
+            case 2:
+                foobar();
+                break;
+            case 3:
+                foobar();
+                break;
+            default:
+                foobar();
+                break;
+        }
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_method_containing_a_for_loop_has_a_complexity_of_two()
+        yield 'this class with many methods and many branches' => [
+            11,
+            <<<'EOC'
+<?php
+class LongClass
+{
+    public function foo()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithForLoop.inc'));
-        $this->assertEquals(1, $this->assess('tests/Unit/Assessor/Assets/ClassWithNoForLoops.inc'));
+        for ($n = 0; $n < $h; $n++) {
+            foobar();
+        }
     }
 
-    /** @test */
-    public function a_class_with_a_method_a_switch_statement_with_three_cases_has_a_complexity_of_four()
+    public function bar()
     {
-        $this->assertEquals(4, $this->assess('tests/Unit/Assessor/Assets/ClassWithSwitchStatement.inc'));
+        if ($true) {
+            return true;
+        } elseif ($false) {
+            return false;
+        }
+
+        if ($true) {
+            if ($true) {
+                return true;
+            }
+        }
+
+        return false;
     }
 
-    /** @test */
-    public function this_class_with_many_methods_and_many_branches_should_have_a_complexity_of_eleven()
+    public function baz()
     {
-        $this->assertEquals(11, $this->assess('tests/Unit/Assessor/Assets/ClassWithManyMethodsAndLotsOfBranches.inc'));
+        if ($true) {
+            return true;
+        }
+
+        return false;
     }
 
-    /** @test */
-    public function a_class_with_a_ternary_opererator_should_return_two()
+    public function uggh()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithTernary.inc'));
+        switch ($z) {
+            case 1:
+                foobar();
+                break;
+            case 2:
+                foobar();
+                break;
+            case 3:
+                foobar();
+                break;
+            default:
+                foobar();
+                break;
+        }
+
+        while ($c == $d) {
+            foobar();
+        }
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_logical_and_should_return_two()
+        yield 'a class with a ternary operator' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithTernary
+{
+    public function foobar()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithLogicalAnd.inc'));
+        $foo == 'bar' ? $baz++ : $zug;
     }
+}
+EOC
+        ];
 
-    /** @test */
-    public function a_class_with_a_logical_or_should_return_two()
+        yield 'a class with a logical AND' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithLogicalAnd
+{
+    public function foobar()
     {
-        $this->assertEquals(2, $this->assess('tests/Unit/Assessor/Assets/ClassWithLogicalOr.inc'));
+        return ($a == $b && $c == $d);
     }
+}
+EOC
+        ];
 
-    protected function assess($filename)
+        yield 'a class with a logical OR' => [
+            2,
+            <<<'EOC'
+<?php
+class ClassWithLogicalOr
+{
+    public function foobar()
     {
-        $contents = \file_get_contents($filename);
-        assert($contents !== false);
+        return ($a == $b || $c == $d);
+    }
+}
+EOC
+        ];
 
-        return (new CyclomaticComplexityAssessor())->assess($contents);
+        yield 'syntax error' => [
+            1,
+            '<?php echo'
+        ];
+
+        yield 'file with commented code' => [
+            1,
+            '<?php // if (true) {if (true) {if (true) {if (true) {}}}}'
+        ];
+
+        if (version_compare(PHP_VERSION, '7.4.0', '>=')) {
+            yield 'file with coalesce equal operator' => [
+                3,
+                <<<'EOC'
+<?php
+$a ??= 'a';
+$a ??= 'a';
+$a ??= 'a';
+EOC
+            ];
+        }
     }
 }
diff --git a/tests/console-application.php b/tests/console-application.php
--- a/tests/console-application.php
+++ b/tests/console-application.php
@@ -13,7 +13,7 @@
 use Symfony\Component\Console\Application;
 
 $application = new Application('churn-php', 'test');
-$application->add(new AssessComplexityCommand());
+$application->add(AssessComplexityCommand::newInstance());
 $application->add(RunCommand::newInstance());
 
 return $application;