1: <?php
2:
3: /*
4: * The MIT License (MIT)
5: *
6: * Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
7: *
8: * Permission is hereby granted, free of charge, to any person obtaining a copy of
9: * this software and associated documentation files (the "Software"), to deal in
10: * the Software without restriction, including without limitation the rights to
11: * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12: * the Software, and to permit persons to whom the Software is furnished to do so,
13: * subject to the following conditions:
14: *
15: * The above copyright notice and this permission notice shall be included in all
16: * copies or substantial portions of the Software.
17: *
18: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24: */
25:
26: namespace Kint;
27:
28: class CallFinder
29: {
30: private static $ignore = array(
31: T_CLOSE_TAG => true,
32: T_COMMENT => true,
33: T_DOC_COMMENT => true,
34: T_INLINE_HTML => true,
35: T_OPEN_TAG => true,
36: T_OPEN_TAG_WITH_ECHO => true,
37: T_WHITESPACE => true,
38: );
39:
40: /**
41: * Things we need to do specially for operator tokens:
42: * - Refuse to strip spaces around them
43: * - Wrap the access path in parentheses if there
44: * are any of these in the final short parameter.
45: */
46: private static $operator = array(
47: T_AND_EQUAL => true,
48: T_BOOLEAN_AND => true,
49: T_BOOLEAN_OR => true,
50: T_ARRAY_CAST => true,
51: T_BOOL_CAST => true,
52: T_CLONE => true,
53: T_CONCAT_EQUAL => true,
54: T_DEC => true,
55: T_DIV_EQUAL => true,
56: T_DOUBLE_CAST => true,
57: T_INC => true,
58: T_INCLUDE => true,
59: T_INCLUDE_ONCE => true,
60: T_INSTANCEOF => true,
61: T_INT_CAST => true,
62: T_IS_EQUAL => true,
63: T_IS_GREATER_OR_EQUAL => true,
64: T_IS_IDENTICAL => true,
65: T_IS_NOT_EQUAL => true,
66: T_IS_NOT_IDENTICAL => true,
67: T_IS_SMALLER_OR_EQUAL => true,
68: T_LOGICAL_AND => true,
69: T_LOGICAL_OR => true,
70: T_LOGICAL_XOR => true,
71: T_MINUS_EQUAL => true,
72: T_MOD_EQUAL => true,
73: T_MUL_EQUAL => true,
74: T_NEW => true,
75: T_OBJECT_CAST => true,
76: T_OR_EQUAL => true,
77: T_PLUS_EQUAL => true,
78: T_REQUIRE => true,
79: T_REQUIRE_ONCE => true,
80: T_SL => true,
81: T_SL_EQUAL => true,
82: T_SR => true,
83: T_SR_EQUAL => true,
84: T_STRING_CAST => true,
85: T_UNSET_CAST => true,
86: T_XOR_EQUAL => true,
87: '!' => true,
88: '%' => true,
89: '&' => true,
90: '*' => true,
91: '+' => true,
92: '-' => true,
93: '.' => true,
94: '/' => true,
95: ':' => true,
96: '<' => true,
97: '=' => true,
98: '>' => true,
99: '?' => true,
100: '^' => true,
101: '|' => true,
102: '~' => true,
103: );
104:
105: private static $strip = array(
106: '(' => true,
107: ')' => true,
108: '[' => true,
109: ']' => true,
110: '{' => true,
111: '}' => true,
112: T_OBJECT_OPERATOR => true,
113: T_DOUBLE_COLON => true,
114: T_NS_SEPARATOR => true,
115: );
116:
117: public static function getFunctionCalls($source, $line, $function)
118: {
119: static $up = array(
120: '(' => true,
121: '[' => true,
122: '{' => true,
123: T_CURLY_OPEN => true,
124: T_DOLLAR_OPEN_CURLY_BRACES => true,
125: );
126: static $down = array(
127: ')' => true,
128: ']' => true,
129: '}' => true,
130: );
131: static $modifiers = array(
132: '!' => true,
133: '@' => true,
134: '~' => true,
135: '+' => true,
136: '-' => true,
137: );
138: static $identifier = array(
139: T_DOUBLE_COLON => true,
140: T_STRING => true,
141: T_NS_SEPARATOR => true,
142: );
143:
144: if (KINT_PHP56) {
145: self::$operator[T_POW] = true;
146: self::$operator[T_POW_EQUAL] = true;
147: }
148:
149: if (KINT_PHP70) {
150: self::$operator[T_SPACESHIP] = true;
151: }
152:
153: if (KINT_PHP74) {
154: self::$operator[T_COALESCE_EQUAL] = true;
155: }
156:
157: $tokens = \token_get_all($source);
158: $cursor = 1;
159: $function_calls = array();
160: /** @var array<int, null|array|string> Performance optimization preventing backwards loops */
161: $prev_tokens = array(null, null, null);
162:
163: if (\is_array($function)) {
164: $class = \explode('\\', $function[0]);
165: $class = \strtolower(\end($class));
166: $function = \strtolower($function[1]);
167: } else {
168: $class = null;
169: $function = \strtolower($function);
170: }
171:
172: // Loop through tokens
173: foreach ($tokens as $index => $token) {
174: if (!\is_array($token)) {
175: continue;
176: }
177:
178: // Count newlines for line number instead of using $token[2]
179: // since certain situations (String tokens after whitespace) may
180: // not have the correct line number unless you do this manually
181: $cursor += \substr_count($token[1], "\n");
182: if ($cursor > $line) {
183: break;
184: }
185:
186: // Store the last real tokens for later
187: if (isset(self::$ignore[$token[0]])) {
188: continue;
189: }
190:
191: $prev_tokens = array($prev_tokens[1], $prev_tokens[2], $token);
192:
193: // Check if it's the right type to be the function we're looking for
194: if (T_STRING !== $token[0] || \strtolower($token[1]) !== $function) {
195: continue;
196: }
197:
198: // Check if it's a function call
199: $nextReal = self::realTokenIndex($tokens, $index);
200: if (!isset($nextReal, $tokens[$nextReal]) || '(' !== $tokens[$nextReal]) {
201: continue;
202: }
203:
204: // Check if it matches the signature
205: if (null === $class) {
206: if ($prev_tokens[1] && \in_array($prev_tokens[1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true)) {
207: continue;
208: }
209: } else {
210: if (!$prev_tokens[1] || T_DOUBLE_COLON !== $prev_tokens[1][0]) {
211: continue;
212: }
213:
214: if (!$prev_tokens[0] || T_STRING !== $prev_tokens[0][0] || \strtolower($prev_tokens[0][1]) !== $class) {
215: continue;
216: }
217: }
218:
219: $inner_cursor = $cursor;
220: $depth = 1; // The depth respective to the function call
221: $offset = $nextReal + 1; // The start of the function call
222: $instring = false; // Whether we're in a string or not
223: $realtokens = false; // Whether the current scope contains anything meaningful or not
224: $paramrealtokens = false; // Whether the current parameter contains anything meaningful
225: $params = array(); // All our collected parameters
226: $shortparam = array(); // The short version of the parameter
227: $param_start = $offset; // The distance to the start of the parameter
228:
229: // Loop through the following tokens until the function call ends
230: while (isset($tokens[$offset])) {
231: $token = $tokens[$offset];
232:
233: // Ensure that the $inner_cursor is correct and
234: // that $token is either a T_ constant or a string
235: if (\is_array($token)) {
236: $inner_cursor += \substr_count($token[1], "\n");
237: }
238:
239: if (!isset(self::$ignore[$token[0]]) && !isset($down[$token[0]])) {
240: $paramrealtokens = $realtokens = true;
241: }
242:
243: // If it's a token that makes us to up a level, increase the depth
244: if (isset($up[$token[0]])) {
245: if (1 === $depth) {
246: $shortparam[] = $token;
247: $realtokens = false;
248: }
249:
250: ++$depth;
251: } elseif (isset($down[$token[0]])) {
252: --$depth;
253:
254: // If this brings us down to the parameter level, and we've had
255: // real tokens since going up, fill the $shortparam with an ellipsis
256: if (1 === $depth) {
257: if ($realtokens) {
258: $shortparam[] = '...';
259: }
260: $shortparam[] = $token;
261: }
262: } elseif ('"' === $token[0]) {
263: // Strings use the same symbol for up and down, but we can
264: // only ever be inside one string, so just use a bool for that
265: if ($instring) {
266: --$depth;
267: if (1 === $depth) {
268: $shortparam[] = '...';
269: }
270: } else {
271: ++$depth;
272: }
273:
274: $instring = !$instring;
275:
276: $shortparam[] = '"';
277: } elseif (1 === $depth) {
278: if (',' === $token[0]) {
279: $params[] = array(
280: 'full' => \array_slice($tokens, $param_start, $offset - $param_start),
281: 'short' => $shortparam,
282: );
283: $shortparam = array();
284: $paramrealtokens = false;
285: $param_start = $offset + 1;
286: } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0] && \strlen($token[1]) > 2) {
287: $shortparam[] = $token[1][0].'...'.$token[1][0];
288: } else {
289: $shortparam[] = $token;
290: }
291: }
292:
293: // Depth has dropped to 0 (So we've hit the closing paren)
294: if ($depth <= 0) {
295: if ($paramrealtokens) {
296: $params[] = array(
297: 'full' => \array_slice($tokens, $param_start, $offset - $param_start),
298: 'short' => $shortparam,
299: );
300: }
301:
302: break;
303: }
304:
305: ++$offset;
306: }
307:
308: // If we're not passed (or at) the line at the end
309: // of the function call, we're too early so skip it
310: if ($inner_cursor < $line) {
311: continue;
312: }
313:
314: // Format the final output parameters
315: foreach ($params as &$param) {
316: $name = self::tokensFormatted($param['short']);
317: $expression = false;
318: foreach ($name as $token) {
319: if (self::tokenIsOperator($token)) {
320: $expression = true;
321: break;
322: }
323: }
324:
325: $param = array(
326: 'name' => self::tokensToString($name),
327: 'path' => self::tokensToString(self::tokensTrim($param['full'])),
328: 'expression' => $expression,
329: );
330: }
331:
332: // Get the modifiers
333: --$index;
334:
335: while (isset($tokens[$index])) {
336: if (!isset(self::$ignore[$tokens[$index][0]]) && !isset($identifier[$tokens[$index][0]])) {
337: break;
338: }
339:
340: --$index;
341: }
342:
343: $mods = array();
344:
345: while (isset($tokens[$index])) {
346: if (isset(self::$ignore[$tokens[$index][0]])) {
347: --$index;
348: continue;
349: }
350:
351: if (isset($modifiers[$tokens[$index][0]])) {
352: $mods[] = $tokens[$index];
353: --$index;
354: continue;
355: }
356:
357: break;
358: }
359:
360: $function_calls[] = array(
361: 'parameters' => $params,
362: 'modifiers' => $mods,
363: );
364: }
365:
366: return $function_calls;
367: }
368:
369: private static function realTokenIndex(array $tokens, $index)
370: {
371: ++$index;
372:
373: while (isset($tokens[$index])) {
374: if (!isset(self::$ignore[$tokens[$index][0]])) {
375: return $index;
376: }
377:
378: ++$index;
379: }
380:
381: return null;
382: }
383:
384: /**
385: * We need a separate method to check if tokens are operators because we
386: * occasionally add "..." to short parameter versions. If we simply check
387: * for `$token[0]` then "..." will incorrectly match the "." operator.
388: *
389: * @param array|string $token The token to check
390: *
391: * @return bool
392: */
393: private static function tokenIsOperator($token)
394: {
395: return '...' !== $token && isset(self::$operator[$token[0]]);
396: }
397:
398: private static function tokensToString(array $tokens)
399: {
400: $out = '';
401:
402: foreach ($tokens as $token) {
403: if (\is_string($token)) {
404: $out .= $token;
405: } elseif (\is_array($token)) {
406: $out .= $token[1];
407: }
408: }
409:
410: return $out;
411: }
412:
413: private static function tokensTrim(array $tokens)
414: {
415: foreach ($tokens as $index => $token) {
416: if (isset(self::$ignore[$token[0]])) {
417: unset($tokens[$index]);
418: } else {
419: break;
420: }
421: }
422:
423: $tokens = \array_reverse($tokens);
424:
425: foreach ($tokens as $index => $token) {
426: if (isset(self::$ignore[$token[0]])) {
427: unset($tokens[$index]);
428: } else {
429: break;
430: }
431: }
432:
433: return \array_reverse($tokens);
434: }
435:
436: private static function tokensFormatted(array $tokens)
437: {
438: $space = false;
439:
440: $tokens = self::tokensTrim($tokens);
441:
442: $output = array();
443: $last = null;
444:
445: foreach ($tokens as $index => $token) {
446: if (isset(self::$ignore[$token[0]])) {
447: if ($space) {
448: continue;
449: }
450:
451: $next = $tokens[self::realTokenIndex($tokens, $index)];
452:
453: if (isset(self::$strip[$last[0]]) && !self::tokenIsOperator($next)) {
454: continue;
455: }
456:
457: if (isset(self::$strip[$next[0]]) && $last && !self::tokenIsOperator($last)) {
458: continue;
459: }
460:
461: $token = ' ';
462: $space = true;
463: } else {
464: $space = false;
465: $last = $token;
466: }
467:
468: $output[] = $token;
469: }
470:
471: return $output;
472: }
473: }
474: