1<?php
2/**
3 * This file is part of phpDocumentor.
4 *
5 * For the full copyright and license information, please view the LICENSE
6 * file that was distributed with this source code.
7 *
8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org>
9 * @license   http://www.opensource.org/licenses/mit-license.php MIT
10 * @link      http://phpdoc.org
11 */
12
13namespace phpDocumentor\Reflection\Types;
14
15/**
16 * Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor.
17 *
18 * For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to
19 * provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to
20 * Fully Qualified names.
21 *
22 * @see Context for more information.
23 */
24final class ContextFactory
25{
26    /** The literal used at the end of a use statement. */
27    const T_LITERAL_END_OF_USE = ';';
28
29    /** The literal used between sets of use statements */
30    const T_LITERAL_USE_SEPARATOR = ',';
31
32    /**
33     * Build a Context given a Class Reflection.
34     *
35     * @param \Reflector $reflector
36     *
37     * @see Context for more information on Contexts.
38     *
39     * @return Context
40     */
41    public function createFromReflector(\Reflector $reflector)
42    {
43        if (method_exists($reflector, 'getDeclaringClass')) {
44            $reflector = $reflector->getDeclaringClass();
45        }
46
47        $fileName = $reflector->getFileName();
48        $namespace = $reflector->getNamespaceName();
49
50        if (file_exists($fileName)) {
51            return $this->createForNamespace($namespace, file_get_contents($fileName));
52        }
53
54        return new Context($namespace, []);
55    }
56
57    /**
58     * Build a Context for a namespace in the provided file contents.
59     *
60     * @param string $namespace It does not matter if a `\` precedes the namespace name, this method first normalizes.
61     * @param string $fileContents the file's contents to retrieve the aliases from with the given namespace.
62     *
63     * @see Context for more information on Contexts.
64     *
65     * @return Context
66     */
67    public function createForNamespace($namespace, $fileContents)
68    {
69        $namespace = trim($namespace, '\\');
70        $useStatements = [];
71        $currentNamespace = '';
72        $tokens = new \ArrayIterator(token_get_all($fileContents));
73
74        while ($tokens->valid()) {
75            switch ($tokens->current()[0]) {
76                case T_NAMESPACE:
77                    $currentNamespace = $this->parseNamespace($tokens);
78                    break;
79                case T_CLASS:
80                    // Fast-forward the iterator through the class so that any
81                    // T_USE tokens found within are skipped - these are not
82                    // valid namespace use statements so should be ignored.
83                    $braceLevel = 0;
84                    $firstBraceFound = false;
85                    while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) {
86                        if ($tokens->current() === '{'
87                            || $tokens->current()[0] === T_CURLY_OPEN
88                            || $tokens->current()[0] === T_DOLLAR_OPEN_CURLY_BRACES) {
89                            if (!$firstBraceFound) {
90                                $firstBraceFound = true;
91                            }
92                            $braceLevel++;
93                        }
94
95                        if ($tokens->current() === '}') {
96                            $braceLevel--;
97                        }
98                        $tokens->next();
99                    }
100                    break;
101                case T_USE:
102                    if ($currentNamespace === $namespace) {
103                        $useStatements = array_merge($useStatements, $this->parseUseStatement($tokens));
104                    }
105                    break;
106            }
107            $tokens->next();
108        }
109
110        return new Context($namespace, $useStatements);
111    }
112
113    /**
114     * Deduce the name from tokens when we are at the T_NAMESPACE token.
115     *
116     * @param \ArrayIterator $tokens
117     *
118     * @return string
119     */
120    private function parseNamespace(\ArrayIterator $tokens)
121    {
122        // skip to the first string or namespace separator
123        $this->skipToNextStringOrNamespaceSeparator($tokens);
124
125        $name = '';
126        while ($tokens->valid() && ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR)
127        ) {
128            $name .= $tokens->current()[1];
129            $tokens->next();
130        }
131
132        return $name;
133    }
134
135    /**
136     * Deduce the names of all imports when we are at the T_USE token.
137     *
138     * @param \ArrayIterator $tokens
139     *
140     * @return string[]
141     */
142    private function parseUseStatement(\ArrayIterator $tokens)
143    {
144        $uses = [];
145        $continue = true;
146
147        while ($continue) {
148            $this->skipToNextStringOrNamespaceSeparator($tokens);
149
150            list($alias, $fqnn) = $this->extractUseStatement($tokens);
151            $uses[$alias] = $fqnn;
152            if ($tokens->current()[0] === self::T_LITERAL_END_OF_USE) {
153                $continue = false;
154            }
155        }
156
157        return $uses;
158    }
159
160    /**
161     * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
162     *
163     * @param \ArrayIterator $tokens
164     *
165     * @return void
166     */
167    private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens)
168    {
169        while ($tokens->valid() && ($tokens->current()[0] !== T_STRING) && ($tokens->current()[0] !== T_NS_SEPARATOR)) {
170            $tokens->next();
171        }
172    }
173
174    /**
175     * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
176     * a USE statement yet.
177     *
178     * @param \ArrayIterator $tokens
179     *
180     * @return string
181     */
182    private function extractUseStatement(\ArrayIterator $tokens)
183    {
184        $result = [''];
185        while ($tokens->valid()
186            && ($tokens->current()[0] !== self::T_LITERAL_USE_SEPARATOR)
187            && ($tokens->current()[0] !== self::T_LITERAL_END_OF_USE)
188        ) {
189            if ($tokens->current()[0] === T_AS) {
190                $result[] = '';
191            }
192            if ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR) {
193                $result[count($result) - 1] .= $tokens->current()[1];
194            }
195            $tokens->next();
196        }
197
198        if (count($result) == 1) {
199            $backslashPos = strrpos($result[0], '\\');
200
201            if (false !== $backslashPos) {
202                $result[] = substr($result[0], $backslashPos + 1);
203            } else {
204                $result[] = $result[0];
205            }
206        }
207
208        return array_reverse($result);
209    }
210}
211