Skip to content

Commit 9d0561a

Browse files
committed
Add Psalm to check more code
1 parent 10378a7 commit 9d0561a

File tree

8 files changed

+99
-49
lines changed

8 files changed

+99
-49
lines changed

.github/main.workflow

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ action "Code style check" {
3333
args = "-d memory_limit=1024M"
3434
needs = ["composer"]
3535
}
36+
37+
action "Psalm" {
38+
uses = "docker://mickaelandrieu/psalm-ga"
39+
secrets = ["GITHUB_TOKEN"]
40+
needs = ["composer"]
41+
}

psalm.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0"?>
2+
<psalm
3+
totallyTyped="false"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns="https://getpsalm.org/schema/config"
6+
xsi:schemaLocation="https://getpsalm.org/schema/config file:///composer/vendor/vimeo/psalm/config.xsd"
7+
>
8+
<projectFiles>
9+
<directory name="src" />
10+
<ignoreFiles>
11+
<directory name="vendor" />
12+
</ignoreFiles>
13+
</projectFiles>
14+
15+
<issueHandlers>
16+
<LessSpecificReturnType errorLevel="info" />
17+
18+
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
19+
20+
<DeprecatedMethod errorLevel="info" />
21+
<DeprecatedProperty errorLevel="info" />
22+
<DeprecatedClass errorLevel="info" />
23+
<DeprecatedConstant errorLevel="info" />
24+
<DeprecatedInterface errorLevel="info" />
25+
<DeprecatedTrait errorLevel="info" />
26+
27+
<InternalMethod errorLevel="info" />
28+
<InternalProperty errorLevel="info" />
29+
<InternalClass errorLevel="info" />
30+
31+
<MissingClosureReturnType errorLevel="info" />
32+
<MissingReturnType errorLevel="info" />
33+
<MissingPropertyType errorLevel="info" />
34+
<InvalidDocblock errorLevel="info" />
35+
<MisplacedRequiredParam errorLevel="info" />
36+
37+
<PropertyNotSetInConstructor errorLevel="info" />
38+
<MissingConstructor errorLevel="info" />
39+
<MissingClosureParamType errorLevel="info" />
40+
<MissingParamType errorLevel="info" />
41+
42+
<RedundantCondition errorLevel="info" />
43+
44+
<DocblockTypeContradiction errorLevel="info" />
45+
<RedundantConditionGivenDocblockType errorLevel="info" />
46+
47+
<UnresolvableInclude errorLevel="info" />
48+
49+
<RawObjectIteration errorLevel="info" />
50+
51+
<InvalidStringClass errorLevel="info" />
52+
</issueHandlers>
53+
</psalm>

src/FqsenResolver.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class FqsenResolver
1919
/** @var string Definition of the NAMESPACE operator in PHP */
2020
const OPERATOR_NAMESPACE = '\\';
2121

22-
public function resolve($fqsen, Context $context = null): Fqsen
22+
public function resolve(string $fqsen, Context $context = null): Fqsen
2323
{
2424
if ($context === null) {
2525
$context = new Context('');
@@ -39,7 +39,7 @@ public function resolve($fqsen, Context $context = null): Fqsen
3939
*
4040
* @return bool
4141
*/
42-
private function isFqsen($type): bool
42+
private function isFqsen(string $type): bool
4343
{
4444
return strpos($type, self::OPERATOR_NAMESPACE) === 0;
4545
}
@@ -52,7 +52,7 @@ private function isFqsen($type): bool
5252
* @return Fqsen
5353
* @throws \InvalidArgumentException when type is not a valid FQSEN.
5454
*/
55-
private function resolvePartialStructuralElementName($type, Context $context): Fqsen
55+
private function resolvePartialStructuralElementName(string $type, Context $context): Fqsen
5656
{
5757
$typeParts = explode(self::OPERATOR_NAMESPACE, $type, 2);
5858

src/TypeResolver.php

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ final class TypeResolver
4242
/** @var int the iterator parser is inside a collection expression context */
4343
const PARSER_IN_COLLECTION_EXPRESSION = 3;
4444

45-
/** @var string[] List of recognized keywords and unto which Value Object they map */
45+
/** @var array<string, string> List of recognized keywords and unto which Value Object they map */
4646
private $keywords = [
4747
'string' => Types\String_::class,
4848
'int' => Types\Integer::class,
@@ -133,7 +133,7 @@ public function resolve(string $type, Context $context = null): Type
133133
* the context where we are in the parsing
134134
* @return Type
135135
*/
136-
private function parseTypes(\ArrayIterator $tokens, Context $context, $parserContext)
136+
private function parseTypes(\ArrayIterator $tokens, Context $context, $parserContext): Type
137137
{
138138
$types = [];
139139
$token = '';
@@ -257,7 +257,7 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
257257
* @param string $type the type string, representing a single type
258258
* @return Type|Array_|Object_
259259
*/
260-
private function resolveSingleType($type, Context $context)
260+
private function resolveSingleType(string $type, Context $context)
261261
{
262262
switch (true) {
263263
case $this->isKeyword($type):
@@ -281,11 +281,8 @@ private function resolveSingleType($type, Context $context)
281281

282282
/**
283283
* Adds a keyword to the list of Keywords and associates it with a specific Value Object.
284-
*
285-
* @param string $keyword
286-
* @param string $typeClassName
287284
*/
288-
public function addKeyword($keyword, $typeClassName)
285+
public function addKeyword(string $keyword, string $typeClassName): void
289286
{
290287
if (!class_exists($typeClassName)) {
291288
throw new \InvalidArgumentException(
@@ -307,10 +304,8 @@ public function addKeyword($keyword, $typeClassName)
307304
* Detects whether the given type represents an array.
308305
*
309306
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
310-
*
311-
* @return bool
312307
*/
313-
private function isTypedArray($type)
308+
private function isTypedArray(string $type): bool
314309
{
315310
return substr($type, -2) === self::OPERATOR_ARRAY;
316311
}
@@ -319,10 +314,8 @@ private function isTypedArray($type)
319314
* Detects whether the given type represents a PHPDoc keyword.
320315
*
321316
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
322-
*
323-
* @return bool
324317
*/
325-
private function isKeyword($type)
318+
private function isKeyword(string $type): bool
326319
{
327320
return in_array(strtolower($type), array_keys($this->keywords), true);
328321
}
@@ -334,45 +327,38 @@ private function isKeyword($type)
334327
*
335328
* @return bool
336329
*/
337-
private function isPartialStructuralElementName($type)
330+
private function isPartialStructuralElementName(string $type): bool
338331
{
339332
return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type);
340333
}
341334

342335
/**
343336
* Tests whether the given type is a Fully Qualified Structural Element Name.
344-
*
345-
* @param string $type
346-
*
347-
* @return bool
348337
*/
349-
private function isFqsen($type)
338+
private function isFqsen(string $type): bool
350339
{
351340
return strpos($type, self::OPERATOR_NAMESPACE) === 0;
352341
}
353342

354343
/**
355344
* Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set.
356-
*
357-
* @param string $type
358-
* @return Array_
359345
*/
360-
private function resolveTypedArray($type, Context $context)
346+
private function resolveTypedArray(string $type, Context $context): Array_
361347
{
362348
return new Array_($this->resolveSingleType(substr($type, 0, -2), $context));
363349
}
364350

365351
/**
366352
* Resolves the given keyword (such as `string`) into a Type object representing that keyword.
367-
*
368-
* @param string $type
369-
*
370-
* @return Type
353+
* @psalm-suppress MoreSpecificReturnType
371354
*/
372-
private function resolveKeyword($type)
355+
private function resolveKeyword(string $type): Type
373356
{
374357
$className = $this->keywords[strtolower($type)];
375-
358+
/**
359+
* @psalm-suppress LessSpecificReturnStatement
360+
* @psalm-suppress InvalidStringClass
361+
*/
376362
return new $className();
377363
}
378364

@@ -384,7 +370,7 @@ private function resolveKeyword($type)
384370
*
385371
* @return Object_
386372
*/
387-
private function resolveTypedObject($type, Context $context = null)
373+
private function resolveTypedObject($type, Context $context = null): Object_
388374
{
389375
return new Object_($this->fqsenResolver->resolve($type, $context));
390376
}
@@ -394,7 +380,7 @@ private function resolveTypedObject($type, Context $context = null)
394380
*
395381
* @return Array_|Collection
396382
*/
397-
private function resolveCollection(\ArrayIterator $tokens, Type $classType, Context $context)
383+
private function resolveCollection(\ArrayIterator $tokens, Type $classType, Context $context): Type
398384
{
399385
$isArray = ('array' === (string) $classType);
400386

@@ -460,16 +446,14 @@ private function resolveCollection(\ArrayIterator $tokens, Type $classType, Cont
460446
return new Array_($valueType, $keyType);
461447
}
462448

463-
if ($classType instanceof Object_) {
464-
return $this->makeCollectionFromObject($classType, $valueType, $keyType);
465-
}
449+
return $this->makeCollectionFromObject($classType, $valueType, $keyType);
466450
}
467451

468452
/**
469453
* @param Type|null $keyType
470454
* @return Collection
471455
*/
472-
private function makeCollectionFromObject(Object_ $object, Type $valueType, Type $keyType = null)
456+
private function makeCollectionFromObject(Object_ $object, Type $valueType, Type $keyType = null): Collection
473457
{
474458
return new Collection($object->getFqsen(), $valueType, $keyType);
475459
}

src/Types/Collection.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ final class Collection extends AbstractList
3636
*
3737
* @param Fqsen|null $fqsen
3838
*/
39-
public function __construct(Fqsen $fqsen = null, Type $valueType, Type $keyType = null)
39+
public function __construct(?Fqsen $fqsen, Type $valueType, Type $keyType = null)
4040
{
4141
parent::__construct($valueType, $keyType);
4242

@@ -48,7 +48,7 @@ public function __construct(Fqsen $fqsen = null, Type $valueType, Type $keyType
4848
*
4949
* @return Fqsen|null
5050
*/
51-
public function getFqsen()
51+
public function getFqsen(): ?Fqsen
5252
{
5353
return $this->fqsen;
5454
}
@@ -58,10 +58,12 @@ public function getFqsen()
5858
*/
5959
public function __toString(): string
6060
{
61+
$objectType = (string) ($this->fqsen ?? 'object');
62+
6163
if ($this->keyType === null) {
62-
return $this->fqsen . '<' . $this->valueType . '>';
64+
return $objectType . '<' . $this->valueType . '>';
6365
}
6466

65-
return $this->fqsen . '<' . $this->keyType . ',' . $this->valueType . '>';
67+
return $objectType . '<' . $this->keyType . ',' . $this->valueType . '>';
6668
}
6769
}

src/Types/Compound.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ final class Compound implements Type, IteratorAggregate
3737
public function __construct(array $types)
3838
{
3939
foreach ($types as $type) {
40+
/** @psalm-suppress RedundantConditionGivenDocblockType */
4041
if (!$type instanceof Type) {
4142
throw new \InvalidArgumentException('A compound type can only have other types as elements');
4243
}

src/Types/ContextFactory.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ private function createFromReflectionParameter(\ReflectionParameter $parameter):
6767
if ($class) {
6868
return $this->createFromReflectionClass($class);
6969
}
70+
71+
throw new \InvalidArgumentException('Unable to get class of ' . $parameter->getName());
7072
}
7173

7274
private function createFromReflectionMethod(\ReflectionMethod $method): Context
@@ -111,7 +113,7 @@ private function createFromReflectionClass(\ReflectionClass $class): Context
111113
*
112114
* @return Context
113115
*/
114-
public function createForNamespace($namespace, $fileContents)
116+
public function createForNamespace($namespace, $fileContents): Context
115117
{
116118
$namespace = trim($namespace, '\\');
117119
$useStatements = [];
@@ -165,7 +167,7 @@ public function createForNamespace($namespace, $fileContents)
165167
*
166168
* @return string
167169
*/
168-
private function parseNamespace(\ArrayIterator $tokens)
170+
private function parseNamespace(\ArrayIterator $tokens): string
169171
{
170172
// skip to the first string or namespace separator
171173
$this->skipToNextStringOrNamespaceSeparator($tokens);
@@ -185,7 +187,7 @@ private function parseNamespace(\ArrayIterator $tokens)
185187
*
186188
* @return string[]
187189
*/
188-
private function parseUseStatement(\ArrayIterator $tokens)
190+
private function parseUseStatement(\ArrayIterator $tokens): array
189191
{
190192
$uses = [];
191193
$continue = true;
@@ -205,7 +207,7 @@ private function parseUseStatement(\ArrayIterator $tokens)
205207
/**
206208
* Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
207209
*/
208-
private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens)
210+
private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens): void
209211
{
210212
while ($tokens->valid() && ($tokens->current()[0] !== T_STRING) && ($tokens->current()[0] !== T_NS_SEPARATOR)) {
211213
$tokens->next();
@@ -216,9 +218,10 @@ private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens)
216218
* Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
217219
* a USE statement yet. This will return a key/value array of the alias => namespace.
218220
*
221+
* @psalm-suppress TypeDoesNotContainType
219222
* @return array
220223
*/
221-
private function extractUseStatements(\ArrayIterator $tokens)
224+
private function extractUseStatements(\ArrayIterator $tokens): array
222225
{
223226
$extractedUseStatements = [];
224227
$groupedNs = '';

tests/unit/CollectionResolverTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
use phpDocumentor\Reflection\Types\Compound;
1818
use phpDocumentor\Reflection\Types\Context;
1919
use phpDocumentor\Reflection\Types\Object_;
20+
use phpDocumentor\Reflection\Types\String_;
2021
use PHPUnit\Framework\TestCase;
2122

2223
/**
2324
* @covers ::<private>
24-
* @coversDefaultClass phpDocumentor\Reflection\TypeResolver
25+
* @coversDefaultClass \phpDocumentor\Reflection\TypeResolver
2526
*/
2627
class CollectionResolverTest extends TestCase
2728
{
@@ -46,7 +47,7 @@ public function testResolvingCollection()
4647

4748
$this->assertEquals('\\ArrayObject', (string) $resolvedType->getFqsen());
4849

49-
/** @var Array_ $valueType */
50+
/** @var String_ $valueType */
5051
$valueType = $resolvedType->getValueType();
5152

5253
/** @var Compound $keyType */

0 commit comments

Comments
 (0)