Skip to content

Commit

Permalink
feat: add support for general key-value notation, e.g. iterable<strin…
Browse files Browse the repository at this point in the history
…g, int>
  • Loading branch information
gskema committed Jun 26, 2023
1 parent 542b925 commit 7a0ac36
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
## 81.3.0 - 2023-06-26
### Added
- Support for `class-string` doc type
- Support for specific key-value notation for iterables, e.g. `Generator<int, string>`

## 81.2.0 - 2023-04-07
### Changed
Expand Down
37 changes: 37 additions & 0 deletions src/Core/Type/DocBlock/KeyValueType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Gskema\TypeSniff\Core\Type\DocBlock;

use Gskema\TypeSniff\Core\Type\TypeInterface;

/**
* E.g. Generator<int, string>, iterable<class-string>, etc.
* Key value type is not stored - not necessary.
*
* Arrays use TypedArrayType.
*/
class KeyValueType implements TypeInterface
{
public function __construct(
protected TypeInterface $type
) {
}

public function getType(): TypeInterface
{
return $this->type;
}

public function containsType(string $typeClassName): bool
{
return is_a($this->type, $typeClassName);
}

/**
* @inheritDoc
*/
public function toString(): string
{
return $this->type->toString() . '<?>';
}
}
3 changes: 3 additions & 0 deletions src/Core/Type/TypeComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
use Gskema\TypeSniff\Core\Type\DocBlock\TypedArrayType;
Expand Down Expand Up @@ -158,6 +159,8 @@ public static function compare(

$flatDocTypes = $docType instanceof UnionType ? $docType->getTypes() : [$docType];
foreach ($flatDocTypes as $flatDocType) {
$flatDocType = $flatDocType instanceof KeyValueType ? $flatDocType->getType() : $flatDocType;

$flatDocTypeClass = get_class($flatDocType);
$coveredFnTypeClasses = static::$coveredFnTypeClassMap[$flatDocTypeClass] ?? [];
$coveredFnTypeClasses[] = $flatDocTypeClass;
Expand Down
5 changes: 4 additions & 1 deletion src/Core/Type/TypeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ResourceType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
Expand Down Expand Up @@ -56,7 +57,9 @@ public static function toExampleDocType(TypeInterface $fnType): ?TypeInterface

public static function toExampleFnType(TypeInterface $docType, bool $isProp): ?TypeInterface
{
if ($docType instanceof UnionType) {
if ($docType instanceof KeyValueType) {
return self::toExampleFnType($docType->getType(), $isProp);
} elseif ($docType instanceof UnionType) {
if ($docType->containsType(MixedType::class)) {
return new MixedType(); // mixed|null -> mixed, mixed type cannot be in union
}
Expand Down
12 changes: 11 additions & 1 deletion src/Core/Type/TypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ResourceType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
Expand Down Expand Up @@ -151,14 +152,23 @@ protected static function fromRawTypes(array $rawTypes): TypeInterface
return new TypedArrayType(static::fromRawTypes([$rawInnerType]), $depth);
}

// All cases above can be instantly detected by start/end symbols. If no matches, we must try to unwrap next.
//
// e.g. (int|string)
// ((int|float)[]|(string|bool)[])
if ('(' === $rawType[0] && ')' === $rawType[-1]) {
// must not trim more than 1 char on each side!
return static::fromRawType(substr($rawType, 1, -1)); // cannot skip split
}

// Have to split again because it raw type was parenthesized (A&(B|C)) or (Node|Location)[]
// Unwrapping is done, so we can safely check cases where some symbol is in the middle of raw type.
$ltPos = strpos($rawType, '<');
if (false !== $ltPos) {
return new KeyValueType(self::fromRawType(substr($rawType, 0, $ltPos)));
}

// Cases like iterable<int, int|string> was handle above, so if any pipes remain - it's DNF types.
// Have to split again because it raw type was parenthesized A&(B|C) or (Node|Location)[]
if (str_contains($rawType, '|')) {
[$rawTypes, ] = static::split($rawType);
return static::fromRawTypes($rawTypes);
Expand Down
4 changes: 3 additions & 1 deletion src/Core/Type/TypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Gskema\TypeSniff\Core\Type\Common\UnionType;
use Gskema\TypeSniff\Core\Type\Common\UndefinedType;
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\TypedArrayType;

/**
Expand All @@ -17,7 +18,8 @@ public static function containsType(?TypeInterface $type, string $typeClassName)
{
return is_a($type, $typeClassName)
|| ($type instanceof UnionType && $type->containsType($typeClassName))
|| ($type instanceof NullableType && $type->containsType($typeClassName));
|| ($type instanceof NullableType && $type->containsType($typeClassName))
|| ($type instanceof KeyValueType && $type->containsType($typeClassName));
}

public static function getFakeTypedArrayType(?TypeInterface $type): ?TypedArrayType
Expand Down
1 change: 1 addition & 0 deletions tests/Core/Type/TypeComparatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public function dataCompare(): array
61 => ['class-string', 'string', '', '', ''],
62 => ['class-string|string', 'string', '', '', ''],
63 => ['class-string|string|int', 'string', '', 'int', ''],
64 => ['Generator<class-string>|int', 'Generator|int', '', '', ''],

// doc_type, fn_type, val_type, wrong_doc, missing_doc
];
Expand Down
9 changes: 9 additions & 0 deletions tests/Core/Type/TypeConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ResourceType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
Expand Down Expand Up @@ -184,6 +185,14 @@ public function dataToExampleFnType(): array
new ClassStringType(),
new StringType(),
],
35 => [
new KeyValueType(new IterableType()),
new IterableType(),
],
36 => [
new KeyValueType(new FqcnType('Generator')),
new FqcnType('Generator'),
],
];
}

Expand Down
9 changes: 9 additions & 0 deletions tests/Core/Type/TypeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ResourceType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
Expand Down Expand Up @@ -269,6 +270,14 @@ public function dataFromRawType(): array
[
'class-string',
new ClassStringType(),
],
[
'Generator<class-string>',
new KeyValueType(new FqcnType('Generator')),
],
[
'iterable<int, string|Acme>',
new KeyValueType(new IterableType()),
]
];

Expand Down
5 changes: 5 additions & 0 deletions tests/Core/Type/TypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
use Gskema\TypeSniff\Core\Type\Common\UnionType;
use Gskema\TypeSniff\Core\Type\Common\VoidType;
use Gskema\TypeSniff\Core\Type\Declaration\NullableType;
use Gskema\TypeSniff\Core\Type\DocBlock\ClassStringType;
use Gskema\TypeSniff\Core\Type\DocBlock\DoubleType;
use Gskema\TypeSniff\Core\Type\DocBlock\KeyValueType;
use Gskema\TypeSniff\Core\Type\DocBlock\ResourceType;
use Gskema\TypeSniff\Core\Type\DocBlock\ThisType;
use Gskema\TypeSniff\Core\Type\DocBlock\TrueType;
Expand Down Expand Up @@ -77,5 +79,8 @@ public function test(): void
self::assertEquals('string[][]', (new TypedArrayType(new StringType(), 2))->toString());
self::assertEquals(new StringType(), (new TypedArrayType(new StringType(), 2))->getType());
self::assertEquals(2, (new TypedArrayType(new StringType(), 2))->getDepth());

self::assertEquals('class-string', (new ClassStringType())->toString());
self::assertEquals('iterable<?>', (new KeyValueType(new IterableType()))->toString());
}
}
4 changes: 4 additions & 0 deletions tests/Sniffs/CompositeCodeElementSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ public function dataProcess(): array
'073 Add type declaration for parameter $prop1, e.g.: "string".',
'073 Add type declaration for return value, e.g.: "string".',
'078 Add type declaration for property $prop4, e.g.: "string". Add default value or keep property in an uninitialized state.',
'081 Add type declaration for property $prop5, e.g.: "iterable". Add default value or keep property in an uninitialized state.',
'089 Add type declaration for parameter $param1, e.g.: "array".',
'089 Add type declaration for parameter $param2, e.g.: "iterable".',
'089 Add type declaration for return value, e.g.: "\Generator".',
],
];

Expand Down
16 changes: 16 additions & 0 deletions tests/Sniffs/fixtures/TestClass14.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,20 @@ public function method8($prop1)

/** @var class-string */
public $prop4;

/** @var iterable<string, int|string> */
public $prop5;

/**
* @param array<string, string|int> $param1
* @param iterable<\stdClass> $param2
* @param Acme<string> $param3
* @return \Generator<class-string>
*/
public function method9($param1, $param2, Acme $param3)
{
}

/** @var array<string> */
public const CONST3 = [];
}

0 comments on commit 7a0ac36

Please sign in to comment.