Skip to content

Commit

Permalink
Detect first class callable syntax calls
Browse files Browse the repository at this point in the history
This is the syntax supported by PHP 8.1:
```
$callable = print_r(...);
$callable(42);
```
or
```
$blade = new \Waldo\Quux\Blade();
$callable = $blade->runner(...);
$callable(303);
```

The errors for the disallowed code are reported on lines with `(...)`, not when the callable is called.

Ref #275
  • Loading branch information
spaze committed Dec 8, 2024
1 parent 9e5b799 commit d38d4e4
Show file tree
Hide file tree
Showing 23 changed files with 586 additions and 132 deletions.
1 change: 1 addition & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ services:
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedNamespaceRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors
Expand Down
2 changes: 1 addition & 1 deletion src/Allowed/Allowed.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ private function getArgType(array $args, Scope $scope, Param $param): ?Type
if (!isset($found)) {
$found = $args[$param->getPosition() - 1] ?? null;
}
return isset($found) ? $scope->getType($found->value) : null;
return isset($found, $found->value) ? $scope->getType($found->value) : null;
}


Expand Down
67 changes: 6 additions & 61 deletions src/Calls/FunctionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,13 @@

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\ErrorIdentifiers;
use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;

/**
* Reports on dynamically calling a disallowed function.
Expand All @@ -29,42 +22,27 @@
class FunctionCalls implements Rule
{

private DisallowedCallsRuleErrors $disallowedCallsRuleErrors;
private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;

/** @var list<DisallowedCall> */
private array $disallowedCalls;

private ReflectionProvider $reflectionProvider;

private Normalizer $normalizer;

private TypeResolver $typeResolver;


/**
* @param DisallowedCallsRuleErrors $disallowedCallsRuleErrors
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param ReflectionProvider $reflectionProvider
* @param Normalizer $normalizer
* @param TypeResolver $typeResolver
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(
DisallowedCallsRuleErrors $disallowedCallsRuleErrors,
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
ReflectionProvider $reflectionProvider,
Normalizer $normalizer,
TypeResolver $typeResolver,
array $forbiddenCalls
) {
$this->disallowedCallsRuleErrors = $disallowedCallsRuleErrors;
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
$this->reflectionProvider = $reflectionProvider;
$this->normalizer = $normalizer;
$this->typeResolver = $typeResolver;
}


Expand All @@ -82,40 +60,7 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->name instanceof Name) {
$namespacedName = $node->name->getAttribute('namespacedName');
if ($namespacedName !== null && !($namespacedName instanceof Name)) {
throw new ShouldNotHappenException();
}
$names = [$namespacedName, $node->name];
} elseif ($node->name instanceof String_) {
$names = [new Name($this->normalizer->normalizeNamespace($node->name->value))];
} elseif ($node->name instanceof Variable) {
$value = $this->typeResolver->getVariableStringValue($node->name, $scope);
if (!is_string($value)) {
return [];
}
$names = [new Name($this->normalizer->normalizeNamespace($value))];
} else {
return [];
}
$displayName = $node->name->getAttribute('originalName');
if ($displayName !== null && !($displayName instanceof Name)) {
throw new ShouldNotHappenException();
}
foreach ($names as $name) {
if ($name && $this->reflectionProvider->hasFunction($name, $scope)) {
$functionReflection = $this->reflectionProvider->getFunction($name, $scope);
$definedIn = $functionReflection->isBuiltin() ? null : $functionReflection->getFileName();
} else {
$definedIn = null;
}
$message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $name), $definedIn, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION);
if ($message) {
return $message;
}
}
return [];
return $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
}

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

namespace Spaze\PHPStan\Rules\Disallowed\Calls;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\FunctionCallableNode;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;

/**
* Reports on first class callable syntax for a disallowed method.
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<FunctionCallableNode>
*/
class FunctionFirstClassCallables implements Rule
{

private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;

/** @var list<DisallowedCall> */
private array $disallowedCalls;


/**
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
array $forbiddenCalls
) {
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
}


public function getNodeType(): string
{
return FunctionCallableNode::class;
}


/**
* @param FunctionCallableNode $node
* @param Scope $scope
* @return list<IdentifierRuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
$originalNode = $node->getOriginalNode();
return $this->disallowedFunctionRuleErrors->get($originalNode, $scope, $this->disallowedCalls);
}

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

namespace Spaze\PHPStan\Rules\Disallowed\Calls;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodCallableNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;

/**
* Reports on first class callable syntax for a disallowed method.
*
* Static callables have a different rule, <code>StaticFirstClassCallables</code>
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<MethodCallableNode>
*/
class MethodFirstClassCallables implements Rule
{

private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;

/** @var list<DisallowedCall> */
private array $disallowedCalls;


/**
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
{
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
}


public function getNodeType(): string
{
return MethodCallableNode::class;
}


/**
* @param MethodCallableNode $node
* @param Scope $scope
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
$originalNode = $node->getOriginalNode();
return $this->disallowedMethodRuleErrors->get($originalNode->var, $originalNode, $scope, $this->disallowedCalls);
}

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

namespace Spaze\PHPStan\Rules\Disallowed\Calls;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\StaticMethodCallableNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;

/**
* Reports on first class callable syntax for a disallowed static method.
*
* Dynamic calls have a different rule, <code>MethodFirstClassCallables</code>
*
* @package Spaze\PHPStan\Rules\Disallowed
* @implements Rule<StaticMethodCallableNode>
*/
class StaticFirstClassCallables implements Rule
{

private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;

/** @var list<DisallowedCall> */
private array $disallowedCalls;


/**
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
{
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
}


public function getNodeType(): string
{
return StaticMethodCallableNode::class;
}


/**
* @param StaticMethodCallableNode $node
* @param Scope $scope
* @return list<RuleError>
* @throws ShouldNotHappenException
*/
public function processNode(Node $node, Scope $scope): array
{
$originalNode = $node->getOriginalNode();
return $this->disallowedMethodRuleErrors->get($originalNode->class, $originalNode, $scope, $this->disallowedCalls);
}

}
Loading

0 comments on commit d38d4e4

Please sign in to comment.