|
12 | 12 | use ReflectionProperty; |
13 | 13 | use UnexpectedValueException; |
14 | 14 |
|
| 15 | +use function assert; |
| 16 | +use function count; |
15 | 17 | use function implode; |
16 | 18 | use function preg_replace; |
17 | 19 | use function strpos; |
18 | 20 | use function strtolower; |
19 | | -use function trim; |
| 21 | +use function substr; |
20 | 22 |
|
21 | 23 | /** |
22 | 24 | * @internal |
@@ -152,33 +154,127 @@ public function getFileAnalyzer(): FileAnalyzer |
152 | 154 | } |
153 | 155 |
|
154 | 156 | /** |
155 | | - * Returns true if $className is the same as, or starts with $namespace, in a case-insensitive comparison. |
| 157 | + * Returns true if $calling_identifier is the same as, or is within with $identifier, in a |
| 158 | + * case-insensitive comparison. Identifiers can be namespaces, classlikes, functions, or methods. |
156 | 159 | * |
| 160 | + * @psalm-pure |
| 161 | + * |
| 162 | + * @throws InvalidArgumentException if $identifier is not a valid identifier |
| 163 | + */ |
| 164 | + public static function isWithin(string $calling_identifier, string $identifier): bool |
| 165 | + { |
| 166 | + $normalized_calling_ident = self::normalizeIdentifier($calling_identifier); |
| 167 | + $normalized_ident = self::normalizeIdentifier($identifier); |
| 168 | + |
| 169 | + if ($normalized_calling_ident === $normalized_ident) { |
| 170 | + return true; |
| 171 | + } |
| 172 | + |
| 173 | + $normalized_calling_ident_parts = self::getIdentifierParts($normalized_calling_ident); |
| 174 | + $normalized_ident_parts = self::getIdentifierParts($normalized_ident); |
| 175 | + |
| 176 | + if (count($normalized_calling_ident_parts) < count($normalized_ident_parts)) { |
| 177 | + return false; |
| 178 | + } |
| 179 | + |
| 180 | + for ($i = 0; $i < count($normalized_ident_parts); ++$i) { |
| 181 | + if ($normalized_ident_parts[$i] !== $normalized_calling_ident_parts[$i]) { |
| 182 | + return false; |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + return true; |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Returns true if $calling_identifier is the same as or is within any identifier |
| 191 | + * in $identifiers in a case-insensitive comparison, or if $identifiers is empty. |
| 192 | + * Identifiers can be namespaces, classlikes, functions, or methods. |
157 | 193 | * |
158 | 194 | * @psalm-pure |
| 195 | + * |
| 196 | + * @psalm-assert-if-false !empty $identifiers |
| 197 | + * |
| 198 | + * @param list<string> $identifiers |
159 | 199 | */ |
160 | | - public static function isWithin(string $calling_namespace, string $namespace): bool |
| 200 | + public static function isWithinAny(string $calling_identifier, array $identifiers): bool |
161 | 201 | { |
162 | | - if ($namespace === '') { |
163 | | - return true; // required to prevent a warning from strpos with empty needle in PHP < 8 |
| 202 | + if (count($identifiers) === 0) { |
| 203 | + return true; |
164 | 204 | } |
165 | 205 |
|
166 | | - $calling_namespace = strtolower(trim($calling_namespace, '\\') . '\\'); |
167 | | - $namespace = strtolower(trim($namespace, '\\') . '\\'); |
| 206 | + foreach ($identifiers as $identifier) { |
| 207 | + if (self::isWithin($calling_identifier, $identifier)) { |
| 208 | + return true; |
| 209 | + } |
| 210 | + } |
168 | 211 |
|
169 | | - return $calling_namespace === $namespace |
170 | | - || strpos($calling_namespace, $namespace) === 0; |
| 212 | + return false; |
171 | 213 | } |
172 | 214 |
|
173 | 215 | /** |
174 | | - * @param string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' |
| 216 | + * @param non-empty-string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' |
175 | 217 | * |
176 | | - * @return string , e.g. 'Psalm' |
| 218 | + * @return non-empty-string , e.g. 'Psalm' |
177 | 219 | * |
178 | 220 | * @psalm-pure |
179 | 221 | */ |
180 | 222 | public static function getNameSpaceRoot(string $fullyQualifiedClassName): string |
181 | 223 | { |
182 | | - return preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); |
| 224 | + $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); |
| 225 | + if ($root_namespace === "") { |
| 226 | + throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); |
| 227 | + } |
| 228 | + return $root_namespace; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * @return ($lowercase is true ? lowercase-string : string) |
| 233 | + * |
| 234 | + * @psalm-pure |
| 235 | + */ |
| 236 | + public static function normalizeIdentifier(string $identifier, bool $lowercase = true): string |
| 237 | + { |
| 238 | + if ($identifier === "") { |
| 239 | + return ""; |
| 240 | + } |
| 241 | + |
| 242 | + $identifier = $identifier[0] === "\\" ? substr($identifier, 1) : $identifier; |
| 243 | + return $lowercase ? strtolower($identifier) : $identifier; |
| 244 | + } |
| 245 | + |
| 246 | + /** |
| 247 | + * Splits an identifier into parts, eg `Foo\Bar::baz` becomes ["Foo", "\\", "Bar", "::", "baz"]. |
| 248 | + * |
| 249 | + * @return list<non-empty-string> |
| 250 | + * |
| 251 | + * @psalm-pure |
| 252 | + */ |
| 253 | + public static function getIdentifierParts(string $identifier): array |
| 254 | + { |
| 255 | + $parts = []; |
| 256 | + while (($pos = strpos($identifier, "\\")) !== false) { |
| 257 | + if ($pos > 0) { |
| 258 | + $part = substr($identifier, 0, $pos); |
| 259 | + assert($part !== ""); |
| 260 | + $parts[] = $part; |
| 261 | + } |
| 262 | + $parts[] = "\\"; |
| 263 | + $identifier = substr($identifier, $pos + 1); |
| 264 | + } |
| 265 | + if (($pos = strpos($identifier, "::")) !== false) { |
| 266 | + if ($pos > 0) { |
| 267 | + $part = substr($identifier, 0, $pos); |
| 268 | + assert($part !== ""); |
| 269 | + $parts[] = $part; |
| 270 | + } |
| 271 | + $parts[] = "::"; |
| 272 | + $identifier = substr($identifier, $pos + 2); |
| 273 | + } |
| 274 | + if ($identifier !== "") { |
| 275 | + $parts[] = $identifier; |
| 276 | + } |
| 277 | + |
| 278 | + return $parts; |
183 | 279 | } |
184 | 280 | } |
0 commit comments