Skip to content

Commit 350ed5d

Browse files
authored
Enforce closure parameter type hints (#57)
1 parent 1eb7b1f commit 350ed5d

File tree

10 files changed

+88
-16
lines changed

10 files changed

+88
-16
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).
99

1010
## Unreleased
1111

12+
## v5.19.0
13+
14+
### Added
15+
16+
- Add PHPStan-Rule `MLL\Utils\PHPStan\Rules\MissingClosureParameterTypehintRule.php`
17+
1218
## v5.18.0
1319

1420
### Added

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
includes:
22
- extension.neon
33
- rules.neon
4+
rules:
5+
#- MLL\Utils\PHPStan\Rules\ThrowableClassNameRule
6+
- MLL\Utils\PHPStan\Rules\VariableNameIdToIDRule
7+
- MLL\Utils\PHPStan\Rules\MissingClosureParameterTypehintRule
48
parameters:
59
level: max
610
paths:

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ rules:
22
# TODO gradually enable those rules
33
#- MLL\Utils\PHPStan\Rules\ThrowableClassNameRule
44
#- MLL\Utils\PHPStan\Rules\VariableNameIdToIDRule
5+
#- MLL\Utils\PHPStan\Rules\MissingClosureParameterTypehintRule
56
parameters:
67
# https://github.com/spaze/phpstan-disallowed-calls/blob/main/docs/custom-rules.md
78
disallowedFunctionCalls:

src/IlluminaSampleSheet/V1/DataSection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ protected function validateDuplicatedSampleIDs(): void
5555
->groupBy(fn (Row $row): string => $row->sampleID);
5656

5757
$duplicates = $groups
58-
->filter(fn ($group): bool => count($group) > 1)
58+
->filter(fn (Collection $group): bool => count($group) > 1)
5959
->keys();
6060
$duplicateIDsAsString = $duplicates->implode(', ');
6161

src/Microplate/AbstractMicroplate.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function sortedWells(FlowDirection $flowDirection): Collection
4949
{
5050
return $this->wells()->sortBy(
5151
/** @param TWell $content */
52-
function ($content, string $key) use ($flowDirection): string {
52+
function ($content, string $key) use ($flowDirection): string { /** @phpstan-ignore missingType.parameter (is in template context) */
5353
switch ($flowDirection->value) {
5454
case FlowDirection::ROW:
5555
return $key;
@@ -72,7 +72,7 @@ public function freeWells(): Collection
7272
{
7373
return $this->wells()->filter(
7474
/** @param TWell $content */
75-
static fn ($content): bool => $content === self::EMPTY_WELL
75+
static fn ($content): bool => $content === self::EMPTY_WELL /** @phpstan-ignore missingType.parameter (is in template context) */
7676
);
7777
}
7878

@@ -81,14 +81,14 @@ public function filledWells(): Collection
8181
{
8282
return $this->wells()->filter(
8383
/** @param TWell $content */
84-
static fn ($content): bool => $content !== self::EMPTY_WELL
84+
static fn ($content): bool => $content !== self::EMPTY_WELL /** @phpstan-ignore missingType.parameter (is in template context) */
8585
);
8686
}
8787

8888
/** @return callable(TWell|null $content, string $coordinatesString): bool */
8989
public function matchRow(string $row): callable
9090
{
91-
return function ($content, string $coordinatesString) use ($row): bool {
91+
return function ($content, string $coordinatesString) use ($row): bool { /** @phpstan-ignore missingType.parameter (is in template context) */
9292
$coordinates = Coordinates::fromString($coordinatesString, $this->coordinateSystem);
9393

9494
return $coordinates->row === $row;
@@ -98,7 +98,7 @@ public function matchRow(string $row): callable
9898
/** @return callable(TWell|null $content, string $coordinatesString): bool */
9999
public function matchColumn(int $column): callable
100100
{
101-
return function ($content, string $coordinatesString) use ($column): bool {
101+
return function ($content, string $coordinatesString) use ($column): bool { /** @phpstan-ignore missingType.parameter (is in template context) */
102102
$coordinates = Coordinates::fromString($coordinatesString, $this->coordinateSystem);
103103

104104
return $coordinates->column === $column;
@@ -119,7 +119,7 @@ public function toWellWithCoordinateMapper(): callable
119119
public function toWellWithCoordinatesMapper(): callable
120120
{
121121
// @phpstan-ignore return.type (generic not inferred)
122-
return fn ($content, string $coordinatesString): WellWithCoordinates => new WellWithCoordinates(
122+
return fn ($content, string $coordinatesString): WellWithCoordinates => new WellWithCoordinates( /** @phpstan-ignore missingType.parameter (is in template context) */
123123
$content,
124124
Coordinates::fromString($coordinatesString, $this->coordinateSystem)
125125
);
@@ -135,7 +135,7 @@ public function isConsecutive(FlowDirection $flowDirection): bool
135135
$positions = $this->filledWells()
136136
->map(
137137
/** @param TWell $content */
138-
fn ($content, string $coordinatesString): int => Coordinates::fromString($coordinatesString, $this->coordinateSystem)->position($flowDirection)
138+
fn ($content, string $coordinatesString): int => Coordinates::fromString($coordinatesString, $this->coordinateSystem)->position($flowDirection) /** @phpstan-ignore missingType.parameter (is in template context) */
139139
);
140140

141141
if ($positions->isEmpty()) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils\PHPStan\Rules;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\ArrowFunction;
7+
use PhpParser\Node\Expr\Closure;
8+
use PhpParser\Node\Expr\Variable;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\IdentifierRuleError;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* @implements Rule<Node\Expr>
16+
*/
17+
final class MissingClosureParameterTypehintRule implements Rule
18+
{
19+
/** @return class-string<Node\Expr> */
20+
public function getNodeType(): string
21+
{
22+
return Node\Expr::class;
23+
}
24+
25+
/**
26+
* @param Node\Expr $node
27+
*
28+
* @return list<IdentifierRuleError>
29+
*/
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
if (! $node instanceof Closure && ! $node instanceof ArrowFunction) {
33+
return [];
34+
}
35+
36+
$errors = [];
37+
foreach ($node->params as $param) {
38+
if ($param->type !== null) {
39+
continue;
40+
}
41+
42+
$paramVar = $param->var;
43+
44+
if (! $paramVar instanceof Variable) {
45+
continue;
46+
}
47+
48+
if (! is_string($paramVar->name)) {
49+
continue;
50+
}
51+
52+
$varName = $paramVar->name;
53+
54+
$errors[] = RuleErrorBuilder::message("Closure parameter {$varName} is missing a native type hint.")
55+
->identifier('missingType.parameter')
56+
->build();
57+
}
58+
59+
return $errors;
60+
}
61+
}

src/QxManager/QxManagerSampleSheet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function toCsvString(Microplate $microplate, CarbonInterface $createdDate
2929
CSV);
3030

3131
$body = $microplate->sortedWells(FlowDirection::ROW())
32-
->map(function ($well, string $coordinateString) use ($microplate): string {
32+
->map(function ($well, string $coordinateString) use ($microplate): string { /** @phpstan-ignore missingType.parameter (is in template context) */
3333
$coordinates = Coordinates::fromString($coordinateString, $microplate->coordinateSystem);
3434

3535
if ($well instanceof FilledWell) {

src/Specification.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Specification
2121
*/
2222
public static function not(callable $specification): callable
2323
{
24-
return fn ($value): bool => ! $specification($value);
24+
return fn ($value): bool => ! $specification($value); /** @phpstan-ignore missingType.parameter (is in template context) */
2525
}
2626

2727
/**
@@ -33,7 +33,7 @@ public static function not(callable $specification): callable
3333
*/
3434
public static function or(callable ...$specifications): callable
3535
{
36-
return function ($value) use ($specifications): bool {
36+
return function ($value) use ($specifications): bool { /** @phpstan-ignore missingType.parameter (is in template context) */
3737
foreach ($specifications as $specification) {
3838
if ($specification($value)) {
3939
return true;
@@ -53,7 +53,7 @@ public static function or(callable ...$specifications): callable
5353
*/
5454
public static function and(callable ...$specifications): callable
5555
{
56-
return function ($value) use ($specifications): bool {
56+
return function ($value) use ($specifications): bool { /** @phpstan-ignore missingType.parameter (is in template context) */
5757
foreach ($specifications as $specification) {
5858
if (! $specification($value)) {
5959
return false;

src/Tecan/Rack/BaseRack.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function __construct(CoordinateSystem $coordinateSystem)
2020
$this->coordinateSystem = $coordinateSystem;
2121
/** @phpstan-ignore-next-line types are correct, but phpstan doesn't understand it */
2222
$this->positions = Collection::times($this->positionCount(), fn () => self::EMPTY_POSITION)
23-
->mapWithKeys(fn ($content, int $position): array => [$position + 1 => $content]);
23+
->mapWithKeys(fn ($content, int $position): array => [$position + 1 => $content]); /** @phpstan-ignore missingType.parameter (is in template context) */
2424
}
2525

2626
public function id(): ?string
@@ -52,7 +52,7 @@ public function assignLastEmptyPosition($content): int
5252
public function findFirstEmptyPosition(): int
5353
{
5454
$firstEmpty = $this->positions
55-
->filter(fn ($content): bool => $content === self::EMPTY_POSITION)
55+
->filter(fn ($content): bool => $content === self::EMPTY_POSITION) /** @phpstan-ignore missingType.parameter (is in template context) */
5656
->keys()
5757
->first();
5858

@@ -66,7 +66,7 @@ public function findFirstEmptyPosition(): int
6666
public function findLastEmptyPosition(): int
6767
{
6868
$lastEmpty = $this->positions
69-
->filter(fn ($content): bool => $content === self::EMPTY_POSITION)
69+
->filter(fn ($content): bool => $content === self::EMPTY_POSITION) /** @phpstan-ignore missingType.parameter (is in template context) */
7070
->keys()
7171
->last();
7272

tests/SpecificationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ final class SpecificationTest extends TestCase
99
{
1010
public function testNot(): void
1111
{
12-
$truthy = fn ($value): bool => (bool) $value;
12+
$truthy = fn (string $value): bool => (bool) $value;
1313

1414
self::assertTrue($truthy('truthy'));
1515

0 commit comments

Comments
 (0)