Passed
Pull Request — master (#584)
by Théo
02:04
created

EnrichedReflector::isSymbolExposed()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 23
nc 7
nop 2
dl 0
loc 42
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Humbug\PhpScoper\Symbol;
6
7
use Humbug\PhpScoper\Configuration\SymbolsConfiguration;
8
use Humbug\PhpScoper\Reflector;
9
use function array_key_exists;
10
use function array_keys;
11
use function array_map;
12
use function array_pop;
13
use function array_unique;
14
use function explode;
15
use function implode;
16
use function preg_match as native_preg_match;
17
use function Safe\array_flip;
18
use function strpos;
19
use function strtolower;
20
21
/**
22
 * Combines the API or the "traditional" reflector which is about to tell
23
 * if a symbol is internal or not with the more PHP-Scoper specific exposed
24
 * API.
25
 */
26
final class EnrichedReflector
27
{
28
    private Reflector $reflector;
29
    private SymbolsConfiguration $symbolsConfiguration;
30
31
    public function __construct(
32
        Reflector $reflector,
33
        SymbolsConfiguration $symbolsConfiguration
34
    ) {
35
        $this->reflector = $reflector;
36
        $this->symbolsConfiguration = $symbolsConfiguration;
37
    }
38
39
    public function belongsToExcludedNamespace(string $name): bool
40
    {
41
        return $this->symbolsConfiguration
42
            ->getExcludedNamespaces()
43
            ->belongsToRegisteredNamespace($name);
44
    }
45
46
    public function isFunctionInternal(string $name): bool
47
    {
48
        return $this->reflector->isFunctionInternal($name);
49
    }
50
51
    public function isFunctionExcluded(string $name): bool
52
    {
53
        return $this->reflector->isFunctionInternal($name)
54
            || $this->belongsToExcludedNamespace($name);
55
    }
56
57
    public function isClassInternal(string $name): bool
58
    {
59
        return $this->reflector->isClassInternal($name);
60
    }
61
62
    public function isClassExcluded(string $name): bool
63
    {
64
        return $this->reflector->isClassInternal($name)
65
            || $this->belongsToExcludedNamespace($name);
66
    }
67
68
    public function isConstantInternal(string $name): bool
69
    {
70
        return $this->reflector->isConstantInternal($name);
71
    }
72
73
    public function isConstantExcluded(string $name): bool
74
    {
75
        // TODO: double check not sure that internal should mean excluded for constants
76
        return $this->reflector->isConstantInternal($name)
77
            || $this->belongsToExcludedNamespace($name);
78
    }
79
80
    public function isExposedFunction(string $resolvedName): bool
81
    {
82
        return !$this->belongsToExcludedNamespace($resolvedName)
83
            && !$this->reflector->isFunctionInternal($resolvedName)
84
            && (
85
                $this->_isExposedFunctionFromGlobalNamespace($resolvedName)
86
                || $this->isSymbolExposed($resolvedName)
87
            );
88
    }
89
90
    public function isExposedFunctionFromGlobalNamespace(string $resolvedName): bool
91
    {
92
        return !$this->belongsToExcludedNamespace($resolvedName)
93
            && !$this->reflector->isFunctionInternal($resolvedName)
94
            && $this->_isExposedFunctionFromGlobalNamespace($resolvedName);
95
    }
96
97
    public function isExposedClass(string $resolvedName): bool
98
    {
99
        return !$this->belongsToExcludedNamespace($resolvedName)
100
            && !$this->reflector->isClassInternal($resolvedName)
101
            && (
102
                $this->_isExposedClassFromGlobalNamespace($resolvedName)
103
                || $this->isSymbolExposed($resolvedName)
104
            );
105
    }
106
107
    public function isExposedClassFromGlobalNamespace(string $resolvedName): bool
108
    {
109
        return !$this->belongsToExcludedNamespace($resolvedName)
110
            && !$this->reflector->isClassInternal($resolvedName)
111
            && $this->_isExposedClassFromGlobalNamespace($resolvedName);
112
    }
113
114
    public function isExposedConstant(string $name): bool
115
    {
116
        // Special case: internal constants must be treated as exposed symbols.
117
        //
118
        // Example: when declaring a new internal constant for compatibility
119
        // reasons, it must remain un-prefixed.
120
        return !$this->belongsToExcludedNamespace($name)
121
            && (
122
                $this->reflector->isConstantInternal($name)
123
                || $this->isExposedConstantFromGlobalNamespace($name)
124
                || $this->isSymbolExposed($name, true)
125
            );
126
    }
127
128
    public function isExposedConstantFromGlobalNamespace(string $constantName): bool
129
    {
130
        return $this->symbolsConfiguration->shouldExposeGlobalConstants() && !strpos($constantName, '\\');
131
    }
132
133
    public function isExcludedNamespace(string $name): bool
134
    {
135
        return $this->symbolsConfiguration
136
            ->getExcludedNamespaces()
137
            ->isRegisteredNamespace($name);
138
    }
139
140
    private function _isExposedFunctionFromGlobalNamespace(string $functionName): bool
141
    {
142
        return $this->symbolsConfiguration->shouldExposeGlobalFunctions() && !strpos($functionName, '\\');
143
    }
144
145
    public function _isExposedClassFromGlobalNamespace(string $className): bool
146
    {
147
        return $this->symbolsConfiguration->shouldExposeGlobalClasses() && !strpos($className, '\\');
148
    }
149
150
    /**
151
     * Tells if a given symbol is exposed. Note however that it does not account for when:
152
     *
153
     * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are exposed
154
     * - Belongs to an excluded namespace
155
     *
156
     * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out
157
     *                       the case where they are not case sensitive.
158
     */
159
    private function isSymbolExposed(string $name, bool $constant = false): bool
160
    {
161
        $exposedSymbols = array_flip(
162
            array_unique([
163
                ...$this->symbolsConfiguration->getExposedClassNames(),
164
                ...$this->symbolsConfiguration->getExposedFunctionNames(),
165
                ...$this->symbolsConfiguration->getExposedConstantNames(),
166
            ]),
167
        );
168
169
        $exposedConstants = array_flip(
170
            array_unique(
171
                array_map(
172
                    static fn (string $symbolName) => self::lowerCaseConstantName($symbolName),
173
                    array_keys($exposedSymbols),
174
                ),
175
            ),
176
        );
177
178
        $exposedSymbolsPatterns = array_unique([
179
            ...$this->symbolsConfiguration->getExposedClassRegexes(),
180
            ...$this->symbolsConfiguration->getExposedFunctionRegexes(),
181
            ...$this->symbolsConfiguration->getExposedConstantRegexes(),
182
        ]);
183
184
        if (!$constant && array_key_exists(strtolower($name), $exposedSymbols)) {
185
            return true;
186
        }
187
188
        if ($constant && array_key_exists(self::lowerCaseConstantName($name), $exposedConstants)) {
189
            return true;
190
        }
191
192
        foreach ($exposedSymbolsPatterns as $pattern) {
193
            $pattern = !$constant ? $pattern.'i' : $pattern;
194
195
            if (1 === native_preg_match($pattern, $name)) {
196
                return true;
197
            }
198
        }
199
200
        return false;
201
    }
202
203
    /**
204
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
205
     * constants regardless of whether or not constants actually are case insensitive.
206
     */
207
    private static function lowerCaseConstantName(string $name): string
208
    {
209
        $parts = explode('\\', $name);
210
211
        $lastPart = array_pop($parts);
212
213
        $parts = array_map('strtolower', $parts);
214
215
        $parts[] = $lastPart;
216
217
        return implode('\\', $parts);
218
    }
219
}
220