MethodPattern::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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