Skip to content

Commit e307495

Browse files
committed
Make methods type declarations more flexible
- methods belonging to classes implementing PSR interfaces (or at least looks like to) will not require any type declation - params explicitly declared as mixed (or mixed|null) in doc bloc will not trigger warnings - return values explicitly declared as mixed (or mixed|null) in doc bloc will not trigger warnings
1 parent d7a4a4a commit e307495

File tree

7 files changed

+356
-23
lines changed

7 files changed

+356
-23
lines changed

Inpsyde/PhpcsHelpers.php

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ public static function functionIsMethod(File $file, int $position): bool
9494
* @return bool
9595
*/
9696
public static function hasOopCondition(File $file, int $position): bool
97+
{
98+
return static::findOopContext($file, $position) !== 0;
99+
}
100+
101+
/**
102+
* @param File $file
103+
* @param int $position
104+
* @return int
105+
*/
106+
public static function findOopContext(File $file, int $position): int
97107
{
98108
/** @var array<int, array<string, mixed>> $tokens */
99109
$tokens = $file->getTokens();
@@ -103,7 +113,7 @@ public static function hasOopCondition(File $file, int $position): bool
103113
|| ((int)($tokens[$position]['level'] ?? 0) <= 0)
104114
|| !is_array($tokens[$position]['conditions'])
105115
) {
106-
return false;
116+
return 0;
107117
}
108118

109119
$targetLevel = (int)$tokens[$position]['level'] - 1;
@@ -115,11 +125,11 @@ public static function hasOopCondition(File $file, int $position): bool
115125
in_array($condCode, Tokens::$ooScopeTokens, true)
116126
&& ($condLevel === $targetLevel)
117127
) {
118-
return true;
128+
return $condPosition;
119129
}
120130
}
121131

122-
return false;
132+
return 0;
123133
}
124134

125135
/**
@@ -409,6 +419,31 @@ public static function functionDocBlockTag(string $tag, File $file, int $positio
409419
return $tags[$tagName];
410420
}
411421

422+
/**
423+
* @param File $file
424+
* @param int $functionPosition
425+
* @return array<string>
426+
*/
427+
public static function functionDocBlockParamTypes(File $file, int $functionPosition): array
428+
{
429+
$params = PhpcsHelpers::functionDocBlockTag('@param', $file, $functionPosition);
430+
if (!$params) {
431+
return [];
432+
}
433+
434+
$types = [];
435+
foreach ($params as $param) {
436+
preg_match('~^([^$]+)\s*(\$(?:[^\s]+))~', trim($param), $matches);
437+
if (empty($matches[1]) || empty($matches[2])) {
438+
continue;
439+
}
440+
441+
$types[$matches[2]] = array_map('trim', explode('|', $matches[1]));
442+
}
443+
444+
return $types;
445+
}
446+
412447
/**
413448
* @param File $file
414449
* @param int $position
@@ -627,4 +662,63 @@ public static function minPhpTestVersion(): string
627662

628663
return $matches[1] ?? '';
629664
}
665+
666+
/**
667+
* @param File $file
668+
* @param int $position
669+
* @return bool
670+
*/
671+
public static function isUntypedPsrMethod(File $file, int $position): bool
672+
{
673+
$tokens = $file->getTokens();
674+
675+
if (($tokens[$position]['type'] ?? '') !== 'T_FUNCTION') {
676+
return false;
677+
}
678+
679+
$classPos = static::findOopContext($file, $position);
680+
$type = $tokens[$classPos]['type'] ?? null;
681+
if (!$classPos || !in_array($type, ['T_CLASS', 'T_ANON_CLASS'], true)) {
682+
return false;
683+
}
684+
685+
$names = $file->findImplementedInterfaceNames($classPos);
686+
687+
if (!$names) {
688+
return false;
689+
}
690+
691+
static $psrInterfaces;
692+
$psrInterfaces or $psrInterfaces = [
693+
'LoggerInterface',
694+
'CacheItemInterface',
695+
'CacheItemPoolInterface',
696+
'MessageInterface',
697+
'RequestInterface',
698+
'ServerRequestInterface',
699+
'ResponseInterface',
700+
'StreamInterface',
701+
'UriInterface',
702+
'UploadedFileInterface',
703+
'ContainerInterface',
704+
'LinkInterface',
705+
'EvolvableLinkInterface',
706+
'LinkProviderInterface',
707+
'EvolvableLinkProviderInterface',
708+
'CacheInterface',
709+
'RequestFactoryInterface',
710+
'ResponseFactoryInterface',
711+
'ServerRequestFactoryInterface',
712+
'StreamFactoryInterface',
713+
];
714+
715+
foreach ($names as $name) {
716+
$lastName = array_slice(explode('\\', $name), -1, 1)[0];
717+
if (in_array($lastName, $psrInterfaces, true)) {
718+
return true;
719+
}
720+
}
721+
722+
return false;
723+
}
630724
}

Inpsyde/Sniffs/CodeQuality/ArgumentTypeDeclarationSniff.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function process(File $file, $position)
5858
PhpcsHelpers::functionIsArrayAccess($file, $position)
5959
|| PhpcsHelpers::isHookClosure($file, $position)
6060
|| PhpcsHelpers::isHookFunction($file, $position)
61+
|| PhpcsHelpers::isUntypedPsrMethod($file, $position)
6162
|| (
6263
PhpcsHelpers::functionIsMethod($file, $position)
6364
&& in_array($file->getDeclarationName($position), self::METHODS_WHITELIST, true)
@@ -75,9 +76,15 @@ public function process(File $file, $position)
7576
return;
7677
}
7778

79+
$docBlockTypes = PhpcsHelpers::functionDocBlockParamTypes($file, $position);
7880
$variables = PhpcsHelpers::filterTokensByType($paramsStart, $paramsEnd, $file, T_VARIABLE);
7981

80-
foreach (array_keys($variables) as $varPosition) {
82+
foreach ($variables as $varPosition => $varToken) {
83+
// Not triggering error for variable explicitly declared as mixed (or mixed|null)
84+
if ($this->isMixed($varToken['content'] ?? '', $docBlockTypes)) {
85+
continue;
86+
}
87+
8188
$typePosition = $file->findPrevious(
8289
[T_WHITESPACE, T_ELLIPSIS, T_BITWISE_AND],
8390
$varPosition - 1,
@@ -92,4 +99,29 @@ public function process(File $file, $position)
9299
}
93100
}
94101
}
102+
103+
/**
104+
* @param string $paramName
105+
* @param array $docBlockTypes
106+
* @return bool
107+
*/
108+
private function isMixed(string $paramName, array $docBlockTypes): bool
109+
{
110+
$paramDocBlockTypes = $paramName ? ($docBlockTypes[$paramName] ?? null) : null;
111+
if (!$paramDocBlockTypes) {
112+
return false;
113+
}
114+
115+
$paramDocBlockTypesCount = count($paramDocBlockTypes);
116+
if (!$paramDocBlockTypesCount || $paramDocBlockTypesCount > 2) {
117+
return false;
118+
}
119+
120+
$paramDocBlockTypes = array_map('trim', $paramDocBlockTypes);
121+
if (!in_array('mixed', $paramDocBlockTypes, true)) {
122+
return false;
123+
}
124+
125+
return ($paramDocBlockTypesCount === 1) || in_array('null', $paramDocBlockTypes, true);
126+
}
95127
}

Inpsyde/Sniffs/CodeQuality/ReturnTypeDeclarationSniff.php

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ public function process(File $file, $position)
5252
{
5353
// phpcs:enable Inpsyde.CodeQuality
5454

55-
if (PhpcsHelpers::functionIsArrayAccess($file, $position)) {
55+
if (
56+
PhpcsHelpers::functionIsArrayAccess($file, $position)
57+
|| PhpcsHelpers::isUntypedPsrMethod($file, $position)
58+
) {
5659
return;
5760
}
5861

@@ -151,17 +154,17 @@ private function maybeErrors(
151154
);
152155
}
153156

157+
$docBlock = $this->hasReturnNullOrMixedDocBloc($file, $position);
158+
154159
if (
155-
PhpcsHelpers::isHookClosure($file, $position)
160+
$docBlock['mixed']
161+
|| PhpcsHelpers::isHookClosure($file, $position)
156162
|| PhpcsHelpers::isHookFunction($file, $position)
157163
) {
158164
return;
159165
}
160166

161-
if (
162-
!$this->areNullableReturnTypesSupported()
163-
&& $this->hasReturnNullDocBloc($file, $position)
164-
) {
167+
if (!$this->areNullableReturnTypesSupported() && $docBlock['null']) {
165168
return;
166169
}
167170

@@ -305,24 +308,32 @@ private function returnTypeInfo(File $file, int $functionPosition): array
305308
/**
306309
* @param File $file
307310
* @param int $functionPosition
308-
* @return bool
311+
* @return array{mixed:bool, null:bool}
309312
*/
310-
private function hasReturnNullDocBloc(File $file, int $functionPosition): bool
313+
private function hasReturnNullOrMixedDocBloc(File $file, int $functionPosition): array
311314
{
312315
$return = PhpcsHelpers::functionDocBlockTag('@return', $file, $functionPosition);
313316
if (!$return) {
314-
return false;
317+
return ['mixed' => false, 'null' => false];
315318
}
316319

317320
$returnContentParts = preg_split('~\s+~', (string)reset($return), PREG_SPLIT_NO_EMPTY);
318-
$returnTypes = $returnContentParts ? explode('|', (string)reset($returnContentParts)) : [];
319-
$returnTypes and $returnTypes = array_map('strtolower', $returnTypes);
320-
321-
return
322-
$returnTypes
323-
&& count($returnTypes) < 3
324-
&& !in_array('mixed', $returnTypes, true)
325-
&& in_array('null', $returnTypes, true);
321+
if (!$returnContentParts) {
322+
return ['mixed' => false, 'null' => false];
323+
}
324+
325+
$returnTypes = array_map('strtolower', explode('|', (string)reset($returnContentParts)));
326+
$returnTypes = array_map('trim', $returnTypes);
327+
$returnTypesCount = count($returnTypes);
328+
// Only if 1 or 2 types
329+
if (!$returnTypesCount || ($returnTypesCount > 2)) {
330+
return ['mixed' => false, 'null' => false];
331+
}
332+
333+
return [
334+
'mixed' => in_array('mixed', $returnTypes, true),
335+
'null' => in_array('null', $returnTypes, true)
336+
];
326337
}
327338

328339
/**

tests/cases/FixturesTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,6 @@ private function validateTotals(
188188
* @param string $fixtureFile
189189
* @param array $properties
190190
* @return File
191-
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException
192-
* @throws \ReflectionException
193191
*/
194192
private function createPhpcsForFixture(
195193
string $sniffName,

0 commit comments

Comments
 (0)