Skip to content

Commit 5d37c37

Browse files
Merge pull request #123 from VincentLanglet/composer
Sniff improvements
2 parents 40f7f70 + 187cc11 commit 5d37c37

13 files changed

+418
-2160
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
composer.lock
23
composer.phar
34
coverage/
45
vendor/

SymfonyCustom/Helpers/SniffHelper.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,70 @@ class SniffHelper extends AbstractHelper
5656
'@var',
5757
];
5858

59+
/**
60+
* <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
61+
* <array> is array of <simple>, eg `int[]` or `\Foo[]`
62+
* <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` or more complex`
63+
* <object> is array key => value type, like `array{type: string, name: string, value: mixed}`
64+
* <type> is <simple>, <array>, <object>, <generic> type
65+
* <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
66+
*/
67+
public const REGEX_TYPES = '
68+
(?<types>
69+
(?<type>
70+
(?<array>
71+
(?&notArray)(?:
72+
\s*\[\s*\]
73+
)+
74+
)
75+
|
76+
(?<notArray>
77+
(?<multiple>
78+
\(\s*(?<mutipleContent>
79+
(?&types)
80+
)\s*\)
81+
)
82+
|
83+
(?<generic>
84+
(?<genericName>
85+
(?&simple)
86+
)
87+
\s*<\s*
88+
(?<genericContent>
89+
(?:(?&types)\s*,\s*)*
90+
(?&types)
91+
)
92+
\s*>
93+
)
94+
|
95+
(?<object>
96+
array\s*{\s*
97+
(?<objectContent>
98+
(?:
99+
(?<objectKeyValue>
100+
(?:\w+\s*\??:\s*)?
101+
(?&types)
102+
)
103+
\s*,\s*
104+
)*
105+
(?&objectKeyValue)
106+
)
107+
\s*}
108+
)
109+
|
110+
(?<simple>
111+
\\\\?\w+(?:\\\\\w+)*
112+
|
113+
\$this
114+
)
115+
)
116+
)
117+
(?:
118+
\s*[\|&]\s*(?&type)
119+
)*
120+
)
121+
';
122+
59123
/**
60124
* @param File $phpcsFile
61125
* @param int $stackPtr
@@ -122,4 +186,20 @@ public static function isGlobalUse(File $phpcsFile, int $stackPtr): bool
122186

123187
return true;
124188
}
189+
190+
/**
191+
* @param string $content
192+
*
193+
* @return array
194+
*/
195+
public static function parseTypeHint(string $content): array
196+
{
197+
preg_match(
198+
'{^'.SniffHelper::REGEX_TYPES.'(?<space>[\s\t]*)?(?<description>.*)?$}six',
199+
$content,
200+
$matches
201+
);
202+
203+
return [$matches['types'] ?? '', $matches['space'] ?? '', $matches['description'] ?? ''];
204+
}
125205
}

SymfonyCustom/Sniffs/Classes/PropertyDeclarationSniff.php

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class PropertyDeclarationSniff implements Sniff
1717
*/
1818
public function register(): array
1919
{
20-
return [T_CLASS];
20+
return [T_CLASS, T_ANON_CLASS];
2121
}
2222

2323
/**
@@ -27,24 +27,91 @@ public function register(): array
2727
* @return void
2828
*/
2929
public function process(File $phpcsFile, $stackPtr): void
30+
{
31+
$this->processProperty($phpcsFile, $stackPtr);
32+
$this->processFunction($phpcsFile, $stackPtr);
33+
}
34+
35+
/**
36+
* @param File $phpcsFile
37+
* @param int $stackPtr
38+
*
39+
* @return void
40+
*/
41+
private function processFunction(File $phpcsFile, int $stackPtr): void
3042
{
3143
$tokens = $phpcsFile->getTokens();
44+
$end = $tokens[$stackPtr]['scope_closer'] ?? null;
3245

33-
$end = null;
34-
if (isset($tokens[$stackPtr]['scope_closer'])) {
35-
$end = $tokens[$stackPtr]['scope_closer'];
46+
$function = $phpcsFile->findNext(T_FUNCTION, $stackPtr, $end);
47+
if (false === $function) {
48+
return;
3649
}
3750

38-
$scope = $phpcsFile->findNext(T_FUNCTION, $stackPtr, $end);
51+
$wantedTokens = [T_CONST, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_ANON_CLASS];
52+
$scope = $phpcsFile->findNext($wantedTokens, $function + 1, $end);
53+
54+
while (false !== $scope) {
55+
if (T_ANON_CLASS === $tokens[$scope]['code']) {
56+
$scope = $tokens[$scope]['scope_closer'];
3957

40-
$wantedTokens = [T_PUBLIC, T_PROTECTED, T_PRIVATE];
58+
continue;
59+
}
60+
61+
if (T_CONST === $tokens[$scope]['code']) {
62+
$phpcsFile->addError('Declare class constants before methods', $scope, 'ConstBeforeFunction');
63+
} elseif (T_VARIABLE === $tokens[$scope + 2]['code']) {
64+
$phpcsFile->addError('Declare class properties before methods', $scope, 'PropertyBeforeFunction');
65+
}
4166

42-
while ($scope) {
4367
$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
68+
}
69+
}
4470

45-
if ($scope && T_VARIABLE === $tokens[$scope + 2]['code']) {
46-
$phpcsFile->addError('Declare class properties before methods', $scope, 'Invalid');
71+
/**
72+
* @param File $phpcsFile
73+
* @param int $stackPtr
74+
*
75+
* @return void
76+
*/
77+
private function processProperty(File $phpcsFile, int $stackPtr): void
78+
{
79+
$tokens = $phpcsFile->getTokens();
80+
$end = $tokens[$stackPtr]['scope_closer'] ?? null;
81+
82+
$wantedTokens = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_ANON_CLASS];
83+
$scope = $phpcsFile->findNext($wantedTokens, $stackPtr + 1, $end);
84+
85+
while (false !== $scope && T_VARIABLE !== $tokens[$scope + 2]['code']) {
86+
if (T_ANON_CLASS === $tokens[$scope]['code']) {
87+
$scope = $tokens[$scope]['scope_closer'];
88+
89+
continue;
4790
}
91+
92+
$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
93+
}
94+
95+
if (false === $scope) {
96+
return;
97+
}
98+
$property = $scope + 2;
99+
100+
$wantedTokens = [T_CONST, T_ANON_CLASS];
101+
$scope = $phpcsFile->findNext($wantedTokens, $property + 1, $end);
102+
103+
while (false !== $scope) {
104+
if (T_ANON_CLASS === $tokens[$scope]['code']) {
105+
$scope = $tokens[$scope]['scope_closer'];
106+
107+
continue;
108+
}
109+
110+
if (T_CONST === $tokens[$scope]['code']) {
111+
$phpcsFile->addError('Declare class property before const', $scope, 'ConstBeforeProperty');
112+
}
113+
114+
$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
48115
}
49116
}
50117
}

SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHP_CodeSniffer\Files\File;
88
use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff;
99
use PHP_CodeSniffer\Util\Tokens;
10+
use SymfonyCustom\Helpers\SniffHelper;
1011

1112
/**
1213
* SymfonyCustom standard customization to PEARs FunctionCommentSniff.
@@ -144,6 +145,21 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart, $has
144145
$error = 'Return type missing for @return tag in function comment';
145146
$phpcsFile->addError($error, $return, 'MissingReturnType');
146147
}
148+
149+
[$type, $space, $description] = SniffHelper::parseTypeHint($content);
150+
if (preg_match('/^\$\S+/', $description)) {
151+
$error = '@return annotations should not contain variable name';
152+
$fix = $phpcsFile->addFixableError($error, $return, 'NamedReturn');
153+
154+
if ($fix) {
155+
$description = preg_replace('/^\$\S+/', '', $description);
156+
if ('' !== $description) {
157+
$phpcsFile->fixer->replaceToken($return + 2, $type.$space.$description);
158+
} else {
159+
$phpcsFile->fixer->replaceToken($return + 2, $type);
160+
}
161+
}
162+
}
147163
} else {
148164
$error = 'Missing @return tag in function comment';
149165
$phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');

SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHP_CodeSniffer\Files\File;
88
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
9+
use SymfonyCustom\Helpers\SniffHelper;
910

1011
/**
1112
* Parses and verifies the variable doc comment.
@@ -97,19 +98,21 @@ public function processMemberVar(File $phpcsFile, $stackPtr): void
9798
return;
9899
}
99100

100-
$content = explode(' ', $tokens[$string]['content']);
101-
$newContent = array_filter($content, function ($value) use ($tokens, $stackPtr) {
102-
return 0 === preg_match('/^\$/', $value);
103-
});
104-
if (count($newContent) < count($content)) {
101+
[$type, $space, $description] = SniffHelper::parseTypeHint($tokens[$string]['content']);
102+
if (preg_match('/^\$\S+/', $description)) {
105103
$fix = $phpcsFile->addFixableError(
106104
'@var annotations should not contain variable name',
107105
$foundVar,
108106
'NamedVar'
109107
);
110108

111109
if ($fix) {
112-
$phpcsFile->fixer->replaceToken($string, implode(' ', $newContent));
110+
$description = preg_replace('/^\$\S+/', '', $description);
111+
if ('' !== $description) {
112+
$phpcsFile->fixer->replaceToken($string, $type.$space.$description);
113+
} else {
114+
$phpcsFile->fixer->replaceToken($string, $type);
115+
}
113116
}
114117
}
115118
}

0 commit comments

Comments
 (0)