Failed Conditions
Pull Request — master (#126)
by
unknown
05:13
created

Mixin::createNullOr()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Webmozart\Assert\Generator;
5
6
use InvalidArgumentException;
7
use Nette\PhpGenerator\ClassType;
8
use Nette\PhpGenerator\PhpFile;
9
use Nette\PhpGenerator\PhpNamespace;
10
use Nette\PhpGenerator\PsrPrinter;
11
use ReflectionClass;
12
use ReflectionMethod;
13
use Webmozart\Assert\Assert;
14
15
final class Mixin
16
{
17
    public function generate(): string
18
    {
19
        $assert = new ReflectionClass(Assert::class);
20
        $staticMethods = $assert->getMethods(ReflectionMethod::IS_STATIC);
21
22
        $supportedMethods = $this->getSupportedMethods($assert);
23
24
        $file = $this->createFile();
25
        $namespace = $this->createNamespace($file, $assert);
26
        $mixin = $this->createInterface($namespace);
27
28
        foreach ($staticMethods as $method) {
29
            if (!$this->isPublicMethod($method)) {
30
                continue;
31
            }
32
33
            $this->createNullOr($method, $supportedMethods, $mixin);
34
            $this->createAll($method, $supportedMethods, $mixin);
35
        }
36
37
        $printer = new PsrPrinter();
38
39
        return $printer->printFile($file);
40
    }
41
42
    private function parseDocComment(string $comment): array
43
    {
44
        $lines = explode("\n", $comment);
45
46
        $result = [];
47
48
        foreach ($lines as $line) {
49
            if (preg_match('~^\*\s+@(\S+)(\s+.*)?$~', trim($line), $matches)) {
50
                if (count($matches) === 2 || $matches[2] === null) {
51
                    $matches[2] = '';
52
                }
53
                $result[$matches[1]][] = trim($matches[2]);
54
            }
55
        }
56
57
        return $result;
58
    }
59
60
    private function splitDocLine(string $line): array
61
    {
62
        if (!preg_match('~^(.*)\s+(\$\S+)$~', $line, $matches)) {
63
            return [$line];
64
        }
65
66
        return [trim($matches[1]), $matches[2]];
67
    }
68
69
    private function createWrapper(ReflectionMethod $method, array $supportedMethods, ClassType $mixin, $methodNameTemplate, $typeTemplate)
70
    {
71
        $newMethodName = sprintf($methodNameTemplate, ucfirst($method->name));
72
73
        if (!in_array($newMethodName, $supportedMethods, true)) {
74
            return;
75
        }
76
77
        $comment = $method->getDocComment();
78
79
        $parsedComment = $this->parseDocComment($comment);
80
81
        if (!in_array('psalm-assert', array_keys($parsedComment))) {
82
            return;
83
        }
84
85
        $mixinMethod = $mixin->addMethod($newMethodName)
86
            ->setStatic();
87
88
        $parameters = $method->getParameters();
89
90
        foreach ($parameters as $parameter) {
91
            $mixinParameter = $mixinMethod->addParameter($parameter->name);
92
93
            if ($parameter->isDefaultValueAvailable()) {
94
                $mixinParameter->setDefaultValue($parameter->getDefaultValue());
95
            }
96
        }
97
98
        foreach ($parsedComment as $key => $values) {
99
            foreach ($values as $value) {
100
                $parts = $this->splitDocLine($value);
101
                $type = $parts[0];
102
103
                if ($key === 'psalm-assert') {
104
                    $type = sprintf($typeTemplate, $type);
105
                }
106
107
                $comment = sprintf('@%s %s', $key, $type);
108
                if (count($parts) >= 2) {
109
                    $comment .= sprintf(' %s', implode(' ', array_slice($parts, 1)));
110
                }
111
112
                $mixinMethod->addComment($comment);
113
            }
114
        }
115
    }
116
117
    private function createNullOr(ReflectionMethod $method, array $supportedMethods, ClassType $mixin)
118
    {
119
        $this->createWrapper($method, $supportedMethods, $mixin, 'nullOr%s', 'null|%s');
120
    }
121
122
    private function createAll(ReflectionMethod $method, array $supportedMethods, ClassType $mixin)
123
    {
124
        $this->createWrapper($method, $supportedMethods, $mixin, 'all%s', 'array<array-key,%s>');
125
    }
126
127
    private function getSupportedMethods(ReflectionClass $class): array
128
    {
129
        $comment = $class->getDocComment();
130
131
        preg_match_all('~@method static void ([^(]+)~', $comment, $matches);
132
133
        $temporaryUnsupported = [
134
            'nullOrNotInstanceOf',
135
            'allNotInstanceOf',
136
            'nullOrNotEmpty',
137
            'allNotEmpty',
138
            'allNotNull',
139
            'nullOrSame',
140
            'allSame',
141
            'nullOrUnicodeLetters',
142
            'allUnicodeLetters',
143
            'nullOrStringNotEmpty',
144
            'allStringNotEmpty',
145
            'nullOrIsNotA',
146
            'allIsNotA',
147
            'nullOrNotFalse',
148
            'allNotFalse',
149
            'nullOrUpper',
150
            'allUpper',
151
            'nullOrIsNonEmptyList',
152
            'allIsNonEmptyList',
153
            'nullOrIsNonEmptyMap',
154
            'allIsNonEmptyMap',
155
        ];
156
157
        return array_diff($matches[1], $temporaryUnsupported);
158
    }
159
160
    private function createFile(): PhpFile
161
    {
162
        $file = new PhpFile();
163
        $file->addComment('Automatically generated');
164
165
        return $file;
166
    }
167
168
    private function createNamespace(PhpFile $file, ReflectionClass $class): PhpNamespace
169
    {
170
        $namespace = $file->addNamespace($class->getNamespaceName());
171
        $namespace->addUse(InvalidArgumentException::class);
172
173
        return $namespace;
174
    }
175
176
    private function createInterface(PhpNamespace $namespace): ClassType
177
    {
178
        return $namespace->addInterface('Mixin');
179
    }
180
181
    private function isPublicMethod(ReflectionMethod $method): bool
182
    {
183
        $modifiers = $method->getModifiers();
184
185
        return ($modifiers & ReflectionMethod::IS_PUBLIC) !== 0;
186
    }
187
}
188