Skip to content

Commit e9df52e

Browse files
committed
[PHP 8.5] Pipe operator basics
1 parent e437c5a commit e9df52e

File tree

11 files changed

+364
-0
lines changed

11 files changed

+364
-0
lines changed

src/Analyser/MutatingScope.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2314,6 +2314,26 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
23142314
}
23152315
}
23162316

2317+
if ($node instanceof BinaryOp\Pipe) {
2318+
if ($node->right instanceof FuncCall && $node->right->isFirstClassCallable()) {
2319+
return $this->getType(new FuncCall($node->right->name, [
2320+
new Arg($node->left),
2321+
]));
2322+
} elseif ($node->right instanceof MethodCall && $node->right->isFirstClassCallable()) {
2323+
return $this->getType(new MethodCall($node->right->var, $node->right->name, [
2324+
new Arg($node->left),
2325+
]));
2326+
} elseif ($node->right instanceof Expr\StaticCall && $node->right->isFirstClassCallable()) {
2327+
return $this->getType(new Expr\StaticCall($node->right->class, $node->right->name, [
2328+
new Arg($node->left),
2329+
]));
2330+
}
2331+
2332+
return $this->getType(new FuncCall($node->right, [
2333+
new Arg($node->left),
2334+
]));
2335+
}
2336+
23172337
if ($node instanceof PropertyFetch) {
23182338
if ($node->name instanceof Node\Identifier) {
23192339
if ($this->nativeTypesPromoted) {

src/Analyser/NodeScopeResolver.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3382,6 +3382,44 @@ static function (): void {
33823382
$throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints());
33833383
$impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints());
33843384
$isAlwaysTerminating = $condResult->isAlwaysTerminating();
3385+
} elseif ($expr instanceof BinaryOp\Pipe) {
3386+
if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) {
3387+
$exprResult = $this->processExprNode($stmt, new FuncCall($expr->right->name, [
3388+
new Arg($expr->left, attributes: $expr->left->getAttributes()),
3389+
], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context);
3390+
$scope = $exprResult->getScope();
3391+
$hasYield = $exprResult->hasYield();
3392+
$throwPoints = $exprResult->getThrowPoints();
3393+
$impurePoints = $exprResult->getImpurePoints();
3394+
$isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3395+
} elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) {
3396+
$exprResult = $this->processExprNode($stmt, new MethodCall($expr->right->var, $expr->right->name, [
3397+
new Arg($expr->left, attributes: $expr->left->getAttributes()),
3398+
], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context);
3399+
$scope = $exprResult->getScope();
3400+
$hasYield = $exprResult->hasYield();
3401+
$throwPoints = $exprResult->getThrowPoints();
3402+
$impurePoints = $exprResult->getImpurePoints();
3403+
$isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3404+
} elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) {
3405+
$exprResult = $this->processExprNode($stmt, new StaticCall($expr->right->class, $expr->right->name, [
3406+
new Arg($expr->left, attributes: $expr->left->getAttributes()),
3407+
], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context);
3408+
$scope = $exprResult->getScope();
3409+
$hasYield = $exprResult->hasYield();
3410+
$throwPoints = $exprResult->getThrowPoints();
3411+
$impurePoints = $exprResult->getImpurePoints();
3412+
$isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3413+
} else {
3414+
$exprResult = $this->processExprNode($stmt, new FuncCall($expr->right, [
3415+
new Arg($expr->left, attributes: $expr->left->getAttributes()),
3416+
], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context);
3417+
$scope = $exprResult->getScope();
3418+
$hasYield = $exprResult->hasYield();
3419+
$throwPoints = $exprResult->getThrowPoints();
3420+
$impurePoints = $exprResult->getImpurePoints();
3421+
$isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3422+
}
33853423
} elseif ($expr instanceof BinaryOp) {
33863424
$result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep());
33873425
$scope = $result->getScope();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php // lint >= 8.5
2+
3+
namespace PipeOperatorTypes;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(string $s): int
11+
{
12+
return 1;
13+
}
14+
15+
public function doBar(): void
16+
{
17+
assertType('int', 'foo' |> $this->doFoo(...));
18+
}
19+
20+
}
21+
22+
class StaticFoo
23+
{
24+
25+
public static function doFoo(string $s): int
26+
{
27+
return 1;
28+
}
29+
30+
public function doBar(): void
31+
{
32+
assertType('int', 'foo' |> self::doFoo(...));
33+
}
34+
35+
}
36+
37+
function doFoo(): int
38+
{
39+
40+
}
41+
42+
class FunctionFoo
43+
{
44+
45+
public function doBar(): void
46+
{
47+
assertType('int', 'foo' |> doFoo(...));
48+
assertType('int', 'foo' |> 'PipeOperatorTypes\\doFoo');
49+
}
50+
51+
}

tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,35 @@ public function testBug10814(): void
322322
]);
323323
}
324324

325+
#[RequiresPhp('>= 8.5')]
326+
public function testPipeOperator(): void
327+
{
328+
$this->analyse([__DIR__ . '/data/call-callable-pipe.php'], [
329+
[
330+
'Callable \'CallCallablePipe…\' invoked with 1 parameter, 2 required.',
331+
20,
332+
],
333+
[
334+
'Parameter #1 $i of callable \'CallCallablePipe…\' expects int, string given.',
335+
24,
336+
],
337+
[
338+
'Parameter #1 $i of callable \'CallCallablePipe…\' expects int, void given.',
339+
26,
340+
],
341+
[
342+
'Callable \'CallCallablePipe…\' invoked with 1 parameter, 2 required.',
343+
26,
344+
],
345+
[
346+
'Result of callable \'CallCallablePipe…\' (void) is used.',
347+
26,
348+
],
349+
[
350+
'Result of callable \'CallCallablePipe…\' (void) is used.',
351+
28,
352+
],
353+
]);
354+
}
355+
325356
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2403,4 +2403,35 @@ public function testArrayRandPhp7(): void
24032403
$this->analyse([__DIR__ . '/data/array_rand.php'], []);
24042404
}
24052405

2406+
#[RequiresPhp('>= 8.5')]
2407+
public function testPipeOperator(): void
2408+
{
2409+
$this->analyse([__DIR__ . '/data/func-call-pipe.php'], [
2410+
[
2411+
'Function FuncCallPipe\doFoo invoked with 1 parameter, 2 required.',
2412+
20,
2413+
],
2414+
[
2415+
'Parameter #1 $i of function FuncCallPipe\doBar expects int, string given.',
2416+
24,
2417+
],
2418+
[
2419+
'Parameter #1 $i of function FuncCallPipe\doBar expects int, null given.',
2420+
26,
2421+
],
2422+
[
2423+
'Function FuncCallPipe\doFoo invoked with 1 parameter, 2 required.',
2424+
26,
2425+
],
2426+
[
2427+
'Result of function FuncCallPipe\doFoo (void) is used.',
2428+
26,
2429+
],
2430+
[
2431+
'Result of function FuncCallPipe\doBar (void) is used.',
2432+
28,
2433+
],
2434+
]);
2435+
}
2436+
24062437
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php // lint >= 8.5
2+
3+
namespace CallCallablePipe;
4+
5+
function doFoo(int $i, int $j): void
6+
{
7+
8+
}
9+
10+
function doBar(int $i): void
11+
{
12+
13+
}
14+
15+
class Foo
16+
{
17+
18+
public function doFoo(): void
19+
{
20+
1 |> 'CallCallablePipe\\doFoo';
21+
22+
1 |> 'CallCallablePipe\\doBar';
23+
24+
'Hello' |> 'CallCallablePipe\\doBar';
25+
26+
('CallCallablePipe\\doFoo')(1) |> 'CallCallablePipe\\doBar';
27+
28+
$a = 1 |> 'CallCallablePipe\\doBar';
29+
}
30+
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php // lint >= 8.5
2+
3+
namespace FuncCallPipe;
4+
5+
function doFoo(int $i, int $j): void
6+
{
7+
8+
}
9+
10+
function doBar(int $i): void
11+
{
12+
13+
}
14+
15+
class Foo
16+
{
17+
18+
public function doFoo(): void
19+
{
20+
1 |> doFoo(...);
21+
22+
1 |> doBar(...);
23+
24+
'Hello' |> doBar(...);
25+
26+
doFoo(1) |> doBar(...);
27+
28+
$a = 1 |> doBar(...);
29+
}
30+
31+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3696,4 +3696,39 @@ public function testRandomizer(): void
36963696
]);
36973697
}
36983698

3699+
#[RequiresPhp('>= 8.5')]
3700+
public function testPipeOperator(): void
3701+
{
3702+
$this->checkThisOnly = false;
3703+
$this->checkNullables = true;
3704+
$this->checkUnionTypes = true;
3705+
$this->checkExplicitMixed = true;
3706+
$this->analyse([__DIR__ . '/data/method-call-pipe.php'], [
3707+
[
3708+
'Method MethodCallPipe\Foo::doFoo() invoked with 1 parameter, 2 required.',
3709+
20,
3710+
],
3711+
[
3712+
'Parameter #1 $i of method MethodCallPipe\Foo::doBar() expects int, string given.',
3713+
24,
3714+
],
3715+
[
3716+
'Parameter #1 $i of method MethodCallPipe\Foo::doBar() expects int, null given.',
3717+
26,
3718+
],
3719+
[
3720+
'Method MethodCallPipe\Foo::doFoo() invoked with 1 parameter, 2 required.',
3721+
26,
3722+
],
3723+
[
3724+
'Result of method MethodCallPipe\Foo::doFoo() (void) is used.',
3725+
26,
3726+
],
3727+
[
3728+
'Result of method MethodCallPipe\Foo::doBar() (void) is used.',
3729+
28,
3730+
],
3731+
]);
3732+
}
3733+
36993734
}

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,4 +916,38 @@ public function testBug13556(): void
916916
$this->analyse([__DIR__ . '/data/bug-13556.php'], []);
917917
}
918918

919+
#[RequiresPhp('>= 8.5')]
920+
public function testPipeOperator(): void
921+
{
922+
$this->checkThisOnly = false;
923+
$this->checkExplicitMixed = true;
924+
$this->checkImplicitMixed = true;
925+
$this->analyse([__DIR__ . '/data/static-call-pipe.php'], [
926+
[
927+
'Static method StaticCallPipe\Foo::doFoo() invoked with 1 parameter, 2 required.',
928+
20,
929+
],
930+
[
931+
'Parameter #1 $i of static method StaticCallPipe\Foo::doBar() expects int, string given.',
932+
24,
933+
],
934+
[
935+
'Parameter #1 $i of static method StaticCallPipe\Foo::doBar() expects int, null given.',
936+
26,
937+
],
938+
[
939+
'Static method StaticCallPipe\Foo::doFoo() invoked with 1 parameter, 2 required.',
940+
26,
941+
],
942+
[
943+
'Result of static method StaticCallPipe\Foo::doFoo() (void) is used.',
944+
26,
945+
],
946+
[
947+
'Result of static method StaticCallPipe\Foo::doBar() (void) is used.',
948+
28,
949+
],
950+
]);
951+
}
952+
919953
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php // lint >= 8.5
2+
3+
namespace MethodCallPipe;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(int $i, int $j): void
9+
{
10+
11+
}
12+
13+
public function doBar(int $i): void
14+
{
15+
16+
}
17+
18+
public function doTest(): void
19+
{
20+
1 |> $this->doFoo(...);
21+
22+
1 |> $this->doBar(...);
23+
24+
'Hello' |> $this->doBar(...);
25+
26+
$this->doFoo(1) |> $this->doBar(...);
27+
28+
$a = 1 |> $this->doBar(...);
29+
}
30+
31+
}

0 commit comments

Comments
 (0)