MethodExtractor   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 11
Bugs 4 Features 0
Metric Value
wmc 48
c 11
b 4
f 0
lcom 1
cbo 7
dl 0
loc 302
rs 8.4865

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A extractVisibility() 0 16 4
A extractState() 0 6 2
A extractContent() 0 8 2
C extractDependencies() 0 33 8
B extractCalls() 0 20 6
C extract() 0 78 8
B extractReturns() 0 28 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;
14
use Hal\Component\OOP\Reflected\ReflectedClass\ReflectedAnonymousClass;
15
use Hal\Component\OOP\Reflected\ReflectedMethod;
16
use Hal\Component\OOP\Reflected\ReflectedReturn;
17
use Hal\Component\OOP\Resolver\TypeResolver;
18
use Hal\Component\Token\TokenCollection;
19
20
21
/**
22
 * Extracts info about classes in one file
23
 * Remember that one file can contains multiple classes
24
 *
25
 * @author Jean-François Lépine <https://twitter.com/Halleck45>
26
 */
27
class MethodExtractor implements ExtractorInterface {
28
29
    /**
30
     * @var Searcher
31
     */
32
    private $searcher;
33
34
    /**
35
     * Constructor
36
     *
37
     * @param Searcher $searcher
38
     */
39
    public function __construct(Searcher $searcher)
40
    {
41
        $this->searcher = $searcher;
42
    }
43
44
    /**
45
     * Extract method from position
46
     *
47
     * @param int $n
48
     * @param TokenCollection$tokens
49
     * @return ReflectedMethod
50
     * @param ReflectedClass
51
     * @throws \Exception
52
     */
53
    public function extract(&$n, TokenCollection $tokens, $currentClass = null)
54
    {
55
        $start = $n;
56
57
        $declaration = $this->searcher->getUnder(array(')'), $n, $tokens);
58
        if(!preg_match('!function\s+(.*)\(\s*(.*)!is', $declaration, $matches)) {
59
            throw new \Exception(sprintf("Closure detected instead of method\nDetails:\n%s", $declaration));
60
        }
61
        list(, $name, $args) = $matches;
62
        $method = new ReflectedMethod($name);
63
64
        // visibility
65
        $this->extractVisibility($method, $p = $start, $tokens); // please keep "p = start"
66
67
        // state
68
        $this->extractState($method, $p = $start, $tokens); // please keep "p = start"
69
70
        $arguments = preg_split('!\s*,\s*!m', $args);
71
        foreach($arguments as $argDecl) {
72
73
            if(0 == strlen($argDecl)) {
74
                continue;
75
            }
76
77
            $elems = preg_split('!([\s=]+)!', $argDecl);
78
            $isRequired = 2 == sizeof($elems, COUNT_NORMAL);
79
80
            if(sizeof($elems, COUNT_NORMAL) == 1) {
81
                list($name, $type) = array_pad($elems, 2, null);
82
            } else {
83
                if('$' == $elems[0][0]) {
84
                    $name = $elems[0];
85
                    $type  = null;
86
                    $isRequired = false;
87
                } else {
88
                    list($type, $name) = array_pad($elems, 2, null);
89
                }
90
            }
91
92
            $argument = new ReflectedArgument($name, $type, $isRequired);
93
            $method->pushArgument($argument);
94
        }
95
96
97
98
        // does method has body ? (example: interface ; abstract classes)
99
        $p = $n  + 1;
100
        $underComma = trim($this->searcher->getUnder(array(';'), $p, $tokens));
101
        if(strlen($underComma) > 0) {
102
            //
103
            // Body
104
            $this->extractContent($method, $n, $tokens);
105
106
            // Calls
107
            $this->extractCalls($method, $n, $tokens);
108
109
            // Tokens
110
            $end = $this->searcher->getPositionOfClosingBrace($n, $tokens);
111
            if($end > 0) {
112
                $method->setTokens($tokens->extract($n, $end));
113
            }
114
        } else {
115
            $method->setTokens($tokens->extract(0, $n));
116
        }
117
118
        //
119
        // Dependencies
120
        $this->extractDependencies($method, 0, $method->getTokens(), $currentClass);
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...
121
122
        // returns
123
        $p = $start;
124
        $this->extractReturns($method, $p, $tokens);
125
126
        // usage
127
        $this->extractUsage($method);
128
129
        return $method;
130
    }
131
132
    /**
133
     * Extracts visibility
134
     *
135
     * @param ReflectedMethod $method
136
     * @param $n
137
     * @param TokenCollection $tokens
138
     * @return $this
139
     */
140
    public function extractVisibility(ReflectedMethod $method, $n, TokenCollection $tokens) {
141
        switch(true) {
142
            case $this->searcher->isPrecededBy(T_PRIVATE, $n, $tokens, 4):
143
                $visibility = ReflectedMethod::VISIBILITY_PRIVATE;
144
                break;
145
            case $this->searcher->isPrecededBy(T_PROTECTED, $n, $tokens, 4):
146
                $visibility = ReflectedMethod::VISIBILITY_PROTECTED;
147
                break;
148
        case $this->searcher->isPrecededBy(T_PUBLIC, $n, $tokens, 4):
149
                default:
150
                $visibility = ReflectedMethod::VISIBILITY_PUBLIC;
151
                break;
152
        }
153
        $method->setVisibility($visibility);
154
        return $this;
155
    }
156
157
    /**
158
     * Extracts state
159
     *
160
     * @param ReflectedMethod $method
161
     * @param $n
162
     * @param TokenCollection $tokens
163
     * @return $this
164
     */
165
    public function extractState(ReflectedMethod $method, $n, TokenCollection $tokens) {
166
        if($this->searcher->isPrecededBy(T_STATIC, $n, $tokens, 4)) {
167
            $method->setState(ReflectedMethod::STATE_STATIC);
168
        }
169
        return $this;
170
    }
171
172
    /**
173
     * Extracts content of method
174
     *
175
     * @param ReflectedMethod $method
176
     * @param integer $n
177
     * @param TokenCollection $tokens
178
     * @return $this
179
     */
180
    private function extractContent(ReflectedMethod $method, $n, TokenCollection $tokens) {
181
        $end = $this->searcher->getPositionOfClosingBrace($n, $tokens);
182
        if($end > 0) {
183
            $collection = $tokens->extract($n, $end);
184
            $method->setContent($collection->asString());
185
        }
186
        return $this;
187
    }
188
189
    /**
190
     * Extracts content of method
191
     *
192
     * @param ReflectedMethod $method
193
     * @param integer $n
194
     * @param TokenCollection $tokens
195
     * @param ReflectedClass $currentClass
196
     * @return $this
197
     */
198
    private function extractDependencies(ReflectedMethod $method, $n, TokenCollection $tokens, ReflectedClass $currentClass = null) {
199
200
        //
201
        // Object creation
202
        $extractor = new CallExtractor($this->searcher, $currentClass);
203
        $start = $n;
204
        $len = sizeof($tokens, COUNT_NORMAL);
205
        for($i = $start; $i < $len; $i++) {
206
            $token = $tokens[$i];
207
            switch($token->getType()) {
208
                case T_PAAMAYIM_NEKUDOTAYIM:
209
                case T_NEW:
210
                    $call = $extractor->extract($i, $tokens, $currentClass);
0 ignored issues
show
Unused Code introduced by
The call to CallExtractor::extract() has too many arguments starting with $currentClass.

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...
211
                    if($call !== 'class') { // anonymous class
212
                        $method->pushDependency($call);
213
                        $method->pushInstanciedClass($call);
214
                    }
215
                    break;
216
            }
217
        }
218
219
        //
220
        // Parameters in Method API
221
        $resolver = new TypeResolver();
222
        foreach($method->getArguments() as $argument) {
223
            $name = $argument->getType();
224
            if(strlen($name) > 0 && !$resolver->isNative($name)) {
225
                $method->pushDependency($name);
226
            }
227
        }
228
229
        return $this;
230
    }
231
232
    /**
233
     * Extracts calls of method
234
     *
235
     * @param ReflectedMethod $method
236
     * @param integer $n
237
     * @param TokenCollection $tokens
238
     * @return $this
239
     */
240
    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...
241
242
        // $this->foo(), $c->foo()
243
        if(preg_match_all('!(\$[\w]*)\-\>(\w*?)\(!', $method->getContent(), $matches, PREG_SET_ORDER)) {
244
            foreach($matches as $m) {
245
                $function = $m[2];
246
                if('$this' == $m[1]) {
247
                    $method->pushInternalCall($function);
248
                } else {
249
                    $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...
250
                }
251
            }
252
        }
253
        // (new X)->foo()
254
        if(preg_match_all('!\(new (\w+?).*?\)\->(\w+)\(!', $method->getContent(), $matches, PREG_SET_ORDER)) {
255
            foreach($matches as $m) {
256
                $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...
257
            }
258
        }
259
    }
260
261
    /**
262
     * Extract the list of returned values
263
     *
264
     * @param ReflectedMethod $method
265
     * @return $this
266
     */
267
    private function extractReturns(ReflectedMethod $method, $n, TokenCollection $tokens) {
268
269
        $resolver = new TypeResolver();
270
271
        // PHP 7
272
        // we cannot use specific token. The ":" delimiter is a T_STRING token
273
        // in regex we match ":" followed by any character except another ":"
274
        $following = $this->searcher->getUnder(array('{', ';'), $n, $tokens);
275
        if(preg_match('@(?<!:):\s*(\w+)@', $following, $matches)) {
276
            $type = trim($matches[1]);
277
            if(empty($type)) {
278
                return $this;
279
            }
280
            $return = new ReflectedReturn($type, ReflectedReturn::VALUE_UNKNOW, ReflectedReturn::STRICT_TYPE_HINT);
1 ignored issue
show
Documentation introduced by
$type is of type string, but the function expects a null.

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...
281
            $method->pushReturn($return);
282
            return $this;
283
        }
284
285
        // array of available values based on code
286
        if(preg_match_all('!([\s;]return\s|^return\s+)(.*?);!', $method->getContent(), $matches)) {
287
            foreach($matches[2] as $m) {
288
                $value = trim($m, ";\t\n\r\0\x0B");
289
                $return = new ReflectedReturn($resolver->resolve($m), $value, ReflectedReturn::ESTIMATED_TYPE_HINT);
1 ignored issue
show
Documentation introduced by
$resolver->resolve($m) is of type string, but the function expects a null.

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...
290
                $method->pushReturn($return);
291
            }
292
        }
293
        return $this;
294
    }
295
296
    /**
297
     * Extracts usage of method
298
     *
299
     * @param ReflectedMethod $method
300
     * @return $this
301
     */
302
    private function extractUsage(ReflectedMethod $method) {
303
        $tokens = $method->getTokens();
304
        $codes = $values = array();
305
        foreach($tokens as $token) {
306
            if(in_array($token->getType(), array(T_WHITESPACE, T_BOOL_CAST, T_INT_CAST, T_STRING_CAST, T_DOUBLE_CAST, T_OBJECT_CAST))) {
307
                continue;
308
            }
309
            array_push($codes, $token->getType());
310
            array_push($values, $token->getValue());
311
        }
312
        switch(true) {
313
            case preg_match('!^(get)|(is)|(has).*!',$method->getName()) && $codes == array(T_RETURN, T_VARIABLE, T_OBJECT_OPERATOR, T_STRING, T_STRING):
314
                $method->setUsage(MethodUsage::USAGE_GETTER);
315
                break;
316
            // basic setter
317
            case preg_match('!^set.*!',$method->getName()) && $codes == array(T_VARIABLE, T_OBJECT_OPERATOR,T_STRING,T_STRING, T_VARIABLE, T_STRING) && $values[3] == '=':
318
            // fluent setter
319
            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)
320
                && $values[3] == '=' && $values[7] == '$this':
321
                $method->setUsage(MethodUsage::USAGE_SETTER);
322
                break;
323
            default:
324
                $method->setUsage(MethodUsage::USAGE_UNKNWON);
325
        }
326
        return $this;
327
    }
328
}
329