Skip to content

Commit

Permalink
Can allow calls with specified params only, either anywhere, or in al…
Browse files Browse the repository at this point in the history
…lowedIn paths

Fix #1
  • Loading branch information
spaze committed Aug 25, 2020
1 parent c6cb5c3 commit d4ec1ca
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 12 deletions.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The `message` key is optional. Don't forget to add the `DisallowedHelper` servic
------ --------------------------------------------------------
```

## Ignore some calls
## Allow some previously disallowed calls

Sometimes, the method or the function needs to be called once in your code, for example in a custom wrapper. You can use PHPStan's [`ignoreErrors` feature](https://github.com/phpstan/phpstan#ignore-error-messages-with-regular-expressions) to ignore that one call:

Expand Down Expand Up @@ -123,6 +123,30 @@ services:

The paths in `allowIn` are relative to the config file location and support [fnmatch()](https://www.php.net/function.fnmatch) patterns.

You can also narrow down the allowed items when called with some parameters. 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:

```
arguments:
forbiddenCalls:
-
method: 'Tracy\ILogger::log()'
message: 'use our own logger instead'
allowIn:
- optional/path/to/*.tests.php
- another/file.php
allowParamsInAllowed:
1: 'foo'
2: true
allowParamsAnywhere:
2: 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`

## Running tests

If you want to contribute (awesome, thanks!), you should add/run tests for your contributions.
Expand Down
53 changes: 50 additions & 3 deletions src/DisallowedHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace Spaze\PHPStan\Rules\Disallowed;

use PhpParser\Node\Arg;
use PhpParser\Node\Scalar;
use PHPStan\File\FileHelper;

class DisallowedHelper
Expand All @@ -20,17 +22,62 @@ public function __construct(FileHelper $fileHelper)

/**
* @param string $file
* @param Arg[] $args
* @param string[] $config
* @return boolean
*/
public function isAllowed(string $file, array $config): bool
public function isAllowed(string $file, array $args, array $config): bool
{
foreach (($config['allowIn'] ?? []) as $allowedPath) {
if (fnmatch($this->fileHelper->absolutizePath($allowedPath), $file)) {
if (fnmatch($this->fileHelper->absolutizePath($allowedPath), $file) && $this->hasAllowedParams($config, $args, 'allowParamsInAllowed', true)) {
return true;
}
}
return false;
return $this->hasAllowedParams($config, $args, 'allowParamsAnywhere', false);
}


/**
* @param string[] $config
* @param Arg[] $args
* @param string $configKey
* @param boolean $default
* @return boolean
*/
private function hasAllowedParams(array $config, array $args, string $configKey, bool $default): bool
{
if (isset($config[$configKey]) && is_array($config[$configKey])) {
$disallowed = false;
foreach ($config[$configKey] as $param => $value) {
if (isset($args[$param - 1])) {
$arg = $args[$param - 1];
$disallowed = $disallowed || $this->isDisallowedParam($value, $arg);
} else {
$disallowed = true;
}
}
if (count($config[$configKey]) > 0) {
return !$disallowed;
}
}
return $default;
}


/**
* @param mixed $value
* @param Arg $arg
* @return boolean
*/
private function isDisallowedParam($value, Arg $arg): bool
{
// 2nd param in print_r(..., true) is returned as Node\Expr\ConstFetch by the parser and I can't find a way to
// get it as a bool, only as a string. So to support booleans in the .neon config file we have to convert to a string manually.
if (is_bool($value)) {
return $this->isDisallowedParam($value ? 'true' : 'false', $arg);
} else {
return ($value !== ($arg->value instanceof Scalar ? $arg->value->value : (string)$arg->value->name));
}
}

}
7 changes: 6 additions & 1 deletion src/FunctionCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
* allowIn:
* - optional/path/to/*.tests.php
* - another/file.php
* allowParamsInAllowed:
* 1: 'foo'
* 2: true
* allowParamsAnywhere:
* 2: true
* -
* function: 'Foo\Bar\baz()'
* message: 'waldo instead'
Expand Down Expand Up @@ -65,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array

$name = $node->name . '()';
foreach ($this->forbiddenCalls as $forbiddenCall) {
if ($name === $forbiddenCall['function'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $forbiddenCall)) {
if ($name === $forbiddenCall['function'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $node->args, $forbiddenCall)) {
return [
sprintf('Calling %s is forbidden, %s', $name, $forbiddenCall['message'] ?? 'because reasons'),
];
Expand Down
7 changes: 6 additions & 1 deletion src/MethodCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
* allowIn:
* - optional/path/to/*.tests.php
* - another/file.php
* allowParamsInAllowed:
* 1: 'foo'
* 2: true
* allowParamsAnywhere:
* 2: true
* -
* method: 'Foo\Bar::baz()'
* message: 'waldo instead'
Expand Down Expand Up @@ -86,7 +91,7 @@ static function (Type $type) use ($name): bool {
foreach ($typeResult->getReferencedClasses() as $referencedClass) {
$fullyQualified = current($typeResult->getReferencedClasses()) . "::{$name}()";
foreach ($this->forbiddenCalls as $forbiddenCall) {
if ($fullyQualified === $forbiddenCall['method'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $forbiddenCall)) {
if ($fullyQualified === $forbiddenCall['method'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $node->args, $forbiddenCall)) {
return [
sprintf('Calling %s is forbidden, %s', $fullyQualified, $forbiddenCall['message'] ?? 'because reasons'),
];
Expand Down
7 changes: 6 additions & 1 deletion src/StaticCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
* allowIn:
* - optional/path/to/*.tests.php
* - another/file.php
* allowParamsInAllowed:
* 1: 'foo'
* 2: true
* allowParamsAnywhere:
* 2: true
* -
* method: 'Foo\Bar::baz()'
* message: 'waldo instead'
Expand Down Expand Up @@ -69,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array
$name = $node->name->name;
$fullyQualified = "{$node->class}::{$name}()";
foreach ($this->forbiddenCalls as $forbiddenCall) {
if ($fullyQualified === $forbiddenCall['method'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $forbiddenCall)) {
if ($fullyQualified === $forbiddenCall['method'] && !$this->disallowedHelper->isAllowed($scope->getFile(), $node->args, $forbiddenCall)) {
return [
sprintf('Calling %s is forbidden, %s', $fullyQualified, $forbiddenCall['message'] ?? 'because reasons'),
];
Expand Down
7 changes: 7 additions & 0 deletions tests/FunctionCallsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ protected function getRule(): Rule
'data/*-allowed.php',
'data/*-allowed.*',
],
'allowParamsAnywhere' => [
2 => true,
]
],
[
'function' => 'printf()',
Expand Down Expand Up @@ -74,6 +77,10 @@ public function testRule(): void
'Calling Foo\Bar\waldo() is forbidden, whoa, a namespace',
11,
],
[
'Calling print_r() is forbidden, nope',
35,
],
]);
$this->analyse([__DIR__ . '/data/disallowed-calls-allowed.php'], []);
}
Expand Down
26 changes: 24 additions & 2 deletions tests/MethodCallsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ protected function getRule(): Rule
'data/*-allowed.php',
'data/*-allowed.*',
],
'allowParamsInAllowed' => [
1 => 42,
2 => true,
3 => '909',
],
],
]
);
Expand All @@ -34,10 +39,27 @@ public function testRule(): void
$this->analyse([__DIR__ . '/data/disallowed-calls.php'], [
[
"Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe",
25,
28,
],
[
"Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe",
30,
],
[
"Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe",
31,
],
]);
$this->analyse([__DIR__ . '/data/disallowed-calls-allowed.php'], [
[
"Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe",
29,
],
[
"Calling Waldo\Quux\Blade::runner() is forbidden, I've seen tests you people wouldn't believe",
32,
],
]);
$this->analyse([__DIR__ . '/data/disallowed-calls-allowed.php'], []);
}

}
36 changes: 36 additions & 0 deletions tests/StaticCallsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,34 @@ protected function getRule(): Rule
'data/*-allowed.php',
'data/*-allowed.*',
],
'allowParamsInAllowed' => [],
],
[
'method' => 'Fiction\Pulp\Royale::withBadCheese()',
'message' => 'a Quarter Pounder with Cheese?',
'allowIn' => [
'data/*-allowed.php',
'data/*-allowed.*',
],
'allowParamsInAllowed' => '',
],
[
'method' => 'Fiction\Pulp\Royale::withoutCheese()',
'message' => 'a Quarter Pounder without Cheese?',
'allowIn' => [
'data/*-allowed.php',
'data/*-allowed.*',
],
'allowParamsInAllowed' => [
1 => 1,
2 => 2,
3 => 3,
],
'allowParamsAnywhere' => [
1 => 1,
2 => 2,
3 => 4,
],
],
]
);
Expand All @@ -39,6 +67,14 @@ public function testRule(): void
'Calling Fiction\Pulp\Royale::withCheese() is forbidden, a Quarter Pounder with Cheese?',
18,
],
[
'Calling Fiction\Pulp\Royale::withBadCheese() is forbidden, a Quarter Pounder with Cheese?',
20,
],
[
'Calling Fiction\Pulp\Royale::withoutCheese() is forbidden, a Quarter Pounder without Cheese?',
21,
],
]);
$this->analyse([__DIR__ . '/data/disallowed-calls-allowed.php'], []);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/data/Blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
class Blade
{

public function runner(): void
public function runner(int $everything = 0, bool $yes = false, string $roland = '303'): void
{
}

Expand Down
11 changes: 10 additions & 1 deletion tests/data/disallowed-calls-allowed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use function Foo\Bar\waldo;

var_dump('foo');
var_dump('foo', true);
print_r('bar');
\printf('foobar');
var_export('not disallowed');
Expand All @@ -18,10 +18,19 @@

Pulp\Royale::withCheese();
Pulp\Royale::leBigMac();
Pulp\Royale::withBadCheese();
Pulp\Royale::withoutCheese(1, 2, 3);
Pulp\Royale::withoutCheese(1, 2, 4);


use Waldo\Quux;

$blade = new Quux\Blade();
$blade->runner();
$blade->server();
$blade->runner(42, true, '909');
$blade->runner(42, true, '808');

print_r('bar bar', true);
print_r('bar bar baz', true, 303);
print_r('bar bar was', false);
11 changes: 10 additions & 1 deletion tests/data/disallowed-calls.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use function Foo\Bar\waldo;

var_dump('foo');
var_dump('foo', true);
print_r('bar');
\printf('foobar');
var_export('not disallowed');
Expand All @@ -17,10 +17,19 @@

Pulp\Royale::withCheese();
Pulp\Royale::leBigMac();
Pulp\Royale::withBadCheese();
Pulp\Royale::withoutCheese(1, 2, 3);
Pulp\Royale::withoutCheese(1, 2, 4);


use Waldo\Quux;

$blade = new Quux\Blade();
$blade->runner();
$blade->server();
$blade->runner(42, true, '909');
$blade->runner(42, true, '808');

print_r('bar bar', true);
print_r('bar bar baz', true, 303);
print_r('bar bar was', false);

0 comments on commit d4ec1ca

Please sign in to comment.