Skip to content

Commit

Permalink
First class callable syntax supported for functions and methods (#279)
Browse files Browse the repository at this point in the history
This is the syntax supported by PHP 8.1:
```php
$func = print_r(...);
$func();
```
or
```php
$obj = new Object();
$func = $obj->method(...);
$func();
```
or
```php
$func = Class::method(...);
$func();
```

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

Ref #275
  • Loading branch information
spaze authored Dec 8, 2024
2 parents 9e5b799 + 4b55ba5 commit 9fdeff6
Show file tree
Hide file tree
Showing 26 changed files with 624 additions and 135 deletions.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
},
"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",
"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",
"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",
"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-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",
"lint-neon": "vendor/bin/neon-lint .",
"phpcs": "vendor/bin/phpcs src/ tests/",
Expand Down
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
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ parameters:
ForbiddenCallsConfig: 'array<array{function?:string|list<string>, method?:string|list<string>, exclude?:string|list<string>, definedIn?:string|list<string>, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>'
DisallowedAttributesConfig: 'array<array{attribute:string|list<string>, exclude?:string|list<string>, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>'
AllowDirectivesConfig: 'array{%typeAliases.AllowDirectives%}'
excludePaths:
analyse:
- src/PHPStan1Compatibility.php

includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
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);
}

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

namespace Spaze\PHPStan\Rules\Disallowed;

use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;

/**
* Provides compatibility layer when running on PHPStan 1.x.
* The whole file can be removed when PHPStan 2.x is required in `composer.json`.
* This file is ignored in `phpstan.neon` in the `excludePaths.analyse` entry.
*/
class PHPStan1Compatibility
{

/**
* @see https://github.com/phpstan/phpstan/blob/2.0.x/UPGRADING.md#minor-backward-compatibility-breaks-1:~:text=Rename%20Type%3A%3AisClassStringType()%20to%20Type%3A%3AisClassString()
*/
public static function isClassString(Type $type): TrinaryLogic
{
if (method_exists($type, 'isClassStringType')) {
// PHPStan 1.x
return $type->isClassStringType();
} else {
// PHPStan 2.x
return $type->isClassString();
}
}

}
Loading

0 comments on commit 9fdeff6

Please sign in to comment.