Completed
Push — master ( 5734eb...a6414e )
by Shcherbak
02:44
created

MethodPattern::withoutModifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
crap 1
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 24
    public function __construct() {
53 24
      $this->withName(Strict::create()->valueLike('!.+!'));
54 24
      $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 21
        return true;
59 24
      });
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 21
        return true;
64 24
      });
65
66 24
    }
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 24
    public function withName($name) {
101 24
      if (is_string($name)) {
102 3
        $this->nameQuery = Strict::create()->valueIs($name);
103 24
      } elseif ($name instanceof QueryStrategy) {
104 24
        $this->nameQuery = $name;
105 24
      } else {
106 3
        throw new \InvalidArgumentException('Invalid name format. Expect string or Query');
107
      }
108
109 24
      return $this;
110
    }
111
112
113
    /**
114
     * @param callable $check
115
     * @return $this
116
     */
117 24
    public function withBody(callable $check) {
118 24
      $this->bodyChecker = $check;
119 24
      return $this;
120
    }
121
122
123
    /**
124
     * @param Collection $body
125
     * @return bool
126
     * @throws \Exception
127
     */
128 21
    private function isValidBody(Collection $body) {
129 21
      $checker = $this->bodyChecker;
130 21
      $result = $checker($body);
131 21
      if (!is_bool($result)) {
132
        throw new \Exception('Body checker should return boolean result');
133
      }
134
135 21
      return $result;
136
    }
137
138
139
    /**
140
     * @param callable $check
141
     * @return $this
142
     */
143 24
    public function withDocComment(callable $check = null) {
144
145 24
      if ($check === null) {
146
        $check = function (Token $token) {
147 3
          return $token->getType() === T_DOC_COMMENT;
148 3
        };
149 3
      }
150 24
      $this->docCommentChecker = $check;
151
152 24
      return $this;
153
    }
154
155
156
    /**
157
     * Find functions without doc comments
158
     */
159 3
    public function withoutDocComment() {
160
      $this->docCommentChecker = function (Token $token) {
161 3
        return $token->isValid() === false;
162
      };
163
164 3
      return $this;
165
    }
166
167
168
    /**
169
     * @return $this
170
     */
171 24
    public function withAnyModifier() {
172 24
      $this->modifierChecker = [];
173
      $this->modifierChecker[] = function () {
174 21
        return true;
175
      };
176 24
      return $this;
177
    }
178
179
180
    /**
181
     * @param string $modifier
182
     * @return $this
183
     */
184 3
    public function withModifier($modifier) {
185
      $this->modifierChecker[] = function ($allModifiers) use ($modifier) {
186 3
        return in_array($modifier, $allModifiers);
187
      };
188
189 3
      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 3
        return !in_array($modifier, $allModifiers);
201
      };
202
203 3
      return $this;
204
    }
205
206
207
    /**
208
     * @param array $modifiers Array<String>
209
     * @return bool
210
     * @throws \Exception
211
     */
212 21
    private function isValidModifiers(array $modifiers) {
213 21
      foreach ($this->modifierChecker as $checkModifier) {
214 21
        $result = $checkModifier($modifiers);
215
216 21
        if (!is_bool($result)) {
217
          throw new \Exception('Modifier checker should return boolean result');
218
        }
219 21
        if ($result === false) {
220 3
          return false;
221
        }
222 21
      }
223
224 21
      return true;
225
    }
226
227
228
    /**
229
     * @param QuerySequence $querySequence
230
     * @return Collection|null
231
     */
232 21
    public function __invoke(QuerySequence $querySequence) {
233
      static $availableModifiers = [
234
        T_STATIC,
235
        T_PRIVATE,
236
        T_PUBLIC,
237
        T_ABSTRACT,
238
        T_FINAL,
239 21
      ];
240
241
242
      # detect function
243 21
      $functionKeyword = $querySequence->strict('function');
244 21
      $querySequence->strict(T_WHITESPACE);
245 21
      $querySequence->process($this->nameQuery);
246 21
      $querySequence->section('(', ')');
247 21
      $querySequence->possible(T_WHITESPACE);
248 21
      $body = $querySequence->section('{', '}');
249
250 21
      if (!$querySequence->isValid()) {
251 21
        return null;
252
      }
253
254 21
      $collection = $querySequence->getCollection();
255 21
      $start = $collection->extractByTokens($collection->getFirst(), $functionKeyword);
1 ignored issue
show
Bug introduced by
It seems like $collection->getFirst() can be null; however, extractByTokens() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
256 21
      $start->slice(0, -1);  // remove last function keyword
257
258
      # start reverse search
259 21
      $items = array_reverse($start->getItems());
260 21
      $startFrom = null;
261
262 21
      $docComment = new Token();
263
264 21
      $modifiers = [];
265
266
267
      /** @var Token[] $items */
268 21
      foreach ($items as $item) {
269
270 21
        if ($item->getType() === T_WHITESPACE) {
271 18
          $startFrom = $item;
272 18
          continue;
273
        }
274
275 21
        if ($item->getType() === T_DOC_COMMENT and $docComment->isValid() === false) {
276
          # Detect only first doc comment
277 6
          $startFrom = $item;
278 6
          $docComment = $item;
279 6
          continue;
280
        }
281
282
283 21
        if (in_array($item->getType(), $availableModifiers)) {
284 15
          $startFrom = $item;
285 15
          $modifiers[] = $item->getValue();
286 15
          continue;
287
        }
288
289 21
        break;
290 21
      }
291
292 21
      if ($this->isValidModifiers($modifiers) === false) {
293 3
        return null;
294
      }
295
296 21
      if ($this->isValidBody($body) === false) {
297 3
        return null;
298
      }
299
300 21
      if ($this->isValidDocComment($docComment) === false) {
301 3
        return null;
302
      }
303
304 21
      if (is_null($startFrom)) {
305 3
        $startFrom = $functionKeyword;
306 3
      }
307
308
309 21
      if ($this->outputType === self::OUTPUT_FULL) {
310
        # all conditions are ok, so extract full function
311 3
        $fullFunction = $collection->extractByTokens($startFrom, $body->getLast());
312 3
        if ($fullFunction->getFirst()->getType() == T_WHITESPACE) {
313 3
          $fullFunction->slice(1);
314 3
        }
315 3
        return $fullFunction;
316
317 21
      } elseif ($this->outputType == self::OUTPUT_DOC_COMMENT) {
318 3
        return new Collection([$docComment]);
319
      }
320
321
      # body by default
322 21
      $body->slice(0, -1);
323 21
      return $body;
324
    }
325
326
327
    /**
328
     * @param Token $token
329
     * @return mixed
330
     * @throws \Exception
331
     */
332 21
    private function isValidDocComment(Token $token) {
333 21
      $checker = $this->docCommentChecker;
334 21
      $result = $checker($token);
335 21
      if (!is_bool($result)) {
336
        throw new \Exception('DocComment checker should return boolean result');
337
      }
338
339 21
      return $result;
340
    }
341
342
343
  }