alanwillms/php-hound

View on GitHub
src/Command.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
namespace phphound;

use League\CLImate\CLImate;
use phphound\output\AbstractOutput;
use phphound\output\filter\DiffOutputFilter;
use ReflectionMethod;
use SebastianBergmann\Diff\Parser;
use SebastianBergmann\Git\Git;
use UnexpectedValueException;

/**
 * Command line tool that run all script analyzers.
 */
class Command
{
    /**
     * CLI tool.
     * @var CLImate CLImate instance.
     */
    protected $cli;

    /**
     * Analyser.
     * @var Analyser analyser instance.
     */
    protected $analyser;

    /**
     * Command line arguments.
     * @var array list of arguments.
     */
    protected $arguments;

    /**
     * Analysis targets paths.
     * @var array list of files and directories paths.
     */
    protected $analysedPaths;

    /**
     * Composer binaries directory path.
     * @var string directory path.
     */
    protected $binariesPath;

    /**
     * Set dependencies and initialize CLI.
     * @param CLImate $climate CLImate instance.
     * @param string $binariesPath Composer binaries path.
     * @param array $arguments command line arguments.
     */
    public function __construct(CLImate $climate, $binariesPath, array $arguments)
    {
        $this->cli = $climate;
        $this->cli->description($this->getDescription());
        $this->cli->arguments->add($this->getArguments());
        $this->cli->arguments->parse($arguments);

        $this->arguments = $arguments;
        $this->binariesPath = $binariesPath;
        $this->setAnalysedPathsFromString($this->getArgumentValue('path'));
    }

    /**
     * Run PHP-Hound command.
     * @return boolean true if it didn't find code issues or ran successfully.
     */
    public function run()
    {
        if ($this->hasArgumentValue('help')) {
            $this->cli->usage();
            return true;
        }

        if ($this->hasArgumentValue('version')) {
            $this->cli->out($this->getDescription());
            return true;
        }

        if ($this->hasArgumentValue('git-diff')) {
            $gitDiff = $this->getArgumentValue('git-diff');
            $filter = $this->getGitDiffFilter($gitDiff);
            $analysedFiles = $filter->getFilesWithAddedCode();
            $this->setAnalysedPaths($analysedFiles);
            $this->getAnalyser()->setResultsFilter($filter);
            $this->getAnalyser()->setAnalysedPaths($analysedFiles);
        }

        return $this->getAnalyser()->run();
    }

    /**
     * Create a DiffOutputFilter based on a git-diff param.
     * @param string $gitDiff git diff arguments.
     * @return DiffOutputFilter filter instance.
     */
    protected function getGitDiffFilter($gitDiff)
    {
        $analysedPaths = $this->getAnalysedPaths();
        $gitPath = array_shift($analysedPaths);
        if (!is_dir($gitPath)) {
            $gitPath = dirname($gitPath);
        }
        $git = new Git($gitPath);
        $executeMethod = new ReflectionMethod($git, 'execute');
        $executeMethod->setAccessible(true);
        $gitRoot = trim(implode("\n", $executeMethod->invoke($git, 'git rev-parse --show-toplevel')));
        list($base, $changed) = explode('..', $gitDiff);
        $diff = $git->getDiff($base, $changed);
        $diffParser = new Parser;
        return new DiffOutputFilter($gitRoot, $diffParser->parse($diff));
    }

    /**
     * Initialize output.
     * @throws UnexpectedValueException on invalid format value.
     * @return AbstractOutput
     */
    protected function getOutput()
    {
        $format = $this->getOutputFormat();
        $formatClasses = $this->getOutputFormatClasses();

        if (!isset($formatClasses[$format])) {
            throw new UnexpectedValueException(
                'Invalid format: "' . $format . '"'
            );
        }

        $outputClassName = $formatClasses[$format];

        return new $outputClassName(
            $this->cli,
            $this->getWorkingDirectory()
        );
    }

    /**
     * Command line arguments list for CLImate.
     * @return array CLI list of arguments.
     */
    protected function getArguments()
    {
        return [
            'help' => [
                'prefix' => 'h',
                'longPrefix' => 'help',
                'description' => 'Prints a usage statement',
                'noValue' => true,
            ],
            'version' => [
                'prefix' => 'v',
                'longPrefix' => 'version',
                'description' => 'Prints installed version',
                'noValue' => true,
            ],
            'ignore' => [
                'prefix' => 'i',
                'longPrefix' => 'ignore',
                'description' => 'Ignore a comma-separated list of directories',
                'castTo' => 'string',
                'defaultValue' => 'vendor,tests,features,spec',
            ],
            'format' => [
                'prefix' => 'f',
                'longPrefix' => 'format',
                'description' => 'Output format',
                'castTo' => 'string',
                'defaultValue' => 'text',
            ],
            'git-diff' => [
                'prefix' => 'g',
                'longPrefix' => 'git-diff',
                'description' => 'Limit to files and lines changed between two '
                                . 'commits or branches, e.g., "master..other".',
                'castTo' => 'string',
            ],
            'path' => [
                'description' => 'File or directory path to analyze',
                'defaultValue' => '.',
            ],
        ];
    }

    /**
     * Get a list of paths to be ignored by the analysis.
     * @return string[] a list of file and/or directory paths.
     */
    public function getIgnoredPaths()
    {
        $ignoredArgument = $this->getArgumentValue('ignore');
        $ignoredPaths = explode(',', $ignoredArgument);
        return array_filter($ignoredPaths);
    }

    /**
     * Parse a string of comma separated files and/or directories to be analysed.
     * @param string $pathsString the path argument value.
     * @return void
     */
    protected function setAnalysedPathsFromString($pathsString)
    {
        $rawAnalysedPaths = explode(',', $pathsString);
        $analysedPaths = array_filter($rawAnalysedPaths);
        $this->setAnalysedPaths(array_filter($rawAnalysedPaths));
    }

    /**
     * Set target files and/or directories to be analysed. Fix relative paths.
     * @param string[] $paths target paths.
     * @return void
     */
    protected function setAnalysedPaths(array $paths)
    {
        foreach ($paths as &$path) {
            if (0 === strpos($path, DIRECTORY_SEPARATOR)) {
                continue;
            }
            $path = $this->getWorkingDirectory() . DIRECTORY_SEPARATOR . $path;
        }
        $this->analysedPaths = $paths;
    }

    /**
     * Analysis target paths.
     * @return string[] a list of analysed paths (usually just one).
     */
    public function getAnalysedPaths()
    {
        return $this->analysedPaths;
    }

    /**
     * Running script path.
     * @return string current script directory.
     */
    public function getWorkingDirectory()
    {
        return getcwd();
    }

    /**
     * Output format.
     * @return string format type.
     */
    public function getOutputFormat()
    {
        return $this->getArgumentValue('format');
    }

    /**
     * CLI output description.
     * @return string description.
     */
    public function getDescription()
    {
        return 'PHP Hound ' . Analyser::VERSION;
    }

    /**
     * Analyser instance.
     * @return Analyser instance.
     */
    public function getAnalyser()
    {
        if (null === $this->analyser) {
            $this->analyser = new Analyser(
                $this->getOutput(),
                $this->binariesPath,
                $this->getAnalysedPaths(),
                $this->getIgnoredPaths()
            );
        }
        return $this->analyser;
    }

    /**
     * List of output format classes.
     * @return array array where the key is a format and its value the class.
     */
    protected function getOutputFormatClasses()
    {
        return [
            'text' => 'phphound\output\TextOutput',
            'json' => 'phphound\output\JsonOutput',
            'xml' => 'phphound\output\XmlOutput',
            'csv' => 'phphound\output\CsvOutput',
            'html' => 'phphound\output\HtmlOutput',
        ];
    }

    /**
     * Get argument value from user informed arguments.
     * @param string $name argument name.
     * @return Mixed argument value.
     */
    protected function getArgumentValue($name)
    {
        return $this->cli->arguments->get($name);
    }

    /**
     * Check if the user supplied an argument.
     * @param string $name argument name.
     * @return boolean if the argument has informed or not.
     */
    protected function hasArgumentValue($name)
    {
        return $this->cli->arguments->defined($name, $this->arguments);
    }
}