Skip to content

Commit 57de3d5

Browse files
authored
Merge pull request #15 from igorsgm/feature/phpcs-pre-commit-hook
PHPCS Pre Commit Hook implementation
2 parents ca57fd2 + a8ae18a commit 57de3d5

18 files changed

+579
-141
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"orchestra/testbench": "^7.0",
3636
"pestphp/pest": "^1.22",
3737
"pestphp/pest-plugin-mock": "^1.0",
38-
"phpunit/phpunit": "^9.0"
38+
"phpunit/phpunit": "^9.0",
39+
"squizlabs/php_codesniffer": "^3.7"
3940
},
4041
"autoload": {
4142
"psr-4": {

config/git-hooks.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@
171171
'laravel_pint' => [
172172
'path' => env('LARAVEL_PINT_PATH', 'vendor/bin/pint'),
173173
'config' => env('LARAVEL_PINT_CONFIG', 'pint.json'),
174+
'preset' => env('LARAVEL_PINT_PRESET', 'psr12'),
175+
],
176+
'php_code_sniffer' => [
177+
'phpcs_path' => env('PHPCS_PATH', 'vendor/bin/phpcs'),
178+
'phpcbf_path' => env('PHPCBF_PATH', 'vendor/bin/phpcbf'),
179+
'standard' => env('PHPCS_STANDARD', 'phpcs.xml'),
174180
],
175181
],
176182

pint.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
"preset": "laravel"
2+
"preset": "laravel",
3+
"notName": [
4+
"*WithFixableIssues.php"
5+
]
36
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
namespace Igorsgm\GitHooks\Console\Commands\Hooks;
4+
5+
use Closure;
6+
use Igorsgm\GitHooks\Exceptions\HookFailException;
7+
use Igorsgm\GitHooks\Facades\GitHooks;
8+
use Igorsgm\GitHooks\Git\ChangedFiles;
9+
use Igorsgm\GitHooks\Traits\ProcessHelper;
10+
use Illuminate\Console\Command;
11+
use Symfony\Component\Console\Terminal;
12+
13+
abstract class BaseCodeAnalyzerPreCommitHook
14+
{
15+
use ProcessHelper;
16+
17+
/**
18+
* Command instance that is bound automatically by Hooks Pipeline, so it can be used inside the Hook.
19+
*
20+
* @var Command
21+
*/
22+
public $command;
23+
24+
/*
25+
* List of files extensions that will be analyzed by the hook
26+
* @var array
27+
*/
28+
public $fileExtensions = [];
29+
30+
/**
31+
* The path to the analyzer executable.
32+
*
33+
* @var string
34+
*/
35+
protected $analyzerExecutable;
36+
37+
/**
38+
* The path to the fixer executable. In multiple cases it's the same of the analyzer executable.
39+
*
40+
* @var string
41+
*/
42+
protected $fixerExecutable;
43+
44+
/**
45+
* The list of paths of files that are badly formatted and should be fixed.
46+
*
47+
* @var array
48+
*/
49+
protected $filesBadlyFormattedPaths = [];
50+
51+
public function __construct()
52+
{
53+
$this->setCwd(base_path());
54+
}
55+
56+
/**
57+
* Handles the committed files and checks if they are properly formatted.
58+
*
59+
* @param ChangedFiles $files The instance of the changed files.
60+
* @param Closure $next The closure to be executed after the files are handled.
61+
* @return mixed|void
62+
*
63+
* @throws HookFailException If the hook fails to analyze the committed files.
64+
*/
65+
public function handleCommittedFiles(ChangedFiles $files, Closure $next)
66+
{
67+
$commitFiles = $files->getAddedToCommit();
68+
69+
if ($commitFiles->isEmpty() || GitHooks::isMergeInProgress()) {
70+
return $next($files);
71+
}
72+
73+
$this->checkAnalyzerInstallation()
74+
->analizeCommittedFiles($commitFiles);
75+
76+
if (empty($this->filesBadlyFormattedPaths)) {
77+
return $next($files);
78+
}
79+
80+
$this->commitFailMessage()
81+
->suggestAutoFixOrExit();
82+
}
83+
84+
/**
85+
* Analyzes the committed files and checks if they are properly formatted.
86+
*
87+
* @param mixed $commitFiles The files to analyze.
88+
* @return $this
89+
*/
90+
protected function analizeCommittedFiles($commitFiles)
91+
{
92+
foreach ($commitFiles as $file) {
93+
if (! in_array($file->extension(), $this->fileExtensions)) {
94+
continue;
95+
}
96+
97+
$filePath = $file->getFilePath();
98+
$command = $this->analyzerCommand().' '.$filePath;
99+
100+
$isProperlyFormatted = $this->runCommands($command)->isSuccessful();
101+
102+
if (! $isProperlyFormatted) {
103+
if (empty($this->filesBadlyFormattedPaths)) {
104+
$this->command->newLine();
105+
}
106+
107+
$this->command->getOutput()->writeln(
108+
sprintf('<fg=red> %s Failed:</> %s', $this->getName(), $filePath)
109+
);
110+
$this->filesBadlyFormattedPaths[] = $filePath;
111+
}
112+
}
113+
114+
return $this;
115+
}
116+
117+
/**
118+
* Returns the message to display when the commit fails.
119+
*
120+
* @return $this
121+
*/
122+
protected function commitFailMessage()
123+
{
124+
$this->command->newLine();
125+
$this->command->getOutput()->writeln(
126+
'<bg=red;fg=white> COMMIT FAILED </> '.
127+
sprintf('Your commit contains files that should pass %s but do not. Please fix the errors in the files above and try again.',
128+
$this->getName())
129+
);
130+
131+
return $this;
132+
}
133+
134+
/**
135+
* Check if the BaseCodeAnalyzerPreCommitHook is installed.
136+
*
137+
* @return $this
138+
*
139+
* @throws HookFailException
140+
*/
141+
protected function checkAnalyzerInstallation()
142+
{
143+
if (file_exists($this->analyzerExecutable)) {
144+
return $this;
145+
}
146+
147+
$this->command->newLine(2);
148+
$this->command->getOutput()->writeln(
149+
sprintf('<bg=red;fg=white> ERROR </> %s is not installed. Please install it and try again.',
150+
$this->getName())
151+
);
152+
$this->command->newLine();
153+
154+
throw new HookFailException();
155+
}
156+
157+
/**
158+
* Suggests attempting to automatically fix the incorrectly formatted files or exit.
159+
*
160+
* @return void
161+
*
162+
* @throws HookFailException
163+
*/
164+
protected function suggestAutoFixOrExit()
165+
{
166+
if (Terminal::hasSttyAvailable() &&
167+
$this->command->confirm('Would you like to attempt to correct files automagically?')
168+
) {
169+
$errorFilesString = implode(' ', $this->filesBadlyFormattedPaths);
170+
171+
$this->runCommands([
172+
$this->fixerCommand().' '.$errorFilesString,
173+
'git add '.$errorFilesString,
174+
]);
175+
} else {
176+
throw new HookFailException();
177+
}
178+
}
179+
180+
/**
181+
* @param array|string $fileExtensions
182+
* @return BaseCodeAnalyzerPreCommitHook
183+
*/
184+
public function setFileExtensions($fileExtensions)
185+
{
186+
$this->fileExtensions = (array) $fileExtensions;
187+
188+
return $this;
189+
}
190+
191+
/**
192+
* @return BaseCodeAnalyzerPreCommitHook
193+
*/
194+
public function setAnalyzerExecutable($executablePath, $isSameAsFixer = false)
195+
{
196+
$this->analyzerExecutable = './'.trim($executablePath, '/');
197+
198+
return $isSameAsFixer ? $this->setFixerExecutable($executablePath) : $this;
199+
}
200+
201+
public function getAnalyzerExecutable(): string
202+
{
203+
return $this->analyzerExecutable;
204+
}
205+
206+
/**
207+
* @return BaseCodeAnalyzerPreCommitHook
208+
*/
209+
public function setFixerExecutable($exacutablePath)
210+
{
211+
$this->fixerExecutable = './'.trim($exacutablePath, '/');
212+
213+
return $this;
214+
}
215+
216+
public function getFixerExecutable(): string
217+
{
218+
return $this->fixerExecutable;
219+
}
220+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Igorsgm\GitHooks\Console\Commands\Hooks;
4+
5+
use Closure;
6+
use Igorsgm\GitHooks\Contracts\CodeAnalyzerPreCommitHook;
7+
use Igorsgm\GitHooks\Git\ChangedFiles;
8+
9+
class PHPCodeSnifferPreCommitHook extends BaseCodeAnalyzerPreCommitHook implements CodeAnalyzerPreCommitHook
10+
{
11+
/**
12+
* @var string
13+
*/
14+
protected $analyzerConfigParam;
15+
16+
/**
17+
* Get the name of the hook.
18+
*/
19+
public function getName(): ?string
20+
{
21+
return 'PHP_CodeSniffer';
22+
}
23+
24+
/**
25+
* Analyze and fix committed PHP files using PHP Code Sniffer and PHP Code Beautifier and Fixer.
26+
*
27+
* @param ChangedFiles $files The files that have been changed in the current commit.
28+
* @param Closure $next A closure that represents the next middleware in the pipeline.
29+
* @return mixed|null
30+
*/
31+
public function handle(ChangedFiles $files, Closure $next)
32+
{
33+
$this->analyzerConfigParam = $this->analyzerConfigParam();
34+
35+
return $this->setFileExtensions(['php'])
36+
->setAnalyzerExecutable(config('git-hooks.code_analyzers.php_code_sniffer.phpcs_path'))
37+
->setFixerExecutable(config('git-hooks.code_analyzers.php_code_sniffer.phpcbf_path'))
38+
->handleCommittedFiles($files, $next);
39+
}
40+
41+
/**
42+
* Returns the command to run PHPCS
43+
*/
44+
public function analyzerCommand(): string
45+
{
46+
return trim(sprintf('%s %s', $this->getAnalyzerExecutable(), $this->analyzerConfigParam));
47+
}
48+
49+
/**
50+
* Returns the command to run PHPCS
51+
*/
52+
public function fixerCommand(): string
53+
{
54+
return trim(sprintf('%s %s', $this->getFixerExecutable(), $this->analyzerConfigParam));
55+
}
56+
57+
/**
58+
* Returns the configuration parameter for the analyzer.
59+
* This method retrieves the PHP CodeSniffer standard from the Git hooks configuration file
60+
* and returns it as a string in the format '--standard=<standard>'.
61+
*
62+
* @return string The configuration parameter for the analyzer.
63+
*/
64+
public function analyzerConfigParam(): string
65+
{
66+
$phpCSStandard = rtrim(config('git-hooks.code_analyzers.php_code_sniffer.standard'), '/');
67+
68+
return empty($phpCSStandard) ? '' : '--standard='.$phpCSStandard;
69+
}
70+
}

0 commit comments

Comments
 (0)