Repository: spaze/phpstan-disallowed-calls (php)

Issue:
allowParamsAnywhere named parameters 
Hello,

it would be awesome if instead of the order of the parameter declared as an integer, the named parameter could be used.

So for example in 
```
function: 'json_decode()'
message: ''
allowParamsAnywhere:
    4: ::JSON_THROW_ON_ERROR
```
instead of `4`, `flags` could be used, so that we don't need to define all the ommited parameters before `flags`.

Pull request:
Support named parameters in allowParams* config keys
Close #140

Patch:
diff --git a/README.md b/README.md
index aff8b28..496d8e2 100644
--- a/README.md
+++ b/README.md
@@ -311,16 +311,25 @@ parameters:
                 - path/to/some/file-*.php
                 - tests/*.test.php
             allowParamsInAllowed:
-                1: 'foo'
-                2: true
+                -
+                    position: 1
+                    name: 'message'
+                    value: 'foo'
+                -
+                    position: 2
+                    name: 'alert'
+                    value: true
             allowParamsAnywhere:
-                2: true
+                -
+                    position: 2
+                    name: 'alert'
+                    value: true
 ```
 
 When using `allowParamsInAllowed`, calls will be allowed only when they are in one of the `allowIn` paths, and are called with all parameters listed in `allowParamsInAllowed`.
 With `allowParamsAnywhere`, calls are allowed when called with all parameters listed no matter in which file. In the example above, the `log()` method will be disallowed unless called as:
-- `log(..., true)` anywhere
-- `log('foo', true)` in `another/file.php` or `optional/path/to/log.tests.php`
+- `log(..., true)` (or `log(..., alert: true)`) anywhere
+- `log('foo', true)` (or `log(message: 'foo', alert: true)`) in `another/file.php` or `optional/path/to/log.tests.php`
 
 Use `allowParamsInAllowedAnyValue` and `allowParamsAnywhereAnyValue` if you don't care about the parameter's value but want to make sure the parameter is passed.
 Following the previous example:
@@ -335,13 +344,17 @@ parameters:
                 - path/to/some/file-*.php
                 - tests/*.test.php
             allowParamsInAllowedAnyValue:
-                - 2
+                -
+                    position: 2
+                    name: 'alert'
             allowParamsAnywhereAnyValue:
-                - 1
+                -
+                    position: 1
+                    name: 'message'
 ```
 means that you should use (`...` means any value):
-- `log(...)` anywhere
-- `log(..., ...)` in `another/file.php` or `optional/path/to/log.tests.php`
+- `log(...)` (or `log(message: ...)`) anywhere
+- `log(..., ...)` (or `log(message: ..., alert: ...)`) in `another/file.php` or `optional/path/to/log.tests.php`
 
 Such configuration only makes sense when both the parameters of `log()` are optional. If they are required, omitting them would result in an error already detected by PHPStan itself.
 
@@ -356,10 +369,13 @@ parameters:
         -
             function: 'hash()'
             allowExceptCaseInsensitiveParams:
-            	1: 'md5'
+                -
+                    position: 1
+                    name: 'algo'
+                    value: 'md5'
 ```
 
-This will disallow `hash()` call where the first parameter is `'md5'`. `allowExceptCaseInsensitiveParams` is used because the first parameter of `hash()` is case-insensitive (so you can also use `'MD5'`, or even `'Md5'` & `'mD5'` if you wish).
+This will disallow `hash()` call where the first parameter (or the named parameter `algo`) is `'md5'`. `allowExceptCaseInsensitiveParams` is used because the first parameter of `hash()` is case-insensitive (so you can also use `'MD5'`, or even `'Md5'` & `'mD5'` if you wish).
 To disallow only exact matches, use `allowExceptParams`:
 
 ```neon
@@ -368,7 +384,9 @@ parameters:
         -
             function: 'foo()'
             allowExceptParams:
-            	2: 'baz'
+                -
+                    position: 2
+                    value: 'baz'
 ```
 will disallow `foo('bar', 'baz')` but not `foo('bar', 'BAZ')`.
 
@@ -384,11 +402,67 @@ parameters:
             allowIn:
                 - 'views/*'
             allowExceptParamsInAllowed:
-                2: 'quux'
+                -
+                    position: 2
+                    value: 'quux'
 ```
 
 Calling `waldo()` is disallowed, and allowed back again only when the file is in the `views/` subdirectory **and** `waldo()` is called in the file with a 2nd parameter being the string `quux`.
 
+As already demonstrated above, named parameters are also supported:
+
+```neon
+parameters:
+    disallowedFunctionCalls:
+        -
+            function: 'json_decode()'
+            message: 'set the $flags parameter to `JSON_THROW_ON_ERROR` to throw a JsonException'
+            allowParamsAnywhere:
+                -
+                    position: 4
+                    name: 'flags'
+                    value: ::JSON_THROW_ON_ERROR
+```
+
+This format allows to detect the value in both cases whether it's used with a traditional positional parameter (e.g. `json_decode($foo, null, 512, JSON_THROW_ON_ERROR)`) or a named parameter (e.g. `json_decode($foo, flags: JSON_THROW_ON_ERROR)`).
+All keys are optional but if you don't specify `name`, the named parameter will not be found in a call like e.g. `json_decode($foo, null, 512, JSON_THROW_ON_ERROR)`.
+And vice versa, if you don't specify the `position` key, only the named parameter will be found matching this definition, not the positional one.
+
+You can use shortcuts like
+```neon
+parameters:
+    disallowedFunctionCalls:
+            # ...
+            allowParamsAnywhere:
+                2: true
+                foo: 'bar'
+            allowParamsAnywhereAnyValue:
+                - 2
+                - foo
+```
+
+which internally expands to
+
+```neon
+parameters:
+    disallowedFunctionCalls:
+            # ...
+            allowParamsAnywhere:
+                -
+                    position: 2
+                    value: true
+                -
+                    name: foo
+                    value: 'bar'
+            allowParamsAnywhereAnyValue:
+                -
+                    position: 2
+                -
+                    name: foo
+```
+
+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.
+
 ## Case-(in)sensitivity
 
 Function names, method names, class names, namespaces are matched irrespective of their case (disallowing `print_r` will also find `print_R` calls), while anything else like constants, file names, paths are not.
diff --git a/composer.json b/composer.json
index ad9f8c3..82b5ca4 100644
--- a/composer.json
+++ b/composer.json
@@ -37,7 +37,7 @@
 	},
 	"scripts": {
 		"lint": "vendor/bin/parallel-lint --colors src/ tests/",
-		"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/libs/TypesEverywhere.php",
+		"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/libs/TypesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php",
 		"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/libs/TypesEverywhere.php",
 		"phpcs": "vendor/bin/phpcs src/ tests/",
 		"cs-fix": "vendor/bin/phpcbf src/ tests/",
diff --git a/phpstan.neon b/phpstan.neon
index 8fb9c6a..a359bc9 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -3,7 +3,9 @@ parameters:
 		- src
 	level: max
 	typeAliases:
-		ForbiddenCallsConfig: 'array<array{function?:string, method?:string, message?:string, allowIn?:string[], allowExceptIn?:string[], disallowIn?:string[], allowInFunctions?:string[], allowInMethods?:string[], allowExceptInFunctions?:string[], allowExceptInMethods?:string[], disallowInFunctions?:string[], disallowInMethods?:string[], allowParamsInAllowed?:array<integer, integer|boolean|string>, allowParamsInAllowedAnyValue?:array<integer, integer>, allowParamsAnywhere?:array<integer, integer|boolean|string>, allowParamsAnywhereAnyValue?:array<integer, integer>, allowExceptParamsInAllowed?:array<integer, integer|boolean|string>, disallowParamsInAllowed?:array<integer, integer|boolean|string>, allowExceptParams?:array<integer, integer|boolean|string>, disallowParams?:array<integer, integer|boolean|string>, allowExceptCaseInsensitiveParams?:array<integer, integer|boolean|string>, disallowCaseInsensitiveParams?:array<integer, integer|boolean|string>, errorIdentifier?:string, errorTip?:string}>'
+		CallParamConfig: 'array<int|string, int|bool|string|array{position:int, value?:int|bool|string, name?:string}>'
+		CallParamAnyValueConfig: 'array<int|string, int|array{position:int, value?:int|bool|string, name?:string}>'
+		ForbiddenCallsConfig: 'array<array{function?:string, method?:string, message?:string, allowIn?:string[], allowExceptIn?:string[], disallowIn?:string[], allowInFunctions?:string[], allowInMethods?:string[], allowExceptInFunctions?:string[], allowExceptInMethods?:string[], disallowInFunctions?:string[], disallowInMethods?:string[], allowParamsInAllowed?:CallParamConfig, allowParamsInAllowedAnyValue?:CallParamAnyValueConfig, allowParamsAnywhere?:CallParamConfig, allowParamsAnywhereAnyValue?:CallParamAnyValueConfig, allowExceptParamsInAllowed?:CallParamConfig, disallowParamsInAllowed?:CallParamConfig, allowExceptParams?:CallParamConfig, disallowParams?:CallParamConfig, allowExceptCaseInsensitiveParams?:CallParamConfig, disallowCaseInsensitiveParams?:CallParamConfig, errorIdentifier?:string, errorTip?:string}>'
 
 includes:
 	- vendor/phpstan/phpstan/conf/bleedingEdge.neon
diff --git a/src/DisallowedCall.php b/src/DisallowedCall.php
index 9dfe14c..bddc919 100644
--- a/src/DisallowedCall.php
+++ b/src/DisallowedCall.php
@@ -26,16 +26,16 @@ class DisallowedCall implements Disallowed
 	/** @var string[] */
 	private $allowExceptInCalls;
 
-	/** @var array<int, DisallowedCallParam> */
+	/** @var array<int|string, DisallowedCallParam> */
 	private $allowParamsInAllowed;
 
-	/** @var array<int, DisallowedCallParam> */
+	/** @var array<int|string, DisallowedCallParam> */
 	private $allowParamsAnywhere;
 
-	/** @var array<int, DisallowedCallParam> */
+	/** @var array<int|string, DisallowedCallParam> */
 	private $allowExceptParamsInAllowed;
 
-	/** @var array<int, DisallowedCallParam> */
+	/** @var array<int|string, DisallowedCallParam> */
 	private $allowExceptParams;
 
 	/** @var string|null */
@@ -52,10 +52,10 @@ class DisallowedCall implements Disallowed
 	 * @param string[] $allowExceptIn
 	 * @param string[] $allowInCalls
 	 * @param string[] $allowExceptInCalls
-	 * @param array<int, DisallowedCallParam> $allowParamsInAllowed
-	 * @param array<int, DisallowedCallParam> $allowParamsAnywhere
-	 * @param array<int, DisallowedCallParam> $allowExceptParamsInAllowed
-	 * @param array<int, DisallowedCallParam> $allowExceptParams
+	 * @param array<int|string, DisallowedCallParam> $allowParamsInAllowed
+	 * @param array<int|string, DisallowedCallParam> $allowParamsAnywhere
+	 * @param array<int|string, DisallowedCallParam> $allowExceptParamsInAllowed
+	 * @param array<int|string, DisallowedCallParam> $allowExceptParams
 	 * @param string|null $errorIdentifier
 	 * @param string|null $errorTip
 	 */
@@ -133,7 +133,7 @@ class DisallowedCall implements Disallowed
 
 
 	/**
-	 * @return array<int, DisallowedCallParam>
+	 * @return array<int|string, DisallowedCallParam>
 	 */
 	public function getAllowParamsInAllowed(): array
 	{
@@ -142,7 +142,7 @@ class DisallowedCall implements Disallowed
 
 
 	/**
-	 * @return array<int, DisallowedCallParam>
+	 * @return array<int|string, DisallowedCallParam>
 	 */
 	public function getAllowParamsAnywhere(): array
 	{
@@ -151,7 +151,7 @@ class DisallowedCall implements Disallowed
 
 
 	/**
-	 * @return array<int, DisallowedCallParam>
+	 * @return array<int|string, DisallowedCallParam>
 	 */
 	public function getAllowExceptParamsInAllowed(): array
 	{
@@ -160,7 +160,7 @@ class DisallowedCall implements Disallowed
 
 
 	/**
-	 * @return array<int, DisallowedCallParam>
+	 * @return array<int|string, DisallowedCallParam>
 	 */
 	public function getAllowExceptParams(): array
 	{
diff --git a/src/DisallowedCallFactory.php b/src/DisallowedCallFactory.php
index 525ca8e..6bcfd63 100644
--- a/src/DisallowedCallFactory.php
+++ b/src/DisallowedCallFactory.php
@@ -4,6 +4,7 @@ declare(strict_types = 1);
 namespace Spaze\PHPStan\Rules\Disallowed;
 
 use PHPStan\ShouldNotHappenException;
+use Spaze\PHPStan\Rules\Disallowed\Params\DisallowedCallParam;
 use Spaze\PHPStan\Rules\Disallowed\Params\DisallowedCallParamExceptCaseInsensitiveValue;
 use Spaze\PHPStan\Rules\Disallowed\Params\DisallowedCallParamExceptValue;
 use Spaze\PHPStan\Rules\Disallowed\Params\DisallowedCallParamWithAnyValue;
@@ -37,25 +38,25 @@ class DisallowedCallFactory
 					$allowExceptInCalls[] = $this->normalizeCall($disallowedCall);
 				}
 				foreach ($disallowed['allowParamsInAllowed'] ?? [] as $param => $value) {
-					$allowParamsInAllowed[$param] = new DisallowedCallParamWithValue($value);
+					$allowParamsInAllowed[$param] = $this->paramFactory(DisallowedCallParamWithValue::class, $param, $value);
 				}
-				foreach ($disallowed['allowParamsInAllowedAnyValue'] ?? [] as $param) {
-					$allowParamsInAllowed[$param] = new DisallowedCallParamWithAnyValue();
+				foreach ($disallowed['allowParamsInAllowedAnyValue'] ?? [] as $param => $value) {
+					$allowParamsInAllowed[$param] = $this->paramFactory(DisallowedCallParamWithAnyValue::class, $param, $value);
 				}
 				foreach ($disallowed['allowParamsAnywhere'] ?? [] as $param => $value) {
-					$allowParamsAnywhere[$param] = new DisallowedCallParamWithValue($value);
+					$allowParamsAnywhere[$param] = $this->paramFactory(DisallowedCallParamWithValue::class, $param, $value);
 				}
-				foreach ($disallowed['allowParamsAnywhereAnyValue'] ?? [] as $param) {
-					$allowParamsAnywhere[$param] = new DisallowedCallParamWithAnyValue();
+				foreach ($disallowed['allowParamsAnywhereAnyValue'] ?? [] as $param => $value) {
+					$allowParamsAnywhere[$param] = $this->paramFactory(DisallowedCallParamWithAnyValue::class, $param, $value);
 				}
 				foreach ($disallowed['allowExceptParamsInAllowed'] ?? $disallowed['disallowParamsInAllowed'] ?? [] as $param => $value) {
-					$allowExceptParamsInAllowed[$param] = new DisallowedCallParamExceptValue($value);
+					$allowExceptParamsInAllowed[$param] = $this->paramFactory(DisallowedCallParamExceptValue::class, $param, $value);
 				}
 				foreach ($disallowed['allowExceptParams'] ?? $disallowed['disallowParams'] ?? [] as $param => $value) {
-					$allowExceptParams[$param] = new DisallowedCallParamExceptValue($value);
+					$allowExceptParams[$param] = $this->paramFactory(DisallowedCallParamExceptValue::class, $param, $value);
 				}
 				foreach ($disallowed['allowExceptCaseInsensitiveParams'] ?? $disallowed['disallowCaseInsensitiveParams'] ?? [] as $param => $value) {
-					$allowExceptParams[$param] = new DisallowedCallParamExceptCaseInsensitiveValue($value);
+					$allowExceptParams[$param] = $this->paramFactory(DisallowedCallParamExceptCaseInsensitiveValue::class, $param, $value);
 				}
 				$disallowedCall = new DisallowedCall(
 					$this->normalizeCall($call),
@@ -84,4 +85,42 @@ class DisallowedCallFactory
 		return ltrim($call, '\\');
 	}
 
+
+	/**
+	 * @template T of DisallowedCallParam
+	 * @param class-string<T> $class
+	 * @param int|string $key
+	 * @param int|bool|string|null|array{position:int, value?:int|bool|string, name?:string} $value
+	 * @return T
+	 */
+	private function paramFactory(string $class, $key, $value): DisallowedCallParam
+	{
+		if (is_numeric($key)) {
+			if (is_array($value)) {
+				$paramPosition = $value['position'];
+				$paramName = $value['name'] ?? null;
+				$paramValue = $value['value'] ?? null;
+			} elseif ($class === DisallowedCallParamWithAnyValue::class) {
+				if (is_numeric($value)) {
+					$paramPosition = $value;
+					$paramName = null;
+				} else {
+					$paramPosition = null;
+					$paramName = $value;
+				}
+				$paramValue = null;
+			} else {
+				$paramPosition = $key;
+				$paramName = null;
+				$paramValue = $value;
+			}
+		} else {
+			$paramPosition = null;
+			$paramName = $key;
+			$paramValue = $value;
+		}
+
+		return new $class($paramPosition, $paramName, $paramValue);
+	}
+
 }
diff --git a/src/DisallowedHelper.php b/src/DisallowedHelper.php
index d0938ad..26e1f6d 100644
--- a/src/DisallowedHelper.php
+++ b/src/DisallowedHelper.php
@@ -97,7 +97,7 @@ class DisallowedHelper
 	/**
 	 * @param Scope $scope
 	 * @param CallLike|null $node
-	 * @param array<int, DisallowedCallParam> $allowConfig
+	 * @param array<int|string, DisallowedCallParam> $allowConfig
 	 * @param bool $paramsRequired
 	 * @return bool
 	 */
@@ -107,12 +107,12 @@ class DisallowedHelper
 			return true;
 		}
 
-		foreach ($allowConfig as $param => $value) {
+		foreach ($allowConfig as $param) {
 			$type = $this->getArgType($node, $scope, $param);
 			if (!$type instanceof ConstantScalarType) {
 				return !$paramsRequired;
 			}
-			if (!$value->matches($type)) {
+			if (!$param->matches($type)) {
 				return false;
 			}
 		}
@@ -120,10 +120,18 @@ class DisallowedHelper
 	}
 
 
-	private function getArgType(CallLike $node, Scope $scope, int $param): ?Type
+	private function getArgType(CallLike $node, Scope $scope, DisallowedCallParam $param): ?Type
 	{
-		$arg = $node->getArgs()[$param - 1] ?? null;
-		return $arg ? $scope->getType($arg->value) : null;
+		foreach ($node->getArgs() as $arg) {
+			if ($arg->name && $arg->name->name === $param->getName()) {
+				$found = $arg;
+				break;
+			}
+		}
+		if (!isset($found)) {
+			$found = $node->getArgs()[$param->getPosition() - 1] ?? null;
+		}
+		return isset($found) ? $scope->getType($found->value) : null;
 	}
 
 
diff --git a/src/Params/DisallowedCallParam.php b/src/Params/DisallowedCallParam.php
index 3817b60..f03685c 100644
--- a/src/Params/DisallowedCallParam.php
+++ b/src/Params/DisallowedCallParam.php
@@ -5,9 +5,53 @@ namespace Spaze\PHPStan\Rules\Disallowed\Params;
 
 use PHPStan\Type\ConstantScalarType;
 
-interface DisallowedCallParam
+abstract class DisallowedCallParam
 {
 
-	public function matches(ConstantScalarType $type): bool;
+	/** @var ?int */
+	private $position;
+
+	/** @var ?string */
+	private $name;
+
+	/** @var int|bool|string|null */
+	private $value;
+
+
+	abstract public function matches(ConstantScalarType $type): bool;
+
+
+	/**
+	 * @param int|null $position
+	 * @param string|null $name
+	 * @param int|bool|string|null $value
+	 */
+	final public function __construct(?int $position, ?string $name, $value)
+	{
+		$this->position = $position;
+		$this->name = $name;
+		$this->value = $value;
+	}
+
+
+	public function getPosition(): ?int
+	{
+		return $this->position;
+	}
+
+
+	public function getName(): ?string
+	{
+		return $this->name;
+	}
+
+
+	/**
+	 * @return bool|int|string|null
+	 */
+	public function getValue()
+	{
+		return $this->value;
+	}
 
 }
diff --git a/src/Params/DisallowedCallParamExceptCaseInsensitiveValue.php b/src/Params/DisallowedCallParamExceptCaseInsensitiveValue.php
index 89e3f1b..254323d 100644
--- a/src/Params/DisallowedCallParamExceptCaseInsensitiveValue.php
+++ b/src/Params/DisallowedCallParamExceptCaseInsensitiveValue.php
@@ -6,36 +6,14 @@ namespace Spaze\PHPStan\Rules\Disallowed\Params;
 use PHPStan\Type\Constant\ConstantStringType;
 use PHPStan\Type\ConstantScalarType;
 
-class DisallowedCallParamExceptCaseInsensitiveValue implements DisallowedCallParam
+class DisallowedCallParamExceptCaseInsensitiveValue extends DisallowedCallParam
 {
 
-	/** @var int|bool|string */
-	private $value;
-
-
-	/**
-	 * @param int|bool|string $value
-	 */
-	public function __construct($value)
-	{
-		$this->value = $value;
-	}
-
-
 	public function matches(ConstantScalarType $type): bool
 	{
-		$a = is_string($this->value) ? strtolower($this->value) : $this->value;
+		$a = is_string($this->getValue()) ? strtolower($this->getValue()) : $this->getValue();
 		$b = $type instanceof ConstantStringType ? strtolower($type->getValue()) : $type->getValue();
 		return $a !== $b;
 	}
 
-
-	/**
-	 * @return int|bool|string
-	 */
-	public function getValue()
-	{
-		return $this->value;
-	}
-
 }
diff --git a/src/Params/DisallowedCallParamExceptValue.php b/src/Params/DisallowedCallParamExceptValue.php
index 196d413..a6c3331 100644
--- a/src/Params/DisallowedCallParamExceptValue.php
+++ b/src/Params/DisallowedCallParamExceptValue.php
@@ -5,34 +5,12 @@ namespace Spaze\PHPStan\Rules\Disallowed\Params;
 
 use PHPStan\Type\ConstantScalarType;
 
-class DisallowedCallParamExceptValue implements DisallowedCallParam
+class DisallowedCallParamExceptValue extends DisallowedCallParam
 {
 
-	/** @var int|bool|string */
-	private $value;
-
-
-	/**
-	 * @param int|bool|string $value
-	 */
-	public function __construct($value)
-	{
-		$this->value = $value;
-	}
-
-
 	public function matches(ConstantScalarType $type): bool
 	{
-		return $this->value !== $type->getValue();
-	}
-
-
-	/**
-	 * @return int|bool|string
-	 */
-	public function getValue()
-	{
-		return $this->value;
+		return $this->getValue() !== $type->getValue();
 	}
 
 }
diff --git a/src/Params/DisallowedCallParamWithAnyValue.php b/src/Params/DisallowedCallParamWithAnyValue.php
index b358024..5f4fd83 100644
--- a/src/Params/DisallowedCallParamWithAnyValue.php
+++ b/src/Params/DisallowedCallParamWithAnyValue.php
@@ -5,7 +5,7 @@ namespace Spaze\PHPStan\Rules\Disallowed\Params;
 
 use PHPStan\Type\ConstantScalarType;
 
-class DisallowedCallParamWithAnyValue implements DisallowedCallParam
+class DisallowedCallParamWithAnyValue extends DisallowedCallParam
 {
 
 	public function matches(ConstantScalarType $type): bool
diff --git a/src/Params/DisallowedCallParamWithValue.php b/src/Params/DisallowedCallParamWithValue.php
index c7c20ff..352367f 100644
--- a/src/Params/DisallowedCallParamWithValue.php
+++ b/src/Params/DisallowedCallParamWithValue.php
@@ -5,34 +5,12 @@ namespace Spaze\PHPStan\Rules\Disallowed\Params;
 
 use PHPStan\Type\ConstantScalarType;
 
-class DisallowedCallParamWithValue implements DisallowedCallParam
+class DisallowedCallParamWithValue extends DisallowedCallParam
 {
 
-	/** @var int|bool|string */
-	private $value;
-
-
-	/**
-	 * @param int|bool|string $value
-	 */
-	public function __construct($value)
-	{
-		$this->value = $value;
-	}
-
-
 	public function matches(ConstantScalarType $type): bool
 	{
-		return $this->value === $type->getValue();
-	}
-
-
-	/**
-	 * @return int|bool|string
-	 */
-	public function getValue()
-	{
-		return $this->value;
+		return $this->getValue() === $type->getValue();
 	}
 
 }


Test patch:
diff --git a/tests/Calls/FunctionCallsNamedParamsTest.php b/tests/Calls/FunctionCallsNamedParamsTest.php
new file mode 100644
index 0000000..05057e5
--- /dev/null
+++ b/tests/Calls/FunctionCallsNamedParamsTest.php
@@ -0,0 +1,197 @@
+<?php
+declare(strict_types = 1);
+
+namespace Spaze\PHPStan\Rules\Disallowed\Calls;
+
+use PHPStan\File\FileHelper;
+use PHPStan\Rules\Rule;
+use PHPStan\Testing\RuleTestCase;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
+use Spaze\PHPStan\Rules\Disallowed\DisallowedHelper;
+use Spaze\PHPStan\Rules\Disallowed\IsAllowedFileHelper;
+
+/**
+ * @requires PHP > 8.0
+ */
+class FunctionCallsNamedParamsTest extends RuleTestCase
+{
+
+	protected function getRule(): Rule
+	{
+		return new FunctionCalls(
+			new DisallowedHelper(new IsAllowedFileHelper(new FileHelper(__DIR__))),
+			new DisallowedCallFactory(),
+			[
+				[
+					'function' => 'Foo\Bar\Waldo\foo()',
+					'allowIn' => [
+						'../src/disallowed-allowed/*.php',
+						'../src/*-allow/*.*',
+					],
+					'allowParamsAnywhereAnyValue' => [
+						[
+							'position' => 2,
+							'name' => 'value',
+						],
+					],
+					'allowParamsInAllowedAnyValue' => [
+						[
+							'position' => 4,
+							'name' => 'path',
+						],
+					],
+				],
+				[
+					'function' => 'Foo\Bar\Waldo\bar()',
+					'allowIn' => [
+						'../src/disallowed-allowed/*.php',
+						'../src/*-allow/*.*',
+					],
+					'allowParamsAnywhereAnyValue' => [
+						1,
+						'name',
+						2,
+						'path',
+					],
+				],
+				[
+					'function' => 'Foo\Bar\Waldo\baz()',
+					'allowIn' => [
+						'../src/disallowed-allowed/*.php',
+						'../src/*-allow/*.*',
+					],
+					'allowParamsInAllowed' => [
+						2 => 'VALUE',
+					],
+				],
+				[
+					'function' => 'Foo\Bar\Waldo\waldo()',
+					'allowIn' => [
+						'../src/disallowed-allowed/*.php',
+						'../src/*-allow/*.*',
+					],
+					'allowParamsInAllowed' => [
+						'value' => 'VALUE',
+					],
+				],
+			]
+		);
+	}
+
+
+	public function testRule(): void
+	{
+		// Based on the configuration above, in this file:
+		$this->analyse([__DIR__ . '/../src/disallowed/functionCallsNamedParams.php'], [
+			[
+				// expect this error message:
+				'Calling Foo\Bar\Waldo\foo() (as foo()) is forbidden, because reasons',
+				// on this line:
+				12,
+			],
+			[
+				// no required param
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				19,
+			],
+			[
+				// missing $name + second param + $path
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				20,
+			],
+			[
+				// missing $name + second param + $path
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				21,
+			],
+			[
+				// missing $name + $path
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				22,
+			],
+			[
+				// missing $name + $path
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				23,
+			],
+			[
+				// missing $name
+				'Calling Foo\Bar\Waldo\bar() (as bar()) is forbidden, because reasons',
+				24,
+			],
+			[
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				29,
+			],
+			[
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				30,
+			],
+			[
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				31,
+			],
+			[
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				32,
+			],
+			[
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				35,
+			],
+			[
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				36,
+			],
+			[
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				37,
+			],
+			[
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				38,
+			],
+		]);
+		// Based on the configuration above, no errors in this file:
+		$this->analyse([__DIR__ . '/../src/disallowed-allow/functionCallsNamedParams.php'], [
+			[
+				'Calling Foo\Bar\Waldo\foo() (as foo()) is forbidden, because reasons',
+				12,
+			],
+			[
+				'Calling Foo\Bar\Waldo\foo() (as foo()) is forbidden, because reasons',
+				13,
+			],
+			[
+				'Calling Foo\Bar\Waldo\foo() (as foo()) is forbidden, because reasons',
+				14,
+			],
+			[
+				// second param not 'VALUE'
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				29,
+			],
+			[
+				// second param not 'VALUE'
+				'Calling Foo\Bar\Waldo\baz() (as baz()) is forbidden, because reasons',
+				31,
+			],
+			[
+				// not $value param
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				35,
+			],
+			[
+				// not $value param
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				36,
+			],
+			[
+				// $value is not 'VALUE'
+				'Calling Foo\Bar\Waldo\waldo() (as waldo()) is forbidden, because reasons',
+				37,
+			],
+		]);
+	}
+
+}
diff --git a/tests/libs/Functions.php b/tests/libs/Functions.php
index ed650e7..b1cc2d2 100644
--- a/tests/libs/Functions.php
+++ b/tests/libs/Functions.php
@@ -14,3 +14,23 @@ function fred(): void
 {
 	$foo = sha1('foo');
 }
+
+
+function foo(string $name, string $value = '', int $expires = 0, string $path = ''): bool
+{
+}
+
+
+function bar(string $name = '', string $value = '', int $expires = 0, string $path = ''): bool
+{
+}
+
+
+function baz(string $name = '', string $value = '', int $expires = 0, string $path = ''): bool
+{
+}
+
+
+function waldo(string $name = '', string $value = '', int $expires = 0, string $path = ''): bool
+{
+}
diff --git a/tests/src/disallowed-allow/functionCallsNamedParams.php b/tests/src/disallowed-allow/functionCallsNamedParams.php
new file mode 100644
index 0000000..d396dea
--- /dev/null
+++ b/tests/src/disallowed-allow/functionCallsNamedParams.php
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+
+namespace Whatever;
+
+use function Foo\Bar\Waldo\bar;
+use function Foo\Bar\Waldo\baz;
+use function Foo\Bar\Waldo\foo;
+use function Foo\Bar\Waldo\waldo;
+
+// fourth/$path param needed
+foo('foo');
+foo('foo', 'bar');
+foo('foo', value: 'bar');
+foo('foo', 'bar', 0, '/');
+foo('foo', 'bar', path: '/');
+
+// all allowed, no allowParamsInAllowedAnyValue rules
+bar();
+bar('name');
+bar(name: 'name');
+bar('name', 'value');
+bar('name', 'value', 123, 'path');
+bar('name', 'value', path: 'path');
+bar(name: 'name', value: 'value', path: 'path');
+bar(path: 'path', name: 'name', value: 'value');
+
+// allowed function if second param = VALUE
+baz('name', 'value');
+baz('name', 'VALUE');
+baz('name', value: 'value');
+baz('name', value: 'VALUE');
+
+// allowed function if $value param = VALUE
+waldo('name', 'value');
+waldo('name', 'VALUE');
+waldo('name', value: 'value');
+waldo('name', value: 'VALUE');
diff --git a/tests/src/disallowed/functionCallsNamedParams.php b/tests/src/disallowed/functionCallsNamedParams.php
new file mode 100644
index 0000000..b0a90e8
--- /dev/null
+++ b/tests/src/disallowed/functionCallsNamedParams.php
@@ -0,0 +1,38 @@
+<?php
+declare(strict_types = 1);
+
+namespace Whatever;
+
+use function Foo\Bar\Waldo\bar;
+use function Foo\Bar\Waldo\baz;
+use function Foo\Bar\Waldo\foo;
+use function Foo\Bar\Waldo\waldo;
+
+// second/$value param needed
+foo('foo');
+foo('foo', 'bar');
+foo('foo', value: 'bar');
+foo('foo', 'bar', 0, '/');
+foo('foo', 'bar', path: '/');
+
+// first param/$name + second param + $path needed
+bar();
+bar('name');
+bar(name: 'name');
+bar('name', 'value');
+bar('name', 'value', 123, 'path');
+bar('name', 'value', path: 'path');
+bar(name: 'name', value: 'value', path: 'path');
+bar(path: 'path', name: 'name', value: 'value');
+
+// disallowed function
+baz('name', 'value');
+baz('name', 'VALUE');
+baz('name', value: 'value');
+baz('name', value: 'VALUE');
+
+// disallowed function
+waldo('name', 'value');
+waldo('name', 'VALUE');
+waldo('name', value: 'value');
+waldo('name', value: 'VALUE');