Skip to content

Commit

Permalink
Detect disallowed functions and methods in callable parameters
Browse files Browse the repository at this point in the history
Reuses existing detection from DisallowedFunctionRuleErrors (for simple `'function'` callables) and DisallowedMethodRuleErrors (for `[$object, 'method']` callables) called from the new DisallowedCallableParameterRuleErrors service.

Ref #275
  • Loading branch information
spaze committed Jan 9, 2025
1 parent 9fdeff6 commit 91d7dd1
Show file tree
Hide file tree
Showing 25 changed files with 1,181 additions and 58 deletions.
1 change: 1 addition & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ services:
- Spaze\PHPStan\Rules\Disallowed\Identifier\Identifier
- Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors(forbiddenFunctionCalls: %disallowedFunctionCalls%, forbiddenMethodCalls: %disallowedMethodCalls%, forbiddenStaticCalls: %disallowedStaticCalls%)
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors
Expand Down
10 changes: 9 additions & 1 deletion src/Calls/FunctionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;

/**
Expand All @@ -24,12 +25,15 @@ class FunctionCalls implements Rule

private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;

private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;

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


/**
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param array $forbiddenCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
Expand All @@ -38,10 +42,12 @@ class FunctionCalls implements Rule
*/
public function __construct(
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
array $forbiddenCalls
) {
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
}

Expand All @@ -60,7 +66,9 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
$errors = $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForFunction($node, $scope);
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
}

}
17 changes: 14 additions & 3 deletions src/Calls/MethodCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;

/**
Expand All @@ -26,22 +27,30 @@ class MethodCalls implements Rule

private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;

private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;

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


/**
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
* @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)
{
public function __construct(
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
array $forbiddenCalls
) {
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
}


Expand All @@ -59,7 +68,9 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedMethodRuleErrors->get($node->var, $node, $scope, $this->disallowedCalls);
$errors = $this->disallowedMethodRuleErrors->get($node->var, $node, $scope, $this->disallowedCalls);
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForMethod($node->var, $node, $scope);
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
}

}
17 changes: 14 additions & 3 deletions src/Calls/StaticCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;

/**
Expand All @@ -26,22 +27,30 @@ class StaticCalls implements Rule

private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;

private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;

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


/**
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
* @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)
{
public function __construct(
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
array $forbiddenCalls
) {
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
}


Expand All @@ -59,7 +68,9 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->disallowedMethodRuleErrors->get($node->class, $node, $scope, $this->disallowedCalls);
$errors = $this->disallowedMethodRuleErrors->get($node->class, $node, $scope, $this->disallowedCalls);
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForMethod($node->class, $node, $scope);
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
}

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

namespace Spaze\PHPStan\Rules\Disallowed\RuleErrors;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\TypeCombinator;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
use Spaze\PHPStan\Rules\Disallowed\PHPStan1Compatibility;
use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver;

class DisallowedCallableParameterRuleErrors
{

private TypeResolver $typeResolver;

private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;

private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;

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

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

private ReflectionProvider $reflectionProvider;


/**
* @param TypeResolver $typeResolver
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
* @param DisallowedCallFactory $disallowedCallFactory
* @param ReflectionProvider $reflectionProvider
* @param array $forbiddenFunctionCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenFunctionCalls
* @param array $forbiddenMethodCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenMethodCalls
* @param array $forbiddenStaticCalls
* @phpstan-param ForbiddenCallsConfig $forbiddenStaticCalls
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
* @throws ShouldNotHappenException
*/
public function __construct(
TypeResolver $typeResolver,
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
DisallowedCallFactory $disallowedCallFactory,
ReflectionProvider $reflectionProvider,
array $forbiddenFunctionCalls,
array $forbiddenMethodCalls,
array $forbiddenStaticCalls
) {
$this->typeResolver = $typeResolver;
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
$this->disallowedFunctionCalls = $disallowedCallFactory->createFromConfig($forbiddenFunctionCalls);
$this->disallowedCalls = $disallowedCallFactory->createFromConfig(array_merge($forbiddenMethodCalls, $forbiddenStaticCalls));
$this->reflectionProvider = $reflectionProvider;
}


/**
* @param FuncCall $node
* @param Scope $scope
* @return list<IdentifierRuleError>
* @throws ShouldNotHappenException
*/
public function getForFunction(FuncCall $node, Scope $scope): array
{
$ruleErrors = [];
foreach ($this->typeResolver->getNamesFromCall($node, $scope) as $name) {
if (!$this->reflectionProvider->hasFunction($name, $scope)) {
continue;
}
$reflection = $this->reflectionProvider->getFunction($name, $scope);
$errors = $this->getErrors($node, $scope, $reflection);
if ($errors) {
$ruleErrors = array_merge($ruleErrors, $errors);
}
}
return $ruleErrors;
}


/**
* @param Name|Expr $class
* @param MethodCall|StaticCall $node
* @param Scope $scope
* @return list<IdentifierRuleError>
* @throws ShouldNotHappenException
*/
public function getForMethod($class, CallLike $node, Scope $scope): array
{
$ruleErrors = [];
$classType = $this->typeResolver->getType($class, $scope);
if (PHPStan1Compatibility::isClassString($classType)->yes()) {
$classType = $classType->getClassStringObjectType();
}
foreach ($classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames() as $className) {
if (!$this->reflectionProvider->hasClass($className)) {
continue;
}
$classReflection = $this->reflectionProvider->getClass($className);
foreach ($this->typeResolver->getNamesFromCall($node, $scope) as $name) {
if (!$classReflection->hasMethod($name->name)) {
continue;
}
$reflection = $classReflection->getMethod($name->name, $scope);
$errors = $this->getErrors($node, $scope, $reflection);
if ($errors) {
$ruleErrors = array_merge($ruleErrors, $errors);
}
}
}
return $ruleErrors;
}


/**
* @param Scope $scope
* @param FuncCall|MethodCall|StaticCall $node
* @param ExtendedMethodReflection|FunctionReflection $reflection
* @return list<IdentifierRuleError>
* @throws ShouldNotHappenException
*/
private function getErrors(CallLike $node, Scope $scope, $reflection): array
{
$ruleErrors = [];
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $reflection->getVariants());
$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptor, $node->getArgs()) ?? $node->getArgs();
foreach ($parametersAcceptor->getParameters() as $key => $parameter) {
if (!TypeCombinator::removeNull($parameter->getType())->isCallable()->yes() || !isset($reorderedArgs[$key])) {
continue;
}
$callableType = $scope->getType($reorderedArgs[$key]->value);
foreach ($callableType->getConstantStrings() as $constantString) {
$errors = $this->disallowedFunctionRuleErrors->getByString($constantString->getValue(), $scope, $this->disallowedFunctionCalls);
if ($errors) {
$ruleErrors = array_merge($ruleErrors, $errors);
}
}
foreach ($callableType->getConstantArrays() as $constantArray) {
foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethodName) {
if ($typeAndMethodName->isUnknown()) {
continue;
}
$method = $typeAndMethodName->getMethod();
foreach ($typeAndMethodName->getType()->getObjectClassNames() as $class) {
$errors = $this->disallowedMethodRuleErrors->getByString($class, $method, $scope, $this->disallowedCalls);
if ($errors) {
$ruleErrors = array_merge($ruleErrors, $errors);
}
}
}
}
}
return $ruleErrors;
}

}
Loading

0 comments on commit 91d7dd1

Please sign in to comment.