diff --git a/docs/allow-with-parameters.md b/docs/allow-with-parameters.md index c20ef82..eab4c4c 100644 --- a/docs/allow-with-parameters.md +++ b/docs/allow-with-parameters.md @@ -96,6 +96,22 @@ parameters: ``` will disallow `foo('bar', 'baz')` but not `foo('bar', 'BAZ')`. +If you don't care about the value but would like to disallow a call based just on the parameter presence, you can use `allowExceptParamsAnyValue` (or `disallowParamsAnyValue`): +```neon +parameters: + disallowedFunctionCalls: + - + function: 'waldo()' + disallowParamsAnyValue: + - + position: 1 + name: 'message' + - + position: 2 + name: 'alert' +``` +This configuration will disallow calls like `waldo('foo', 'bar')` or `waldo('*', '*')`, but `waldo('foo')` or `waldo()` will be still allowed. + It's also possible to disallow functions and methods previously allowed by path (using `allowIn`) or by function/method name (`allowInMethods`) when they're called with specified parameters, and allow when called with any other parameter. This is done using the `allowExceptParamsInAllowed` config option. Take this example configuration: diff --git a/extension.neon b/extension.neon index 4f3a5ec..1cfcb31 100644 --- a/extension.neon +++ b/extension.neon @@ -67,6 +67,8 @@ parametersSchema: ?disallowParamsInAllowed: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?allowExceptParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?disallowParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), + ?allowExceptParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), + ?disallowParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?disallowParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptCaseInsensitiveParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), @@ -103,6 +105,8 @@ parametersSchema: ?disallowParamsInAllowed: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?allowExceptParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?disallowParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), + ?allowExceptParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), + ?disallowParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?disallowParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptCaseInsensitiveParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), @@ -139,6 +143,8 @@ parametersSchema: ?disallowParamsInAllowed: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?allowExceptParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?disallowParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), + ?allowExceptParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), + ?disallowParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?disallowParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptCaseInsensitiveParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), @@ -196,6 +202,8 @@ parametersSchema: ?disallowParamsInAllowed: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?allowExceptParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), ?disallowParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), + ?allowExceptParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), + ?disallowParamsAnyValue: arrayOf(anyOf(int(), structure([position: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?disallowParamFlags: arrayOf(anyOf(int(), structure([position: int(), ?value: int(), ?name: string()])), anyOf(int(), string())), ?allowExceptCaseInsensitiveParams: arrayOf(anyOf(int(), string(), bool(), structure([position: int(), ?value: anyOf(int(), string(), bool()), ?name: string()])), anyOf(int(), string())), diff --git a/phpstan.neon b/phpstan.neon index 93a1589..fe1052d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,7 +6,7 @@ parameters: CallParamConfig: 'array' CallParamAnyValueConfig: 'array' CallParamFlagAnyValueConfig: 'array' - AllowDirectives: 'allowIn?:list, allowExceptIn?:list, disallowIn?:list, allowInFunctions?:list, allowInMethods?:list, allowExceptInFunctions?:list, allowExceptInMethods?:list, disallowInFunctions?:list, disallowInMethods?:list, allowParamsInAllowed?:CallParamConfig, allowParamsInAllowedAnyValue?:CallParamAnyValueConfig, allowParamFlagsInAllowed?:CallParamFlagAnyValueConfig, allowParamsAnywhere?:CallParamConfig, allowParamsAnywhereAnyValue?:CallParamAnyValueConfig, allowParamFlagsAnywhere?:CallParamFlagAnyValueConfig, allowExceptParamsInAllowed?:CallParamConfig, allowExceptParamFlagsInAllowed?:CallParamFlagAnyValueConfig, disallowParamFlagsInAllowed?:CallParamFlagAnyValueConfig, disallowParamsInAllowed?:CallParamConfig, allowExceptParams?:CallParamConfig, disallowParams?:CallParamConfig, allowExceptParamFlags?:CallParamFlagAnyValueConfig, disallowParamFlags?:CallParamFlagAnyValueConfig, allowExceptCaseInsensitiveParams?:CallParamConfig, disallowCaseInsensitiveParams?:CallParamConfig' + AllowDirectives: 'allowIn?:list, allowExceptIn?:list, disallowIn?:list, allowInFunctions?:list, allowInMethods?:list, allowExceptInFunctions?:list, allowExceptInMethods?:list, disallowInFunctions?:list, disallowInMethods?:list, allowParamsInAllowed?:CallParamConfig, allowParamsInAllowedAnyValue?:CallParamAnyValueConfig, allowParamFlagsInAllowed?:CallParamFlagAnyValueConfig, allowParamsAnywhere?:CallParamConfig, allowParamsAnywhereAnyValue?:CallParamAnyValueConfig, allowParamFlagsAnywhere?:CallParamFlagAnyValueConfig, allowExceptParamsInAllowed?:CallParamConfig, allowExceptParamFlagsInAllowed?:CallParamFlagAnyValueConfig, disallowParamFlagsInAllowed?:CallParamFlagAnyValueConfig, disallowParamsInAllowed?:CallParamConfig, allowExceptParams?:CallParamConfig, disallowParams?:CallParamConfig, allowExceptParamsAnyValue?:CallParamAnyValueConfig, disallowParamsAnyValue?:CallParamAnyValueConfig, allowExceptParamFlags?:CallParamFlagAnyValueConfig, disallowParamFlags?:CallParamFlagAnyValueConfig, allowExceptCaseInsensitiveParams?:CallParamConfig, disallowCaseInsensitiveParams?:CallParamConfig' ForbiddenCallsConfig: 'array, method?:string|list, exclude?:string|list, definedIn?:string|list, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>' DisallowedAttributesConfig: 'array, exclude?:string|list, message?:string, %typeAliases.AllowDirectives%, errorIdentifier?:string, errorTip?:string}>' AllowDirectivesConfig: 'array{%typeAliases.AllowDirectives%}' diff --git a/src/Allowed/Allowed.php b/src/Allowed/Allowed.php index ca7563e..d5fc886 100644 --- a/src/Allowed/Allowed.php +++ b/src/Allowed/Allowed.php @@ -19,6 +19,7 @@ use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueAny; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueCaseInsensitiveExcept; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueExcept; +use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueExceptAny; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueFlagExcept; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueFlagSpecific; use Spaze\PHPStan\Rules\Disallowed\Params\ParamValueSpecific; @@ -111,6 +112,7 @@ private function hasAllowedParams(Scope $scope, ?array $args, array $allowConfig return true; } + $disallowedParams = false; foreach ($allowConfig as $param) { $type = $this->getArgType($args, $scope, $param); if ($type === null) { @@ -123,15 +125,13 @@ private function hasAllowedParams(Scope $scope, ?array $args, array $allowConfig } foreach ($types as $type) { try { - if (!$param->matches($type)) { - return false; - } + $disallowedParams = $disallowedParams || !$param->matches($type); } catch (UnsupportedParamTypeException $e) { return !$paramsRequired; } } } - return true; + return !$disallowedParams; } @@ -216,6 +216,9 @@ public function getConfig(array $allowed): AllowedConfig foreach ($allowed['allowExceptParams'] ?? $allowed['disallowParams'] ?? [] as $param => $value) { $allowExceptParams[$param] = $this->paramFactory(ParamValueExcept::class, $param, $value); } + foreach ($allowed['allowExceptParamsAnyValue'] ?? $allowed['disallowParamsAnyValue'] ?? [] as $param => $value) { + $allowExceptParams[$param] = $this->paramFactory(ParamValueExceptAny::class, $param, $value); + } foreach ($allowed['allowExceptParamFlags'] ?? $allowed['disallowParamFlags'] ?? [] as $param => $value) { $allowExceptParams[$param] = $this->paramFactory(ParamValueFlagExcept::class, $param, $value); } @@ -250,7 +253,7 @@ private function paramFactory(string $class, $key, $value): ParamValue $paramPosition = $value['position']; $paramName = $value['name'] ?? null; $paramValue = $value['value'] ?? null; - } elseif ($class === ParamValueAny::class) { + } elseif (in_array($class, [ParamValueAny::class, ParamValueExceptAny::class], true)) { if (is_numeric($value)) { $paramPosition = (int)$value; $paramName = null; diff --git a/src/Params/ParamValueExceptAny.php b/src/Params/ParamValueExceptAny.php new file mode 100644 index 0000000..91b443e --- /dev/null +++ b/src/Params/ParamValueExceptAny.php @@ -0,0 +1,19 @@ + + */ +final class ParamValueExceptAny extends ParamValue +{ + + public function matches(Type $type): bool + { + return false; + } + +} diff --git a/tests/Calls/FunctionCallsParamsMessagesTest.php b/tests/Calls/FunctionCallsParamsMessagesTest.php new file mode 100644 index 0000000..0c1e7d4 --- /dev/null +++ b/tests/Calls/FunctionCallsParamsMessagesTest.php @@ -0,0 +1,136 @@ +getByType(DisallowedCallsRuleErrors::class), + $container->getByType(DisallowedCallFactory::class), + $this->createReflectionProvider(), + [ + [ + 'function' => '\Foo\Bar\Waldo\config()', + 'message' => 'foo & bar', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + 'disallowParamsAnyValue' => [ + 1, + 2, + ], + ], + [ + 'function' => '\Foo\Bar\Waldo\config()', + 'message' => 'foo', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + 'disallowParamsAnyValue' => [ + 1, + ], + ], + [ + 'function' => '\Foo\Bar\Waldo\config()', + 'message' => 'nothing', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + ], + [ + 'function' => '\Foo\Bar\Waldo\bar()', + 'message' => 'foo & bar', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + 'disallowParams' => [ + 1 => 'foo', + 2 => 'bar', + ], + ], + [ + 'function' => '\Foo\Bar\Waldo\bar()', + 'message' => 'foo', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + 'disallowParams' => [ + 1 => 'foo', + ], + ], + [ + 'function' => '\Foo\Bar\Waldo\bar()', + 'message' => 'nothing', + 'allowIn' => [ + __DIR__ . '/../src/disallowed-allow/*.php', + __DIR__ . '/../src/*-allow/*.*', + ], + ], + ] + ); + } + + + public function testRule(): void + { + // Based on the configuration above, in this file: + $this->analyse([__DIR__ . '/../src/disallowed/functionCallsParamsMessages.php'], [ + [ + // expect this error message: + 'Calling Foo\Bar\Waldo\config() is forbidden, nothing.', + // on this line: + 5, + ], + [ + 'Calling Foo\Bar\Waldo\config() is forbidden, foo.', + 6, + ], + [ + 'Calling Foo\Bar\Waldo\config() is forbidden, foo & bar.', + 7, + ], + [ + 'Calling Foo\Bar\Waldo\bar() is forbidden, nothing.', + 8, + ], + [ + 'Calling Foo\Bar\Waldo\bar() is forbidden, foo.', + 9, + ], + [ + 'Calling Foo\Bar\Waldo\bar() is forbidden, foo & bar.', + 10, + ], + ]); + // Based on the configuration above, no errors in this file: + $this->analyse([__DIR__ . '/../src/disallowed-allow/functionCallsParamsMessages.php'], []); + } + + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + ]; + } + +} diff --git a/tests/src/disallowed-allow/functionCallsParamsMessages.php b/tests/src/disallowed-allow/functionCallsParamsMessages.php new file mode 100644 index 0000000..b2cdf7b --- /dev/null +++ b/tests/src/disallowed-allow/functionCallsParamsMessages.php @@ -0,0 +1,10 @@ +