GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Parser   F
last analyzed

Complexity

Total Complexity 104

Size/Duplication

Total Lines 528
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 104
lcom 1
cbo 2
dl 0
loc 528
ccs 189
cts 189
cp 1
rs 2
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getPartialBlock() 0 9 2
A getBlockParams() 0 8 2
A getLiteral() 0 4 3
F getExpression() 0 83 24
B parse() 0 21 10
A getPartialName() 0 7 3
A subexpression() 0 12 4
A isSubExp() 0 4 4
D advancedVariable() 0 69 20
B detectQuote() 0 37 8
F analyze() 0 83 24

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
4
MIT License
5
Copyright 2013-2020 Zordius Chen. All Rights Reserved.
6
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9
10
Origin: https://github.com/zordius/lightncandy
11
*/
12
13
/**
14
 * file to keep LightnCandy Parser
15
 *
16
 * @package    LightnCandy
17
 * @author     Zordius <[email protected]>
18
 */
19
20
namespace LightnCandy;
21
22
/**
23
 * LightnCandy Parser
24
 */
25
class Parser extends Token
26
{
27
    // Compile time error handling flags
28
    const BLOCKPARAM = 9999;
29
    const PARTIALBLOCK = 9998;
30
    const LITERAL = -1;
31
    const SUBEXP = -2;
32
33
    /**
34
     * Get partial block id and fix the variable list
35
     *
36
     * @param array<boolean|integer|string|array> $vars parsed token
37
     *
38
     * @return integer Return partial block id
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|integer|string|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
39
     *
40
     */
41 110
    public static function getPartialBlock(&$vars)
42
    {
43 110
        if (isset($vars[static::PARTIALBLOCK])) {
44 27
            $id = $vars[static::PARTIALBLOCK];
45 27
            unset($vars[static::PARTIALBLOCK]);
46 27
            return $id;
47
        }
48 100
        return 0;
49
    }
50
51
    /**
52
     * Get block params and fix the variable list
53
     *
54
     * @param array<boolean|integer|string|array> $vars parsed token
55
     *
56
     * @return array<string>|null Return list of block params or null
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|integer|string|array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
57
     *
58
     */
59 243
    public static function getBlockParams(&$vars)
60
    {
61 243
        if (isset($vars[static::BLOCKPARAM])) {
62 11
            $list = $vars[static::BLOCKPARAM];
63 11
            unset($vars[static::BLOCKPARAM]);
64 11
            return $list;
65
        }
66 235
    }
67
68
    /**
69
     * Return array presentation for a literal
70
     *
71
     * @param string $name variable name.
72
     * @param boolean $asis keep the name as is or not
73
     * @param boolean $quote add single quote or not
74
     *
75
     * @return array<integer|string> Return variable name array
76
     *
77
     */
78 124
    protected static function getLiteral($name, $asis, $quote = false)
79
    {
80 124
        return $asis ? array($name) : array(static::LITERAL, $quote ? "'$name'" : $name);
81
    }
82
83
    /**
84
     * Return array presentation for an expression
85
     *
86
     * @param string $v analyzed expression names.
87
     * @param array<string,array|string|integer> $context Current compile content.
88
     * @param integer $pos expression position
89
     *
90
     * @return array<integer,string> Return variable name array
91
     *
92
     * @expect array('this') when input 'this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0)), 0
93
     * @expect array() when input 'this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1)), 0
94
     * @expect array(1) when input '..', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
95
     * @expect array(1) when input '../', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
96
     * @expect array(1) when input '../.', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
97
     * @expect array(1) when input '../this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
98
     * @expect array(1, 'a') when input '../a', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
99
     * @expect array(2, 'a', 'b') when input '../../a.b', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
100
     * @expect array(2, '[a]', 'b') when input '../../[a].b', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
101
     * @expect array(2, 'a', 'b') when input '../../[a].b', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
102
     * @expect array(0, 'id') when input 'this.id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
103
     * @expect array('this', 'id') when input 'this.id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
104
     * @expect array(0, 'id') when input './id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
105
     * @expect array(\LightnCandy\Parser::LITERAL, '\'a.b\'') when input '"a.b"', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
106
     * @expect array(\LightnCandy\Parser::LITERAL, '123') when input '123', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
107
     * @expect array(\LightnCandy\Parser::LITERAL, 'null') when input 'null', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
108
     */
109 738
    protected static function getExpression($v, &$context, $pos)
110
    {
111 738
        $asis = ($pos === 0);
112
113
        // handle number
114 738
        if (is_numeric($v)) {
115 35
            return static::getLiteral(strval(1 * $v), $asis);
116
        }
117
118
        // handle double quoted string
119 732
        if (preg_match('/^"(.*)"$/', $v, $matched)) {
120 69
            return static::getLiteral(preg_replace('/([^\\\\])\\\\\\\\"/', '$1"', preg_replace('/^\\\\\\\\"/', '"', $matched[1])), $asis, true);
121
        }
122
123
        // handle single quoted string
124 726
        if (preg_match('/^\\\\\'(.*)\\\\\'$/', $v, $matched)) {
125 21
            return static::getLiteral($matched[1], $asis, true);
126
        }
127
128
        // handle boolean, null and undefined
129 723
        if (preg_match('/^(true|false|null|undefined)$/', $v)) {
130 29
            return static::getLiteral($v, $asis);
131
        }
132
133 718
        $ret = array();
134 718
        $levels = 0;
135
136
        // handle ..
137 718
        if ($v === '..') {
138 7
            $v = '../';
139
        }
140
141
        // Trace to parent for ../ N times
142
        $v = preg_replace_callback('/\\.\\.\\//', function () use (&$levels) {
143 48
            $levels++;
144 48
            return '';
145 718
        }, trim($v));
146
147
        // remove ./ in path
148 718
        $v = preg_replace('/\\.\\//', '', $v, -1, $scoped);
149
150 718
        $strp = (($pos !== 0) && $context['flags']['strpar']);
151 718
        if ($levels && !$strp) {
152 45
            $ret[] = $levels;
153 45
            if (!$context['flags']['parent']) {
154 4
                $context['error'][] = 'Do not support {{../var}}, you should do compile with LightnCandy::FLAG_PARENT flag';
155
            }
156 45
            $context['usedFeature']['parent'] ++;
157
        }
158
159 718
        if ($context['flags']['advar'] && preg_match('/\\]/', $v)) {
160 30
            preg_match_all(static::VARNAME_SEARCH, $v, $matchedall);
161
        } else {
162 694
            preg_match_all('/([^\\.\\/]+)/', $v, $matchedall);
163
        }
164
165 718
        if ($v !== '.') {
166 712
            $vv = implode('.', $matchedall[1]);
167 712
            if (strlen($v) !== strlen($vv)) {
168 8
                $context['error'][] = "Unexpected charactor in '$v' ! (should it be '$vv' ?)";
169
            }
170
        }
171
172 718
        foreach ($matchedall[1] as $m) {
173 711
            if ($context['flags']['advar'] && substr($m, 0, 1) === '[') {
174 24
                $ret[] = substr($m, 1, -1);
175 705
            } elseif ((!$context['flags']['this'] || ($m !== 'this')) && ($m !== '.')) {
176 703
                $ret[] = $m;
177
            } else {
178 17
                $scoped++;
179
            }
180
        }
181
182 718
        if ($strp) {
183 10
            return array(static::LITERAL, "'" . implode('.', $ret) . "'");
184
        }
185
186 718
        if (($scoped > 0) && ($levels === 0) && (count($ret) > 0)) {
187 9
            array_unshift($ret, 0);
188
        }
189
190 718
        return $ret;
191
    }
192
193
    /**
194
     * Parse the token and return parsed result.
195
     *
196
     * @param array<string> $token preg_match results
197
     * @param array<string,array|string|integer> $context current compile context
198
     *
199
     * @return array<boolean|integer|array> Return parsed result
200
     *
201
     * @expect array(false, array(array())) when input array(0,0,0,0,0,0,0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
202
     * @expect array(true, array(array())) when input array(0,0,0,'{{',0,'{',0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
203
     * @expect array(true, array(array())) when input array(0,0,0,0,0,0,0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 1), 'rawblock' => false)
204
     * @expect array(false, array(array('a'))) when input array(0,0,0,0,0,0,0,'a'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
205
     * @expect array(false, array(array('a'), array('b'))) when input array(0,0,0,0,0,0,0,'a  b'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
206
     * @expect array(false, array(array('a'), array('"b'), array('c"'))) when input array(0,0,0,0,0,0,0,'a "b c"'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
207
     * @expect array(false, array(array('a'), array(-1, '\'b c\''))) when input array(0,0,0,0,0,0,0,'a "b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
208
     * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
209
     * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
210
     * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
211
     * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
212
     * @expect array(false, array(array('a'), 'q' => array('b c'))) when input array(0,0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
213
     * @expect array(false, array(array('a'), array('q=[b c'))) when input array(0,0,0,0,0,0,0,'a [q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
214
     * @expect array(false, array(array('a'), 'q' => array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
215
     * @expect array(false, array(array('a'), 'q' => array('b'), array('c'))) when input array(0,0,0,0,0,0,0,'a [q]=b c'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
216
     * @expect array(false, array(array('a'), 'q' => array(-1, '\'b c\''))) when input array(0,0,0,0,0,0,0,'a q="b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
217
     * @expect array(false, array(array(-2, array(array('foo'), array('bar')), '(foo bar)'))) when input array(0,0,0,0,0,0,0,'(foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 1, 'lambda' => 0), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'rawblock' => false)
218
     * @expect array(false, array(array('foo'), array("'=='"), array('bar'))) when input array(0,0,0,0,0,0,0,"foo '==' bar"), array('flags' => array('strpar' => 0, 'advar' => 1, 'namev' => 1, 'noesc' => 0, 'this' => 0), 'rawblock' => false)
219
     * @expect array(false, array(array(-2, array(array('foo'), array('bar')), '( foo bar)'))) when input array(0,0,0,0,0,0,0,'( foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 1, 'lambda' => 0), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'rawblock' => false)
220
     * @expect array(false, array(array('a'), array(-1, '\' b c\''))) when input array(0,0,0,0,0,0,0,'a " b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
221
     * @expect array(false, array(array('a'), 'q' => array(-1, '\' b c\''))) when input array(0,0,0,0,0,0,0,'a q=" b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
222
     * @expect array(false, array(array('foo'), array(-1, "' =='"), array('bar'))) when input array(0,0,0,0,0,0,0,"foo \' ==\' bar"), array('flags' => array('strpar' => 0, 'advar' => 1, 'namev' => 1, 'noesc' => 0, 'this' => 0), 'rawblock' => false)
223
     * @expect array(false, array(array('a'), array(' b c'))) when input array(0,0,0,0,0,0,0,'a [ b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
224
     * @expect array(false, array(array('a'), 'q' => array(-1, "' d e'"))) when input array(0,0,0,0,0,0,0,"a q=\' d e\'"), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
225
     * @expect array(false, array('q' => array(-2, array(array('foo'), array('bar')), '( foo bar)'))) when input array(0,0,0,0,0,0,0,'q=( foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false, 'helperresolver' => 0)
226
     * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','foo'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
227
     * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','"foo"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
228
     * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','[foo] '), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
229
     * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','\\\'foo\\\''), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
230
     */
231 744
    public static function parse(&$token, &$context)
232
    {
233 744
        $vars = static::analyze($token[static::POS_INNERTAG], $context);
234 744
        if ($token[static::POS_OP] === '>') {
235 108
            $fn = static::getPartialName($vars);
236 709
        } elseif ($token[static::POS_OP] === '#*') {
237 16
            $fn = static::getPartialName($vars, 1);
238
        }
239
240 744
        $avars = static::advancedVariable($vars, $context, static::toString($token));
241
242 744
        if (isset($fn) && ($fn !== null)) {
243 106
            if ($token[static::POS_OP] === '>') {
244 101
                $avars[0] = $fn;
245 15
            } elseif ($token[static::POS_OP] === '#*') {
246 15
                $avars[1] = $fn;
247
            }
248
        }
249
250 744
        return array(($token[static::POS_BEGINRAW] === '{') || ($token[static::POS_OP] === '&') || $context['flags']['noesc'] || $context['rawblock'], $avars);
251
    }
252
253
    /**
254
     * Get partial name from "foo" or [foo] or \'foo\'
255
     *
256
     * @param array<boolean|integer|array> $vars parsed token
257
     * @param integer $pos position of partial name
258
     *
259
     * @return array<string>|null Return one element partial name array
260
     *
261
     * @expect null when input array()
262
     * @expect array('foo') when input array('foo')
263
     * @expect array('foo') when input array('"foo"')
264
     * @expect array('foo') when input array('[foo]')
265
     * @expect array('foo') when input array("\\'foo\\'")
266
     * @expect array('foo') when input array(0, 'foo'), 1
267
     */
268 114
    public static function getPartialName(&$vars, $pos = 0)
269
    {
270 114
        if (!isset($vars[$pos])) {
271 2
            return;
272
        }
273 113
        return preg_match(SafeString::IS_SUBEXP_SEARCH, $vars[$pos]) ? null : array(preg_replace('/^("(.+)")|(\\[(.+)\\])|(\\\\\'(.+)\\\\\')$/', '$2$4$6', $vars[$pos]));
274
    }
275
276
    /**
277
     * Parse a subexpression then return parsed result.
278
     *
279
     * @param string $expression the full string of a sub expression
280
     * @param array<string,array|string|integer> $context current compile context
281
     *
282
     * @return array<boolean|integer|array> Return parsed result
283
     *
284
     * @expect array(\LightnCandy\Parser::SUBEXP, array(array('a'), array('b')), '(a b)') when input '(a b)', array('usedFeature' => array('subexp' => 0), 'flags' => array('advar' => 0, 'namev' => 0, 'this' => 0, 'exhlp' => 1, 'strpar' => 0))
285
     */
286 49
    public static function subexpression($expression, &$context)
287
    {
288 49
        $context['usedFeature']['subexp']++;
289 49
        $vars = static::analyze(substr($expression, 1, -1), $context);
290 49
        $avars = static::advancedVariable($vars, $context, $expression);
291 49
        if (isset($avars[0][0]) && !$context['flags']['exhlp']) {
292 26
            if (!Validator::helper($context, $avars, true)) {
293 2
                $context['error'][] = "Can not find custom helper function defination {$avars[0][0]}() !";
294
            }
295
        }
296 49
        return array(static::SUBEXP, $avars, $expression);
297
    }
298
299
    /**
300
     * Check a parsed result is a subexpression or not
301
     *
302
     * @param array<string|integer|array> $var
303
     *
304
     * @return boolean return true when input is a subexpression
305
     *
306
     * @expect false when input 0
307
     * @expect false when input array()
308
     * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0)
309
     * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0, 0)
310
     * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0, '', 0)
311
     * @expect true when input array(\LightnCandy\Parser::SUBEXP, 0, '')
312
     */
313 435
    public static function isSubExp($var)
314
    {
315 435
        return is_array($var) && (count($var) === 3) && ($var[0] === static::SUBEXP) && is_string($var[2]);
316
    }
317
318
    /**
319
     * Analyze parsed token for advanved variables.
320
     *
321
     * @param array<boolean|integer|array> $vars parsed token
322
     * @param array<string,array|string|integer> $context current compile context
323
     * @param string $token original token
324
     *
325
     * @return array<boolean|integer|array> Return parsed result
326
     *
327
     * @expect array(array('this')) when input array('this'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0,)), 0
328
     * @expect array(array()) when input array('this'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 1)), 0
329
     * @expect array(array('a')) when input array('a'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
330
     * @expect array(array('a'), array('b')) when input array('a', 'b'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
331
     * @expect array('a' => array('b')) when input array('a=b'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
332
     * @expect array('fo o' => array(\LightnCandy\Parser::LITERAL, '123')) when input array('[fo o]=123'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0)), 0
333
     * @expect array('fo o' => array(\LightnCandy\Parser::LITERAL, '\'bar\'')) when input array('[fo o]="bar"'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0)), 0
334
     */
335 744
    protected static function advancedVariable($vars, &$context, $token)
336
    {
337 744
        $ret = array();
338 744
        $i = 0;
339 744
        foreach ($vars as $idx => $var) {
340
            // handle (...)
341 738
            if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
342 41
                $ret[$i] = static::subexpression($var, $context);
343 41
                $i++;
344 41
                continue;
345
            }
346
347
            // handle |...|
348 738
            if (preg_match(SafeString::IS_BLOCKPARAM_SEARCH, $var, $matched)) {
349 11
                $ret[static::BLOCKPARAM] = explode(' ', $matched[1]);
350 11
                continue;
351
            }
352
353 738
            if ($context['flags']['namev']) {
354 505
                if (preg_match('/^((\\[([^\\]]+)\\])|([^=^["\']+))=(.+)$/', $var, $m)) {
355 42
                    if (!$context['flags']['advar'] && $m[3]) {
356 1
                        $context['error'][] = "Wrong argument name as '[$m[3]]' in $token ! You should fix your template or compile with LightnCandy::FLAG_ADVARNAME flag.";
357
                    }
358 42
                    $idx = $m[3] ? $m[3] : $m[4];
359 42
                    $var = $m[5];
360
                    // handle foo=(...)
361 42
                    if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
362 8
                        $ret[$idx] = static::subexpression($var, $context);
363 8
                        continue;
364
                    }
365
                }
366
            }
367
368 738
            if ($context['flags']['advar'] && !preg_match("/^(\"|\\\\')(.*)(\"|\\\\')$/", $var)) {
369
                // foo]  Rule 1: no starting [ or [ not start from head
370 521
                if (preg_match('/^[^\\[\\.]+[\\]\\[]/', $var)
371
                    // [bar  Rule 2: no ending ] or ] not in the end
372 515
                    || preg_match('/[\\[\\]][^\\]\\.]+$/', $var)
373
                    // ]bar. Rule 3: middle ] not before .
374 513
                    || preg_match('/\\][^\\]\\[\\.]+\\./', $var)
375
                    // .foo[ Rule 4: middle [ not after .
376 521
                    || preg_match('/\\.[^\\]\\[\\.]+\\[/', preg_replace('/^(..\\/)+/', '', preg_replace('/\\[[^\\]]+\\]/', '[XXX]', $var)))
377
                ) {
378 12
                    $context['error'][] = "Wrong variable naming as '$var' in $token !";
379
                } else {
380 509
                    $name = preg_replace('/(\\[.+?\\])/', '', $var);
381
                    // Scan for invalid charactors which not be protected by [ ]
382
                    // now make ( and ) pass, later fix
383 509
                    if (preg_match('/[!"#%\'*+,;<=>{|}~]/', $name)) {
384 3
                        if (!$context['flags']['namev'] && preg_match('/.+=.+/', $name)) {
385 1
                            $context['error'][] = "Wrong variable naming as '$var' in $token ! If you try to use foo=bar param, you should enable LightnCandy::FLAG_NAMEDARG !";
386
                        } else {
387 2
                            $context['error'][] = "Wrong variable naming as '$var' in $token ! You should wrap ! \" # % & ' * + , ; < = > { | } ~ into [ ]";
388
                        }
389
                    }
390
                }
391
            }
392
393 738
            $var = static::getExpression($var, $context, $idx);
394
395 738
            if (is_string($idx)) {
396 36
                $ret[$idx] = $var;
397
            } else {
398 737
                $ret[$i] = $var;
399 737
                $i++;
400
            }
401
        }
402 744
        return $ret;
403
    }
404
405
    /**
406
     * Detect quote charactors
407
     *
408
     * @param string $string the string to be detect the quote charactors
409
     *
410
     * @return array<string,integer>|null Expected ending string when quote charactor be detected
411
     */
412 533
    protected static function detectQuote($string)
413
    {
414
        // begin with '(' without ending ')'
415 533
        if (preg_match('/^\([^\)]*$/', $string)) {
416 34
            return array(')', 1);
417
        }
418
419
        // begin with '"' without ending '"'
420 533
        if (preg_match('/^"[^"]*$/', $string)) {
421 10
            return array('"', 0);
422
        }
423
424
        // begin with \' without ending '
425 533
        if (preg_match('/^\\\\\'[^\']*$/', $string)) {
426 7
            return array('\'', 0);
427
        }
428
429
        // '="' exists without ending '"'
430 533
        if (preg_match('/^[^"]*="[^"]*$/', $string)) {
431 3
            return array('"', 0);
432
        }
433
434
        // '[' exists without ending ']'
435 533
        if (preg_match('/^([^"\'].+)?\\[[^\\]]*$/', $string)) {
436 9
            return array(']', 0);
437
        }
438
439
        // =\' exists without ending '
440 527
        if (preg_match('/^[^\']*=\\\\\'[^\']*$/', $string)) {
441 2
            return array('\'', 0);
442
        }
443
444
        // continue to next match when =( exists without ending )
445 527
        if (preg_match('/.+(\(+)[^\)]*$/', $string, $m)) {
446 6
            return array(')', strlen($m[1]));
447
        }
448 527
    }
449
450
    /**
451
     * Analyze a token string and return parsed result.
452
     *
453
     * @param string $token preg_match results
454
     * @param array<string,array|string|integer> $context current compile context
455
     *
456
     * @return array<boolean|integer|array> Return parsed result
457
     *
458
     * @expect array('foo', 'bar') when input 'foo bar', array('flags' => array('advar' => 1))
459
     * @expect array('foo', "'bar'") when input "foo 'bar'", array('flags' => array('advar' => 1))
460
     * @expect array('[fo o]', '"bar"') when input '[fo o] "bar"', array('flags' => array('advar' => 1))
461
     * @expect array('fo=123', 'bar="45', '6"') when input 'fo=123 bar="45 6"', array('flags' => array('advar' => 0))
462
     * @expect array('fo=123', 'bar="45 6"') when input 'fo=123 bar="45 6"', array('flags' => array('advar' => 1))
463
     * @expect array('[fo', 'o]=123') when input '[fo o]=123', array('flags' => array('advar' => 0))
464
     * @expect array('[fo o]=123') when input '[fo o]=123', array('flags' => array('advar' => 1))
465
     * @expect array('[fo o]=123', 'bar="456"') when input '[fo o]=123 bar="456"', array('flags' => array('advar' => 1))
466
     * @expect array('[fo o]="1 2 3"') when input '[fo o]="1 2 3"', array('flags' => array('advar' => 1))
467
     * @expect array('foo', 'a=(foo a=(foo a="ok"))') when input 'foo a=(foo a=(foo a="ok"))', array('flags' => array('advar' => 1))
468
     */
469 744
    protected static function analyze($token, &$context)
470
    {
471 744
        $count = preg_match_all('/(\s*)([^\s]+)/', $token, $matchedall);
472
        // Parse arguments and deal with "..." or [...] or (...) or \'...\' or |...|
473 744
        if (($count > 0) && $context['flags']['advar']) {
474 534
            $vars = array();
475 534
            $prev = '';
476 534
            $expect = 0;
477 534
            $quote = 0;
478 534
            $stack = 0;
479
480 534
            foreach ($matchedall[2] as $index => $t) {
481 534
                $detected = static::detectQuote($t);
482
483 534
                if ($expect === ')') {
484 40
                    if ($detected && ($detected[0] !== ')')) {
485 6
                        $quote = $detected[0];
486
                    }
487 40
                    if (substr($t, -1, 1) === $quote) {
488 3
                        $quote = 0;
489
                    }
490
                }
491
492
                // continue from previous match when expect something
493 534
                if ($expect) {
494 62
                    $prev .= "{$matchedall[1][$index]}$t";
495 62
                    if (($quote === 0) && ($stack > 0) && preg_match('/(.+=)*(\\(+)/', $t, $m)) {
496 12
                        $stack += strlen($m[2]);
497
                    }
498
                    // end an argument when end with expected charactor
499 62
                    if (substr($t, -1, 1) === $expect) {
500 62
                        if ($stack > 0) {
501 51
                            preg_match('/(\\)+)$/', $t, $matchedq);
502 51
                            $stack -= isset($matchedq[0]) ? strlen($matchedq[0]) : 1;
503 51
                            if ($stack > 0) {
504 4
                                continue;
505
                            }
506 51
                            if ($stack < 0) {
507 1
                                $context['error'][] = "Unexcepted ')' in expression '$token' !!";
508 1
                                $expect = 0;
509 1
                                break;
510
                            }
511
                        }
512 61
                        $vars[] = $prev;
513 61
                        $prev = '';
514 61
                        $expect = 0;
515 61
                        continue;
516 34
                    } elseif (($expect == ']') && (strpos($t, $expect) !== false)) {
517 1
                        $t = $prev;
518 1
                        $detected = static::detectQuote($t);
519 1
                        $expect = 0;
520
                    } else {
521 34
                        continue;
522
                    }
523
                }
524
525
526 534
                if ($detected) {
527 57
                    $prev = $t;
528 57
                    $expect = $detected[0];
529 57
                    $stack = $detected[1];
530 57
                    continue;
531
                }
532
533
                // continue to next match when 'as' without ending '|'
534 524
                if (($t === 'as') && (count($vars) > 0)) {
535 11
                    $prev = '';
536 11
                    $expect = '|';
537 11
                    $stack=1;
538 11
                    continue;
539
                }
540
541 524
                $vars[] = $t;
542
            }
543
544 534
            if ($expect) {
545 6
                $context['error'][] = "Error in '$token': expect '$expect' but the token ended!!";
546
            }
547
548 534
            return $vars;
549
        }
550 234
        return ($count > 0) ? $matchedall[2] : explode(' ', $token);
551
    }
552
}
553