Skip to content

Commit

Permalink
Can get method/function attributes for params of disallowed classes
Browse files Browse the repository at this point in the history
When a disallowed classname is detected in method or function params, and it is allowed by method/function attributes, this change will allow to get the method/func attributes. It's way hacky as it seems to be not possible natively in PHPStan because `$scope->getFunction()` returns null for `FullyQualified` nodes in method/func parameter types. So we store the method name in `ClassMethod` rule, use it in `Allowed` service when set, and unset the function name in `InClassMethodNode` which is a virtual node which marks the beginning of the method body. Similar for functions in `Function_` and `InFunctionBody`.

Close #314
  • Loading branch information
spaze committed Mar 2, 2025
1 parent a1eec11 commit 24ef765
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 9 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/Bar.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 --exclude tests/src/disallowed-allow/callableParameters.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/AttributeClass --exclude tests/src/Bar.php --exclude tests/src/Enums.php --exclude tests/src/Functions.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 --exclude tests/src/disallowed-allow/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
17 changes: 17 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ services:
- Spaze\PHPStan\Rules\Disallowed\Allowed\Allowed
- Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory
- Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedPath
- Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature
- Spaze\PHPStan\Rules\Disallowed\DisallowedAttributeFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory
- Spaze\PHPStan\Rules\Disallowed\DisallowedConstantFactory
Expand Down Expand Up @@ -575,3 +576,19 @@ services:
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\WhileControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\SetCurrentClassMethodNameHelperRule
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\UnsetCurrentClassMethodNameHelperRule
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\SetCurrentFunctionNameHelperRule
tags:
- phpstan.rules.rule
-
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\UnsetCurrentFunctionNameHelperRule
tags:
- phpstan.rules.rule
8 changes: 8 additions & 0 deletions src/Allowed/Allowed.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,22 @@ class Allowed

private Identifier $identifier;

private GetAttributesWhenInSignature $attributesWhenInSignature;

private AllowedPath $allowedPath;


public function __construct(
Formatter $formatter,
Reflector $reflector,
Identifier $identifier,
GetAttributesWhenInSignature $attributesWhenInSignature,
AllowedPath $allowedPath
) {
$this->formatter = $formatter;
$this->reflector = $reflector;
$this->identifier = $identifier;
$this->attributesWhenInSignature = $attributesWhenInSignature;
$this->allowedPath = $allowedPath;
}

Expand Down Expand Up @@ -269,6 +273,10 @@ private function getCallAttributes(?Node $node, Scope $scope): array
} elseif ($node instanceof Function_) {
return $this->reflector->reflectFunction($node->name->name)->getAttributes();
}
$attributes = $this->attributesWhenInSignature->get($scope);
if ($attributes !== null) {
return $attributes;
}
}
return [];
}
Expand Down
92 changes: 92 additions & 0 deletions src/Allowed/GetAttributesWhenInSignature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed\Allowed;

use PHPStan\Analyser\Scope;
use PHPStan\BetterReflection\Reflection\Adapter\FakeReflectionAttribute;
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionAttribute;
use PHPStan\BetterReflection\Reflection\ReflectionAttribute as BetterReflectionAttribute;
use PHPStan\BetterReflection\Reflector\Reflector;

class GetAttributesWhenInSignature
{

private Reflector $reflector;

/** @var class-string|null */
private ?string $currentClass = null;

private ?string $currentMethod = null;

/** @var non-empty-string|null */
private ?string $currentFunction = null;


public function __construct(Reflector $reflector)
{
$this->reflector = $reflector;
}


/**
* Emulates the missing $scope->getMethodOrFunctionSignature().
*
* Because $scope->getFunction() returns null when the node, like for example a namespace node (instance of FullyQualified),
* is inside the method or the function signature, it's impossible to get to the current method or function reflection using $scope to get its attributes.
* The hacky solution is to store the current method name in a ClassMethod rule, read it here, and unset it in a InClassMethodNode rule,
* or the function name in a Function_ and a InFunctionNode rules.
*
* @param Scope $scope
* @return list<FakeReflectionAttribute|ReflectionAttribute|BetterReflectionAttribute>|null
*/
public function get(Scope $scope): ?array
{
if (
$this->currentClass !== null
&& $this->currentMethod !== null
&& $scope->isInClass()
&& $scope->getClassReflection()->getName() === $this->currentClass
) {
return $scope->getClassReflection()->getNativeReflection()->getMethod($this->currentMethod)->getAttributes();
} elseif ($this->currentFunction !== null) {
return $this->reflector->reflectFunction($this->currentFunction)->getAttributes();
}
return null;
}


/**
* @param class-string $className
* @param string $methodName
* @return void
*/
public function setCurrentClassMethodName(string $className, string $methodName): void
{
$this->currentClass = $className;
$this->currentMethod = $methodName;
}


public function unsetCurrentClassMethodName(): void
{
$this->currentClass = $this->currentMethod = null;
}


/**
* @param non-empty-string $functionName
* @return void
*/
public function setCurrentFunctionName(string $functionName): void
{
$this->currentFunction = $functionName;
}


public function unsetCurrentFunctionName(): void
{
$this->currentFunction = null;
}

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

namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;

/**
* @implements Rule<ClassMethod>
*/
class SetCurrentClassMethodNameHelperRule implements Rule
{

private GetAttributesWhenInSignature $attributesWhenInSignature;


public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
{
$this->attributesWhenInSignature = $attributesWhenInSignature;
}


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


/**
* @param ClassMethod $node
* @param Scope $scope
* @return array{}
*/
public function processNode(Node $node, Scope $scope): array
{
if ($scope->isInClass()) {
$this->attributesWhenInSignature->setCurrentClassMethodName($scope->getClassReflection()->getName(), $node->name->name);
}
return [];
}

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

namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;

use PhpParser\Node;
use PhpParser\Node\Stmt\Function_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;

/**
* @implements Rule<Function_>
*/
class SetCurrentFunctionNameHelperRule implements Rule
{

private GetAttributesWhenInSignature $attributesWhenInSignature;


public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
{
$this->attributesWhenInSignature = $attributesWhenInSignature;
}


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


/**
* @param Function_ $node
* @param Scope $scope
* @return array{}
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->namespacedName !== null) {
$this->attributesWhenInSignature->setCurrentFunctionName($node->namespacedName->name);

Check failure on line 41 in src/HelperRules/SetCurrentFunctionNameHelperRule.php

View workflow job for this annotation

GitHub Actions / lint-phpcs-phpstan (8.0, composer phpstan, --prefer-lowest)

Access to an undefined property PhpParser\Node\Name::$name.

Check failure on line 41 in src/HelperRules/SetCurrentFunctionNameHelperRule.php

View workflow job for this annotation

GitHub Actions / lint-phpcs-phpstan (7.4, composer phpstan, --prefer-lowest)

Access to an undefined property PhpParser\Node\Name::$name.
}
return [];
}

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

namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Rules\Rule;
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;

/**
* @implements Rule<InClassMethodNode>
*/
class UnsetCurrentClassMethodNameHelperRule implements Rule
{

private GetAttributesWhenInSignature $attributesWhenInSignature;


public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
{
$this->attributesWhenInSignature = $attributesWhenInSignature;
}


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


/**
* @param InClassMethodNode $node
* @param Scope $scope
* @return array{}
*/
public function processNode(Node $node, Scope $scope): array
{
$this->attributesWhenInSignature->unsetCurrentClassMethodName();
return [];
}

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

namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InFunctionNode;
use PHPStan\Rules\Rule;
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;

/**
* @implements Rule<InFunctionNode>
*/
class UnsetCurrentFunctionNameHelperRule implements Rule
{

private GetAttributesWhenInSignature $attributesWhenInSignature;


public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
{
$this->attributesWhenInSignature = $attributesWhenInSignature;
}


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


/**
* @param InFunctionNode $node
* @param Scope $scope
* @return array{}
*/
public function processNode(Node $node, Scope $scope): array
{
$this->attributesWhenInSignature->unsetCurrentFunctionName();
return [];
}

}
Loading

0 comments on commit 24ef765

Please sign in to comment.