1dd6f0edd243d7e7aeb5677ca45771cb0ab31016
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Resources / PHP / ClassMapGenerator.php
1 <?php
2
3 /*
4 * This file is part of Composer.
5 *
6 * (c) Nils Adermann <naderman@naderman.de>
7 * Jordi Boggiano <j.boggiano@seld.be>
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13 /*
14 * This file is copied from the Symfony package.
15 *
16 * (c) Fabien Potencier <fabien@symfony.com>
17 */
18
19 namespace Composer\Autoload;
20
21 use Composer\IO\IOInterface;
22 use Symfony\Component\Finder\Finder;
23
24 /**
25 * ClassMapGenerator
26 *
27 * @author Gyula Sallai <salla016@gmail.com>
28 * @author Jordi Boggiano <j.boggiano@seld.be>
29 */
30 class ClassMapGenerator
31 {
32 /**
33 * Generate a class map file
34 *
35 * @param \Traversable $dirs Directories or a single path to search in
36 * @param string $file The name of the class map file
37 */
38 public static function dump($dirs, $file)
39 {
40 $maps = [[]];
41
42 foreach ($dirs as $dir) {
43 $maps[] = static::createMap($dir);
44 }
45
46 file_put_contents($file, sprintf('<?php return %s;', var_export(array_merge(...$maps), true)));
47 }
48
49 /**
50 * Iterate over all files in the given directory searching for classes
51 *
52 * @param \Iterator|string $path The path to search in or an iterator
53 * @param string $blacklist Regex that matches against the file path that exclude from the classmap.
54 * @param IOInterface $io IO object
55 * @param string $namespace Optional namespace prefix to filter by
56 *
57 * @throws \RuntimeException When the path is neither an existing file nor directory
58 * @return array A class map array
59 */
60 public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null)
61 {
62 if (is_string($path)) {
63 if (is_file($path)) {
64 $path = [new \SplFileInfo($path)];
65 } elseif (is_dir($path)) {
66 $path = Finder::create()->files()->followLinks()->name('/\.(php|inc|hh)$/')->in($path);
67 } else {
68 throw new \RuntimeException(
69 'Could not scan for classes inside "' . $path .
70 '" which does not appear to be a file nor a folder',
71 1476049953
72 );
73 }
74 }
75
76 $map = [];
77
78 foreach ($path as $file) {
79 $filePath = $file->getRealPath();
80
81 if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), ['php', 'inc', 'hh'])) {
82 continue;
83 }
84
85 if ($blacklist && preg_match($blacklist, str_replace('\\', '/', $filePath))) {
86 continue;
87 }
88
89 $classes = self::findClasses($filePath);
90
91 foreach ($classes as $class) {
92 // skip classes not within the given namespace prefix
93 if (null !== $namespace && 0 !== strpos($class, $namespace)) {
94 continue;
95 }
96
97 if (!isset($map[$class])) {
98 $map[$class] = $filePath;
99 } elseif ($io && $map[$class] !== $filePath && !preg_match(
100 '{/(test|fixture|example|stub)s?/}i',
101 str_replace('\\', '/', $map[$class] . ' ' . $filePath)
102 )) {
103 $io->writeError(
104 '<warning>Warning: Ambiguous class resolution, "' . $class . '"' .
105 ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.</warning>'
106 );
107 }
108 }
109 }
110
111 return $map;
112 }
113
114 /**
115 * Extract the classes in the given file
116 *
117 * @param string $path The file to check
118 * @throws \RuntimeException
119 * @return array The found classes
120 */
121 private static function findClasses($path)
122 {
123 $extraTypes = PHP_VERSION_ID < 50400 ? '' : '|trait';
124 if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>=')) {
125 $extraTypes .= '|enum';
126 }
127
128 try {
129 $contents = @php_strip_whitespace($path);
130 if (!$contents) {
131 if (!file_exists($path)) {
132 throw new \Exception('File does not exist', 1476049981);
133 }
134 if (!is_readable($path)) {
135 throw new \Exception('File is not readable', 1476049990);
136 }
137 }
138 } catch (\Exception $e) {
139 throw new \RuntimeException('Could not scan for classes inside ' . $path . ": \n" . $e->getMessage(), 1476050009, $e);
140 }
141
142 // return early if there is no chance of matching anything in this file
143 if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) {
144 return [];
145 }
146
147 // strip heredocs/nowdocs
148 $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents);
149 // strip strings
150 $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents);
151 // strip leading non-php code if needed
152 if (strpos($contents, '<?') !== 0) {
153 $contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements);
154 if ($replacements === 0) {
155 return [];
156 }
157 }
158 // strip non-php blocks in the file
159 $contents = preg_replace('{\?>.+<\?}s', '?><?', $contents);
160 // strip trailing non-php code if needed
161 $pos = strrpos($contents, '?>');
162 if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) {
163 $contents = substr($contents, 0, $pos);
164 }
165
166 preg_match_all('{
167 (?:
168 \b(?<![\$:>])(?P<type>class|interface' . $extraTypes . ') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+)
169 | \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;]
170 )
171 }ix', $contents, $matches);
172
173 $classes = [];
174 $namespace = '';
175
176 $typeCount = count($matches['type']);
177 for ($i = 0, $len = $typeCount; $i < $len; $i++) {
178 if (!empty($matches['ns'][$i])) {
179 $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\';
180 } else {
181 $name = $matches['name'][$i];
182 if ($name[0] === ':') {
183 // This is an XHP class, https://github.com/facebook/xhp
184 $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1);
185 } elseif ($matches['type'][$i] === 'enum') {
186 // In Hack, something like:
187 // enum Foo: int { HERP = '123'; }
188 // The regex above captures the colon, which isn't part of
189 // the class name.
190 $name = rtrim($name, ':');
191 }
192 $classes[] = ltrim($namespace . $name, '\\');
193 }
194 }
195
196 return $classes;
197 }
198 }