ergebnis / classy
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * Copyright (c) 2017-2020 Andreas Möller
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE.md file that was distributed with this source code.
10
 *
11
 * @see https://github.com/ergebnis/classy
12
 */
13

14
namespace Ergebnis\Classy;
15

16
final class Constructs
17
{
18
    /**
19
     * Returns an array of names of classy constructs (classes, interfaces, traits) found in source.
20
     *
21
     * @throws Exception\ParseError
22
     *
23
     * @return Construct[]
24
     */
25 1
    public static function fromSource(string $source): array
26
    {
27 1
        $constructs = [];
28

29
        try {
30 1
            $sequence = \token_get_all(
31 1
                $source,
32 1
                \TOKEN_PARSE
33
            );
34 1
        } catch (\ParseError $exception) {
35 1
            throw Exception\ParseError::fromParseError($exception);
36
        }
37

38 1
        $count = \count($sequence);
39 1
        $namespacePrefix = '';
40

41 1
        $namespaceSegmentOrNamespaceToken = \T_STRING;
42

43
        // https://wiki.php.net/rfc/namespaced_names_as_token
44 1
        if (\PHP_VERSION_ID >= 80000 && \defined('T_NAME_QUALIFIED')) {
45
            /** @var int $namespaceSegmentOrNamespaceToken */
46 0
            $namespaceSegmentOrNamespaceToken = T_NAME_QUALIFIED;
47
        }
48

49 1
        for ($index = 0; $index < $count; ++$index) {
50 1
            $token = $sequence[$index];
51

52
            // collect namespace name
53 1
            if (\is_array($token) && \T_NAMESPACE === $token[0]) {
54 1
                $namespaceSegments = [];
55

56
                // collect namespace segments
57 1
                for ($index = self::significantAfter($index, $sequence, $count); $index < $count; ++$index) {
58 1
                    $token = $sequence[$index];
59

60 1
                    if (\is_array($token) && $namespaceSegmentOrNamespaceToken !== $token[0]) {
61 1
                        continue;
62
                    }
63

64 1
                    $content = self::content($token);
65

66 1
                    if (\in_array($content, ['{', ';'], true)) {
67 1
                        break;
68
                    }
69

70 1
                    $namespaceSegments[] = $content;
71
                }
72

73 1
                $namespace = \implode('\\', $namespaceSegments);
74 1
                $namespacePrefix = $namespace . '\\';
75
            }
76

77
            // skip non-classy tokens
78 1
            if (!\is_array($token) || !\in_array($token[0], [\T_CLASS, \T_INTERFACE, \T_TRAIT], true)) {
79 1
                continue;
80
            }
81

82
            // skip anonymous classes
83 1
            if (\T_CLASS === $token[0]) {
84 1
                $current = self::significantBefore($index, $sequence);
85 1
                $token = $sequence[$current];
86

87
                // if significant token before T_CLASS is T_NEW, it's an instantiation of an anonymous class
88 1
                if (\is_array($token) && \T_NEW === $token[0]) {
89 1
                    continue;
90
                }
91
            }
92

93 1
            $index = self::significantAfter($index, $sequence, $count);
94 1
            $token = $sequence[$index];
95

96 1
            $constructs[] = Construct::fromName($namespacePrefix . self::content($token));
97
        }
98

99
        \usort($constructs, static function (Construct $a, Construct $b): int {
100 1
            return \strcmp(
101 1
                $a->name(),
102 1
                $b->name()
103
            );
104 1
        });
105

106 1
        return $constructs;
107
    }
108

109
    /**
110
     * Returns an array of constructs defined in a directory.
111
     *
112
     * @throws Exception\DirectoryDoesNotExist
113
     * @throws Exception\MultipleDefinitionsFound
114
     *
115
     * @return Construct[]
116
     */
117 1
    public static function fromDirectory(string $directory): array
118
    {
119 1
        if (!\is_dir($directory)) {
120 1
            throw Exception\DirectoryDoesNotExist::fromDirectory($directory);
121
        }
122

123 1
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
124
            $directory,
125 1
            \RecursiveDirectoryIterator::FOLLOW_SYMLINKS
126
        ));
127

128 1
        $constructs = [];
129

130 1
        foreach ($iterator as $fileInfo) {
131
            /** @var \SplFileInfo $fileInfo */
132 1
            if (!$fileInfo->isFile()) {
133 1
                continue;
134
            }
135

136 1
            if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) {
137 1
                continue;
138
            }
139

140
            /** @var string $fileName */
141 1
            $fileName = $fileInfo->getRealPath();
142

143
            /** @var string $source */
144 1
            $source = \file_get_contents($fileName);
145

146
            try {
147 1
                $constructsFromFile = self::fromSource($source);
148 1
            } catch (Exception\ParseError $exception) {
149 1
                throw Exception\ParseError::fromFileNameAndParseError(
150 1
                    $fileName,
151
                    $exception
152
                );
153
            }
154

155 1
            if (0 === \count($constructsFromFile)) {
156 1
                continue;
157
            }
158

159 1
            foreach ($constructsFromFile as $construct) {
160 1
                $name = $construct->name();
161

162 1
                if (\array_key_exists($name, $constructs)) {
163 1
                    $construct = $constructs[$name];
164
                }
165

166 1
                $constructs[$name] = $construct->definedIn($fileName);
167
            }
168
        }
169

170
        \usort($constructs, static function (Construct $a, Construct $b): int {
171 1
            return \strcmp(
172 1
                $a->name(),
173 1
                $b->name()
174
            );
175 1
        });
176

177
        $constructsWithMultipleDefinitions = \array_filter($constructs, static function (Construct $construct): bool {
178 1
            return 1 < \count($construct->fileNames());
179 1
        });
180

181 1
        if (0 < \count($constructsWithMultipleDefinitions)) {
182 1
            throw Exception\MultipleDefinitionsFound::fromConstructs($constructsWithMultipleDefinitions);
183
        }
184

185 1
        return \array_values($constructs);
186
    }
187

188
    /**
189
     * Returns the index of the significant token after the index.
190
     *
191
     * @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
192
     */
193 1
    private static function significantAfter(int $index, array $sequence, int $count): int
194
    {
195 1
        for ($current = $index + 1; $current < $count; ++$current) {
196 1
            $token = $sequence[$current];
197

198 1
            if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
199 1
                continue;
200
            }
201

202 1
            return $current;
203
        }
204

205 0
        throw Exception\ShouldNotHappen::create();
206
    }
207

208
    /**
209
     * Returns the index of the significant token after the index.
210
     *
211
     * @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
212
     */
213 1
    private static function significantBefore(int $index, array $sequence): int
214
    {
215 1
        for ($current = $index - 1; -1 < $current; --$current) {
216 1
            $token = $sequence[$current];
217

218 1
            if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
219 1
                continue;
220
            }
221

222 1
            return $current;
223
        }
224

225 0
        throw Exception\ShouldNotHappen::create();
226
    }
227

228
    /**
229
     * Returns the string content of a token.
230
     *
231
     * @param array{0: int, 1: string, 2: int}|string $token
232
     */
233 1
    private static function content($token): string
234
    {
235 1
        if (\is_array($token)) {
236 1
            return $token[1];
237
        }
238

239 1
        return $token;
240
    }
241
}

Read our documentation on viewing source code .

Loading