Skip to content

Commit

Permalink
Detect callable parameters (#281)
Browse files Browse the repository at this point in the history
Detects disallowed methods and functions in callable parameters

For example:
- `array_map('function', []);`
- `array_map([$object, 'method'], []);`
- `array_map([Class::class, 'staticMethod']);`
- `IntlChar::enumCharTypes('function');`
- also in custom functions and methods

Ref #275
  • Loading branch information
spaze authored Jan 9, 2025
2 parents 9fdeff6 + c2df61a commit 3c11864
Show file tree
Hide file tree
Showing 26 changed files with 1,202 additions and 79 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
"scripts": {
"lint": "vendor/bin/parallel-lint --colors src/ tests/",
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php",
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php --exclude tests/src/disallowed/callableParameters.php",
"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php",
"lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php",
"lint-8.2": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
Expand Down
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->toString())) {
continue;
}
$reflection = $classReflection->getMethod($name->toString(), $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 3c11864

Please sign in to comment.