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