Skip to content

Commit

Permalink
Can specify params with a doctype in typeString config option
Browse files Browse the repository at this point in the history
Close #233
  • Loading branch information
spaze committed Dec 22, 2023
1 parent ee8ec5f commit 29a5955
Show file tree
Hide file tree
Showing 20 changed files with 665 additions and 151 deletions.
2 changes: 2 additions & 0 deletions docs/allow-attributes.md
Expand Up @@ -12,3 +12,5 @@ parameters:
position: 1
name: repositoryClass
```

You can also use `value` or `typeString` directives, just like with functions or methods.
3 changes: 3 additions & 0 deletions docs/allow-with-flags.md
Expand Up @@ -26,3 +26,6 @@ parameters:
position: 2
value: ::JSON_HEX_APOS
```

Just like with regular parameters, you can also use `typeString` instead of `value`.
The extra bonus this brings is unions: if you want to (dis)allow a parameter when either the flag `1` or `2` is set, use `typeString: 1 | 2`. Note that the `|` operator here is not the PHP's _bitwise or_ operator.
30 changes: 29 additions & 1 deletion docs/allow-with-parameters.md
@@ -1,6 +1,9 @@
## Allow with specified parameters only

You can also narrow down the allowed items when called with some parameters (applies only to disallowed method, static & function calls, for obvious reasons). _Please note that for now, only scalar values are supported in the configuration, not arrays._
You can also narrow down the allowed items when called with some parameters (applies only to disallowed method, static & function calls, for obvious reasons).
Only scalar values and no arrays are supported with the `value` configuration directive, but with the `typeString` directive,
arrays and unions are also supported, and generally anything you can express with a PHPDoc type string, if it makes sense.
When `typeString` is specified, `value` directive is ignored for the given parameter.

For example, you want to disallow calling `print_r()` but want to allow `print_r(..., true)`.
This can be done with optional `allowParamsInAllowed` or `allowParamsAnywhere` configuration keys:
Expand Down Expand Up @@ -184,3 +187,28 @@ parameters:
```

But because the "positional _or_ named" limitation described above applies here as well, I generally don't recommend using these shortcuts and instead recommend specifying both `position` and `name` keys.

### PHPDoc type strings

Instead of the `value` directive, you can use the `typeString` directive which allows you to specify arrays, unions, and anything that can be expressed with PHPDoc:

```neon
parameters:
disallowedFunctionCalls:
# ...
allowParamsInAllowed:
-
position: 1
name: 'message'
typeString: "'foo'"
```

The above example is the same as writing `value: foo` but because you want to specify a literal type string, you need to enclose the string in single quotes to indicate it's a string, not a class name. With integers, `typeString: 1` is the same as `value: 1`.

Type string allows you to specify:
- Arrays, e.g. `typeString: array{}` meaning empty array, or vice versa with `typeString: non-empty-array`, or even `typeString: array{foo:'bar'}` meaning an array with a `foo` key and `bar` string value
- Unions, e.g. `typeString: 1|2`, `typeString: "'foo'|'bar'"`, where the former example means the value must be an integer `1` or an integer `2`, and the latter means the value must be a string `foo` or `bar`
- Classes, e.g. `typeString: DateTime` which means an object of that class, or a child class od that class
- Any type as [understood by PHPStan](https://phpstan.org/writing-php-code/phpdoc-types), but not everything may make sense in your case

If both `typeString` and `value` directives are specified, the `value` directive is ignored.
112 changes: 56 additions & 56 deletions extension.neon

Large diffs are not rendered by default.

38 changes: 32 additions & 6 deletions src/Allowed/Allowed.php
Expand Up @@ -5,8 +5,13 @@

use PhpParser\Node\Arg;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Spaze\PHPStan\Rules\Disallowed\DisallowedWithParams;
Expand Down Expand Up @@ -36,12 +41,20 @@ class Allowed
/** @var AllowedPath */
private $allowedPath;

/** @var TypeStringResolver */
private $typeStringResolver;

public function __construct(Formatter $formatter, Normalizer $normalizer, AllowedPath $allowedPath)
{

public function __construct(
Formatter $formatter,
Normalizer $normalizer,
AllowedPath $allowedPath,
TypeStringResolver $typeStringResolver
) {
$this->formatter = $formatter;
$this->normalizer = $normalizer;
$this->allowedPath = $allowedPath;
$this->typeStringResolver = $typeStringResolver;
}


Expand Down Expand Up @@ -242,7 +255,7 @@ public function getConfig(array $allowed): AllowedConfig
* @template T of ParamValue
* @param class-string<T> $class
* @param int|string $key
* @param int|bool|string|null|array{position:int, value?:int|bool|string, name?:string} $value
* @param int|bool|string|null|array{position:int, value?:int|bool|string, typeString?:string, name?:string} $value
* @return T
* @throws UnsupportedParamTypeInConfigException
*/
Expand All @@ -253,6 +266,7 @@ private function paramFactory(string $class, $key, $value): ParamValue
$paramPosition = $value['position'];
$paramName = $value['name'] ?? null;
$paramValue = $value['value'] ?? null;
$typeString = $value['typeString'] ?? null;
} elseif (in_array($class, [ParamValueAny::class, ParamValueExceptAny::class], true)) {
if (is_numeric($value)) {
$paramPosition = (int)$value;
Expand All @@ -261,22 +275,34 @@ private function paramFactory(string $class, $key, $value): ParamValue
$paramPosition = null;
$paramName = (string)$value;
}
$paramValue = null;
$paramValue = $typeString = null;
} else {
$paramPosition = (int)$key;
$paramName = null;
$paramValue = $value;
$typeString = null;
}
} else {
$paramPosition = null;
$paramName = $key;
$paramValue = $value;
$typeString = null;
}

if (!is_int($paramValue) && !is_bool($paramValue) && !is_string($paramValue) && !is_null($paramValue)) {
if ($typeString) {
$type = $this->typeStringResolver->resolve($typeString);
} elseif (is_int($paramValue)) {
$type = new ConstantIntegerType($paramValue);
} elseif (is_bool($paramValue)) {
$type = new ConstantBooleanType($paramValue);
} elseif (is_string($paramValue)) {
$type = new ConstantStringType($paramValue);
} elseif (is_null($paramValue)) {
$type = new NullType();
} else {
throw new UnsupportedParamTypeInConfigException($paramPosition, $paramName, gettype($paramValue));
}
return new $class($paramPosition, $paramName, $paramValue);
return new $class($paramPosition, $paramName, $type);
}

}
5 changes: 1 addition & 4 deletions src/Params/Param.php
Expand Up @@ -21,9 +21,6 @@ public function getPosition(): ?int;
public function getName(): ?string;


/**
* @return int|bool|string|null
*/
public function getValue();
public function getType(): Type;

}
20 changes: 7 additions & 13 deletions src/Params/ParamValue.php
Expand Up @@ -5,9 +5,6 @@

use PHPStan\Type\Type;

/**
* @template T of int|bool|string|null
*/
abstract class ParamValue implements Param
{

Expand All @@ -17,8 +14,8 @@ abstract class ParamValue implements Param
/** @var ?string */
private $name;

/** @var T */
private $value;
/** @var Type */
private $type;


abstract public function matches(Type $type): bool;
Expand All @@ -27,13 +24,13 @@ abstract public function matches(Type $type): bool;
/**
* @param int|null $position
* @param string|null $name
* @param T $value
* @param Type $type
*/
final public function __construct(?int $position, ?string $name, $value)
final public function __construct(?int $position, ?string $name, Type $type)
{
$this->position = $position;
$this->name = $name;
$this->value = $value;
$this->type = $type;
}


Expand All @@ -49,12 +46,9 @@ public function getName(): ?string
}


/**
* @return T
*/
public function getValue()
public function getType(): Type
{
return $this->value;
return $this->type;
}

}
3 changes: 0 additions & 3 deletions src/Params/ParamValueAny.php
Expand Up @@ -5,9 +5,6 @@

use PHPStan\Type\Type;

/**
* @extends ParamValue<int|bool|string|null>
*/
final class ParamValueAny extends ParamValue
{

Expand Down
30 changes: 5 additions & 25 deletions src/Params/ParamValueCaseInsensitiveExcept.php
Expand Up @@ -3,38 +3,18 @@

namespace Spaze\PHPStan\Rules\Disallowed\Params;

use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;

/**
* @extends ParamValue<int|bool|string|null>
*/
class ParamValueCaseInsensitiveExcept extends ParamValue
{

/**
* @throws UnsupportedParamTypeException
*/
public function matches(Type $type): bool
{
if (!$type->isConstantScalarValue()->yes()) {
throw new UnsupportedParamTypeException();
}
$values = [];
foreach ($type->getConstantScalarValues() as $value) {
$values[] = $this->getLowercaseValue($value);
}
return !in_array($this->getLowercaseValue($this->getValue()), $values, true);
}


/**
* @param mixed $value
* @return mixed
*/
private function getLowercaseValue($value)
{
return is_string($value) ? strtolower($value) : $value;
$fn = function (ConstantStringType $string): string {
return strtolower($string->getValue());
};
return array_intersect(array_map($fn, $type->getConstantStrings()), array_map($fn, $this->getType()->getConstantStrings())) === [];
}

}
12 changes: 1 addition & 11 deletions src/Params/ParamValueExcept.php
Expand Up @@ -4,23 +4,13 @@
namespace Spaze\PHPStan\Rules\Disallowed\Params;

use PHPStan\Type\Type;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;

/**
* @extends ParamValue<int|bool|string|null>
*/
class ParamValueExcept extends ParamValue
{

/**
* @throws UnsupportedParamTypeException
*/
public function matches(Type $type): bool
{
if (!$type->isConstantScalarValue()->yes()) {
throw new UnsupportedParamTypeException();
}
return !in_array($this->getValue(), $type->getConstantScalarValues(), true);
return !$this->getType()->isSuperTypeOf($type)->yes();
}

}
3 changes: 0 additions & 3 deletions src/Params/ParamValueExceptAny.php
Expand Up @@ -5,9 +5,6 @@

use PHPStan\Type\Type;

/**
* @extends ParamValue<int|bool|string|null>
*/
final class ParamValueExceptAny extends ParamValue
{

Expand Down
35 changes: 35 additions & 0 deletions src/Params/ParamValueFlag.php
@@ -0,0 +1,35 @@
<?php
declare(strict_types = 1);

namespace Spaze\PHPStan\Rules\Disallowed\Params;

use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException;

abstract class ParamValueFlag extends ParamValue
{

/**
* @throws UnsupportedParamTypeException
* @throws UnsupportedParamTypeInConfigException
*/
protected function isFlagSet(Type $type): bool
{
if (!$type instanceof ConstantIntegerType) {
throw new UnsupportedParamTypeException();
}
foreach ($this->getType()->getConstantScalarValues() as $value) {
if (!is_int($value)) {
throw new UnsupportedParamTypeInConfigException($this->getPosition(), $this->getName(), gettype($value) . ' of ' . $this->getType()->describe(VerbosityLevel::precise()));
}
if (($value & $type->getValue()) !== 0) {
return true;
}
}
return false;
}

}
13 changes: 4 additions & 9 deletions src/Params/ParamValueFlagExcept.php
Expand Up @@ -3,25 +3,20 @@

namespace Spaze\PHPStan\Rules\Disallowed\Params;

use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Type;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException;

/**
* @extends ParamValue<int>
*/
class ParamValueFlagExcept extends ParamValue
class ParamValueFlagExcept extends ParamValueFlag
{

/**
* @throws UnsupportedParamTypeException
* @throws UnsupportedParamTypeInConfigException
*/
public function matches(Type $type): bool
{
if (!$type instanceof ConstantIntegerType) {
throw new UnsupportedParamTypeException();
}
return ($this->getValue() & $type->getValue()) === 0;
return !$this->isFlagSet($type);
}

}
13 changes: 4 additions & 9 deletions src/Params/ParamValueFlagSpecific.php
Expand Up @@ -3,25 +3,20 @@

namespace Spaze\PHPStan\Rules\Disallowed\Params;

use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Type;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeException;
use Spaze\PHPStan\Rules\Disallowed\Exceptions\UnsupportedParamTypeInConfigException;

/**
* @extends ParamValue<int>
*/
class ParamValueFlagSpecific extends ParamValue
class ParamValueFlagSpecific extends ParamValueFlag
{

/**
* @throws UnsupportedParamTypeException
* @throws UnsupportedParamTypeInConfigException
*/
public function matches(Type $type): bool
{
if (!$type instanceof ConstantIntegerType) {
throw new UnsupportedParamTypeException();
}
return ($this->getValue() & $type->getValue()) !== 0;
return $this->isFlagSet($type);
}

}

0 comments on commit 29a5955

Please sign in to comment.