Skip to content

Commit f03f804

Browse files
Merge pull request #101 from VincentLanglet/null
Null At last and handle space
2 parents da33337 + 2c58176 commit f03f804

File tree

4 files changed

+339
-147
lines changed

4 files changed

+339
-147
lines changed

SymfonyCustom/Sniffs/NamingConventions/ValidTypeHintSniff.php

Lines changed: 211 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,112 @@
44

55
namespace SymfonyCustom\Sniffs\NamingConventions;
66

7+
use PHP_CodeSniffer\Exceptions\DeepExitException;
78
use PHP_CodeSniffer\Files\File;
89
use PHP_CodeSniffer\Sniffs\Sniff;
9-
use PHP_CodeSniffer\Util\Common;
1010
use SymfonyCustom\Helpers\SniffHelper;
1111

1212
/**
1313
* Throws errors if PHPDocs type hint are not valid.
1414
*/
1515
class ValidTypeHintSniff implements Sniff
1616
{
17-
private const TEXT = '[\\\\a-z0-9]';
18-
private const OPENER = '\<|\[|\{|\(';
19-
private const MIDDLE = '\,|\:|\=\>';
20-
private const CLOSER = '\>|\]|\}|\)';
21-
private const SEPARATOR = '\&|\|';
22-
23-
/*
17+
/**
2418
* <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
2519
* <array> is array of <simple>, eg `int[]` or `\Foo[]`
26-
* <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` and more complex like `Collection<int, \null|SubCollection<string>>`
27-
* <type> is <simple>, <array> or <generic> type, like `int`, `bool[]` or `Collection<ItemKey, ItemVal>`
20+
* <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` or more complex`
21+
* <object> is array key => value type, like `array{type: string, name: string, value: mixed}`
22+
* <type> is <simple>, <array>, <object>, <generic> type
2823
* <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
2924
*/
3025
private const REGEX_TYPES = '
3126
(?<types>
3227
(?<type>
3328
(?<array>
34-
(?&simple)(\[\])*
35-
)
36-
|
37-
(?<simple>
38-
[@$?]?[\\\\\w]+
29+
(?&notArray)(?:
30+
\s*\[\s*\]
31+
)+
3932
)
4033
|
41-
(?<generic>
42-
(?<genericName>(?&simple))
43-
<
44-
(?:(?<genericKey>(?&types)),\s*)?(?<genericValue>(?&types)|(?&generic))
45-
>
34+
(?<notArray>
35+
(?<multiple>
36+
\(\s*(?<mutipleContent>
37+
(?&types)
38+
)\s*\)
39+
)
40+
|
41+
(?<generic>
42+
(?<genericName>
43+
(?&simple)
44+
)
45+
\s*<\s*
46+
(?<genericContent>
47+
(?:(?&types)\s*,\s*)*
48+
(?&types)
49+
)
50+
\s*>
51+
)
52+
|
53+
(?<object>
54+
array\s*{\s*
55+
(?<objectContent>
56+
(?:
57+
(?<objectKeyValue>
58+
(?:\w+\s*\??:\s*)?
59+
(?&types)
60+
)
61+
\s*,\s*
62+
)*
63+
(?&objectKeyValue)
64+
)
65+
\s*}
66+
)
67+
|
68+
(?<simple>
69+
\\\\?\w+(?:\\\\\w+)*
70+
|
71+
\$this
72+
)
4673
)
4774
)
4875
(?:
49-
\|
50-
(?:(?&simple)|(?&array)|(?&generic))
76+
\s*[\|&]\s*(?&type)
5177
)*
5278
)
5379
';
5480

81+
/**
82+
* False if the type is not a reserved keyword and the check can't be case insensitive
83+
**/
84+
private const TYPES = [
85+
'array' => true,
86+
'bool' => true,
87+
'callable' => true,
88+
'false' => true,
89+
'float' => true,
90+
'int' => true,
91+
'iterable' => true,
92+
'mixed' => false,
93+
'null' => true,
94+
'number' => false,
95+
'object' => true,
96+
'resource' => false,
97+
'self' => true,
98+
'static' => true,
99+
'string' => true,
100+
'true' => true,
101+
'void' => true,
102+
'$this' => true,
103+
];
104+
105+
private const ALIAS_TYPES = [
106+
'boolean' => 'bool',
107+
'integer' => 'int',
108+
'double' => 'float',
109+
'real' => 'float',
110+
'callback' => 'callable',
111+
];
112+
55113
/**
56114
* @return array
57115
*/
@@ -68,80 +126,166 @@ public function process(File $phpcsFile, $stackPtr): void
68126
{
69127
$tokens = $phpcsFile->getTokens();
70128

71-
if (in_array($tokens[$stackPtr]['content'], SniffHelper::TAGS_WITH_TYPE)) {
72-
$matchingResult = preg_match(
73-
'{^'.self::REGEX_TYPES.'(?:[ \t].*)?$}sx',
74-
$tokens[$stackPtr + 2]['content'],
75-
$matches
76-
);
129+
if (!in_array($tokens[$stackPtr]['content'], SniffHelper::TAGS_WITH_TYPE)) {
130+
return;
131+
}
77132

78-
$content = 1 === $matchingResult ? $matches['types'] : '';
79-
$endOfContent = preg_replace('/'.preg_quote($content, '/').'/', '', $tokens[$stackPtr + 2]['content'], 1);
133+
$matchingResult = preg_match(
134+
'{^'.self::REGEX_TYPES.'(?:[\s\t].*)?$}six',
135+
$tokens[$stackPtr + 2]['content'],
136+
$matches
137+
);
80138

139+
$content = 1 === $matchingResult ? $matches['types'] : '';
140+
$endOfContent = substr($tokens[$stackPtr + 2]['content'], strlen($content));
141+
142+
try {
81143
$suggestedType = $this->getValidTypes($content);
144+
} catch (DeepExitException $exception) {
145+
$phpcsFile->addError(
146+
$exception->getMessage(),
147+
$stackPtr + 2,
148+
'Exception'
149+
);
82150

83-
if ($content !== $suggestedType) {
84-
$fix = $phpcsFile->addFixableError(
85-
'For type-hinting in PHPDocs, use %s instead of %s',
86-
$stackPtr + 2,
87-
'Invalid',
88-
[$suggestedType, $content]
89-
);
151+
return;
152+
}
153+
154+
if ($content !== $suggestedType) {
155+
$fix = $phpcsFile->addFixableError(
156+
'For type-hinting in PHPDocs, use %s instead of %s',
157+
$stackPtr + 2,
158+
'Invalid',
159+
[$suggestedType, $content]
160+
);
90161

91-
if ($fix) {
92-
$phpcsFile->fixer->replaceToken($stackPtr + 2, $suggestedType.$endOfContent);
93-
}
162+
if ($fix) {
163+
$phpcsFile->fixer->replaceToken($stackPtr + 2, $suggestedType.$endOfContent);
94164
}
95165
}
96166
}
97167

98168
/**
99169
* @param string $content
100170
*
101-
* @return array
171+
* @return string
172+
*
173+
* @throws DeepExitException
102174
*/
103-
private function getTypes(string $content): array
175+
private function getValidTypes(string $content): string
104176
{
177+
$content = preg_replace('/\s/', '', $content);
178+
105179
$types = [];
180+
$separators = [];
106181
while ('' !== $content && false !== $content) {
107-
preg_match('{^'.self::REGEX_TYPES.'$}x', $content, $matches);
182+
preg_match('{^'.self::REGEX_TYPES.'$}ix', $content, $matches);
183+
184+
if (isset($matches['array']) && '' !== $matches['array']) {
185+
$validType = $this->getValidTypes(substr($matches['array'], 0, -2)).'[]';
186+
} elseif (isset($matches['multiple']) && '' !== $matches['multiple']) {
187+
$validType = '('.$this->getValidTypes($matches['mutipleContent']).')';
188+
} elseif (isset($matches['generic']) && '' !== $matches['generic']) {
189+
$validType = $this->getValidGenericType($matches['genericName'], $matches['genericContent']);
190+
} elseif (isset($matches['object']) && '' !== $matches['object']) {
191+
$validType = $this->getValidObjectType($matches['objectContent']);
192+
} else {
193+
$validType = $this->getValidType($matches['type']);
194+
}
108195

109-
$types[] = $matches['type'];
196+
$types[] = $validType;
197+
198+
$separators[] = substr($content, strlen($matches['type']), 1);
110199
$content = substr($content, strlen($matches['type']) + 1);
111200
}
112201

202+
// Remove last separator since it's an empty string
203+
array_pop($separators);
204+
205+
$uniqueSeparators = array_unique($separators);
206+
switch (count($uniqueSeparators)) {
207+
case 0:
208+
return implode('', $types);
209+
case 1:
210+
return implode($uniqueSeparators[0], $this->orderTypes($types));
211+
default:
212+
throw new DeepExitException(
213+
'Union and intersection types must be grouped with parenthesis when used in the same expression'
214+
);
215+
}
216+
}
217+
218+
/**
219+
* @param array $types
220+
*
221+
* @return array
222+
*/
223+
private function orderTypes(array $types): array
224+
{
225+
$types = array_unique($types);
226+
usort($types, function ($type1, $type2) {
227+
if ('null' === $type1) {
228+
return 1;
229+
}
230+
231+
if ('null' === $type2) {
232+
return -1;
233+
}
234+
235+
return 0;
236+
});
237+
113238
return $types;
114239
}
115240

116241
/**
117-
* @param string $content
242+
* @param string $genericName
243+
* @param string $genericContent
118244
*
119245
* @return string
246+
*
247+
* @throws DeepExitException
120248
*/
121-
private function getValidTypes(string $content): string
249+
private function getValidGenericType(string $genericName, string $genericContent): string
122250
{
123-
$types = $this->getTypes($content);
251+
$validType = $this->getValidType($genericName).'<';
124252

125-
foreach ($types as $index => $type) {
126-
$type = str_replace(' ', '', $type);
253+
while ('' !== $genericContent && false !== $genericContent) {
254+
preg_match('{^'.self::REGEX_TYPES.',?}ix', $genericContent, $matches);
127255

128-
preg_match('{^'.self::REGEX_TYPES.'$}x', $type, $matches);
129-
if (isset($matches['generic'])) {
130-
$validType = $this->getValidType($matches['genericName']).'<';
256+
$validType .= $this->getValidTypes($matches['types']).', ';
257+
$genericContent = substr($genericContent, strlen($matches['types']) + 1);
258+
}
131259

132-
if ('' !== $matches['genericKey']) {
133-
$validType .= $this->getValidTypes($matches['genericKey']).', ';
134-
}
260+
return preg_replace('/,\s$/', '>', $validType);
261+
}
135262

136-
$validType .= $this->getValidTypes($matches['genericValue']).'>';
137-
} else {
138-
$validType = $this->getValidType($type);
263+
/**
264+
* @param string $objectContent
265+
*
266+
* @return string
267+
*
268+
* @throws DeepExitException
269+
*/
270+
private function getValidObjectType(string $objectContent): string
271+
{
272+
$validType = 'array{';
273+
274+
while ('' !== $objectContent && false !== $objectContent) {
275+
$split = preg_split('/(\??:|,)/', $objectContent, 2, PREG_SPLIT_DELIM_CAPTURE);
276+
277+
if (isset($split[1]) && ',' !== $split[1]) {
278+
$validType .= $split[0].$split[1].' ';
279+
$objectContent = $split[2];
139280
}
140281

141-
$types[$index] = $validType;
282+
preg_match('{^'.self::REGEX_TYPES.',?}ix', $objectContent, $matches);
283+
284+
$validType .= $this->getValidTypes($matches['types']).', ';
285+
$objectContent = substr($objectContent, strlen($matches['types']) + 1);
142286
}
143287

144-
return implode('|', $types);
288+
return preg_replace('/,\s$/', '}', $validType);
145289
}
146290

147291
/**
@@ -151,20 +295,16 @@ private function getValidTypes(string $content): string
151295
*/
152296
private function getValidType(string $typeName): string
153297
{
154-
if ('[]' === substr($typeName, -2)) {
155-
return $this->getValidType(substr($typeName, 0, -2)).'[]';
298+
$lowerType = strtolower($typeName);
299+
if (isset(self::TYPES[$lowerType])) {
300+
return self::TYPES[$lowerType] ? $lowerType : $typeName;
156301
}
157302

158-
$lowerType = strtolower($typeName);
159-
switch ($lowerType) {
160-
case 'bool':
161-
case 'boolean':
162-
return 'bool';
163-
case 'int':
164-
case 'integer':
165-
return 'int';
303+
// This can't be case insensitive since this is not reserved keyword
304+
if (isset(self::ALIAS_TYPES[$typeName])) {
305+
return self::ALIAS_TYPES[$typeName];
166306
}
167307

168-
return Common::suggestType($typeName);
308+
return $typeName;
169309
}
170310
}

0 commit comments

Comments
 (0)