├── bin ├── static-review └── static-review.php ├── src ├── Review │ ├── ReviewableInterface.php │ ├── ReviewInterface.php │ ├── AbstractFileReview.php │ ├── AbstractMessageReview.php │ ├── Message │ │ ├── WorkInProgressReview.php │ │ ├── SubjectLinePeriodReview.php │ │ ├── SubjectLineCapitalReview.php │ │ ├── SubjectLineLengthReview.php │ │ ├── BodyLineLengthReview.php │ │ └── SubjectImperativeReview.php │ ├── Composer │ │ ├── ComposerLintReview.php │ │ └── ComposerSecurityReview.php │ ├── General │ │ ├── NoCommitTagReview.php │ │ └── LineEndingsReview.php │ ├── PHP │ │ ├── PhpLeadingLineReview.php │ │ ├── PhpLintReview.php │ │ └── PhpCodeSnifferReview.php │ └── AbstractReview.php ├── VersionControl │ ├── VersionControlInterface.php │ └── GitVersionControl.php ├── Issue │ ├── IssueInterface.php │ └── Issue.php ├── Commit │ ├── CommitMessageInterface.php │ └── CommitMessage.php ├── File │ ├── FileInterface.php │ └── File.php ├── Reporter │ ├── ReporterInterface.php │ └── Reporter.php ├── Collection │ ├── FileCollection.php │ ├── IssueCollection.php │ ├── ReviewCollection.php │ └── Collection.php ├── Command │ ├── HookListCommand.php │ ├── HookRunCommand.php │ └── HookInstallCommand.php └── StaticReview.php ├── LICENSE ├── hooks ├── example-pre-commit.php ├── php-pre-commit.php ├── static-review-pre-commit.php └── static-review-commit-msg.php └── composer.json /bin/static-review: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE.md 13 | */ 14 | 15 | include(__DIR__ . '/static-review.php'); 16 | -------------------------------------------------------------------------------- /src/Review/ReviewableInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review; 15 | 16 | interface ReviewableInterface 17 | { 18 | public function getName(); 19 | } 20 | -------------------------------------------------------------------------------- /src/VersionControl/VersionControlInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\VersionControl; 15 | 16 | interface VersionControlInterface 17 | { 18 | public function getStagedFiles(); 19 | } 20 | -------------------------------------------------------------------------------- /src/Issue/IssueInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Issue; 15 | 16 | interface IssueInterface 17 | { 18 | public function getReviewName(); 19 | 20 | public function getLevelName(); 21 | 22 | public function getMessage(); 23 | 24 | public function getSubject(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Commit/CommitMessageInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Commit; 15 | 16 | use StaticReview\Review\ReviewableInterface; 17 | 18 | interface CommitMessageInterface extends ReviewableInterface 19 | { 20 | public function getSubject(); 21 | 22 | public function getBody(); 23 | 24 | public function getHash(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Review/ReviewInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | 18 | interface ReviewInterface 19 | { 20 | public function canReview(ReviewableInterface $subject); 21 | 22 | public function review(ReporterInterface $reporter, ReviewableInterface $subject); 23 | } 24 | -------------------------------------------------------------------------------- /src/Review/AbstractFileReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review; 15 | 16 | use StaticReview\Commit\CommitMessageInterface; 17 | use Symfony\Component\Process\Process; 18 | 19 | abstract class AbstractFileReview extends AbstractReview 20 | { 21 | protected function canReviewMessage(CommitMessageInterface $message) 22 | { 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Review/AbstractMessageReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review; 15 | 16 | use StaticReview\Commit\CommitMessageInterface; 17 | use StaticReview\File\FileInterface; 18 | use Symfony\Component\Process\Process; 19 | 20 | abstract class AbstractMessageReview extends AbstractReview 21 | { 22 | protected function canReviewFile(FileInterface $file) 23 | { 24 | return false; 25 | } 26 | 27 | protected function canReviewMessage(CommitMessageInterface $message) 28 | { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/File/FileInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\File; 15 | 16 | use StaticReview\Review\ReviewableInterface; 17 | 18 | interface FileInterface extends ReviewableInterface 19 | { 20 | public function getFileName(); 21 | 22 | public function getRelativePath(); 23 | 24 | public function getFullPath(); 25 | 26 | public function getCachedPath(); 27 | 28 | public function setCachedPath($path); 29 | 30 | public function getExtension(); 31 | 32 | public function getStatus(); 33 | 34 | public function getFormattedStatus(); 35 | 36 | public function getMimeType(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Review/Message/WorkInProgressReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | class WorkInProgressReview extends AbstractMessageReview 21 | { 22 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 23 | { 24 | $fulltext = $commit->getSubject() . PHP_EOL . $commit->getBody(); 25 | 26 | if (preg_match('/\bwip\b/i', $fulltext)) { 27 | $message = 'Do not commit WIP to shared branches'; 28 | $reporter->error($message, $this, $commit); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Reporter/ReporterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Reporter; 15 | 16 | use StaticReview\Review\ReviewInterface; 17 | use StaticReview\Review\ReviewableInterface; 18 | 19 | interface ReporterInterface 20 | { 21 | public function report($level, $message, ReviewInterface $review, ReviewableInterface $subject); 22 | 23 | public function info($message, ReviewInterface $review, ReviewableInterface $subject); 24 | 25 | public function warning($message, ReviewInterface $review, ReviewableInterface $subject); 26 | 27 | public function error($message, ReviewInterface $review, ReviewableInterface $subject); 28 | 29 | public function hasIssues(); 30 | 31 | public function getIssues(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Review/Message/SubjectLinePeriodReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | /** 21 | * Rule 4: Do not end the subject line with a period 22 | * 23 | * 24 | */ 25 | class SubjectLinePeriodReview extends AbstractMessageReview 26 | { 27 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 28 | { 29 | if (substr($commit->getSubject(), -1) === '.') { 30 | $message = 'Subject line must not end with a period'; 31 | $reporter->error($message, $this, $commit); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Review/Message/SubjectLineCapitalReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | /** 21 | * Rule 3: Capitalize the subject line 22 | * 23 | * 24 | */ 25 | class SubjectLineCapitalReview extends AbstractMessageReview 26 | { 27 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 28 | { 29 | if (!preg_match('/^[A-Z]/u', $commit->getSubject())) { 30 | $message = 'Subject line must begin with a capital letter'; 31 | $reporter->error($message, $this, $commit); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Samuel Parkinson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/static-review.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 13 | */ 14 | 15 | $included = false; 16 | foreach ([ 17 | __DIR__ . '/../../../autoload.php', 18 | __DIR__ . '/../../vendor/autoload.php', 19 | __DIR__ . '/../vendor/autoload.php' 20 | ] as $file) { 21 | if (file_exists($file)) { 22 | $included = include $file; 23 | break; 24 | } 25 | } 26 | 27 | if (! $included) { 28 | echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL 29 | . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL 30 | . 'php composer.phar install' . PHP_EOL; 31 | 32 | exit(1); 33 | } 34 | 35 | use StaticReview\Command\HookInstallCommand; 36 | use StaticReview\Command\HookListCommand; 37 | use StaticReview\Command\HookRunCommand; 38 | use Symfony\Component\Console\Application; 39 | 40 | $name = 'StaticReview Command Line Tool'; 41 | $version = '3.0.0'; 42 | 43 | $console = new Application($name, $version); 44 | 45 | $console->addCommands([ 46 | new HookListCommand(), 47 | new HookInstallCommand(), 48 | new HookRunCommand(), 49 | ]); 50 | 51 | $console->run(); 52 | -------------------------------------------------------------------------------- /src/Collection/FileCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Collection; 15 | 16 | use StaticReview\File\FileInterface; 17 | 18 | class FileCollection extends Collection 19 | { 20 | /** 21 | * Validates that $object is an instance of FileInterface. 22 | * 23 | * @param FileInterface $object 24 | * @return bool 25 | * @throws InvalidArgumentException 26 | */ 27 | public function validate($object) 28 | { 29 | if ($object instanceof FileInterface) { 30 | return true; 31 | } 32 | 33 | $exceptionMessage = $object . ' was not an instance of FileInterface.'; 34 | 35 | throw new \InvalidArgumentException($exceptionMessage); 36 | } 37 | 38 | /** 39 | * Filters the collection with the given closure, returning a new collection. 40 | * 41 | * @return FileCollection 42 | */ 43 | public function select(callable $filter) 44 | { 45 | if (! $this->collection) { 46 | return new FileCollection(); 47 | } 48 | 49 | $filtered = array_filter($this->collection, $filter); 50 | 51 | return new FileCollection($filtered); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Review/Message/SubjectLineLengthReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | /** 21 | * Rule 2: Limit the subject line to 50 characters 22 | * 23 | * 24 | */ 25 | class SubjectLineLengthReview extends AbstractMessageReview 26 | { 27 | /** 28 | * @var integer Allowed length limit. 29 | */ 30 | protected $maximum = 50; 31 | 32 | public function setMaximumLength($length) 33 | { 34 | $this->maximum = $length; 35 | } 36 | 37 | public function getMaximumLength() 38 | { 39 | return $this->maximum; 40 | } 41 | 42 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 43 | { 44 | if (strlen($commit->getSubject()) > $this->getMaximumLength()) { 45 | $message = sprintf( 46 | 'Subject line is greater than %d characters', 47 | $this->getMaximumLength() 48 | ); 49 | $reporter->error($message, $this, $commit); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /hooks/example-pre-commit.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 13 | */ 14 | 15 | $included = include file_exists(__DIR__ . '/../vendor/autoload.php') 16 | ? __DIR__ . '/../vendor/autoload.php' 17 | : __DIR__ . '/../../../autoload.php'; 18 | 19 | if (! $included) { 20 | echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL 21 | . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL 22 | . 'php composer.phar install' . PHP_EOL; 23 | 24 | exit(1); 25 | } 26 | 27 | // Reference the required classes and the reviews you want to use. 28 | use StaticReview\Reporter\Reporter; 29 | use StaticReview\Review\General\LineEndingsReview; 30 | use StaticReview\StaticReview; 31 | use StaticReview\VersionControl\GitVersionControl; 32 | 33 | $reporter = new Reporter(); 34 | $review = new StaticReview($reporter); 35 | 36 | // Add any reviews to the StaticReview instance, supports a fluent interface. 37 | $review->addReview(new LineEndingsReview()); 38 | 39 | $git = new GitVersionControl(); 40 | 41 | // Review the staged files. 42 | $review->files($git->getStagedFiles()); 43 | 44 | echo PHP_EOL; 45 | 46 | // Check if any issues were found. 47 | // Exit with a non-zero to block the commit. 48 | ($reporter->hasIssues()) ? exit(1) : exit(0); 49 | -------------------------------------------------------------------------------- /src/Command/HookListCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Command; 15 | 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | class HookListCommand extends Command 21 | { 22 | protected function configure() 23 | { 24 | $this->setName('hook:list'); 25 | 26 | $this->setDescription('Lists all the included hooks.'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output) 30 | { 31 | $hooksPath = realpath(__DIR__ . '/../../hooks/'); 32 | 33 | $output->writeln("Avaliable hooks:"); 34 | 35 | if ($handle = opendir($hooksPath)) { 36 | while (false !== ($entry = readdir($handle))) { 37 | if (pathinfo($entry, PATHINFO_EXTENSION) === 'php') { 38 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 39 | $output->writeln($hooksPath . DIRECTORY_SEPARATOR . $entry); 40 | } else { 41 | $output->writeln(pathinfo($entry, PATHINFO_FILENAME)); 42 | } 43 | } 44 | } 45 | 46 | closedir($handle); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Review/Composer/ComposerLintReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Composer; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class ComposerLintReview extends AbstractFileReview 22 | { 23 | /** 24 | * Lint only the composer.json file. 25 | * 26 | * @param FileInterface $file 27 | * @return bool 28 | */ 29 | public function canReviewFile(FileInterface $file) 30 | { 31 | // only if the filename is "composer.json" 32 | return ($file->getFileName() === 'composer.json'); 33 | } 34 | 35 | /** 36 | * Check the composer.json file is valid. 37 | * 38 | * @param ReporterInterface $reporter 39 | * @param FileInterface $file 40 | */ 41 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 42 | { 43 | $cmd = sprintf('composer validate %s', $file->getFullPath()); 44 | 45 | $process = $this->getProcess($cmd); 46 | $process->run(); 47 | 48 | if (! $process->isSuccessful()) { 49 | $message = 'The composer configuration is not valid'; 50 | $reporter->error($message, $this, $file); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Review/General/NoCommitTagReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\General; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class NoCommitTagReview extends AbstractFileReview 22 | { 23 | /** 24 | * Review any text based file. 25 | * 26 | * @link http://stackoverflow.com/a/632786 27 | * 28 | * @param FileInterface $file 29 | * @return bool 30 | */ 31 | public function canReviewFile(FileInterface $file) 32 | { 33 | $mime = $file->getMimeType(); 34 | 35 | // check to see if the mime-type starts with 'text' 36 | return (substr($mime, 0, 4) === 'text'); 37 | } 38 | 39 | /** 40 | * Checks if the file contains `NOCOMMIT`. 41 | * 42 | * @link http://stackoverflow.com/a/4749368 43 | */ 44 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 45 | { 46 | $cmd = sprintf('grep --fixed-strings --ignore-case --quiet "NOCOMMIT" %s', $file->getFullPath()); 47 | 48 | $process = $this->getProcess($cmd); 49 | $process->run(); 50 | 51 | if ($process->isSuccessful()) { 52 | $message = 'A NOCOMMIT tag was found'; 53 | $reporter->error($message, $this, $file); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Review/General/LineEndingsReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\General; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class LineEndingsReview extends AbstractFileReview 22 | { 23 | /** 24 | * Review any text based file. 25 | * 26 | * @link http://stackoverflow.com/a/632786 27 | * 28 | * @param FileInterface $file 29 | * @return bool 30 | */ 31 | public function canReviewFile(FileInterface $file) 32 | { 33 | $mime = $file->getMimeType(); 34 | 35 | // check to see if the mime-type starts with 'text' 36 | return (substr($mime, 0, 4) === 'text'); 37 | } 38 | 39 | /** 40 | * Checks if the set file contains any CRLF line endings. 41 | * 42 | * @link http://stackoverflow.com/a/3570574 43 | */ 44 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 45 | { 46 | $cmd = sprintf('file %s | grep --fixed-strings --quiet "CRLF"', $file->getFullPath()); 47 | 48 | $process = $this->getProcess($cmd); 49 | $process->run(); 50 | 51 | if ($process->isSuccessful()) { 52 | $message = 'File contains CRLF line endings'; 53 | $reporter->error($message, $this, $file); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Review/PHP/PhpLeadingLineReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\PHP; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class PhpLeadingLineReview extends AbstractFileReview 22 | { 23 | /** 24 | * Determins if the given file should be revewed. 25 | * 26 | * @param FileInterface $file 27 | * @return bool 28 | */ 29 | public function canReviewFile(FileInterface $file) 30 | { 31 | return ($file->getExtension() === 'php'); 32 | } 33 | 34 | /** 35 | * Checks if the set file starts with the correct character sequence, which 36 | * helps to stop any rouge whitespace making it in before the first php tag. 37 | * 38 | * @link http://stackoverflow.com/a/2440685 39 | */ 40 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 41 | { 42 | $cmd = sprintf('read -r LINE < %s && echo $LINE', $file->getFullPath()); 43 | 44 | $process = $this->getProcess($cmd); 45 | $process->run(); 46 | 47 | if (! in_array(trim($process->getOutput()), ['error($message, $this, $file); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Review/Composer/ComposerSecurityReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Composer; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class ComposerSecurityReview extends AbstractFileReview 22 | { 23 | /** 24 | * Check the composer.lock file for security issues. 25 | * 26 | * @param FileInterface $file 27 | * @return bool 28 | */ 29 | public function canReviewFile(FileInterface $file) 30 | { 31 | if ($file->getFileName() === 'composer.lock') { 32 | return true; 33 | } 34 | 35 | return false; 36 | } 37 | 38 | /** 39 | * Check the composer.lock file doesn't contain dependencies 40 | * with known security vulnerabilities. 41 | * 42 | * @param ReporterInterface $reporter 43 | * @param FileInterface $file 44 | */ 45 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 46 | { 47 | $executable = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, 'vendor/bin/security-checker'); 48 | 49 | $cmd = sprintf('%s security:check %s', $executable, $file->getFullPath()); 50 | 51 | $process = $this->getProcess($cmd); 52 | $process->run(); 53 | 54 | if (! $process->isSuccessful()) { 55 | $message = 'The composer project dependencies contain known vulnerabilities'; 56 | $reporter->error($message, $this, $file); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sjparkinson/static-review", 3 | 4 | "description": "An extendable framework for version control hooks.", 5 | 6 | "license": "MIT", 7 | 8 | "authors": 9 | [ 10 | { 11 | "name": "Samuel Parkinson", 12 | "email": "sam.james.parkinson@gmail.com", 13 | "homepage": "http://samp.im" 14 | } 15 | ], 16 | 17 | "support": { 18 | "source": "https://github.com/sjparkinson/static-review", 19 | "issues": "https://github.com/sjparkinson/static-review/issues" 20 | }, 21 | 22 | "autoload": { 23 | "psr-4": { 24 | "StaticReview\\": "src/" 25 | } 26 | }, 27 | 28 | "autoload-dev": { 29 | "psr-4": { 30 | "StaticReview\\Test\\Unit\\": "tests/unit/", 31 | "StaticReview\\Test\\Functional\\": "tests/functional/" 32 | } 33 | }, 34 | 35 | "bin": [ 36 | "bin/static-review.php" 37 | ], 38 | 39 | "require": { 40 | "php": "^5.5 || ^7.0", 41 | "league/climate": "^2.0 || ^3.0", 42 | "symfony/console": "^2.0", 43 | "symfony/process": "^2.1" 44 | }, 45 | 46 | "require-dev": { 47 | "mockery/mockery": "^0.9", 48 | "phpunit/phpunit": "^4.6 || ^5.0", 49 | "sensiolabs/security-checker": "^3.0", 50 | "squizlabs/php_codesniffer": "^2.2" 51 | }, 52 | 53 | "suggest": { 54 | "sensiolabs/security-checker": "required by ComposerSecurityReview.", 55 | "squizlabs/php_codesniffer": "required by PhpCodeSnifferReview." 56 | }, 57 | 58 | "scripts": { 59 | "test": [ 60 | "vendor/bin/phpcs --colors --standard=PSR2 src/ bin/ hooks/ tests/", 61 | "vendor/bin/phpunit --color=always --testsuite unit", 62 | "vendor/bin/phpunit --color=always --testsuite functional" 63 | ] 64 | }, 65 | 66 | "config": { 67 | "optimize-autoloader": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Review/PHP/PhpLintReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\PHP; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class PhpLintReview extends AbstractFileReview 22 | { 23 | /** 24 | * Determins if a given file should be reviewed. 25 | * 26 | * @param FileInterface $file 27 | * @return bool 28 | */ 29 | public function canReviewFile(FileInterface $file) 30 | { 31 | $extension = $file->getExtension(); 32 | 33 | return ($extension === 'php' || $extension === 'phtml'); 34 | } 35 | 36 | /** 37 | * Checks PHP files using the builtin PHP linter, `php -l`. 38 | */ 39 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 40 | { 41 | $cmd = sprintf('php --syntax-check %s', $file->getFullPath()); 42 | 43 | $process = $this->getProcess($cmd); 44 | $process->run(); 45 | 46 | // Create the array of outputs and remove empty values. 47 | $output = array_filter(explode(PHP_EOL, $process->getOutput())); 48 | 49 | $needle = 'Parse error: syntax error, '; 50 | 51 | if (! $process->isSuccessful()) { 52 | foreach (array_slice($output, 0, count($output) - 1) as $error) { 53 | $raw = ucfirst(substr($error, strlen($needle))); 54 | $message = str_replace(' in ' . $file->getFullPath(), '', $raw); 55 | $reporter->error($message, $this, $file); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Collection/IssueCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Collection; 15 | 16 | use StaticReview\Issue\IssueInterface; 17 | 18 | class IssueCollection extends Collection 19 | { 20 | /** 21 | * Validates that $object is an instance of IssueInterface. 22 | * 23 | * @param IssueInterface $object 24 | * @return bool 25 | * @throws InvalidArgumentException 26 | */ 27 | public function validate($object) 28 | { 29 | if ($object instanceof IssueInterface) { 30 | return true; 31 | } 32 | 33 | throw new \InvalidArgumentException($object . ' was not an instance of IssueInterface.'); 34 | } 35 | 36 | /** 37 | * Filters the collection with the given closure, returning a new collection. 38 | * 39 | * @return IssueCollection 40 | */ 41 | public function select(callable $filter) 42 | { 43 | if (! $this->collection) { 44 | return new IssueCollection(); 45 | } 46 | 47 | $filtered = array_filter($this->collection, $filter); 48 | 49 | return new IssueCollection($filtered); 50 | } 51 | 52 | /** 53 | * Returns a new IssueCollection filtered by the given level option. 54 | * 55 | * @param int $level 56 | * @return IssueCollection 57 | */ 58 | public function forLevel($option) 59 | { 60 | // Only return issues matching the level. 61 | $filter = function ($issue) use ($option) { 62 | if ($issue->matches($option)) { 63 | return true; 64 | } 65 | 66 | return false; 67 | }; 68 | 69 | return $this->select($filter); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Command/HookRunCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Command; 15 | 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\Process\Process; 21 | 22 | class HookRunCommand extends Command 23 | { 24 | const ARGUMENT_HOOK = 'hook'; 25 | 26 | protected function configure() 27 | { 28 | $this->setName('hook:run'); 29 | 30 | $this->setDescription('Run the specified hook.'); 31 | 32 | $this->addArgument(self::ARGUMENT_HOOK, InputArgument::REQUIRED, 'The hook file to run.'); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $hookArg = $input->getArgument(self::ARGUMENT_HOOK); 38 | $path = $this->getTargetPath($hookArg); 39 | 40 | if (file_exists($path)) { 41 | $cmd = 'php ' . $path; 42 | 43 | $process = new Process($cmd); 44 | 45 | $process->run(function ($type, $buffer) use ($output) { 46 | $output->write($buffer); 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * @param $hookArgument string 53 | * @return string 54 | */ 55 | protected function getTargetPath($hookArgument) 56 | { 57 | if (file_exists($hookArgument)) { 58 | $target = realpath($hookArgument); 59 | } else { 60 | $path = '%s/%s.php'; 61 | $target = sprintf($path, realpath(__DIR__ . '/../../hooks/'), $hookArgument); 62 | } 63 | 64 | if (! file_exists($target)) { 65 | $error = sprintf('The hook %s does not exist!', $target); 66 | $output->writeln($error); 67 | exit(1); 68 | } 69 | 70 | return $target; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /hooks/php-pre-commit.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 13 | */ 14 | 15 | $included = include file_exists(__DIR__ . '/../vendor/autoload.php') 16 | ? __DIR__ . '/../vendor/autoload.php' 17 | : __DIR__ . '/../../../autoload.php'; 18 | 19 | if (! $included) { 20 | echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL 21 | . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL 22 | . 'php composer.phar install' . PHP_EOL; 23 | 24 | exit(1); 25 | } 26 | 27 | // Reference the required classes and the reviews you want to use. 28 | use League\CLImate\CLImate; 29 | use StaticReview\Reporter\Reporter; 30 | use StaticReview\Review\Composer\ComposerLintReview; 31 | use StaticReview\Review\General\LineEndingsReview; 32 | use StaticReview\Review\General\NoCommitTagReview; 33 | use StaticReview\Review\PHP\PhpLeadingLineReview; 34 | use StaticReview\Review\PHP\PhpLintReview; 35 | use StaticReview\StaticReview; 36 | use StaticReview\VersionControl\GitVersionControl; 37 | 38 | $reporter = new Reporter(); 39 | $climate = new CLImate(); 40 | $git = new GitVersionControl(); 41 | 42 | $review = new StaticReview($reporter); 43 | 44 | // Add any reviews to the StaticReview instance, supports a fluent interface. 45 | $review->addReview(new LineEndingsReview()) 46 | ->addReview(new PhpLeadingLineReview()) 47 | ->addReview(new NoCommitTagReview()) 48 | ->addReview(new PhpLintReview()) 49 | ->addReview(new ComposerLintReview()); 50 | 51 | // Review the staged files. 52 | $review->files($git->getStagedFiles()); 53 | 54 | // Check if any matching issues were found. 55 | if ($reporter->hasIssues()) { 56 | $climate->out('')->out(''); 57 | 58 | foreach ($reporter->getIssues() as $issue) { 59 | $climate->red($issue); 60 | } 61 | 62 | $climate->out('')->red('✘ Please fix the errors above.'); 63 | 64 | exit(1); 65 | } else { 66 | $climate->out('')->green('✔ Looking good.')->white('Have you tested everything?'); 67 | 68 | exit(0); 69 | } 70 | -------------------------------------------------------------------------------- /src/Review/Message/BodyLineLengthReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | /** 21 | * Rule 6: Wrap the body at 72 characters 22 | * 23 | * 24 | */ 25 | class BodyLineLengthReview extends AbstractMessageReview 26 | { 27 | /** 28 | * @var integer Allowed length limit. 29 | */ 30 | protected $maximum = 72; 31 | 32 | /** 33 | * @var boolean Allow long URLs to exceed the maximum. 34 | */ 35 | protected $urls = true; 36 | 37 | public function setMaximumLength($length) 38 | { 39 | $this->maximum = $length; 40 | } 41 | 42 | public function getMaximumLength() 43 | { 44 | return $this->maximum; 45 | } 46 | 47 | public function setAllowLongUrls($enable) 48 | { 49 | $this->urls = (bool) $enable; 50 | } 51 | 52 | public function getAllowLongUrls() 53 | { 54 | return $this->urls; 55 | } 56 | 57 | private function isLineTooLong($line) 58 | { 59 | return strlen($line) > $this->getMaximumLength(); 60 | } 61 | 62 | private function doesContainUrl($line) 63 | { 64 | if ($this->getAllowLongUrls()) { 65 | // It might contain a URL, but URL checking is disabled. 66 | return false; 67 | } 68 | 69 | return strpos($line, '://') !== false; 70 | } 71 | 72 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 73 | { 74 | $lines = preg_split('/(\r?\n)+/', $commit->getBody()); 75 | foreach ($lines as $line) { 76 | if ($this->isLineTooLong($line) && !$this->doesContainUrl($line)) { 77 | $message = sprintf( 78 | 'Body line is greater than %d characters ( "%s ..." )', 79 | $this->getMaximumLength(), 80 | substr($line, 0, 16) 81 | ); 82 | $reporter->error($message, $this, $commit); 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /hooks/static-review-pre-commit.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 13 | */ 14 | 15 | $included = include file_exists(__DIR__ . '/../vendor/autoload.php') 16 | ? __DIR__ . '/../vendor/autoload.php' 17 | : __DIR__ . '/../../../autoload.php'; 18 | 19 | if (! $included) { 20 | echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL 21 | . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL 22 | . 'php composer.phar install' . PHP_EOL; 23 | 24 | exit(1); 25 | } 26 | 27 | // Reference the required classes and the reviews you want to use. 28 | use League\CLImate\CLImate; 29 | use StaticReview\Issue\Issue; 30 | use StaticReview\Reporter\Reporter; 31 | use StaticReview\Review\Composer\ComposerLintReview; 32 | use StaticReview\Review\Composer\ComposerSecurityReview; 33 | use StaticReview\Review\General\LineEndingsReview; 34 | use StaticReview\Review\PHP\PhpCodeSnifferReview; 35 | use StaticReview\Review\PHP\PhpLeadingLineReview; 36 | use StaticReview\Review\PHP\PhpLintReview; 37 | use StaticReview\StaticReview; 38 | use StaticReview\VersionControl\GitVersionControl; 39 | 40 | $reporter = new Reporter(); 41 | $climate = new CLImate(); 42 | $git = new GitVersionControl(); 43 | 44 | $review = new StaticReview($reporter); 45 | 46 | // Add any reviews to the StaticReview instance, supports a fluent interface. 47 | $review->addReview(new LineEndingsReview()) 48 | ->addReview(new PhpLeadingLineReview()) 49 | ->addReview(new PhpLintReview()) 50 | ->addReview(new ComposerLintReview()) 51 | ->addReview(new ComposerSecurityReview()); 52 | 53 | $codeSniffer = new PhpCodeSnifferReview(); 54 | $codeSniffer->setOption('standard', 'PSR2'); 55 | $review->addReview($codeSniffer); 56 | 57 | // Review the staged files. 58 | $review->files($git->getStagedFiles()); 59 | 60 | // Check if any matching issues were found. 61 | if ($reporter->hasIssues()) { 62 | $climate->out('')->out(''); 63 | 64 | foreach ($reporter->getIssues() as $issue) { 65 | $climate->red($issue); 66 | } 67 | 68 | $climate->out('')->red('✘ Please fix the errors above.'); 69 | 70 | exit(1); 71 | } else { 72 | $climate->out('')->green('✔ Looking good.')->white('Have you tested everything?'); 73 | 74 | exit(0); 75 | } 76 | -------------------------------------------------------------------------------- /src/Commit/CommitMessage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Commit; 15 | 16 | class CommitMessage implements CommitMessageInterface 17 | { 18 | /** 19 | * @var string Commit message subject. 20 | */ 21 | protected $subject; 22 | 23 | /** 24 | * @var string Commit message body. 25 | */ 26 | protected $body = ''; 27 | 28 | /** 29 | * @var boolean Commit identifier. 30 | */ 31 | protected $hash; 32 | 33 | /** 34 | * Initializes a new instance of the CommitMessage class. 35 | * 36 | * @param string $message 37 | * @param string $hash 38 | */ 39 | public function __construct($message, $hash = null) 40 | { 41 | // Strip out the diff that is included in the commit message when 42 | // using `git commit -v` as we should not check it as text. 43 | list($message) = preg_split('/# \-+ >8 \-+/', $message, 2); 44 | 45 | // Remove all comment lines from the message 46 | $message = preg_replace('/^#.*/m', '', $message); 47 | 48 | // Split the message by newlines 49 | $message = preg_split('/(\r?\n)+/', trim($message)); 50 | 51 | $this->subject = array_shift($message); 52 | 53 | if ($message) { 54 | $this->body = implode("\n", $message); 55 | } 56 | 57 | $this->hash = $hash; 58 | } 59 | 60 | /** 61 | * Get the commit message subject. 62 | * 63 | * @return string 64 | */ 65 | public function getSubject() 66 | { 67 | return $this->subject; 68 | } 69 | 70 | /** 71 | * Get the commit message body. 72 | * 73 | * @return string 74 | */ 75 | public function getBody() 76 | { 77 | return $this->body; 78 | } 79 | 80 | /** 81 | * Is the commit current or historical? 82 | * 83 | * @return string|null 84 | */ 85 | public function getHash() 86 | { 87 | return $this->hash ?: null; 88 | } 89 | 90 | /** 91 | * Get the reviewable name for the commit. 92 | * 93 | * @return string 94 | */ 95 | public function getName() 96 | { 97 | return $this->getHash() ?: 'current commit'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Collection/ReviewCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Collection; 15 | 16 | use StaticReview\Commit\CommitMessageInterface; 17 | use StaticReview\File\FileInterface; 18 | use StaticReview\Review\ReviewInterface; 19 | 20 | class ReviewCollection extends Collection 21 | { 22 | /** 23 | * Validates that $object is an instance of ReviewInterface. 24 | * 25 | * @param ReviewInterface $object 26 | * @return bool 27 | * @throws InvalidArgumentException 28 | */ 29 | public function validate($object) 30 | { 31 | if ($object instanceof ReviewInterface) { 32 | return true; 33 | } 34 | 35 | throw new \InvalidArgumentException($object . ' was not an instance of ReviewInterface.'); 36 | } 37 | 38 | /** 39 | * Filters the collection with the given closure, returning a new collection. 40 | * 41 | * @return ReviewCollection 42 | */ 43 | public function select(callable $filter) 44 | { 45 | if (! $this->collection) { 46 | return new ReviewCollection(); 47 | } 48 | 49 | $filtered = array_filter($this->collection, $filter); 50 | 51 | return new ReviewCollection($filtered); 52 | } 53 | 54 | /** 55 | * Returns a filtered ReviewCollection that should be run against the given 56 | * file. 57 | * 58 | * @param FileInterface $file 59 | * @return ReviewCollection 60 | */ 61 | public function forFile(FileInterface $file) 62 | { 63 | $filter = function ($review) use ($file) { 64 | if ($review->canReview($file)) { 65 | return true; 66 | } 67 | 68 | return false; 69 | }; 70 | 71 | return $this->select($filter); 72 | } 73 | 74 | /** 75 | * Returns a filtered ReviewCollection that should be run against the given 76 | * message. 77 | * 78 | * @param CommitMessage $message 79 | * @return ReviewCollection 80 | */ 81 | public function forMessage(CommitMessageInterface $message) 82 | { 83 | $filter = function ($review) use ($message) { 84 | if ($review->canReview($message)) { 85 | return true; 86 | } 87 | 88 | return false; 89 | }; 90 | 91 | return $this->select($filter); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Review/AbstractReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Commit\CommitMessageInterface; 18 | use Symfony\Component\Process\Process; 19 | 20 | abstract class AbstractReview implements ReviewInterface 21 | { 22 | abstract protected function canReviewFile(FileInterface $file); 23 | 24 | abstract protected function canReviewMessage(CommitMessageInterface $message); 25 | 26 | /** 27 | * Determine if the subject can be reviewed. 28 | * 29 | * @param ReviewableInterface $subject 30 | * @return boolean 31 | */ 32 | public function canReview(ReviewableInterface $subject) 33 | { 34 | if ($subject instanceof FileInterface) { 35 | return $this->canReviewFile($subject); 36 | } 37 | if ($subject instanceof CommitMessageInterface) { 38 | return $this->canReviewMessage($subject); 39 | } 40 | return false; 41 | } 42 | 43 | /** 44 | * @param string $commandline 45 | * @param null|string $cwd 46 | * @param null|array $env 47 | * @param null|string $input 48 | * @param int $timeout 49 | * @param array $options 50 | * 51 | * @return Process 52 | */ 53 | public function getProcess( 54 | $commandline, 55 | $cwd = null, 56 | array $env = null, 57 | $input = null, 58 | $timeout = 60, 59 | array $options = [] 60 | ) { 61 | if (null === $cwd) { 62 | $cwd = $this->getRootDirectory(); 63 | } 64 | return new Process($commandline, $cwd, $env, $input, $timeout, $options); 65 | } 66 | 67 | /** 68 | * Get the root directory for a process command. 69 | * 70 | * @return string 71 | */ 72 | private function getRootDirectory() 73 | { 74 | static $root; 75 | 76 | if (!$root) { 77 | $working = getcwd(); 78 | $myself = __DIR__; 79 | 80 | if (0 === strpos($myself, $working)) { 81 | // Local installation, the working directory is the root 82 | $root = $working; 83 | } else { 84 | // Global installation, back up above the vendor/ directory 85 | $root = realpath($myself . '/../../../../../'); 86 | } 87 | } 88 | 89 | return $root; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Collection/Collection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Collection; 15 | 16 | use \Countable; 17 | use \Iterator; 18 | 19 | abstract class Collection implements Iterator, Countable 20 | { 21 | protected $collection = []; 22 | 23 | /** 24 | * Initializes a new instance of the Collection class. 25 | */ 26 | public function __construct(array $items = []) 27 | { 28 | foreach ($items as $item) { 29 | $this->append($item); 30 | } 31 | } 32 | 33 | /** 34 | * Method should throw an InvalidArgumentException if $item is not the 35 | * expected type. 36 | * 37 | * @return bool 38 | * @throws InvalidArgumentException 39 | */ 40 | abstract public function validate($item); 41 | 42 | /** 43 | * @param callable $filter 44 | * @return Collection 45 | */ 46 | abstract public function select(callable $filter); 47 | 48 | /** 49 | * @return Collection 50 | */ 51 | public function append($item) 52 | { 53 | if ($this->validate($item)) { 54 | $this->collection[] = $item; 55 | } 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return int 62 | */ 63 | public function count() 64 | { 65 | return count($this->collection); 66 | } 67 | 68 | /** 69 | * @return mixed 70 | */ 71 | public function current() 72 | { 73 | return current($this->collection); 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | public function key() 80 | { 81 | return key($this->collection); 82 | } 83 | 84 | /** 85 | * @return mixed 86 | */ 87 | public function next() 88 | { 89 | return next($this->collection); 90 | } 91 | 92 | /** 93 | * @return Collection 94 | */ 95 | public function rewind() 96 | { 97 | reset($this->collection); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @return bool 104 | */ 105 | public function valid() 106 | { 107 | return key($this->collection) !== null; 108 | } 109 | 110 | /** 111 | * @return string 112 | */ 113 | public function __toString() 114 | { 115 | return sprintf('%s(%s)', get_class($this), $this->count()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /hooks/static-review-commit-msg.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | * 12 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 13 | */ 14 | 15 | $included = include file_exists(__DIR__ . '/../vendor/autoload.php') 16 | ? __DIR__ . '/../vendor/autoload.php' 17 | : __DIR__ . '/../../../autoload.php'; 18 | 19 | if (! $included) { 20 | echo 'You must set up the project dependencies, run the following commands:' . PHP_EOL 21 | . 'curl -sS https://getcomposer.org/installer | php' . PHP_EOL 22 | . 'php composer.phar install' . PHP_EOL; 23 | 24 | exit(1); 25 | } 26 | 27 | if (empty($argv[1]) || !is_file($argv[1])) { 28 | echo 'WARNING: Skipping commit message check because the Git hook was not ' . PHP_EOL 29 | . 'passed the commit message file path; normally `.git/COMMIT_EDITMSG`' . PHP_EOL; 30 | 31 | exit(1); 32 | } 33 | 34 | // Reference the required classes and the reviews you want to use. 35 | use League\CLImate\CLImate; 36 | use StaticReview\Issue\Issue; 37 | use StaticReview\Reporter\Reporter; 38 | use StaticReview\Review\Message\BodyLineLengthReview; 39 | use StaticReview\Review\Message\SubjectImperativeReview; 40 | use StaticReview\Review\Message\SubjectLineCapitalReview; 41 | use StaticReview\Review\Message\SubjectLineLengthReview; 42 | use StaticReview\Review\Message\SubjectLinePeriodReview; 43 | use StaticReview\Review\Message\WorkInProgressReview; 44 | use StaticReview\StaticReview; 45 | use StaticReview\VersionControl\GitVersionControl; 46 | 47 | $reporter = new Reporter(); 48 | $climate = new CLImate(); 49 | $git = new GitVersionControl(); 50 | 51 | $review = new StaticReview($reporter); 52 | 53 | // Add any reviews to the StaticReview instance, supports a fluent interface. 54 | $review->addReview(new BodyLineLengthReview()) 55 | ->addReview(new SubjectImperativeReview()) 56 | ->addReview(new SubjectLineCapitalReview()) 57 | ->addReview(new SubjectLineLengthReview()) 58 | ->addReview(new SubjectLinePeriodReview()) 59 | ->addReview(new WorkInProgressReview()); 60 | 61 | // Check the commit message. 62 | $review->message($git->getCommitMessage($argv[1])); 63 | 64 | // Check if any matching issues were found. 65 | if ($reporter->hasIssues()) { 66 | $climate->out('')->out(''); 67 | 68 | foreach ($reporter->getIssues() as $issue) { 69 | $climate->red($issue); 70 | } 71 | 72 | $climate->out('')->red('✘ Please fix the errors above using: git commit --amend'); 73 | 74 | exit(0); 75 | } else { 76 | $climate->green('✔ That commit looks good!'); 77 | 78 | exit(0); 79 | } 80 | -------------------------------------------------------------------------------- /src/Command/HookInstallCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Command; 15 | 16 | use Symfony\Component\Console\Command\Command; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | class HookInstallCommand extends Command 23 | { 24 | const ARG_SOURCE = 'source'; 25 | const ARG_TARGET = 'target'; 26 | 27 | protected function configure() 28 | { 29 | $this->setName('hook:install'); 30 | 31 | $this->setDescription('Symlink a hook to the given target.'); 32 | 33 | $this->addArgument( 34 | self::ARG_SOURCE, 35 | InputArgument::REQUIRED, 36 | 'The hook to link, either a path to a file or the filename of a hook in the hooks folder.' 37 | ); 38 | 39 | $this->addArgument( 40 | self::ARG_TARGET, 41 | InputArgument::REQUIRED, 42 | 'The target location, including the filename (e.g. .git/hooks/pre-commit).' 43 | ); 44 | 45 | $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Overrite any existing files at the symlink target.'); 46 | } 47 | 48 | protected function execute(InputInterface $input, OutputInterface $output) 49 | { 50 | $source = realpath($input->getArgument(self::ARG_SOURCE)); 51 | $target = $input->getArgument(self::ARG_TARGET); 52 | $force = $input->getOption('force'); 53 | 54 | if ($output->isVeryVerbose()) { 55 | $message = sprintf('Using %s as the hook.', $source); 56 | $output->writeln($message); 57 | 58 | $message = sprintf('Using %s for the install path.', $target); 59 | $output->writeln($message); 60 | } 61 | 62 | if (! file_exists($source)) { 63 | $error = sprintf('The hook %s does not exist!', $source); 64 | $output->writeln($error); 65 | exit(1); 66 | } 67 | 68 | if (! is_dir(dirname($target))) { 69 | $message = sprintf('The directory at %s does not exist.', $target); 70 | $output->writeln($message); 71 | exit(1); 72 | } 73 | 74 | if (file_exists($target) && $force) { 75 | unlink($target); 76 | 77 | $message = sprintf('Removed existing file at %s.', $target); 78 | $output->writeln($message); 79 | } 80 | 81 | if (! file_exists($target) || $force) { 82 | symlink($source, $target); 83 | chmod($target, 0755); 84 | $output->writeln('Symlink created.'); 85 | } else { 86 | $message = sprintf('A file at %s already exists.', $target); 87 | $output->writeln($message); 88 | exit(1); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Reporter/Reporter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Reporter; 15 | 16 | use StaticReview\Collection\IssueCollection; 17 | use StaticReview\Issue\Issue; 18 | use StaticReview\Review\ReviewInterface; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class Reporter implements ReporterInterface 22 | { 23 | protected $issues; 24 | 25 | /** 26 | * Initializes a new instance of the Reporter class. 27 | * 28 | * @param IssueCollection $issues 29 | * @return Reporter 30 | */ 31 | public function __construct() 32 | { 33 | $this->issues = new IssueCollection(); 34 | } 35 | 36 | public function progress($current, $total) 37 | { 38 | echo sprintf("Reviewing %d of %d.\r", $current, $total); 39 | } 40 | 41 | /** 42 | * Reports an Issue raised by a Review. 43 | * 44 | * @param int $level 45 | * @param string $message 46 | * @param ReviewInterface $review 47 | * @param ReviewableInterface $subject 48 | * @return Reporter 49 | */ 50 | public function report($level, $message, ReviewInterface $review, ReviewableInterface $subject) 51 | { 52 | $issue = new Issue($level, $message, $review, $subject); 53 | 54 | $this->issues->append($issue); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Reports an Info Issue raised by a Review. 61 | * 62 | * @param string $message 63 | * @param ReviewInterface $review 64 | * @param ReviewableInterface $subject 65 | * @return Reporter 66 | */ 67 | public function info($message, ReviewInterface $review, ReviewableInterface $subject) 68 | { 69 | $this->report(Issue::LEVEL_INFO, $message, $review, $subject); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Reports an Warning Issue raised by a Review. 76 | * 77 | * @param string $message 78 | * @param ReviewInterface $review 79 | * @param ReviewableInterface $subject 80 | * @return Reporter 81 | */ 82 | public function warning($message, ReviewInterface $review, ReviewableInterface $subject) 83 | { 84 | $this->report(Issue::LEVEL_WARNING, $message, $review, $subject); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Reports an Error Issue raised by a Review. 91 | * 92 | * @param string $message 93 | * @param ReviewInterface $review 94 | * @param ReviewableInterface $subject 95 | * @return Reporter 96 | */ 97 | public function error($message, ReviewInterface $review, ReviewableInterface $subject) 98 | { 99 | $this->report(Issue::LEVEL_ERROR, $message, $review, $subject); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Checks if the reporter has revieved any Issues. 106 | * 107 | * @return IssueCollection 108 | */ 109 | public function hasIssues() 110 | { 111 | return (count($this->issues) > 0); 112 | } 113 | 114 | /** 115 | * Gets the reporters IssueCollection. 116 | * 117 | * @return IssueCollection 118 | */ 119 | public function getIssues() 120 | { 121 | return $this->issues; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Review/PHP/PhpCodeSnifferReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\PHP; 15 | 16 | use StaticReview\File\FileInterface; 17 | use StaticReview\Reporter\ReporterInterface; 18 | use StaticReview\Review\AbstractFileReview; 19 | use StaticReview\Review\ReviewableInterface; 20 | 21 | class PhpCodeSnifferReview extends AbstractFileReview 22 | { 23 | protected $options = []; 24 | 25 | /** 26 | * Gets the value of an option. 27 | * 28 | * @param string $option 29 | * @return string 30 | */ 31 | public function getOption($option) 32 | { 33 | return $this->options[$option]; 34 | } 35 | 36 | /** 37 | * Gets a string of the set options to pass to the command line. 38 | * 39 | * @return string 40 | */ 41 | public function getOptionsForConsole() 42 | { 43 | $builder = ''; 44 | 45 | foreach ($this->options as $option => $value) { 46 | $builder .= '--' . $option; 47 | 48 | if ($value) { 49 | $builder .= '=' . $value; 50 | } 51 | 52 | $builder .= ' '; 53 | } 54 | 55 | return $builder; 56 | } 57 | 58 | /** 59 | * Adds an option to be included when running PHP_CodeSniffer. Overwrites the values of options with the same name. 60 | * 61 | * @param string $option 62 | * @param string $value 63 | * @return PhpCodeSnifferReview 64 | */ 65 | public function setOption($option, $value) 66 | { 67 | if ($option === 'report') { 68 | throw new \RuntimeException('"report" is not a valid option name.'); 69 | } 70 | 71 | $this->options[$option] = $value; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Determins if a given file should be reviewed. 78 | * 79 | * @param FileInterface $file 80 | * @return bool 81 | */ 82 | public function canReviewFile(FileInterface $file) 83 | { 84 | return ($file->getExtension() === 'php'); 85 | } 86 | 87 | /** 88 | * Checks PHP files using PHP_CodeSniffer. 89 | */ 90 | public function review(ReporterInterface $reporter, ReviewableInterface $file) 91 | { 92 | $bin = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, 'vendor/bin/phpcs'); 93 | $cmd = $bin . ' --report=json '; 94 | 95 | if ($this->getOptionsForConsole()) { 96 | $cmd .= $this->getOptionsForConsole(); 97 | } 98 | 99 | $cmd .= $file->getFullPath(); 100 | 101 | $process = $this->getProcess($cmd); 102 | $process->run(); 103 | 104 | if (! $process->isSuccessful()) { 105 | // Create the array of outputs and remove empty values. 106 | $output = json_decode($process->getOutput(), true); 107 | 108 | $filter = function ($acc, $file) { 109 | if ($file['errors'] > 0 || $file['warnings'] > 0) { 110 | return $acc + $file['messages']; 111 | } 112 | }; 113 | 114 | foreach (array_reduce($output['files'], $filter, []) as $error) { 115 | $message = $error['message'] . ' on line ' . $error['line']; 116 | $reporter->warning($message, $this, $file); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/StaticReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview; 15 | 16 | use StaticReview\Collection\FileCollection; 17 | use StaticReview\Collection\ReviewCollection; 18 | use StaticReview\Commit\CommitMessageInterface; 19 | use StaticReview\Reporter\ReporterInterface; 20 | use StaticReview\Review\ReviewInterface; 21 | 22 | class StaticReview 23 | { 24 | /** 25 | * A ReviewCollection. 26 | */ 27 | protected $reviews; 28 | 29 | /** 30 | * A Reporter. 31 | */ 32 | protected $reporter; 33 | 34 | /** 35 | * Initializes a new instance of the StaticReview class. 36 | * 37 | * @param ReporterInterface $reporter 38 | */ 39 | public function __construct(ReporterInterface $reporter) 40 | { 41 | $this->reviews = new ReviewCollection(); 42 | 43 | $this->setReporter($reporter); 44 | } 45 | 46 | /** 47 | * Gets the ReporterInterface instance. 48 | * 49 | * @return ReporterInterface 50 | */ 51 | public function getReporter() 52 | { 53 | return $this->reporter; 54 | } 55 | 56 | /** 57 | * Sets the ReporterInterface instance. 58 | * 59 | * @param ReporterInterface $reporter 60 | * @return StaticReview 61 | */ 62 | public function setReporter(ReporterInterface $reporter) 63 | { 64 | $this->reporter = $reporter; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Returns the list of reviews. 71 | * 72 | * @return ReviewCollection 73 | */ 74 | public function getReviews() 75 | { 76 | return $this->reviews; 77 | } 78 | 79 | /** 80 | * Adds a Review to be run. 81 | * 82 | * @param ReviewInterface $check 83 | * @return StaticReview 84 | */ 85 | public function addReview(ReviewInterface $review) 86 | { 87 | $this->reviews->append($review); 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Appends a ReviewCollection to the current list of reviews. 94 | * 95 | * @param ReviewCollection $checks 96 | * @return StaticReview 97 | */ 98 | public function addReviews(ReviewCollection $reviews) 99 | { 100 | foreach ($reviews as $review) { 101 | $this->reviews->append($review); 102 | } 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Runs through each review on each file, collecting any errors. 109 | * 110 | * @return StaticReview 111 | */ 112 | public function files(FileCollection $files) 113 | { 114 | foreach ($files as $key => $file) { 115 | $this->getReporter()->progress($key + 1, count($files)); 116 | 117 | foreach ($this->getReviews()->forFile($file) as $review) { 118 | $review->review($this->getReporter(), $file); 119 | } 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Runs through each review on the commit, collecting any errors. 127 | * 128 | * @return StaticReview 129 | */ 130 | public function message(CommitMessageInterface $message) 131 | { 132 | foreach ($this->getReviews()->forMessage($message) as $review) { 133 | $review->review($this->getReporter(), $message); 134 | } 135 | 136 | return $this; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Issue/Issue.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Issue; 15 | 16 | use StaticReview\Review\ReviewableInterface; 17 | use StaticReview\Review\ReviewInterface; 18 | 19 | class Issue implements IssueInterface 20 | { 21 | /** 22 | * Issue level flags. 23 | */ 24 | const LEVEL_INFO = 1; 25 | const LEVEL_WARNING = 2; 26 | const LEVEL_ERROR = 4; 27 | const LEVEL_ALL = 7; 28 | 29 | private $level; 30 | 31 | private $message; 32 | 33 | private $review; 34 | 35 | private $subject; 36 | 37 | /** 38 | * Initializes a new instance of the Issue class. 39 | * 40 | * @param int $level 41 | * @param string $message 42 | * @param ReviewInterface $review 43 | * @param ReviewableInterface $subject 44 | */ 45 | public function __construct( 46 | $level, 47 | $message, 48 | ReviewInterface $review, 49 | ReviewableInterface $subject 50 | ) { 51 | $this->level = $level; 52 | $this->message = $message; 53 | $this->review = $review; 54 | $this->subject = $subject; 55 | } 56 | 57 | /** 58 | * Gets the Issues level. 59 | */ 60 | public function getLevel() 61 | { 62 | return $this->level; 63 | } 64 | 65 | /** 66 | * Gets the Issues message. 67 | */ 68 | public function getMessage() 69 | { 70 | return $this->message; 71 | } 72 | 73 | /** 74 | * Gets the name of the Issues Review. 75 | */ 76 | public function getReviewName() 77 | { 78 | $classPath = explode('\\', get_class($this->review)); 79 | 80 | return end($classPath); 81 | } 82 | 83 | /** 84 | * Gets the Issues Reviewable instance. 85 | */ 86 | public function getSubject() 87 | { 88 | return $this->subject; 89 | } 90 | 91 | /** 92 | * Gets the Issues level as a string. 93 | */ 94 | public function getLevelName() 95 | { 96 | switch ($this->getLevel()) { 97 | case self::LEVEL_INFO: 98 | return 'Info'; 99 | 100 | case self::LEVEL_WARNING: 101 | return 'Warning'; 102 | 103 | case self::LEVEL_ERROR: 104 | return 'Error'; 105 | 106 | default: 107 | throw new \UnexpectedValueException('Level was set to ' . $this->getLevel()); 108 | } 109 | } 110 | 111 | /** 112 | * Gets the colour to use when echoing to the console. 113 | * 114 | * @return string 115 | */ 116 | public function getColour() 117 | { 118 | switch ($this->level) { 119 | case self::LEVEL_INFO: 120 | return 'cyan'; 121 | 122 | case self::LEVEL_WARNING: 123 | return 'brown'; 124 | 125 | case self::LEVEL_ERROR: 126 | return 'red'; 127 | 128 | default: 129 | throw new \UnexpectedValueException('Could not get a colour. Level was set to ' . $this->getLevel()); 130 | } 131 | } 132 | 133 | /** 134 | * Check that the Issue matches the possible level options. 135 | * 136 | * @link http://php.net/manual/en/language.operators.bitwise.php#108679 137 | */ 138 | public function matches($option) 139 | { 140 | $result = ($this->getLevel() & $option); 141 | 142 | return ($result === $this->getLevel()); 143 | } 144 | 145 | /** 146 | * Overrides the toString method. 147 | */ 148 | public function __toString() 149 | { 150 | return sprintf( 151 | "%s %s: %s in %s", 152 | $this->getReviewName(), 153 | $this->getLevelName(), 154 | $this->getMessage(), 155 | $this->getSubject()->getName() 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/VersionControl/GitVersionControl.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\VersionControl; 15 | 16 | use StaticReview\Collection\FileCollection; 17 | use StaticReview\Commit\CommitMessage; 18 | use StaticReview\File\File; 19 | use StaticReview\File\FileInterface; 20 | use Symfony\Component\Process\Process; 21 | 22 | class GitVersionControl implements VersionControlInterface 23 | { 24 | const CACHE_DIR = '/sjparkinson.static-review/cached/'; 25 | 26 | /** 27 | * Gets a list of the files currently staged under git. 28 | * 29 | * Returns either an empty array or a tab separated list of staged files and 30 | * their git status. 31 | * 32 | * @link http://git-scm.com/docs/git-status 33 | * 34 | * @return FileCollection 35 | */ 36 | public function getStagedFiles() 37 | { 38 | $base = $this->getProjectBase(); 39 | 40 | $files = new FileCollection(); 41 | 42 | foreach ($this->getFiles() as $file) { 43 | $fileData = explode("\t", $file); 44 | $status = reset($fileData); 45 | $relativePath = end($fileData); 46 | 47 | $fullPath = rtrim($base . DIRECTORY_SEPARATOR . $relativePath); 48 | 49 | $file = new File($status, $fullPath, $base); 50 | $this->saveFileToCache($file); 51 | $files->append($file); 52 | } 53 | 54 | return $files; 55 | } 56 | 57 | /** 58 | * Get a commit message by file or log. 59 | * 60 | * If no file name is provided, the last commit message will be used. 61 | * 62 | * @param string $file 63 | * @return CommitMessage 64 | */ 65 | public function getCommitMessage($file = null) 66 | { 67 | if ($file) { 68 | $hash = null; 69 | $message = file_get_contents($file); 70 | } else { 71 | list($hash, $message) = explode(PHP_EOL, $this->getLastCommitMessage(), 2); 72 | } 73 | 74 | return new CommitMessage($message, $hash); 75 | } 76 | 77 | /** 78 | * Gets the projects base directory. 79 | * 80 | * @return string 81 | */ 82 | private function getProjectBase() 83 | { 84 | $process = new Process('git rev-parse --show-toplevel'); 85 | $process->run(); 86 | 87 | return trim($process->getOutput()); 88 | } 89 | 90 | /** 91 | * Gets the list of files from the index. 92 | * 93 | * @return array 94 | */ 95 | private function getFiles() 96 | { 97 | $process = new Process('git diff --cached --name-status --diff-filter=ACMR'); 98 | $process->run(); 99 | 100 | if ($process->isSuccessful()) { 101 | return array_filter(explode("\n", $process->getOutput())); 102 | } 103 | 104 | return []; 105 | } 106 | 107 | /** 108 | * Saves a copy of the cached version of the given file to a temp directory. 109 | * 110 | * @param FileInterface $file 111 | * @return FileInterface 112 | */ 113 | private function saveFileToCache(FileInterface $file) 114 | { 115 | $cachedPath = sys_get_temp_dir() . self::CACHE_DIR . $file->getRelativePath(); 116 | 117 | if (! is_dir(dirname($cachedPath))) { 118 | mkdir(dirname($cachedPath), 0700, true); 119 | } 120 | 121 | $cmd = sprintf('git show :%s > %s', $file->getRelativePath(), $cachedPath); 122 | $process = new Process($cmd); 123 | $process->run(); 124 | 125 | $file->setCachedPath($cachedPath); 126 | 127 | return $file; 128 | } 129 | 130 | /** 131 | * Get the last commit message subject and body. 132 | * 133 | * @return string 134 | */ 135 | private function getLastCommitMessage() 136 | { 137 | // hash 138 | // subject 139 | // body 140 | $process = new Process('git log -1 --format="%h%n%s%n%b"'); 141 | $process->run(); 142 | 143 | return trim($process->getOutput()); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Review/Message/SubjectImperativeReview.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\Review\Message; 15 | 16 | use StaticReview\Reporter\ReporterInterface; 17 | use StaticReview\Review\AbstractMessageReview; 18 | use StaticReview\Review\ReviewableInterface; 19 | 20 | /** 21 | * Rule 5: Use the imperative mood in the subject line. 22 | * 23 | * 24 | * 25 | * [Word list][1] taken from [m1foley/fit-commit][2] by Mike Foley. 26 | * 27 | * [1]: http://git.io/vnzxb 28 | * [2]: https://github.com/m1foley/fit-commit/ 29 | */ 30 | class SubjectImperativeReview extends AbstractMessageReview 31 | { 32 | /** 33 | * @var array Words in the wrong tense. 34 | */ 35 | protected $incorrect = [ 36 | // Taken from m1foley/fit-commit 37 | // Copyright (c) 2015 Mike Foley 38 | 'adds', 'adding', 'added', 39 | 'allows', 'allowing', 'allowed', 40 | 'amends', 'amending', 'amended', 41 | 'bumps', 'bumping', 'bumped', 42 | 'calculates', 'calculating', 'calculated', 43 | 'changes', 'changing', 'changed', 44 | 'cleans', 'cleaning', 'cleaned', 45 | 'commits', 'committing', 'committed', 46 | 'corrects', 'correcting', 'corrected', 47 | 'creates', 'creating', 'created', 48 | 'darkens', 'darkening', 'darkened', 49 | 'disables', 'disabling', 'disabled', 50 | 'displays', 'displaying', 'displayed', 51 | 'drys', 'drying', 'dryed', 52 | 'ends', 'ending', 'ended', 53 | 'enforces', 'enforcing', 'enforced', 54 | 'enqueues', 'enqueuing', 'enqueued', 55 | 'extracts', 'extracting', 'extracted', 56 | 'finishes', 'finishing', 'finished', 57 | 'fixes', 'fixing', 'fixed', 58 | 'formats', 'formatting', 'formatted', 59 | 'guards', 'guarding', 'guarded', 60 | 'handles', 'handling', 'handled', 61 | 'hides', 'hiding', 'hid', 62 | 'increases', 'increasing', 'increased', 63 | 'ignores', 'ignoring', 'ignored', 64 | 'implements', 'implementing', 'implemented', 65 | 'improves', 'improving', 'improved', 66 | 'keeps', 'keeping', 'kept', 67 | 'kills', 'killing', 'killed', 68 | 'makes', 'making', 'made', 69 | 'merges', 'merging', 'merged', 70 | 'moves', 'moving', 'moved', 71 | 'permits', 'permitting', 'permitted', 72 | 'prevents', 'preventing', 'prevented', 73 | 'pushes', 'pushing', 'pushed', 74 | 'rebases', 'rebasing', 'rebased', 75 | 'refactors', 'refactoring', 'refactored', 76 | 'removes', 'removing', 'removed', 77 | 'renames', 'renaming', 'renamed', 78 | 'reorders', 'reordering', 'reordered', 79 | 'requires', 'requiring', 'required', 80 | 'restores', 'restoring', 'restored', 81 | 'sends', 'sending', 'sent', 82 | 'sets', 'setting', 83 | 'separates', 'separating', 'separated', 84 | 'shows', 'showing', 'showed', 85 | 'skips', 'skipping', 'skipped', 86 | 'sorts', 'sorting', 87 | 'speeds', 'speeding', 'sped', 88 | 'starts', 'starting', 'started', 89 | 'supports', 'supporting', 'supported', 90 | 'takes', 'taking', 'took', 91 | 'tests', 'testing', 'tested', 92 | 'truncates', 'truncating', 'truncated', 93 | 'updates', 'updating', 'updated', 94 | 'uses', 'using', 'used', 95 | ]; 96 | 97 | public function review(ReporterInterface $reporter, ReviewableInterface $commit) 98 | { 99 | $regex = '/^(?:' . implode('|', $this->incorrect) . ')\b/i'; 100 | if (preg_match($regex, $commit->getSubject())) { 101 | $message = 'Subject line must use imperative present tense'; 102 | $reporter->error($message, $this, $commit); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/File/File.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @see http://github.com/sjparkinson/static-review/blob/master/LICENSE 12 | */ 13 | 14 | namespace StaticReview\File; 15 | 16 | class File implements FileInterface 17 | { 18 | const STATUS_ADDED = 'A'; 19 | 20 | const STATUS_COPIED = 'C'; 21 | 22 | const STATUS_MODIFIED = 'M'; 23 | 24 | const STATUS_RENAMED = 'R'; 25 | 26 | /** 27 | * The full path to the file. 28 | */ 29 | private $filePath; 30 | 31 | /** 32 | * The files status. 33 | */ 34 | private $fileStatus; 35 | 36 | /** 37 | * The projects base directory. 38 | */ 39 | private $projectPath; 40 | 41 | /** 42 | * The cached location of the file. 43 | */ 44 | private $cachedPath; 45 | 46 | /** 47 | * Initializes a new instance of the File class. 48 | * 49 | * @param string $fileStatus 50 | * @param string $filePath 51 | * @param string $projectPath 52 | */ 53 | public function __construct( 54 | $fileStatus, 55 | $filePath, 56 | $projectPath 57 | ) { 58 | $this->fileStatus = $fileStatus; 59 | $this->filePath = $filePath; 60 | $this->projectPath = $projectPath; 61 | } 62 | 63 | /** 64 | * Returns the name of the file including its extension. 65 | * 66 | * @return string 67 | */ 68 | public function getFileName() 69 | { 70 | return basename($this->filePath); 71 | } 72 | 73 | /** 74 | * Returns the local path to the file from the base of the git repository. 75 | * 76 | * @return string 77 | */ 78 | public function getRelativePath() 79 | { 80 | return str_replace($this->projectPath . DIRECTORY_SEPARATOR, '', $this->filePath); 81 | } 82 | 83 | /** 84 | * Returns the full path to the file. 85 | * 86 | * @return string 87 | */ 88 | public function getFullPath() 89 | { 90 | if (file_exists($this->getCachedPath())) { 91 | return $this->getCachedPath(); 92 | } 93 | 94 | return $this->filePath; 95 | } 96 | 97 | /** 98 | * Returns the path to the cached copy of the file. 99 | * 100 | * @return string 101 | */ 102 | public function getCachedPath() 103 | { 104 | return $this->cachedPath; 105 | } 106 | 107 | /** 108 | * Sets the path to the cached copy of the file. 109 | * 110 | * @param string $path 111 | * @return File 112 | */ 113 | public function setCachedPath($path) 114 | { 115 | $this->cachedPath = $path; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Returns the files extension. 122 | * 123 | * @return string 124 | */ 125 | public function getExtension() 126 | { 127 | return pathinfo($this->filePath, PATHINFO_EXTENSION); 128 | } 129 | 130 | /** 131 | * Returns the short hand git status of the file. 132 | * 133 | * @return string 134 | */ 135 | public function getStatus() 136 | { 137 | return $this->fileStatus; 138 | } 139 | 140 | /** 141 | * Returns the git status of the file as a word. 142 | * 143 | * @return string 144 | * 145 | * @throws UnexpectedValueException 146 | */ 147 | public function getFormattedStatus() 148 | { 149 | switch ($this->fileStatus) { 150 | case 'A': 151 | return 'added'; 152 | case 'C': 153 | return 'copied'; 154 | case 'M': 155 | return 'modified'; 156 | case 'R': 157 | return 'renamed'; 158 | default: 159 | throw new \UnexpectedValueException("Unknown file status: $this->fileStatus."); 160 | } 161 | } 162 | 163 | /** 164 | * Get the mime type for the file. 165 | * 166 | * @param FileInterface $file 167 | * @return string 168 | */ 169 | public function getMimeType() 170 | { 171 | // return mime type ala mimetype extension 172 | $finfo = finfo_open(FILEINFO_MIME); 173 | 174 | $mime = finfo_file($finfo, $this->getFullPath()); 175 | 176 | return $mime; 177 | } 178 | 179 | /** 180 | * Get the relative path name as the reviewable name. 181 | * 182 | * @return string 183 | */ 184 | public function getName() 185 | { 186 | return $this->getRelativePath(); 187 | } 188 | } 189 | --------------------------------------------------------------------------------