Skip to content

Commit

Permalink
Can re-allow namespace if in a class with given attributes
Browse files Browse the repository at this point in the history
And can add `allowInUse` which is handy when using the `allowExceptIn...` (or `disallowIn...`) config option.
  • Loading branch information
spaze committed Jan 27, 2025
1 parent 92c3b5b commit db9c015
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 82 deletions.
59 changes: 57 additions & 2 deletions docs/allow-in-class-with-attributes.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
## Allow in class with given attributes

It is possible to allow a previously disallowed function, method call or an attribute usage when done in a class with specified attributes.
It is possible to allow a previously disallowed item when done in a class with specified attributes.
You can use the `allowInClassWithAttributes` configuration option.

This is supported for the following items:
- function calls
- method calls
- attribute usages
- namespace usages
- classname usages

For example, if you'd have a configuration like this:

```neon
Expand All @@ -27,7 +34,7 @@ class Foo
}
```

On the other hand, if you need to disallow a method call, a function call, or an attribute usage only when present in a method from a class with a given attribute,
On the other hand, if you need to disallow an item only when present in a method from a class with a given attribute,
use `allowExceptInClassWithAttributes` (or the `disallowInClassWithAttributes` alias):

```neon
Expand Down Expand Up @@ -65,3 +72,51 @@ class Foo
```

The attribute names in the _allow_ directives support [fnmatch()](https://www.php.net/function.fnmatch) patterns.

### Allow namespace or classname use in `use` imports

You can allow a namespace or a classname to be used in `use` imports with `allowInUse: true`.
This can be useful when you want to disallow a namespace usage in a class with an attribute (with `allowExceptInClassWithAttributes` or `disallowInClassWithAttributes`),
but don't want the error to be reported on line with the `use` statement.

Let's have a class like this:

```php
use Foo\Bar\DisallowedClass; // line 1

#[MyAttribute]
class Waldo
{

public function fred(DisallowedClass $param) // line 7
{
}

}
```

Then with a configuration like this:

```neon
parameters:
disallowedNamespace:
-
namespace: 'Foo\Bar\DisallowedClass'
allowExceptInClassWithAttributes:
- MyAttribute
```

the error would be reported both on line 1, because `use Foo\Bar\DisallowedClass;` uses a disallowed namespace, and on line 7 because `$param` has the disallowed type.
But maybe you'd expect the error to be reported only on line 7, because _that_ is a disallowed class used in a class with the `MyAttribute` attribute.

To omit the `use` finding, you can add the `allowInUse` line, like this:

```neon
parameters:
disallowedNamespace:
-
namespace: 'Foo\Bar\DisallowedClass'
allowExceptInClassWithAttributes:
- MyAttribute
allowInUse: true
```
21 changes: 21 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ parametersSchema:
?allowIn: listOf(string()),
?allowExceptIn: listOf(string()),
?disallowIn: listOf(string()),
?allowInClassWithAttributes: listOf(string()),
?allowExceptInClassWithAttributes: listOf(string()),
?disallowInClassWithAttributes: listOf(string()),
?allowInCallWithAttributes: listOf(string()),
?allowExceptInCallWithAttributes: listOf(string()),
?disallowInCallWithAttributes: listOf(string()),
?allowInClassWithMethodAttributes: listOf(string()),
?allowExceptInClassWithMethodAttributes: listOf(string()),
?disallowInClassWithMethodAttributes: listOf(string()),
?allowInUse: bool(),
?errorIdentifier: string(),
?errorTip: string(),
])
Expand All @@ -37,6 +47,16 @@ parametersSchema:
?allowIn: listOf(string()),
?allowExceptIn: listOf(string()),
?disallowIn: listOf(string()),
?allowInClassWithAttributes: listOf(string()),
?allowExceptInClassWithAttributes: listOf(string()),
?disallowInClassWithAttributes: listOf(string()),
?allowInCallWithAttributes: listOf(string()),
?allowExceptInCallWithAttributes: listOf(string()),
?disallowInCallWithAttributes: listOf(string()),
?allowInClassWithMethodAttributes: listOf(string()),
?allowExceptInClassWithMethodAttributes: listOf(string()),
?disallowInClassWithMethodAttributes: listOf(string()),
?allowInUse: bool(),
?errorIdentifier: string(),
?errorTip: string(),
])
Expand Down Expand Up @@ -298,6 +318,7 @@ services:
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedVariableRuleErrors
- Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver
- Spaze\PHPStan\Rules\Disallowed\Usages\NamespaceUsageFactory
-
factory: Spaze\PHPStan\Rules\Disallowed\Usages\NamespaceUsages(forbiddenNamespaces: %disallowedNamespaces%)
tags:
Expand Down
14 changes: 8 additions & 6 deletions src/Allowed/Allowed.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Spaze\PHPStan\Rules\Disallowed\Disallowed;
use Spaze\PHPStan\Rules\Disallowed\DisallowedWithParams;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;
use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter;
Expand All @@ -36,14 +37,15 @@ public function __construct(
/**
* @param Scope $scope
* @param array<Arg>|null $args
* @param DisallowedWithParams $disallowed
* @param Disallowed|DisallowedWithParams $disallowed
* @return bool
*/
public function isAllowed(Scope $scope, ?array $args, DisallowedWithParams $disallowed): bool
public function isAllowed(Scope $scope, ?array $args, Disallowed $disallowed): bool
{
$hasParams = $disallowed instanceof DisallowedWithParams;
foreach ($disallowed->getAllowInCalls() as $call) {
if ($this->callMatches($scope, $call)) {
return $this->hasAllowedParamsInAllowed($scope, $args, $disallowed);
return !$hasParams || $this->hasAllowedParamsInAllowed($scope, $args, $disallowed);
}
}
foreach ($disallowed->getAllowExceptInCalls() as $call) {
Expand All @@ -53,7 +55,7 @@ public function isAllowed(Scope $scope, ?array $args, DisallowedWithParams $disa
}
foreach ($disallowed->getAllowIn() as $allowedPath) {
if ($this->allowedPath->matches($scope, $allowedPath)) {
return $this->hasAllowedParamsInAllowed($scope, $args, $disallowed);
return !$hasParams || $this->hasAllowedParamsInAllowed($scope, $args, $disallowed);
}
}
if ($disallowed->getAllowExceptIn()) {
Expand All @@ -64,10 +66,10 @@ public function isAllowed(Scope $scope, ?array $args, DisallowedWithParams $disa
}
return true;
}
if ($disallowed->getAllowExceptParams()) {
if ($hasParams && $disallowed->getAllowExceptParams()) {
return $this->hasAllowedParams($scope, $args, $disallowed->getAllowExceptParams(), false);
}
if ($disallowed->getAllowParamsAnywhere()) {
if ($hasParams && $disallowed->getAllowParamsAnywhere()) {
return $this->hasAllowedParams($scope, $args, $disallowed->getAllowParamsAnywhere(), true);
}
if ($disallowed->getAllowInClassWithAttributes() && $scope->isInClass()) {
Expand Down
41 changes: 22 additions & 19 deletions src/DisallowedNamespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Spaze\PHPStan\Rules\Disallowed;

use Spaze\PHPStan\Rules\Disallowed\Exceptions\NotImplementedYetException;
use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfig;

class DisallowedNamespace implements Disallowed
{
Expand All @@ -15,40 +15,37 @@ class DisallowedNamespace implements Disallowed

private ?string $message;

/** @var list<string> */
private array $allowIn;

/** @var list<string> */
private array $allowExceptIn;

private ?string $errorIdentifier;

private ?string $errorTip;

private AllowedConfig $allowedConfig;

private bool $allowInUse;


/**
* @param string $namespace
* @param list<string> $excludes
* @param string|null $message
* @param list<string> $allowIn
* @param list<string> $allowExceptIn
* @param AllowedConfig $allowedConfig
* @param string|null $errorIdentifier
* @param string|null $errorTip
*/
public function __construct(
string $namespace,
array $excludes,
?string $message,
array $allowIn,
array $allowExceptIn,
AllowedConfig $allowedConfig,
bool $allowInUse,
?string $errorIdentifier,
?string $errorTip
) {
$this->namespace = $namespace;
$this->excludes = $excludes;
$this->message = $message;
$this->allowIn = $allowIn;
$this->allowExceptIn = $allowExceptIn;
$this->allowedConfig = $allowedConfig;
$this->allowInUse = $allowInUse;
$this->errorIdentifier = $errorIdentifier;
$this->errorTip = $errorTip;
}
Expand Down Expand Up @@ -78,38 +75,44 @@ public function getMessage(): ?string
/** @inheritDoc */
public function getAllowIn(): array
{
return $this->allowIn;
return $this->allowedConfig->getAllowIn();
}


/** @inheritDoc */
public function getAllowExceptIn(): array
{
return $this->allowExceptIn;
return $this->allowedConfig->getAllowExceptIn();
}


public function getAllowInCalls(): array
{
throw new NotImplementedYetException();
return $this->allowedConfig->getAllowInCalls();
}


public function getAllowExceptInCalls(): array
{
throw new NotImplementedYetException();
return $this->allowedConfig->getAllowExceptInCalls();
}


public function getAllowInClassWithAttributes(): array
{
throw new NotImplementedYetException();
return $this->allowedConfig->getAllowInClassWithAttributes();
}


public function getAllowExceptInClassWithAttributes(): array
{
throw new NotImplementedYetException();
return $this->allowedConfig->getAllowExceptInClassWithAttributes();
}


public function isAllowInUse(): bool
{
return $this->allowInUse;
}


Expand Down
41 changes: 28 additions & 13 deletions src/DisallowedNamespaceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,33 @@
namespace Spaze\PHPStan\Rules\Disallowed;

use PHPStan\ShouldNotHappenException;
use Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException;
use Spaze\PHPStan\Rules\Disallowed\Formatter\Formatter;
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;

class DisallowedNamespaceFactory
{

private Formatter $formatter;

private Normalizer $normalizer;

private AllowedConfigFactory $allowedConfigFactory;


public function __construct(Normalizer $normalizer)
public function __construct(Formatter $formatter, Normalizer $normalizer, AllowedConfigFactory $allowedConfigFactory)
{
$this->formatter = $formatter;
$this->normalizer = $normalizer;
$this->allowedConfigFactory = $allowedConfigFactory;
}


/**
* @param array<array{namespace?:string|list<string>, class?:string|list<string>, exclude?:string|list<string>, message?:string, allowIn?:list<string>, allowExceptIn?:list<string>, disallowIn?:list<string>, errorIdentifier?:string, errorTip?:string}> $config
* @param array<array{namespace?:string|list<string>, class?:string|list<string>, exclude?:string|list<string>, message?:string, allowIn?:list<string>, allowExceptIn?:list<string>, disallowIn?:list<string>, allowInUse?:bool, errorIdentifier?:string, errorTip?:string}> $config
* @return list<DisallowedNamespace>
* @throws ShouldNotHappenException
*/
public function createFromConfig(array $config): array
{
Expand All @@ -35,17 +45,22 @@ public function createFromConfig(array $config): array
foreach ((array)($disallowed['exclude'] ?? []) as $exclude) {
$excludes[] = $this->normalizer->normalizeNamespace($exclude);
}
foreach ((array)$namespaces as $namespace) {
$disallowedNamespace = new DisallowedNamespace(
$this->normalizer->normalizeNamespace($namespace),
$excludes,
$disallowed['message'] ?? null,
$disallowed['allowIn'] ?? [],
$disallowed['allowExceptIn'] ?? $disallowed['disallowIn'] ?? [],
$disallowed['errorIdentifier'] ?? null,
$disallowed['errorTip'] ?? null
);
$disallowedNamespaces[$disallowedNamespace->getNamespace()] = $disallowedNamespace;
$namespaces = (array)$namespaces;
try {
foreach ($namespaces as $namespace) {
$disallowedNamespace = new DisallowedNamespace(
$this->normalizer->normalizeNamespace($namespace),
$excludes,
$disallowed['message'] ?? null,
$this->allowedConfigFactory->getConfig($disallowed),
$disallowed['allowInUse'] ?? false,
$disallowed['errorIdentifier'] ?? null,
$disallowed['errorTip'] ?? null
);
$disallowedNamespaces[$disallowedNamespace->getNamespace()] = $disallowedNamespace;
}
} catch (UnsupportedParamTypeInConfigException $e) {
throw new ShouldNotHappenException(sprintf('%s: %s', $this->formatter->formatIdentifier($namespaces), $e->getMessage()));
}
}
return array_values($disallowedNamespaces);
Expand Down
Loading

0 comments on commit db9c015

Please sign in to comment.