Skip to content

Commit

Permalink
Normalize leading backslashes in banned function names (ekino#30)
Browse files Browse the repository at this point in the history
* Normalize leading slashed in function names

* Fix type hint

* Fix codestyle

* Fix types

* changelog
  • Loading branch information
spawnia authored Oct 11, 2023
1 parent ac7406a commit 65a5752
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ master
* Added rule to ban print statements
* Allow Composer plugin ergebnis/composer-normalize
* Add Composer keyword for asking user to add this package to require-dev instead of require
* Normalize leading backslashes in banned function names

v1.0.0
------
Expand Down
32 changes: 28 additions & 4 deletions src/Rules/BannedNodesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@
class BannedNodesRule implements Rule
{
/**
* @var array<mixed>
* @var array<string, mixed>
*/
private $bannedNodes;

/**
* @param array<mixed> $bannedNodes
* @var array<string>
*/
private $bannedFunctions;

/**
* @param array<array{type: string, functions?: array<string>}> $bannedNodes
*/
public function __construct(array $bannedNodes)
{
$this->bannedNodes = array_column($bannedNodes, null, 'type');
$this->bannedNodes = array_column($bannedNodes, null, 'type');
$this->bannedFunctions = $this->normalizeFunctionNames($this->bannedNodes);
}

/**
Expand Down Expand Up @@ -64,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array

$function = $node->name->toString();

if (\in_array($function, $this->bannedNodes[$type]['functions'])) {
if (\in_array($function, $this->bannedFunctions)) {
return [sprintf('Should not use function "%s", please change the code.', $function)];
}

Expand All @@ -73,4 +79,22 @@ public function processNode(Node $node, Scope $scope): array

return [sprintf('Should not use node with type "%s", please change the code.', $type)];
}

/**
* Strip leading slashes from function names.
*
* php-parser makes the same normalization.
*
* @param array<string, mixed> $bannedNodes
* @return array<string>
*/
protected function normalizeFunctionNames(array $bannedNodes): array
{
return array_map(
static function (string $function): string {
return ltrim($function, '\\');
},
$bannedNodes['Expr_FuncCall']['functions'] ?? []
);
}
}
71 changes: 55 additions & 16 deletions tests/Rules/BannedNodesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PhpParser\Node\Expr\ShellExec;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\LNumber;
use PHPStan\Analyser\Scope;
use PHPUnit\Framework\MockObject\MockObject;
Expand Down Expand Up @@ -53,7 +54,7 @@ protected function setUp(): void
['type' => 'Stmt_Echo'],
['type' => 'Expr_Eval'],
['type' => 'Expr_Exit'],
['type' => 'Expr_FuncCall', 'functions' => ['debug_backtrace', 'dump']],
['type' => 'Expr_FuncCall', 'functions' => ['debug_backtrace', 'dump', 'Safe\namespaced']],
['type' => 'Expr_Print'],
['type' => 'Expr_ShellExec'],
]);
Expand All @@ -80,35 +81,73 @@ public function testProcessNodeWithUnhandledType(Expr $node): void
$this->assertCount(0, $this->rule->processNode($node, $this->scope));
}

/**
* Tests processNode with banned/allowed functions.
*/
public function testProcessNodeWithFunctions(): void
public function testProcessNodeWithBannedFunctions(): void
{
$ruleWithoutLeadingSlashes = new BannedNodesRule([
[
'type' => 'Expr_FuncCall',
'functions' => [
'root',
'Safe\namespaced',
]
],
]);

$ruleWithLeadingSlashes = new BannedNodesRule([
[
'type' => 'Expr_FuncCall',
'functions' => [
'\root',
'\Safe\namespaced',
]
],
]);

$rootFunction = new FuncCall(new Name('root'));
$this->assertNodeTriggersError($ruleWithoutLeadingSlashes, $rootFunction);
$this->assertNodeTriggersError($ruleWithLeadingSlashes, $rootFunction);

$namespacedFunction = new FuncCall(new FullyQualified('Safe\namespaced'));
$this->assertNodeTriggersError($ruleWithoutLeadingSlashes, $namespacedFunction);
$this->assertNodeTriggersError($ruleWithLeadingSlashes, $namespacedFunction);
}

protected function assertNodeTriggersError(BannedNodesRule $rule, Node $node): void
{
foreach (['debug_backtrace', 'dump'] as $bannedFunction) {
$node = new FuncCall(new Name($bannedFunction));
$this->assertCount(1, $rule->processNode($node, $this->scope));
}

$this->assertCount(1, $this->rule->processNode($node, $this->scope));
}
protected function assertNodePasses(BannedNodesRule $rule, Node $node): void
{
$this->assertCount(0, $rule->processNode($node, $this->scope));
}

foreach (['array_search', 'sprintf'] as $allowedFunction) {
$node = new FuncCall(new Name($allowedFunction));
public function testProcessNodeWithAllowedFunctions(): void
{
$rootFunction = new FuncCall(new Name('allowed'));
$this->assertNodePasses($this->rule, $rootFunction);

$this->assertCount(0, $this->rule->processNode($node, $this->scope));
}
$namespacedFunction = new FuncCall(new FullyQualified('Safe\allowed'));
$this->assertNodePasses($this->rule, $namespacedFunction);
}

public function testProcessNodeWithFunctionInClosure(): void
{
$node = new FuncCall(new Variable('myClosure'));

$this->assertCount(0, $this->rule->processNode($node, $this->scope));
$this->assertNodePasses($this->rule, $node);
}

public function testProcessNodeWithArrayDimFetch(): void
{
$node = new FuncCall(
new Expr\ArrayDimFetch(
new Variable('myArray'),
LNumber::fromString('0', ['kind' => LNumber::KIND_DEC])
)
);

$this->assertCount(0, $this->rule->processNode($node, $this->scope));
$this->assertNodePasses($this->rule, $node);
}

/**
Expand All @@ -120,7 +159,7 @@ public function testProcessNodeWithFunctions(): void
*/
public function testProcessNodeWithHandledTypes(Expr $node): void
{
$this->assertCount(1, $this->rule->processNode($node, $this->scope));
$this->assertNodeTriggersError($this->rule, $node);
}

/**
Expand Down

0 comments on commit 65a5752

Please sign in to comment.