1<?php
2
3namespace Gettext\Utils;
4
5use Gettext\Extractors\PhpCode;
6
7class PhpFunctionsScanner extends FunctionsScanner
8{
9    /**
10     * PHP tokens of the code to be parsed.
11     *
12     * @var array
13     */
14    protected $tokens;
15
16    /**
17     * If not false, comments will be extracted.
18     *
19     * @var string|false|array
20     */
21    protected $extractComments = false;
22
23    /**
24     * Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted).
25     *
26     * @param mixed $tag
27     */
28    public function enableCommentsExtraction($tag = '')
29    {
30        $this->extractComments = $tag;
31    }
32
33    /**
34     * Disable comments extraction.
35     */
36    public function disableCommentsExtraction()
37    {
38        $this->extractComments = false;
39    }
40
41    /**
42     * Constructor.
43     *
44     * @param string $code The php code to scan
45     */
46    public function __construct($code)
47    {
48        $this->tokens = array_values(
49            array_filter(
50                token_get_all($code),
51                function ($token) {
52                    return !is_array($token) || $token[0] !== T_WHITESPACE;
53                }
54            )
55        );
56    }
57
58    /**
59     * {@inheritdoc}
60     */
61    public function getFunctions(array $constants = [])
62    {
63        $count = count($this->tokens);
64        /* @var ParsedFunction[] $bufferFunctions */
65        $bufferFunctions = [];
66        /* @var ParsedComment[] $bufferComments */
67        $bufferComments = [];
68        /* @var array $functions */
69        $functions = [];
70
71        for ($k = 0; $k < $count; ++$k) {
72            $value = $this->tokens[$k];
73
74            if (is_string($value)) {
75                if (isset($bufferFunctions[0])) {
76                    switch ($value) {
77                        case ',':
78                            $bufferFunctions[0]->nextArgument();
79                            break;
80                        case ')':
81                            $functions[] = array_shift($bufferFunctions)->close();
82                            break;
83                        case '.':
84                            break;
85                        default:
86                            $bufferFunctions[0]->stopArgument();
87                            break;
88                    }
89                }
90                continue;
91            }
92
93            switch ($value[0]) {
94                case T_CONSTANT_ENCAPSED_STRING:
95                    //add an argument to the current function
96                    if (isset($bufferFunctions[0])) {
97                        $bufferFunctions[0]->addArgumentChunk(PhpCode::convertString($value[1]));
98                    }
99                    break;
100
101                case T_STRING:
102                    if (isset($bufferFunctions[0])) {
103                        if (isset($constants[$value[1]])) {
104                            $bufferFunctions[0]->addArgumentChunk($constants[$value[1]]);
105                            break;
106                        }
107
108                        if (strtolower($value[1]) === 'null') {
109                            $bufferFunctions[0]->addArgumentChunk(null);
110                            break;
111                        }
112
113                        $bufferFunctions[0]->stopArgument();
114                    }
115
116                    //new function found
117                    for ($j = $k + 1; $j < $count; ++$j) {
118                        $nextToken = $this->tokens[$j];
119
120                        if (is_array($nextToken) && $nextToken[0] === T_COMMENT) {
121                            continue;
122                        }
123
124                        if ($nextToken === '(') {
125                            $newFunction = new ParsedFunction($value[1], $value[2]);
126
127                            // add comment that was on the line before.
128                            if (isset($bufferComments[0])) {
129                                $comment = $bufferComments[0];
130
131                                if ($comment->isRelatedWith($newFunction)) {
132                                    $newFunction->addComment($comment->getComment());
133                                }
134                            }
135
136                            array_unshift($bufferFunctions, $newFunction);
137                            $k = $j;
138                        }
139                        break;
140                    }
141                    break;
142
143                case T_COMMENT:
144                    $comment = $this->parsePhpComment($value[1], $value[2]);
145
146                    if ($comment) {
147                        array_unshift($bufferComments, $comment);
148
149                        // The comment is inside the function call.
150                        if (isset($bufferFunctions[0])) {
151                            $bufferFunctions[0]->addComment($comment->getComment());
152                        }
153                    }
154                    break;
155
156                default:
157                    if (isset($bufferFunctions[0])) {
158                        $bufferFunctions[0]->stopArgument();
159                    }
160                    break;
161            }
162        }
163
164        return $functions;
165    }
166
167    /**
168     * Extract the actual text from a PHP comment.
169     *
170     * If set, only returns comments that match the prefix(es).
171     *
172     * @param string $value The PHP comment.
173     * @param int $line Line number.
174     *
175     * @return null|ParsedComment Comment or null if comment extraction is disabled or if there is a prefix mismatch.
176     */
177    protected function parsePhpComment($value, $line)
178    {
179        if ($this->extractComments === false) {
180            return null;
181        }
182
183        //this returns a comment or null
184        $comment = ParsedComment::create($value, $line);
185
186        $prefixes = array_filter((array) $this->extractComments);
187
188        if ($comment && $comment->checkPrefixes($prefixes)) {
189            return $comment;
190        }
191
192        return null;
193    }
194}
195