257 lines
7.3 KiB
PHP
257 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Copyright (c) 2017-2022 Andreas Möller
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE.md file that was distributed with this source code.
|
|
*
|
|
* @see https://github.com/ergebnis/classy
|
|
*/
|
|
|
|
namespace Ergebnis\Classy;
|
|
|
|
final class Constructs
|
|
{
|
|
/**
|
|
* Returns an array of names of classy constructs (classes, interfaces, traits) found in source.
|
|
*
|
|
* @throws Exception\ParseError
|
|
*
|
|
* @return array<int, Construct>
|
|
*/
|
|
public static function fromSource(string $source): array
|
|
{
|
|
$constructs = [];
|
|
|
|
try {
|
|
$sequence = \token_get_all(
|
|
$source,
|
|
\TOKEN_PARSE,
|
|
);
|
|
} catch (\ParseError $exception) {
|
|
throw Exception\ParseError::fromParseError($exception);
|
|
}
|
|
|
|
$count = \count($sequence);
|
|
$namespacePrefix = '';
|
|
|
|
$namespaceSegmentOrNamespaceTokens = [
|
|
\T_STRING,
|
|
\T_NAME_QUALIFIED,
|
|
];
|
|
|
|
$classyTokens = [
|
|
\T_CLASS,
|
|
\T_INTERFACE,
|
|
\T_TRAIT,
|
|
];
|
|
|
|
// https://wiki.php.net/rfc/enumerations
|
|
if (\PHP_VERSION_ID >= 80100 && \defined('T_ENUM')) {
|
|
$classyTokens = [
|
|
\T_CLASS,
|
|
\T_ENUM,
|
|
\T_INTERFACE,
|
|
\T_TRAIT,
|
|
];
|
|
}
|
|
|
|
for ($index = 0; $index < $count; ++$index) {
|
|
$token = $sequence[$index];
|
|
|
|
// collect namespace name
|
|
if (\is_array($token) && \T_NAMESPACE === $token[0]) {
|
|
$namespaceSegments = [];
|
|
|
|
// collect namespace segments
|
|
for ($index = self::significantAfter($index, $sequence, $count); $index < $count; ++$index) {
|
|
$token = $sequence[$index];
|
|
|
|
if (\is_array($token) && !\in_array($token[0], $namespaceSegmentOrNamespaceTokens, true)) {
|
|
continue;
|
|
}
|
|
|
|
$content = self::content($token);
|
|
|
|
if (\in_array($content, ['{', ';'], true)) {
|
|
break;
|
|
}
|
|
|
|
$namespaceSegments[] = $content;
|
|
}
|
|
|
|
$namespace = \implode('\\', $namespaceSegments);
|
|
$namespacePrefix = $namespace . '\\';
|
|
}
|
|
|
|
// skip non-classy tokens
|
|
if (!\is_array($token) || !\in_array($token[0], $classyTokens, true)) {
|
|
continue;
|
|
}
|
|
|
|
// skip anonymous classes
|
|
if (\T_CLASS === $token[0]) {
|
|
$current = self::significantBefore($index, $sequence);
|
|
$token = $sequence[$current];
|
|
|
|
// if significant token before T_CLASS is T_NEW, it's an instantiation of an anonymous class
|
|
if (\is_array($token) && \T_NEW === $token[0]) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$index = self::significantAfter($index, $sequence, $count);
|
|
$token = $sequence[$index];
|
|
|
|
$constructs[] = Construct::fromName($namespacePrefix . self::content($token));
|
|
}
|
|
|
|
\usort($constructs, static function (Construct $a, Construct $b): int {
|
|
return \strcmp(
|
|
$a->name(),
|
|
$b->name(),
|
|
);
|
|
});
|
|
|
|
return $constructs;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of constructs defined in a directory.
|
|
*
|
|
* @throws Exception\DirectoryDoesNotExist
|
|
* @throws Exception\MultipleDefinitionsFound
|
|
*
|
|
* @return array<int, Construct>
|
|
*/
|
|
public static function fromDirectory(string $directory): array
|
|
{
|
|
if (!\is_dir($directory)) {
|
|
throw Exception\DirectoryDoesNotExist::fromDirectory($directory);
|
|
}
|
|
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
|
|
$directory,
|
|
\RecursiveDirectoryIterator::FOLLOW_SYMLINKS,
|
|
));
|
|
|
|
$constructs = [];
|
|
|
|
foreach ($iterator as $fileInfo) {
|
|
/** @var \SplFileInfo $fileInfo */
|
|
if (!$fileInfo->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) {
|
|
continue;
|
|
}
|
|
|
|
/** @var string $fileName */
|
|
$fileName = $fileInfo->getRealPath();
|
|
|
|
/** @var string $source */
|
|
$source = \file_get_contents($fileName);
|
|
|
|
try {
|
|
$constructsFromFile = self::fromSource($source);
|
|
} catch (Exception\ParseError $exception) {
|
|
throw Exception\ParseError::fromFileNameAndParseError(
|
|
$fileName,
|
|
$exception,
|
|
);
|
|
}
|
|
|
|
if (0 === \count($constructsFromFile)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($constructsFromFile as $construct) {
|
|
$name = $construct->name();
|
|
|
|
if (\array_key_exists($name, $constructs)) {
|
|
$construct = $constructs[$name];
|
|
}
|
|
|
|
$constructs[$name] = $construct->definedIn($fileName);
|
|
}
|
|
}
|
|
|
|
\usort($constructs, static function (Construct $a, Construct $b): int {
|
|
return \strcmp(
|
|
$a->name(),
|
|
$b->name(),
|
|
);
|
|
});
|
|
|
|
$constructsWithMultipleDefinitions = \array_filter($constructs, static function (Construct $construct): bool {
|
|
return 1 < \count($construct->fileNames());
|
|
});
|
|
|
|
if (0 < \count($constructsWithMultipleDefinitions)) {
|
|
throw Exception\MultipleDefinitionsFound::fromConstructs($constructsWithMultipleDefinitions);
|
|
}
|
|
|
|
return \array_values($constructs);
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the significant token after the index.
|
|
*
|
|
* @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
|
|
*/
|
|
private static function significantAfter(int $index, array $sequence, int $count): int
|
|
{
|
|
for ($current = $index + 1; $current < $count; ++$current) {
|
|
$token = $sequence[$current];
|
|
|
|
if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
|
|
continue;
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
|
|
throw Exception\ShouldNotHappen::create();
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the significant token after the index.
|
|
*
|
|
* @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
|
|
*/
|
|
private static function significantBefore(
|
|
int $index,
|
|
array $sequence,
|
|
): int {
|
|
for ($current = $index - 1; -1 < $current; --$current) {
|
|
$token = $sequence[$current];
|
|
|
|
if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
|
|
continue;
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
|
|
throw Exception\ShouldNotHappen::create();
|
|
}
|
|
|
|
/**
|
|
* Returns the string content of a token.
|
|
*
|
|
* @param array{0: int, 1: string, 2: int}|string $token
|
|
*/
|
|
private static function content($token): string
|
|
{
|
|
if (\is_array($token)) {
|
|
return $token[1];
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
}
|