Repository: bmitch/churn-php (php)

Issue:
Build a "binary" for this project
It would be awesome if one could run this tool against a code base without installing it (if said project supports Composer dependencies at all ;)). Sounds like a `.phar` file would be useful. I'll look into what's needed for that and if we can make the build process automated.


Discussion:
@matthiasnoback I just `composer global require` install things and then put ~/.config/composer/vendor/bin into my PATH.
@matthewbaggett That's a viable solution, but past experiences have learned me that this can lead to some impossible to solve dependency issues due to incompatible globally installed packages. Hence, for tools like this, I do like to have a .phar handy ;)

Patch:
diff --git a/.gitattributes b/.gitattributes
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,6 +5,7 @@
 .gitignore export-ignore
 .scrutinizer.yml export-ignore
 .travis.yml export-ignore
+box.json.dist export-ignore
 CHANGELOG.md export-ignore
 churn.yml export-ignore
 codor.xml export-ignore
diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/build-phar.yml
@@ -0,0 +1,54 @@
+name: "Phar construction"
+
+on:
+  push:
+    paths-ignore:
+      - '**.md'
+      - 'img/**'
+  pull_request:
+    paths-ignore:
+      - '**.md'
+      - 'img/**'
+
+jobs:
+  build:
+    name: "Build Phar"
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: "Checkout"
+        uses: "actions/checkout@v2"
+
+      - name: "Install PHP"
+        uses: "shivammathur/setup-php@v2"
+        with:
+          coverage: "none"
+          php-version: "7.2"
+          tools: composer:v2
+
+      - name: "Install dependencies"
+        run: |
+          composer config platform.php 7.1.3
+          composer update --no-dev --no-interaction --no-progress --prefer-dist
+          composer config --unset platform.php
+
+      - name: "Download Box"
+        run: |
+          curl -sL https://github.com/box-project/box/releases/download/3.8.5/box.phar -o box.phar
+          chmod +x box.phar
+
+      - name: "Compile"
+        run: ./box.phar compile
+
+      - name: "Test Version"
+        run: diff <(bin/churn -V) <(./churn.phar -V)
+
+      - name: "Test Phar"
+        run: diff <(bin/churn run src) <(./churn.phar run src)
+
+      - name: "Save Phar"
+        uses: actions/upload-artifact@v2
+        with:
+          name: churn.phar
+          path: ./churn.phar
+          if-no-files-found: error
diff --git a/.github/workflows/composer-normalize.yml b/.github/workflows/composer-normalize.yml
--- a/.github/workflows/composer-normalize.yml
+++ b/.github/workflows/composer-normalize.yml
@@ -16,7 +16,7 @@ jobs:
 
     steps:
       - name: "Checkout"
-        uses: "actions/checkout@v2.0.0"
+        uses: "actions/checkout@v2"
 
       - name: "Run composer normalize"
         uses: "docker://ergebnis/composer-normalize-action:latest"
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -1,6 +1,6 @@
 name: "Continuous Integration"
 
-on: 
+on:
   push:
     paths-ignore:
       - '**.md'
diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ composer.lock
 docs
 vendor
 coverage.xml
-.idea
\ No newline at end of file
+*.phar
+.idea
diff --git a/bin/churn b/bin/churn
--- a/bin/churn
+++ b/bin/churn
@@ -3,6 +3,7 @@
 
 require_once(__DIR__ . '/bootstrap.php');
 
+use Churn\Command\AssessComplexityCommand;
 use Churn\Command\RunCommand;
 use DI\ContainerBuilder;
 use PackageVersions\Versions;
@@ -12,5 +13,6 @@ $container = ContainerBuilder::buildDevContainer();
 
 $version = substr(Versions::getVersion('bmitch/churn-php'), 0, -33);
 $application = new Application('churn-php', $version);
+$application->add(new AssessComplexityCommand());
 $application->add($container->get(RunCommand::class));
 $application->run();
diff --git a/box.json.dist b/box.json.dist
new file mode 100644
--- /dev/null
+++ b/box.json.dist
@@ -0,0 +1,16 @@
+{
+    "dump-autoload": false,
+    "finder": [
+        {
+            "in": "bin"
+        },
+        {
+            "in": "src"
+        },
+        {
+            "name": "*.php",
+            "in": "vendor"
+        }
+    ],
+   "output": "churn.phar"
+}
\ No newline at end of file
diff --git a/src/Command/AssessComplexityCommand.php b/src/Command/AssessComplexityCommand.php
new file mode 100644
--- /dev/null
+++ b/src/Command/AssessComplexityCommand.php
@@ -0,0 +1,37 @@
+<?php declare(strict_types = 1);
+
+namespace Churn\Command;
+
+use Churn\Assessors\CyclomaticComplexity\CyclomaticComplexityAssessor;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class AssessComplexityCommand extends Command
+{
+    /**
+     * Configure the command
+     * @return void
+     */
+    protected function configure(): void
+    {
+        $this->setName('assess-complexity')
+            ->addArgument('file', InputArgument::REQUIRED, 'Path to file to analyze.')
+            ->setDescription('Calculate the Cyclomatic Complexity');
+    }
+
+    /**
+     * Execute the command
+     * @param InputInterface  $input  Input.
+     * @param OutputInterface $output Output.
+     * @return integer
+     */
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $file = $input->getArgument('file');
+        $assessor = new CyclomaticComplexityAssessor();
+        $output->writeln($assessor->assess($file));
+        return 0;
+    }
+}
diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php
--- a/src/Command/RunCommand.php
+++ b/src/Command/RunCommand.php
@@ -132,8 +132,11 @@ private function getDirectoriesToScan(InputInterface $input, array $dirsConfigur
      * @param ResultAccumulator $accumulator The object accumulating the results.
      * @return OnSuccess
      */
-    private function getOnSuccessObserver(InputInterface $input, OutputInterface $output, ResultAccumulator $accumulator): OnSuccess
-    {
+    private function getOnSuccessObserver(
+        InputInterface $input,
+        OutputInterface $output,
+        ResultAccumulator $accumulator
+    ): OnSuccess {
         $observer = new OnSuccessAccumulate($accumulator);
 
         if ((bool)$input->getOption('progress')) {
@@ -171,7 +174,11 @@ private function writeResult(InputInterface $input, OutputInterface $output, Res
             $output->writeln("\n");
         }
         if (!empty($input->getOption('output'))) {
-            $output = new StreamOutput(fopen($input->getOption('output'), 'w+'), OutputInterface::VERBOSITY_NORMAL, false);
+            $output = new StreamOutput(
+                fopen($input->getOption('output'), 'w+'),
+                OutputInterface::VERBOSITY_NORMAL,
+                false
+            );
         }
 
         $renderer = $this->renderFactory->getRenderer($input->getOption('format'));
diff --git a/src/Process/ProcessFactory.php b/src/Process/ProcessFactory.php
--- a/src/Process/ProcessFactory.php
+++ b/src/Process/ProcessFactory.php
@@ -2,8 +2,12 @@
 
 namespace Churn\Process;
 
+use function array_merge;
 use Churn\File\File;
 use function getcwd;
+use function is_callable;
+use Phar;
+use function strlen;
 use Symfony\Component\Process\PhpExecutableFinder;
 use Symfony\Component\Process\Process;
 
@@ -47,10 +51,25 @@ public function createGitCommitProcess(File $file): ChurnProcess
      */
     public function createCyclomaticComplexityProcess(File $file): ChurnProcess
     {
-        $php = (new PhpExecutableFinder())->find();
-        $script = __DIR__ . '/../../bin/CyclomaticComplexityAssessorRunner';
-        $process = new Process([$php, $script, $file->getFullPath()]);
+        $command = array_merge(
+            [(new PhpExecutableFinder())->find()],
+            $this->getAssessorArguments(),
+            [$file->getFullPath()]
+        );
+        $process = new Process($command);
 
         return new ChurnProcess($file, $process, 'CyclomaticComplexityProcess');
     }
+
+    /**
+     * @return string[]
+     */
+    private function getAssessorArguments(): array
+    {
+        if (is_callable([Phar::class, 'running']) && strlen(Phar::running(false)) > 0) {
+            return [Phar::running(false), 'assess-complexity'];
+        }
+
+        return [__DIR__ . '/../../bin/CyclomaticComplexityAssessorRunner'];
+    }
 }


Test patch:
diff --git a/tests/Integration/Command/AssessComplexityCommandTest.php b/tests/Integration/Command/AssessComplexityCommandTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Integration/Command/AssessComplexityCommandTest.php
@@ -0,0 +1,42 @@
+<?php declare(strict_types = 1);
+
+namespace Churn\Tests\Integration\Command;
+
+use Churn\Command\AssessComplexityCommand;
+use Churn\Tests\BaseTestCase;
+use function ctype_digit;
+use DI\ContainerBuilder;
+use function rtrim;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class AssessComplexityCommandTest extends BaseTestCase
+{
+    /** @var CommandTester */
+    private $commandTester;
+
+    protected function setUp()
+    {
+        $container = ContainerBuilder::buildDevContainer();
+        $application = new Application('churn-php', 'test');
+        $application->add($container->get(AssessComplexityCommand::class));
+        $command = $application->find('assess-complexity');
+        $this->commandTester = new CommandTester($command);
+    }
+ 
+    protected function tearDown()
+    {
+        $this->commandTester = null;
+    }
+
+    /** @test */
+    public function it_returns_the_cyclomatic_complexity()
+    {
+        $exitCode = $this->commandTester->execute(['file' => __FILE__]);
+        $result = rtrim($this->commandTester->getDisplay());
+
+        $this->assertEquals(0, $exitCode);
+        $this->assertTrue(ctype_digit($result), 'The result of the command must be an integer');
+        $this->assertGreaterThan(0, (int) $result);
+    }
+}