Completed
Branch master (a6414e)
by Shcherbak
02:17
created

MethodPattern::isValidBody()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0116

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 6
cts 7
cp 0.8571
rs 9.6666
cc 2
eloc 6
nc 2
nop 1
crap 2.0116
1
<?php
2
3
  namespace Funivan\PhpTokenizer\Pattern\Patterns;
4
5
  use Funivan\PhpTokenizer\Collection;
6
  use Funivan\PhpTokenizer\QuerySequence\QuerySequence;
7
  use Funivan\PhpTokenizer\Strategy\QueryStrategy;
8
  use Funivan\PhpTokenizer\Strategy\Strict;
9
  use Funivan\PhpTokenizer\Token;
10
11
  /**
12
   * Find method in code
13
   */
14
  class MethodPattern implements PatternInterface {
15
16
    const OUTPUT_BODY = 0;
17
18
    const OUTPUT_FULL = 1;
19
20
    const OUTPUT_DOC_COMMENT = 2;
21
22
23
    /**
24
     * @var QueryStrategy
25
     */
26
    private $nameQuery;
27
28
    /**
29
     * @var callable[]
30
     */
31
    private $modifierChecker = [];
32
33
    /**
34
     * @var callable
35
     */
36
    private $bodyChecker;
37
38
    /**
39
     * @var callable
40
     */
41
    private $docCommentChecker;
42
43
    /**
44
     * @var int
45
     */
46
    private $outputType = self::OUTPUT_BODY;
47
48
49
    /**
50
     *
51
     */
52 19
    public function __construct() {
53 16
      $this->withName(Strict::create()->valueLike('!.+!'));
54 16
      $this->withAnyModifier();
55
56
      /** @noinspection PhpUnusedParameterInspection */
57
      $this->withBody(function (Collection $body) {
1 ignored issue
show
Unused Code introduced by
The parameter $body 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...
58 15
        return true;
59 16
      });
60
61
      /** @noinspection PhpUnusedParameterInspection */
62
      $this->withDocComment(function (Token $token) {
1 ignored issue
show
Unused Code introduced by
The parameter $token 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...
63 15
        return true;
64 16
      });
65
66 19
    }
67
68
69
    /**
70
     * @return $this
71
     */
72 3
    public function outputFull() {
73 3
      $this->outputType = self::OUTPUT_FULL;
74 3
      return $this;
75
    }
76
77
78
    /**
79
     * @return $this
80
     */
81 3
    public function outputBody() {
82 3
      $this->outputType = self::OUTPUT_BODY;
83 3
      return $this;
84
    }
85
86
87
    /**
88
     * @return $this
89
     */
90 3
    public function outputDocComment() {
91 3
      $this->outputType = self::OUTPUT_DOC_COMMENT;
92 3
      return $this;
93
    }
94
95
96
    /**
97
     * @param string|QueryStrategy $name
98
     * @return $this
99
     */
100 19
    public function withName($name) {
101 16
      if (is_string($name)) {
102 2
        $this->nameQuery = Strict::create()->valueIs($name);
103 10
      } elseif ($name instanceof QueryStrategy) {
104 19
        $this->nameQuery = $name;
105 8
      } else {
106 2
        throw new \InvalidArgumentException('Invalid name format. Expect string or Query');
107 3
      }
108
109 19
      return $this;
110
    }
111
112
113
    /**
114
     * @param callable $check
115
     * @return $this
116
     */
117 19
    public function withBody(callable $check) {
118 19
      $this->bodyChecker = $check;
119 19
      return $this;
120 3
    }
121
122
123
    /**
124
     * @param Collection $body
125
     * @return bool
126
     * @throws \Exception
127
     */
128 15
    private function isValidBody(Collection $body) {
129 15
      $checker = $this->bodyChecker;
130 14
      $result = $checker($body);
131 14
      if (!is_bool($result)) {
132
        throw new \Exception('Body checker should return boolean result');
133
      }
134
135 15
      return $result;
136 1
    }
137
138
139
    /**
140
     * @param callable $check
141
     * @return $this
142
     */
143 19
    public function withDocComment(callable $check = null) {
144
145 19
      if ($check === null) {
146
        $check = function (Token $token) {
147 2
          return $token->getType() === T_DOC_COMMENT;
148 2
        };
149 1
      }
150 19
      $this->docCommentChecker = $check;
151
152 19
      return $this;
153 3
    }
154
155
156
    /**
157
     * Find functions without doc comments
158
     */
159 3
    public function withoutDocComment() {
160
      $this->docCommentChecker = function (Token $token) {
161 2
        return $token->isValid() === false;
162
      };
163
164 3
      return $this;
165
    }
166
167
168
    /**
169
     * @return $this
170
     */
171 19
    public function withAnyModifier() {
172 19
      $this->modifierChecker = [];
173
      $this->modifierChecker[] = function () {
174 15
        return true;
175 3
      };
176 19
      return $this;
177
    }
178
179
180
    /**
181
     * @param string $modifier
182
     * @return $this
183
     */
184 2
    public function withModifier($modifier) {
185
      $this->modifierChecker[] = function ($allModifiers) use ($modifier) {
186 2
        return in_array($modifier, $allModifiers);
187
      };
188
189 2
      return $this;
190
    }
191
192
193
    /**
194
     * @param string $modifier
195
     * @return $this
196
     */
197
    public function withoutModifier($modifier) {
198
199 3
      $this->modifierChecker[] = function ($allModifiers) use ($modifier) {
200 2
        return !in_array($modifier, $allModifiers);
201 1
      };
202
203 3
      return $this;
204
    }
205
206
207
    /**
208
     * @param array $modifiers Array<String>
209
     * @return bool
210
     * @throws \Exception
211
     */
212 15
    private function isValidModifiers(array $modifiers) {
213 15
      foreach ($this->modifierChecker as $checkModifier) {
214 14
        $result = $checkModifier($modifiers);
215
216 14
        if (!is_bool($result)) {
217
          throw new \Exception('Modifier checker should return boolean result');
218
        }
219 15
        if ($result === false) {
220 8
          return false;
221
        }
222 7
      }
223
224 15
      return true;
225 1
    }
226
227
228
    /**
229
     * @param QuerySequence $querySequence
230
     * @return Collection|null
231
     */
232 16
    public function __invoke(QuerySequence $querySequence) {
233 7
      static $availableModifiers = [
234 1
        T_STATIC,
235 1
        T_PRIVATE,
236 1
        T_PUBLIC,
237 1
        T_ABSTRACT,
238 1
        T_FINAL,
239 7
      ];
240
241
242
      # detect function
243 14
      $functionKeyword = $querySequence->strict('function');
244 14
      $querySequence->strict(T_WHITESPACE);
245 14
      $querySequence->process($this->nameQuery);
246 14
      $querySequence->section('(', ')');
247 14
      $querySequence->possible(T_WHITESPACE);
248 14
      $body = $querySequence->section('{', '}');
249
250 14
      if (!$querySequence->isValid()) {
251 14
        return null;
252
      }
253
254 14
      $collection = $querySequence->getCollection();
255 14
      $start = $collection->extractByTokens($collection->getFirst(), $functionKeyword);
256 14
      $start->slice(0, -1);  // remove last function keyword
257
258
      # start reverse search
259 14
      $items = array_reverse($start->getItems());
260 15
      $startFrom = null;
261
262 14
      $docComment = new Token();
263
264 15
      $modifiers = [];
265
266
267
      /** @var Token[] $items */
268 14
      foreach ($items as $item) {
269
270 14
        if ($item->getType() === T_WHITESPACE) {
271 12
          $startFrom = $item;
272 12
          continue;
273
        }
274
275 14
        if ($item->getType() === T_DOC_COMMENT and $docComment->isValid() === false) {
276
          # Detect only first doc comment
277 4
          $startFrom = $item;
278 4
          $docComment = $item;
279 4
          continue;
280
        }
281
282
283 14
        if (in_array($item->getType(), $availableModifiers)) {
284 10
          $startFrom = $item;
285 10
          $modifiers[] = $item->getValue();
286 10
          continue;
287
        }
288
289 16
        break;
290 8
      }
291
292 14
      if ($this->isValidModifiers($modifiers) === false) {
293 2
        return null;
294
      }
295
296 14
      if ($this->isValidBody($body) === false) {
297 2
        return null;
298
      }
299
300 14
      if ($this->isValidDocComment($docComment) === false) {
301 2
        return null;
302
      }
303
304 14
      if (is_null($startFrom)) {
305 3
        $startFrom = $functionKeyword;
306 1
      }
307
308
309 15
      if ($this->outputType === self::OUTPUT_FULL) {
310
        # all conditions are ok, so extract full function
311 2
        $fullFunction = $collection->extractByTokens($startFrom, $body->getLast());
312 2
        if ($fullFunction->getFirst()->getType() == T_WHITESPACE) {
313 2
          $fullFunction->slice(1);
314 1
        }
315 3
        return $fullFunction;
316
317 15
      } elseif ($this->outputType == self::OUTPUT_DOC_COMMENT) {
318 2
        return new Collection([$docComment]);
319
      }
320
321
      # body by default
322 14
      $body->slice(0, -1);
323 15
      return $body;
324
    }
325
326
327
    /**
328
     * @param Token $token
329
     * @return mixed
330
     * @throws \Exception
331
     */
332 15
    private function isValidDocComment(Token $token) {
333 15
      $checker = $this->docCommentChecker;
334 14
      $result = $checker($token);
335 14
      if (!is_bool($result)) {
336
        throw new \Exception('DocComment checker should return boolean result');
337
      }
338
339 15
      return $result;
340 1
    }
341
342
343
  }