Completed
Pull Request — master (#162)
by personal
10:09 queued 05:21
created

MethodExtractor   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 9
Bugs 3 Features 0
Metric Value
wmc 47
c 9
b 3
f 0
lcom 1
cbo 7
dl 0
loc 298
rs 8.439

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C extract() 0 78 8
A extractVisibility() 0 16 4
A extractState() 0 6 2
A extractContent() 0 8 2
C extractDependencies() 0 32 7
B extractCalls() 0 20 6
B extractReturns() 0 27 5
C extractUsage() 0 26 12

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/*
4
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Hal\Component\OOP\Extractor;
11
use Hal\Component\OOP\Reflected\MethodUsage;
12
use Hal\Component\OOP\Reflected\ReflectedArgument;
13
use Hal\Component\OOP\Reflected\ReflectedClass\ReflectedAnonymousClass;
14
use Hal\Component\OOP\Reflected\ReflectedMethod;
15
use Hal\Component\OOP\Reflected\ReflectedReturn;
16
use Hal\Component\OOP\Resolver\TypeResolver;
17
use Hal\Component\Token\TokenCollection;
18
19
20
/**
21
 * Extracts info about classes in one file
22
 * Remember that one file can contains multiple classes
23
 *
24
 * @author Jean-François Lépine <https://twitter.com/Halleck45>
25
 */
26
class MethodExtractor implements ExtractorInterface {
27
28
    /**
29
     * @var Searcher
30
     */
31
    private $searcher;
32
33
    /**
34
     * Constructor
35
     *
36
     * @param Searcher $searcher
37
     */
38
    public function __construct(Searcher $searcher)
39
    {
40
        $this->searcher = $searcher;
41
    }
42
43
    /**
44
     * Extract method from position
45
     *
46
     * @param int $n
47
     * @param TokenCollection$tokens
48
     * @return ReflectedMethod
49
     * @throws \Exception
50
     */
51
    public function extract(&$n, TokenCollection $tokens)
52
    {
53
        $start = $n;
54
55
        $declaration = $this->searcher->getUnder(array(')'), $n, $tokens);
56
        if(!preg_match('!function\s+(.*)\(\s*(.*)!is', $declaration, $matches)) {
57
            throw new \Exception(sprintf("Closure detected instead of method\nDetails:\n%s", $declaration));
58
        }
59
        list(, $name, $args) = $matches;
60
        $method = new ReflectedMethod($name);
61
62
        // visibility
63
        $this->extractVisibility($method, $p = $start, $tokens); // please keep "p = start"
64
65
        // state
66
        $this->extractState($method, $p = $start, $tokens); // please keep "p = start"
67
68
        $arguments = preg_split('!\s*,\s*!m', $args);
69
        foreach($arguments as $argDecl) {
70
71
            if(0 == strlen($argDecl)) {
72
                continue;
73
            }
74
75
            $elems = preg_split('!([\s=]+)!', $argDecl);
76
            $isRequired = 2 == sizeof($elems, COUNT_NORMAL);
77
78
            if(sizeof($elems, COUNT_NORMAL) == 1) {
79
                list($name, $type) = array_pad($elems, 2, null);
80
            } else {
81
                if('$' == $elems[0][0]) {
82
                    $name = $elems[0];
83
                    $type  = null;
84
                    $isRequired = false;
85
                } else {
86
                    list($type, $name) = array_pad($elems, 2, null);
87
                }
88
            }
89
90
            $argument = new ReflectedArgument($name, $type, $isRequired);
91
            $method->pushArgument($argument);
92
        }
93
94
95
96
        // does method has body ? (example: interface ; abstract classes)
97
        $p = $n  + 1;
98
        $underComma = trim($this->searcher->getUnder(array(';'), $p, $tokens));
99
        if(strlen($underComma) > 0) {
100
            //
101
            // Body
102
            $this->extractContent($method, $n, $tokens);
103
104
            // Calls
105
            $this->extractCalls($method, $n, $tokens);
106
107
            // Tokens
108
            $end = $this->searcher->getPositionOfClosingBrace($n, $tokens);
109
            if($end > 0) {
110
                $method->setTokens($tokens->extract($n, $end));
111
            }
112
        } else {
113
            $method->setTokens($tokens->extract(0, $n));
114
        }
115
116
        //
117
        // Dependencies
118
        $this->extractDependencies($method, 0, $method->getTokens());
0 ignored issues
show
Documentation introduced by
$method->getTokens() is of type array, but the function expects a object<Hal\Component\Token\TokenCollection>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
119
120
        // returns
121
        $p = $start;
122
        $this->extractReturns($method, $p, $tokens);
123
124
        // usage
125
        $this->extractUsage($method);
126
127
        return $method;
128
    }
129
130
    /**
131
     * Extracts visibility
132
     *
133
     * @param ReflectedMethod $method
134
     * @param $n
135
     * @param TokenCollection $tokens
136
     * @return $this
137
     */
138
    public function extractVisibility(ReflectedMethod $method, $n, TokenCollection $tokens) {
139
        switch(true) {
140
            case $this->searcher->isPrecededBy(T_PRIVATE, $n, $tokens, 4):
141
                $visibility = ReflectedMethod::VISIBILITY_PRIVATE;
142
                break;
143
            case $this->searcher->isPrecededBy(T_PROTECTED, $n, $tokens, 4):
144
                $visibility = ReflectedMethod::VISIBILITY_PROTECTED;
145
                break;
146
        case $this->searcher->isPrecededBy(T_PUBLIC, $n, $tokens, 4):
147
                default:
148
                $visibility = ReflectedMethod::VISIBILITY_PUBLIC;
149
                break;
150
        }
151
        $method->setVisibility($visibility);
152
        return $this;
153
    }
154
155
    /**
156
     * Extracts state
157
     *
158
     * @param ReflectedMethod $method
159
     * @param $n
160
     * @param TokenCollection $tokens
161
     * @return $this
162
     */
163
    public function extractState(ReflectedMethod $method, $n, TokenCollection $tokens) {
164
        if($this->searcher->isPrecededBy(T_STATIC, $n, $tokens, 4)) {
165
            $method->setState(ReflectedMethod::STATE_STATIC);
166
        }
167
        return $this;
168
    }
169
170
    /**
171
     * Extracts content of method
172
     *
173
     * @param ReflectedMethod $method
174
     * @param integer $n
175
     * @param TokenCollection $tokens
176
     * @return $this
177
     */
178
    private function extractContent(ReflectedMethod $method, $n, TokenCollection $tokens) {
179
        $end = $this->searcher->getPositionOfClosingBrace($n, $tokens);
180
        if($end > 0) {
181
            $collection = $tokens->extract($n, $end);
182
            $method->setContent($collection->asString());
183
        }
184
        return $this;
185
    }
186
187
    /**
188
     * Extracts content of method
189
     *
190
     * @param ReflectedMethod $method
191
     * @param integer $n
192
     * @param TokenCollection $tokens
193
     * @return $this
194
     */
195
    private function extractDependencies(ReflectedMethod $method, $n, TokenCollection $tokens) {
196
197
        //
198
        // Object creation
199
        $extractor = new CallExtractor($this->searcher);
200
        $start = $n;
201
        $len = sizeof($tokens, COUNT_NORMAL);
202
        for($i = $start; $i < $len; $i++) {
203
            $token = $tokens[$i];
204
            switch($token->getType()) {
205
                case T_PAAMAYIM_NEKUDOTAYIM:
206
                case T_NEW:
207
                    $call = $extractor->extract($i, $tokens);
208
                    if($call !== 'class') { // anonymous class
209
                        $method->pushDependency($call);
210
                        $method->pushInstanciedClass($call);
211
                    }
212
                    break;
213
            }
214
        }
215
216
        //
217
        // Parameters in Method API
218
        foreach($method->getArguments() as $argument) {
219
            $name = $argument->getType();
220
            if(!in_array($argument->getType(), array(null, 'array'))) {
221
                $method->pushDependency($name);
222
            }
223
        }
224
225
        return $this;
226
    }
227
228
    /**
229
     * Extracts calls of method
230
     *
231
     * @param ReflectedMethod $method
232
     * @param integer $n
233
     * @param TokenCollection $tokens
234
     * @return $this
235
     */
236
    private function extractCalls(ReflectedMethod $method, $n, TokenCollection $tokens) {
0 ignored issues
show
Unused Code introduced by
The parameter $n is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $tokens is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
237
238
        // $this->foo(), $c->foo()
239
        if(preg_match_all('!(\$[\w]*)\-\>(\w*?)\(!', $method->getContent(), $matches, PREG_SET_ORDER)) {
240
            foreach($matches as $m) {
241
                $function = $m[2];
242
                if('$this' == $m[1]) {
243
                    $method->pushInternalCall($function);
244
                } else {
245
                    $method->pushExternalCall($m[1], $function);
0 ignored issues
show
Unused Code introduced by
The call to ReflectedMethod::pushExternalCall() has too many arguments starting with $function.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
246
                }
247
            }
248
        }
249
        // (new X)->foo()
250
        if(preg_match_all('!\(new (\w+?).*?\)\->(\w+)\(!', $method->getContent(), $matches, PREG_SET_ORDER)) {
251
            foreach($matches as $m) {
252
                $method->pushExternalCall($m[1], $m[2]);
0 ignored issues
show
Unused Code introduced by
The call to ReflectedMethod::pushExternalCall() has too many arguments starting with $m[2].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
253
            }
254
        }
255
    }
256
257
    /**
258
     * Extract the list of returned values
259
     *
260
     * @param ReflectedMethod $method
261
     * @return $this
262
     */
263
    private function extractReturns(ReflectedMethod $method, $n, TokenCollection $tokens) {
264
265
        $resolver = new TypeResolver();
266
267
        // PHP 7
268
        // we cannot use specific token. The ":" delimiter is a T_STRING token
269
        $following = $this->searcher->getUnder(array('{'), $n, $tokens);
270
        if(preg_match('!:(.*)!', $following, $matches)) {
271
            $type = trim($matches[1]);
272
            if(empty($type)) {
273
                return $this;
274
            }
275
            $return = new ReflectedReturn($type, ReflectedReturn::VALUE_UNKNOW, ReflectedReturn::STRICT_TYPE_HINT);
276
            $method->pushReturn($return);
277
            return $this;
278
        }
279
280
        // array of available values based on code
281
        if(preg_match_all('!([\s;]return\s|^return\s+)(.*?);!', $method->getContent(), $matches)) {
282
            foreach($matches[2] as $m) {
283
                $value = trim($m, ";\t\n\r\0\x0B");
284
                $return = new ReflectedReturn($resolver->resolve($m), $value, ReflectedReturn::ESTIMATED_TYPE_HINT);
285
                $method->pushReturn($return);
286
            }
287
        }
288
        return $this;
289
    }
290
291
    /**
292
     * Extracts usage of method
293
     *
294
     * @param ReflectedMethod $method
295
     * @return $this
296
     */
297
    private function extractUsage(ReflectedMethod $method) {
298
        $tokens = $method->getTokens();
299
        $codes = $values = array();
300
        foreach($tokens as $token) {
301
            if(in_array($token->getType(), array(T_WHITESPACE, T_BOOL_CAST, T_INT_CAST, T_STRING_CAST, T_DOUBLE_CAST, T_OBJECT_CAST))) {
302
                continue;
303
            }
304
            array_push($codes, $token->getType());
305
            array_push($values, $token->getValue());
306
        }
307
        switch(true) {
308
            case preg_match('!^(get)|(is)|(has).*!',$method->getName()) && $codes == array(T_RETURN, T_VARIABLE, T_OBJECT_OPERATOR, T_STRING, T_STRING):
309
                $method->setUsage(MethodUsage::USAGE_GETTER);
310
                break;
311
            // basic setter
312
            case preg_match('!^set.*!',$method->getName()) && $codes == array(T_VARIABLE, T_OBJECT_OPERATOR,T_STRING,T_STRING, T_VARIABLE, T_STRING) && $values[3] == '=':
313
            // fluent setter
314
            case preg_match('!^set.*!',$method->getName()) && $codes == array(T_VARIABLE, T_OBJECT_OPERATOR,T_STRING,T_STRING, T_VARIABLE, T_STRING, T_RETURN, T_VARIABLE, T_STRING)
315
                && $values[3] == '=' && $values[7] == '$this':
316
                $method->setUsage(MethodUsage::USAGE_SETTER);
317
                break;
318
            default:
319
                $method->setUsage(MethodUsage::USAGE_UNKNWON);
320
        }
321
        return $this;
322
    }
323
}
324