Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/Analyser/CollectedDataEmitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PHPStan\Collectors\Collector;

/**
* The interface CollectedDataEmitter can be typehinted in 2nd parameter of Rule::processNode():
*
* ```php
* public function processNode(Node $node, Scope&CollectedDataEmitter $scope): array
* ```
*
* It allows rules to emit collected data directly, without having to write
* a separate complex Collector class. The emitted data is aggregated the same way
* as data from Collectors and can be consumed by rules registered
* for CollectedDataNode.
*
* The actual MyCollector class in the example has to exist, to verify
* the data type statically, and to identify the collected data.
*
* The referenced MyCollector class should NOT be registered
* as a collector, unless you also want it to collect data on its own.
*
* ```php
* $scope->emitCollectedData(MyCollector::class, ['some', 'data']);
* ```
*
* @api
*/
interface CollectedDataEmitter
{

/**
* @template TCollector of Collector<Node, mixed>
* @param class-string<TCollector> $collectorType
* @param template-type<TCollector, Collector, 'TValue'> $data
*/
public function emitCollectedData(string $collectorType, mixed $data): void;

}
8 changes: 7 additions & 1 deletion src/Analyser/FileAnalyserCallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Collectors\Registry as CollectorRegistry;
use PHPStan\Dependency\DependencyResolver;
use PHPStan\Dependency\RootExportedNode;
use PHPStan\Node\EmitCollectedDataNode;
use PHPStan\Node\InClassNode;
use PHPStan\Node\InTraitNode;
use PHPStan\Parser\Parser;
Expand Down Expand Up @@ -77,9 +78,14 @@ public function __construct(

public function __invoke(Node $node, Scope $scope): void
{
if ($node instanceof EmitCollectedDataNode) {
$this->fileCollectedData[$scope->getFile()][$node->getCollectorType()][] = $node->getData();
return;
}

$parserNodes = $this->parserNodes;

/** @var Scope&NodeCallbackInvoker $scope */
/** @var Scope&NodeCallbackInvoker&CollectedDataEmitter $scope */
if ($node instanceof Node\Stmt\Trait_) {
foreach (array_keys($this->linesToIgnore[$this->file] ?? []) as $lineToIgnore) {
if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) {
Expand Down
20 changes: 19 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
use PhpParser\Node\Stmt\Function_;
use PhpParser\NodeFinder;
use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser;
use PHPStan\Collectors\Collector;
use PHPStan\DependencyInjection\Container;
use PHPStan\Node\EmitCollectedDataNode;
use PHPStan\Node\Expr\AlwaysRememberedExpr;
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
Expand Down Expand Up @@ -133,7 +135,7 @@
use const PHP_INT_MIN;
use const PHP_VERSION_ID;

class MutatingScope implements Scope, NodeCallbackInvoker
class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter
{

public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
Expand Down Expand Up @@ -4624,4 +4626,20 @@ public function invokeNodeCallback(Node $node): void
$nodeCallback($node, $this);
}

/**
* @template TNodeType of Node
* @template TValue
* @param class-string<Collector<TNodeType, TValue>> $collectorType
* @param TValue $data
*/
public function emitCollectedData(string $collectorType, mixed $data): void
{
$nodeCallback = $this->nodeCallback;
if ($nodeCallback === null) {
throw new ShouldNotHappenException('Node callback is not present in this scope');
}

$nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this);
}

}
60 changes: 60 additions & 0 deletions src/Node/EmitCollectedDataNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node;

use Override;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use PHPStan\Collectors\Collector;

/**
* @template TNodeType of Node
* @template TValue
*/
final class EmitCollectedDataNode extends NodeAbstract implements VirtualNode
{

/**
* @param class-string<Collector<TNodeType, TValue>> $collectorType
* @param TValue $data
*/
public function __construct(
private string $collectorType,
private mixed $data,
)
{
parent::__construct([]);
}

/**
* @return class-string<Collector<TNodeType, TValue>>
*/
public function getCollectorType(): string
{
return $this->collectorType;
}

/**
* @return TValue
*/
public function getData(): mixed
{
return $this->data;
}

#[Override]
public function getType(): string
{
return 'PHPStan_Node_EmitCollectedDataNode';
}

/**
* @return list<string>
*/
#[Override]
public function getSubNodeNames(): array
{
return [];
}

}
5 changes: 3 additions & 2 deletions src/Rules/Methods/OverridingMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpParser\Node;
use PhpParser\Node\Attribute;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
Expand Down Expand Up @@ -49,7 +50,7 @@ public function getNodeType(): string
return InClassMethodNode::class;
}

public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$method = $node->getMethodReflection();
$prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName());
Expand Down Expand Up @@ -329,7 +330,7 @@ private function filterOverrideAttribute(array $attrGroups): array
private function addErrors(
array $errors,
InClassMethodNode $classMethod,
Scope&NodeCallbackInvoker $scope,
Scope&NodeCallbackInvoker&CollectedDataEmitter $scope,
): array
{
if (count($errors) > 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Playground/PromoteParameterRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules\Playground;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\Container;
Expand Down Expand Up @@ -88,7 +89,7 @@ private function getOriginalRule(): ?Rule
return $this->originalRule = $originalRule;
}

public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
if ($this->parameterValue) {
return [];
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;

Expand Down Expand Up @@ -35,6 +36,6 @@ public function getNodeType(): string;
* @param TNodeType $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array;
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array;

}
3 changes: 2 additions & 1 deletion src/Testing/CompositeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Testing;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\DirectRegistry;
Expand Down Expand Up @@ -37,7 +38,7 @@ public function getNodeType(): string
return Node::class;
}

public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$errors = [];

Expand Down
3 changes: 2 additions & 1 deletion src/Testing/DelayedRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Testing;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\DirectRegistry;
Expand Down Expand Up @@ -43,7 +44,7 @@ public function getDelayedErrors(): array
return $this->errors;
}

public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$nodeType = get_class($node);
foreach ($this->registry->getRules($nodeType) as $rule) {
Expand Down
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/CollectedDataEmitterRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;

/**
* @implements Rule<Node\Expr\MethodCall>
*/
final class CollectedDataEmitterRule implements Rule
{

public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array
{
// same implementation as DummyCollector, but is actually a rule!
if (!$node->name instanceof Node\Identifier) {
return [];
}

$scope->emitCollectedData(DummyCollector::class, $node->name->toString());

return [];
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/CollectedDataEmitterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PHPStan\Testing\CompositeRule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<CompositeRule>
*/
class CollectedDataEmitterTest extends RuleTestCase
{

protected function getRule(): Rule
{
// @phpstan-ignore argument.type
return new CompositeRule([
new CollectedDataEmitterRule(),
new DummyCollectorRule(),
]);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/dummy-collector.php'], [
[
'2× doFoo, 2× doBar',
5,
],
]);
}

}
Loading