Skip to content

Commit 3eb759a

Browse files
committed
Initial commit
0 parents  commit 3eb759a

20 files changed

+446
-0
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/tests export-ignore
2+
/phpunit.xml export-ignore
3+
/phpstan.neon export-ignore

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.idea/
2+
composer.lock
3+
/vendor/
4+
/.phpunit.result.cache
5+
cghooks.lock
6+

.php-cs-fixer.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
$finder = PhpCsFixer\Finder
5+
::create()
6+
->exclude(__DIR__.'/vendor') // Vendor Verzeichnis
7+
// ausschließen
8+
->in(__DIR__);
9+
10+
$config = new PhpCsFixer\Config();
11+
12+
return $config->setRules([ // Regelsets festlegen
13+
'@PSR12' => true,
14+
])->setFinder($finder);

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# 1.0.0
2+
Initial Release

composer.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "mintware-de/phpstan-namespace-constraints",
3+
"description": "A PHPStan rule for restricting namespace usings to control dependency inheritance.",
4+
"type": "library",
5+
"require": {
6+
"php": "^7.0 || ^8.0",
7+
"phpstan/phpstan": "^0.12.99",
8+
"nikic/php-parser": "^4.13"
9+
},
10+
"license": "MIT",
11+
"autoload": {
12+
"psr-4": {
13+
"MintwareDe\\PhpStanNamespaceConstraints\\": "src/",
14+
"MintwareDe\\Tests\\PhpStanNamespaceConstraints\\": "tests/"
15+
}
16+
},
17+
"authors": [
18+
{
19+
"name": "Julian Finkler",
20+
"email": "[email protected]"
21+
}
22+
],
23+
"minimum-stability": "stable",
24+
"require-dev": {
25+
"phpunit/phpunit": "^9.5",
26+
"friendsofphp/php-cs-fixer": "^3.7",
27+
"brainmaestro/composer-git-hooks": "^2.8"
28+
},
29+
"scripts": {
30+
"post-install-cmd": "cghooks add --ignore-lock; cghooks update",
31+
"post-update-cmd": "cghooks update",
32+
"phpunit": "phpunit",
33+
"php-cs-fixer": "php-cs-fixer --using-cache=no fix",
34+
"php-cs-fixer:dry-run": "php-cs-fixer --using-cache=no --dry-run fix"
35+
},
36+
"extra": {
37+
"hooks": {
38+
"pre-commit": [
39+
"composer run phpunit",
40+
"composer run phpstan",
41+
"composer run php-cs-fixer:dry-run"
42+
]
43+
}
44+
}
45+
}

phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
level: max
3+
paths:
4+
- src
5+
- tests
6+
excludePaths:
7+
- vendor/

phpunit.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4+
bootstrap="tests/autoload.php">
5+
<coverage processUncoveredFiles="true">
6+
<include>
7+
<directory suffix=".php">./src</directory>
8+
</include>
9+
</coverage>
10+
<testsuites>
11+
<testsuite name="UnitTests">
12+
<directory suffix="Test.php">./tests/</directory>
13+
</testsuite>
14+
</testsuites>
15+
</phpunit>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MintwareDe\PhpStanNamespaceConstraints\Rules;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Use_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use ReflectionClass;
13+
use ReflectionFunction;
14+
15+
/**
16+
* @implements Rule<Use_>
17+
*/
18+
class NamespaceRestrictionRule implements Rule
19+
{
20+
public const CONSTRAINT_FROM = 'from';
21+
public const CONSTRAINT_TO = 'to';
22+
23+
/** @var array<array{'from': string|null, "to": string[]}> $constraints */
24+
private $constraints;
25+
26+
/** @var array<string, bool> */
27+
private $builtInChecks = [];
28+
29+
/**
30+
* @param array<array{'from': string|null, "to": string[]}> $constraints
31+
*/
32+
public function __construct(array $constraints = [])
33+
{
34+
$this->constraints = $constraints;
35+
}
36+
37+
public function getNodeType(): string
38+
{
39+
return Use_::class;
40+
}
41+
42+
public function processNode(Node $node, Scope $scope): array
43+
{
44+
if (empty($this->constraints)) {
45+
return [];
46+
}
47+
48+
/** @var array<string, string[]> $constraints */
49+
$constraints = [];
50+
foreach ($this->constraints as $constraint) {
51+
if ($constraint[self::CONSTRAINT_FROM] === null && $scope->getNamespace() !== null) {
52+
continue;
53+
}
54+
$escaped = addcslashes($constraint[self::CONSTRAINT_FROM] ?? '', '\\');
55+
if ($escaped == null) {
56+
$constraints[$constraint[self::CONSTRAINT_FROM]][] = [];
57+
continue;
58+
}
59+
if (preg_match('~^'.$escaped.'$~', $scope->getNamespace() ?? '')) {
60+
if (!isset($constraints[$constraint[self::CONSTRAINT_FROM]])) {
61+
$constraints[$constraint[self::CONSTRAINT_FROM]] = [];
62+
}
63+
array_push($constraints[$constraint[self::CONSTRAINT_FROM]], ...$constraint[self::CONSTRAINT_TO]);
64+
}
65+
}
66+
67+
68+
if (empty($constraints)) {
69+
return [];
70+
}
71+
72+
/** @var Use_ $useNode */
73+
$useNode = $node;
74+
$badUsings = [];
75+
foreach ($useNode->uses as $use) {
76+
$name = (string)$use->name;
77+
78+
$allTargets = [];
79+
foreach ($constraints as $targets) {
80+
array_push($allTargets, ...$targets);
81+
}
82+
83+
/** @var string[] $values */
84+
$values = array_values($allTargets);
85+
86+
$fullPattern = '~^('.implode('|', $values).')$~';
87+
88+
if (!preg_match(addcslashes($fullPattern, '\\'), $name)) {
89+
if (!isset($this->builtInChecks[$name])) {
90+
$reflection = null;
91+
if (class_exists($name)) {
92+
$reflection = new ReflectionClass($name);
93+
} elseif (function_exists($name)) {
94+
$reflection = new ReflectionFunction($name);
95+
}
96+
$this->builtInChecks[$name] = $reflection != null && $reflection->getFileName() === false;
97+
}
98+
$isBuiltIn = $this->builtInChecks[$name];
99+
100+
if (!$isBuiltIn) {
101+
$badUsings[] = $name;
102+
}
103+
}
104+
}
105+
106+
if (!empty($badUsings)) {
107+
return [
108+
RuleErrorBuilder::message(
109+
'Usings to '.implode(
110+
', ',
111+
$badUsings
112+
).' are forbidden from '.($scope->getNamespace() ?? $scope->getFile())
113+
)->build(),
114+
];
115+
}
116+
117+
return [];
118+
}
119+
}

src/Rules/rules.neon

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
rules:
2+
- MintwareDe\PhpStanNamespaceConstraints\Rules\NamespaceRestrictionRule
3+
4+
services:
5+
-
6+
class: \MintwareDe\PhpStanNamespaceConstraints\Rules\NamespaceRestrictionRule
7+
arguments:
8+
constraints: '%namespace_restriction.constraints%'
9+
tags:
10+
- phpstan.rules.rule
11+
12+
parametersSchema:
13+
namespace_restriction: structure([
14+
constraints: listOf(structure([
15+
from: anyOf(null, string())
16+
to: listOf(string())
17+
]))
18+
])
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MintwareDe\Tests\PhpStanNamespaceConstraints\Cases\Core;
6+
7+
class CoreDependency
8+
{
9+
}

0 commit comments

Comments
 (0)