Completed
Push — master ( 895d73...5bc057 )
by Federico
02:36
created

MixinVisitor   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 280
rs 8.439
wmc 47
lcom 1
cbo 2

How to fix   Complexity   

Complex Class

Complex classes like MixinVisitor 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 MixinVisitor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Jade\Compiler;
4
5
use Jade\Nodes\Mixin;
6
7
abstract class MixinVisitor extends CodeVisitor
8
{
9
    protected function getMixinArgumentAssign($argument)
10
    {
11
        $argument = trim($argument);
12
13
        if (preg_match('`^[a-zA-Z][a-zA-Z0-9:_-]*\s*=`', $argument)) {
14
            return explode('=', $argument, 2);
15
        }
16
    }
17
18
    protected function parseMixinArguments(&$arguments, &$containsOnlyArrays, &$defaultAttributes)
19
    {
20
        $newArrayKey = null;
21
        $arguments = is_null($arguments) ? array() : explode(',', $arguments);
22
        foreach ($arguments as $key => &$argument) {
23
            if ($tab = $this->getMixinArgumentAssign($argument)) {
24
                if (is_null($newArrayKey)) {
25
                    $newArrayKey = $key;
26
                    $argument = array();
27
                } else {
28
                    unset($arguments[$key]);
29
                }
30
31
                $defaultAttributes[] = var_export($tab[0], true) . ' => ' . $tab[1];
32
                $arguments[$newArrayKey][$tab[0]] = static::decodeValue($tab[1]);
33
                continue;
34
            }
35
36
            $containsOnlyArrays = false;
37
            $newArrayKey = null;
38
        }
39
40
        return array_map(function ($argument) {
41
            if (is_array($argument)) {
42
                $argument = var_export($argument, true);
43
            }
44
45
            return $argument;
46
        }, $arguments);
47
    }
48
49
    protected function parseMixinStringAttribute($data)
50
    {
51
        $value = is_array($data['value'])
52
            ? preg_split('`\s+`', trim(implode(' ', $data['value'])))
53
            : trim($data['value']);
54
55
        return $data['escaped'] === true
56
            ? is_array($value)
57
                ? array_map('htmlspecialchars', $value)
58
                : htmlspecialchars($value)
59
            : $value;
60
    }
61
62
    protected function parseMixinAttribute($data)
63
    {
64
        if ($data['value'] === 'null' || $data['value'] === 'undefined' || is_null($data['value'])) {
65
            return;
66
        }
67
68
        if (is_bool($data['value'])) {
69
            return $data['value'];
70
        }
71
72
        return $this->parseMixinStringAttribute($data);
73
    }
74
75
    protected function parseMixinAttributes($attributes, $defaultAttributes, $mixinAttributes)
76
    {
77
        if (!count($attributes)) {
78
            return "(isset(\$attributes)) ? \$attributes : array($defaultAttributes)";
79
        }
80
81
        $parsedAttributes = array();
82
        foreach ($attributes as $data) {
83
            $parsedAttributes[$data['name']] = $this->parseMixinAttribute($data);
84
        }
85
86
        $attributes = var_export($parsedAttributes, true);
87
        $mixinAttributes = var_export(static::decodeAttributes($mixinAttributes), true);
88
89
        return "array_merge(\\Jade\\Compiler::withMixinAttributes($attributes, $mixinAttributes), (isset(\$attributes)) ? \$attributes : array($defaultAttributes))";
90
    }
91
92
    protected function renderClosureOpenning()
93
    {
94
        $arguments = func_get_args();
95
        $begin = array_shift($arguments);
96
        $begin = is_array($begin)
97
            ? $begin[0] . 'function ' . $begin[1]
98
            : $begin . 'function ';
99
        $params = implode(', ', array_map(function ($name) {
100
            return (substr($name, 0, 1) === '$' ? '' : '$') . $name;
101
        }, $arguments));
102
103
        if ($this->restrictedScope) {
104
            return $this->buffer($this->createCode($begin . '(' . $params . ') {'));
105
        }
106
107
        $params = '&$__varHandler, ' . $params;
108
109
        $this->buffer(
110
            $this->createCode($begin . '(' . $params . ') {') .
111
            $this->createCode($this->indent() . 'extract($__varHandler, EXTR_SKIP);')
112
        );
113
    }
114
115
    protected function renderClosureClosing($code, $arguments = array())
116
    {
117
        if (!$this->restrictedScope) {
118
            $arguments = array_filter(array_map(function ($argument) {
119
                $argument = explode('=', $argument);
120
                $argument = trim($argument[0]);
121
122
                return substr($argument, 0, 1) === '$'
123
                    ? substr($argument, 1)
124
                    : false;
125
            }, array_slice($arguments, 1)));
126
            $exception = count($arguments)
127
                ? ' && !in_array($key, ' . var_export($arguments, true) . ')'
128
                : '';
129
            $this->buffer($this->createCode(
130
                'foreach ($__varHandler as $key => &$val) {' .
131
                'if ($key !== \'__varHandler\'' . $exception . ') {' .
132
                '$val = ${$key};' .
133
                '}' .
134
                '}'
135
            ));
136
        }
137
138
        $this->buffer($this->createCode($code));
139
    }
140
141
    /**
142
     * @param Nodes\Mixin $mixin
143
     */
144
    protected function visitMixinCall(Mixin $mixin, $name, $blockName, $attributes)
145
    {
146
        $arguments = $mixin->arguments;
147
        $block = $mixin->block;
148
        $defaultAttributes = array();
149
        $containsOnlyArrays = true;
150
        $arguments = $this->parseMixinArguments($mixin->arguments, $containsOnlyArrays, $defaultAttributes);
151
152
        $defaultAttributes = implode(', ', $defaultAttributes);
153
        $attributes = $this->parseMixinAttributes($attributes, $defaultAttributes, $mixin->attributes);
154
155
        if ($block) {
156
            $this->renderClosureOpenning("\\Jade\\Compiler::recordMixinBlock($blockName, ", 'attributes');
157
            $this->visit($block);
158
            $this->renderClosureClosing('});');
159
        }
160
161
        $strings = array();
162
        $arguments = preg_replace_callback(
163
            '#([\'"])(.*(?!<\\\\)(?:\\\\{2})*)\\1#U',
164
            function ($match) use (&$strings) {
165
                $nextIndex = count($strings);
166
                $strings[] = $match[0];
167
168
                return 'stringToReplaceBy' . $nextIndex . 'ThCapture';
169
            },
170
            $arguments
171
        );
172
        $arguments = array_map(
173
            function ($arg) use ($strings) {
174
                return preg_replace_callback(
175
                    '#stringToReplaceBy([0-9]+)ThCapture#',
176
                    function ($match) use ($strings) {
177
                        return $strings[intval($match[1])];
178
                    },
179
                    $arg
180
                );
181
            },
182
            $arguments
183
        );
184
185
        array_unshift($arguments, $attributes);
186
        $arguments = array_filter($arguments, 'strlen');
187
        $statements = $this->apply('createStatements', $arguments);
188
189
        $variables = array_pop($statements);
190
        if ($mixin->call && $containsOnlyArrays) {
191
            array_splice($variables, 1, 0, array('null'));
192
        }
193
        $variables = implode(', ', $variables);
194
        array_push($statements, $variables);
195
196
        $arguments = $statements;
197
198
        $paramsPrefix = '';
199
        if (!$this->restrictedScope) {
200
            $this->buffer($this->createCode('$__varHandler = get_defined_vars();'));
201
            $paramsPrefix = '$__varHandler, ';
202
        }
203
        $codeFormat = str_repeat('%s;', count($arguments) - 1) . "{$name}({$paramsPrefix}%s)";
204
205
        array_unshift($arguments, $codeFormat);
206
207
        $this->buffer($this->apply('createCode', $arguments));
208
        if (!$this->restrictedScope) {
209
            $this->buffer(
210
                $this->createCode(
211
                    'extract(array_diff_key($__varHandler, array(\'__varHandler\' => 1, \'attributes\' => 1)));'
212
                )
213
            );
214
        }
215
216
        if ($block) {
217
            $code = $this->createCode("\\Jade\\Compiler::terminateMixinBlock($blockName);");
218
            $this->buffer($code);
219
        }
220
    }
221
222
    protected function visitMixinCodeAndBlock($name, $block, $arguments)
223
    {
224
        $this->renderClosureOpenning(
225
            $this->allowMixinOverride
226
                ? "{$name} = "
227
                : array("if(!function_exists('{$name}')) { ", $name),
228
            implode(',', $arguments)
229
        );
230
        $this->indents++;
231
        $this->visit($block);
232
        $this->indents--;
233
        $this->renderClosureClosing($this->allowMixinOverride ? '};' : '} }', $arguments);
234
    }
235
236
    /**
237
     * @param Nodes\Mixin $mixin
238
     */
239
    protected function visitMixinDeclaration(Mixin $mixin, $name)
240
    {
241
        $arguments = $mixin->arguments;
242
        $block = $mixin->block;
243
        $previousVisitedMixin = isset($this->visitedMixin) ? $this->visitedMixin : null;
244
        $this->visitedMixin = $mixin;
245
        if ($arguments === null || empty($arguments)) {
246
            $arguments = array();
247
        } elseif (!is_array($arguments)) {
248
            $arguments = array($arguments);
249
        }
250
251
        array_unshift($arguments, 'attributes');
252
        $arguments = implode(',', $arguments);
253
        $arguments = explode(',', $arguments);
254
        array_walk($arguments, array(get_class(), 'initArgToNull'));
255
        $this->visitMixinCodeAndBlock($name, $block, $arguments);
256
257
        if (is_null($previousVisitedMixin)) {
258
            unset($this->visitedMixin);
259
260
            return;
261
        }
262
263
        $this->visitedMixin = $previousVisitedMixin;
264
    }
265
266
    /**
267
     * @param Nodes\Mixin $mixin
268
     */
269
    protected function visitMixin(Mixin $mixin)
270
    {
271
        $name = strtr($mixin->name, '-', '_') . '_mixin';
272
        $blockName = var_export($mixin->name, true);
273
        if ($this->allowMixinOverride) {
274
            $name = '$GLOBALS[\'' . $name . '\']';
275
        }
276
        $attributes = static::decodeAttributes($mixin->attributes);
277
278
        if ($mixin->call) {
279
            $this->visitMixinCall($mixin, $name, $blockName, $attributes);
280
281
            return;
282
        }
283
284
        $this->visitMixinDeclaration($mixin, $name);
285
    }
286
}
287