-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathExceptionDeclarationSniff.php
More file actions
143 lines (119 loc) · 4.33 KB
/
ExceptionDeclarationSniff.php
File metadata and controls
143 lines (119 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php
declare(strict_types = 1);
namespace Consistence\Sniffs\Exceptions;
use PHP_CodeSniffer\Files\File as PhpCsFile;
use SlevomatCodingStandard\Helpers\ClassHelper;
use SlevomatCodingStandard\Helpers\FunctionHelper;
use SlevomatCodingStandard\Helpers\StringHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
class ExceptionDeclarationSniff implements \PHP_CodeSniffer\Sniffs\Sniff
{
public const CODE_NOT_ENDING_WITH_EXCEPTION = 'NotEndingWithException';
public const CODE_NOT_CHAINABLE = 'NotChainable';
public const CODE_INCORRECT_EXCEPTION_DIRECTORY = 'IncorrectExceptionDirectory';
public string $exceptionsDirectoryName = 'exceptions';
/**
* @return int[]
*/
public function register(): array
{
return [
T_CLASS,
T_INTERFACE,
];
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $classPointer
*/
public function process(PhpCsFile $phpcsFile, $classPointer): void
{
$extendedClassName = $phpcsFile->findExtendedClassName($classPointer);
if ($extendedClassName === false) {
return; //does not extend anything
}
if (!StringHelper::endsWith($extendedClassName, 'Exception')) {
return; // does not extend Exception, is not an exception
}
$this->checkExceptionName($phpcsFile, $classPointer);
$this->checkExceptionDirectoryName($phpcsFile, $classPointer);
$this->checkThatExceptionIsChainable($phpcsFile, $classPointer);
}
private function checkExceptionName(PhpCsFile $phpcsFile, int $classPointer): void
{
$className = ClassHelper::getName($phpcsFile, $classPointer);
if (!StringHelper::endsWith($className, 'Exception')) {
$phpcsFile->addError(sprintf(
'Exception class name "%s" must end with "Exception".',
$className
), $classPointer, self::CODE_NOT_ENDING_WITH_EXCEPTION);
}
}
private function checkExceptionDirectoryName(PhpCsFile $phpcsFile, int $classPointer): void
{
$filename = $phpcsFile->getFilename();
// normalize path for Windows (PHP_CodeSniffer detects it with backslashes on Windows)
$filename = str_replace('\\', '/', $filename);
$pathInfo = pathinfo($filename);
$pathSegments = explode('/', $pathInfo['dirname']);
$exceptionDirectoryName = array_pop($pathSegments);
if ($exceptionDirectoryName !== $this->exceptionsDirectoryName) {
$phpcsFile->addError(sprintf(
'Exception file "%s" must be placed in "%s" directory (is in "%s").',
$pathInfo['basename'],
$this->exceptionsDirectoryName,
$exceptionDirectoryName
), $classPointer, self::CODE_INCORRECT_EXCEPTION_DIRECTORY);
}
}
private function checkThatExceptionIsChainable(PhpCsFile $phpcsFile, int $classPointer): void
{
$constructorPointer = $this->findConstructorMethodPointer($phpcsFile, $classPointer);
if ($constructorPointer === null) {
return;
}
$typeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $constructorPointer);
if (count($typeHints) === 0) {
$phpcsFile->addError(
'Exception is not chainable. It must have optional \Throwable as last constructor argument.',
$constructorPointer,
self::CODE_NOT_CHAINABLE
);
return;
}
$lastArgument = array_pop($typeHints);
if ($lastArgument === null) {
$phpcsFile->addError(
'Exception is not chainable. It must have optional \Throwable as last constructor argument and has none.',
$constructorPointer,
self::CODE_NOT_CHAINABLE
);
return;
}
if (
$lastArgument->getTypeHint() !== '\Throwable'
&& !StringHelper::endsWith($lastArgument->getTypeHint(), 'Exception')
&& !StringHelper::endsWith($lastArgument->getTypeHint(), 'Error')
) {
$phpcsFile->addError(sprintf(
'Exception is not chainable. It must have optional \Throwable as last constructor argument and has "%s".',
$lastArgument->getTypeHint()
), $constructorPointer, self::CODE_NOT_CHAINABLE);
return;
}
}
private function findConstructorMethodPointer(PhpCsFile $phpcsFile, int $classPointer): ?int
{
$functionPointerToScan = $classPointer;
while (($functionPointer = TokenHelper::findNext($phpcsFile, T_FUNCTION, $functionPointerToScan)) !== null) {
$functionName = FunctionHelper::getName($phpcsFile, $functionPointer);
if ($functionName === '__construct') {
return $functionPointer;
}
$functionPointerToScan = $functionPointer + 1;
}
return null;
}
}