Skip to content

Commit cb4a2a4

Browse files
committed
Allow custom rules to emit collector data for CollectedDataNode
1 parent 0aad2ac commit cb4a2a4

File tree

11 files changed

+205
-8
lines changed

11 files changed

+205
-8
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Collectors\Collector;
7+
8+
/**
9+
* The interface CollectedDataEmitter can be typehinted in 2nd parameter of Rule::processNode():
10+
*
11+
* ```php
12+
* public function processNode(Node $node, Scope&CollectedDataEmitter $scope): array
13+
* ```
14+
*
15+
* It allows rules to emit collected data directly, without having to write
16+
* a separate complex Collector class. The emitted data is aggregated the same way
17+
* as data from Collectors and can be consumed by rules registered
18+
* for CollectedDataNode.
19+
*
20+
* The actual MyCollector class in the example has to exist, to verify
21+
* the data type statically, and to identify the collected data.
22+
*
23+
* The referenced MyCollector class should NOT be registered
24+
* as a collector, unless you also want it to collect data on its own.
25+
*
26+
* ```php
27+
* $scope->emitCollectedData(MyCollector::class, ['some', 'data']);
28+
* ```
29+
*
30+
* @api
31+
*/
32+
interface CollectedDataEmitter
33+
{
34+
35+
/**
36+
* @template TCollector of Collector<Node, mixed>
37+
* @param class-string<TCollector> $collectorType
38+
* @param template-type<TCollector, Collector, 'TValue'> $data
39+
*/
40+
public function emitCollectedData(string $collectorType, mixed $data): void;
41+
42+
}

src/Analyser/FileAnalyserCallback.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Collectors\Registry as CollectorRegistry;
1212
use PHPStan\Dependency\DependencyResolver;
1313
use PHPStan\Dependency\RootExportedNode;
14+
use PHPStan\Node\EmitCollectedDataNode;
1415
use PHPStan\Node\InClassNode;
1516
use PHPStan\Node\InTraitNode;
1617
use PHPStan\Parser\Parser;
@@ -77,9 +78,14 @@ public function __construct(
7778

7879
public function __invoke(Node $node, Scope $scope): void
7980
{
81+
if ($node instanceof EmitCollectedDataNode) {
82+
$this->fileCollectedData[$scope->getFile()][$node->getCollectorType()][] = $node->getData();
83+
return;
84+
}
85+
8086
$parserNodes = $this->parserNodes;
8187

82-
/** @var Scope&NodeCallbackInvoker $scope */
88+
/** @var Scope&NodeCallbackInvoker&CollectedDataEmitter $scope */
8389
if ($node instanceof Node\Stmt\Trait_) {
8490
foreach (array_keys($this->linesToIgnore[$this->file] ?? []) as $lineToIgnore) {
8591
if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) {

src/Analyser/MutatingScope.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
use PhpParser\Node\Stmt\Function_;
2424
use PhpParser\NodeFinder;
2525
use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser;
26+
use PHPStan\Collectors\Collector;
2627
use PHPStan\DependencyInjection\Container;
28+
use PHPStan\Node\EmitCollectedDataNode;
2729
use PHPStan\Node\Expr\AlwaysRememberedExpr;
2830
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
2931
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
@@ -133,7 +135,7 @@
133135
use const PHP_INT_MIN;
134136
use const PHP_VERSION_ID;
135137

136-
class MutatingScope implements Scope, NodeCallbackInvoker
138+
class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter
137139
{
138140

139141
public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
@@ -4624,4 +4626,20 @@ public function invokeNodeCallback(Node $node): void
46244626
$nodeCallback($node, $this);
46254627
}
46264628

4629+
/**
4630+
* @template TNodeType of Node
4631+
* @template TValue
4632+
* @param class-string<Collector<TNodeType, TValue>> $collectorType
4633+
* @param TValue $data
4634+
*/
4635+
public function emitCollectedData(string $collectorType, mixed $data): void
4636+
{
4637+
$nodeCallback = $this->nodeCallback;
4638+
if ($nodeCallback === null) {
4639+
throw new ShouldNotHappenException('Node callback is not present in this scope');
4640+
}
4641+
4642+
$nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this);
4643+
}
4644+
46274645
}

src/Node/EmitCollectedDataNode.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use Override;
6+
use PhpParser\Node;
7+
use PhpParser\NodeAbstract;
8+
use PHPStan\Collectors\Collector;
9+
10+
/**
11+
* @template TNodeType of Node
12+
* @template TValue
13+
*/
14+
final class EmitCollectedDataNode extends NodeAbstract implements VirtualNode
15+
{
16+
17+
/**
18+
* @param class-string<Collector<TNodeType, TValue>> $collectorType
19+
* @param TValue $data
20+
*/
21+
public function __construct(
22+
private string $collectorType,
23+
private mixed $data,
24+
)
25+
{
26+
parent::__construct([]);
27+
}
28+
29+
/**
30+
* @return class-string<Collector<TNodeType, TValue>>
31+
*/
32+
public function getCollectorType(): string
33+
{
34+
return $this->collectorType;
35+
}
36+
37+
/**
38+
* @return TValue
39+
*/
40+
public function getData(): mixed
41+
{
42+
return $this->data;
43+
}
44+
45+
#[Override]
46+
public function getType(): string
47+
{
48+
return 'PHPStan_Node_EmitCollectedDataNode';
49+
}
50+
51+
/**
52+
* @return list<string>
53+
*/
54+
#[Override]
55+
public function getSubNodeNames(): array
56+
{
57+
return [];
58+
}
59+
60+
}

src/Rules/Methods/OverridingMethodRule.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Attribute;
7+
use PHPStan\Analyser\CollectedDataEmitter;
78
use PHPStan\Analyser\NodeCallbackInvoker;
89
use PHPStan\Analyser\Scope;
910
use PHPStan\DependencyInjection\AutowiredParameter;
@@ -49,7 +50,7 @@ public function getNodeType(): string
4950
return InClassMethodNode::class;
5051
}
5152

52-
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
53+
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
5354
{
5455
$method = $node->getMethodReflection();
5556
$prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName());
@@ -329,7 +330,7 @@ private function filterOverrideAttribute(array $attrGroups): array
329330
private function addErrors(
330331
array $errors,
331332
InClassMethodNode $classMethod,
332-
Scope&NodeCallbackInvoker $scope,
333+
Scope&NodeCallbackInvoker&CollectedDataEmitter $scope,
333334
): array
334335
{
335336
if (count($errors) > 0) {

src/Rules/Playground/PromoteParameterRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Playground;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\CollectedDataEmitter;
67
use PHPStan\Analyser\NodeCallbackInvoker;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\DependencyInjection\Container;
@@ -88,7 +89,7 @@ private function getOriginalRule(): ?Rule
8889
return $this->originalRule = $originalRule;
8990
}
9091

91-
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
92+
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
9293
{
9394
if ($this->parameterValue) {
9495
return [];

src/Rules/Rule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\CollectedDataEmitter;
67
use PHPStan\Analyser\NodeCallbackInvoker;
78
use PHPStan\Analyser\Scope;
89

@@ -35,6 +36,6 @@ public function getNodeType(): string;
3536
* @param TNodeType $node
3637
* @return list<IdentifierRuleError>
3738
*/
38-
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array;
39+
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array;
3940

4041
}

src/Testing/CompositeRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Testing;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\CollectedDataEmitter;
67
use PHPStan\Analyser\NodeCallbackInvoker;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Rules\DirectRegistry;
@@ -37,7 +38,7 @@ public function getNodeType(): string
3738
return Node::class;
3839
}
3940

40-
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
41+
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
4142
{
4243
$errors = [];
4344

src/Testing/DelayedRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Testing;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\CollectedDataEmitter;
67
use PHPStan\Analyser\NodeCallbackInvoker;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Rules\DirectRegistry;
@@ -43,7 +44,7 @@ public function getDelayedErrors(): array
4344
return $this->errors;
4445
}
4546

46-
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
47+
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
4748
{
4849
$nodeType = get_class($node);
4950
foreach ($this->registry->getRules($nodeType) as $rule) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\CollectedDataEmitter;
7+
use PHPStan\Analyser\NodeCallbackInvoker;
8+
use PHPStan\Analyser\Scope;
9+
10+
/**
11+
* @implements Rule<Node\Expr\MethodCall>
12+
*/
13+
final class CollectedDataEmitterRule implements Rule
14+
{
15+
16+
public function getNodeType(): string
17+
{
18+
return Node\Expr\MethodCall::class;
19+
}
20+
21+
public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array
22+
{
23+
// same implementation as DummyCollector, but is actually a rule!
24+
if (!$node->name instanceof Node\Identifier) {
25+
return [];
26+
}
27+
28+
$scope->emitCollectedData(DummyCollector::class, $node->name->toString());
29+
30+
return [];
31+
}
32+
33+
}

0 commit comments

Comments
 (0)