Total Complexity | 82 |
Total Lines | 422 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like HeaderCommentFixer 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.
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 HeaderCommentFixer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
36 | final class HeaderCommentFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface |
||
37 | { |
||
38 | /** |
||
39 | * @internal |
||
40 | */ |
||
41 | public const HEADER_PHPDOC = 'PHPDoc'; |
||
42 | |||
43 | /** |
||
44 | * @internal |
||
45 | */ |
||
46 | public const HEADER_COMMENT = 'comment'; |
||
47 | |||
48 | /** |
||
49 | * {@inheritdoc} |
||
50 | */ |
||
51 | public function getDefinition(): FixerDefinitionInterface |
||
52 | { |
||
53 | return new FixerDefinition( |
||
54 | 'Add, replace or remove header comment.', |
||
55 | [ |
||
56 | new CodeSample( |
||
57 | '<?php |
||
58 | declare(strict_types=1); |
||
59 | |||
60 | namespace A\B; |
||
61 | |||
62 | echo 1; |
||
63 | ', |
||
64 | [ |
||
65 | 'header' => 'Made with love.', |
||
66 | ] |
||
67 | ), |
||
68 | new CodeSample( |
||
69 | '<?php |
||
70 | declare(strict_types=1); |
||
71 | |||
72 | namespace A\B; |
||
73 | |||
74 | echo 1; |
||
75 | ', |
||
76 | [ |
||
77 | 'header' => 'Made with love.', |
||
78 | 'comment_type' => 'PHPDoc', |
||
79 | 'location' => 'after_open', |
||
80 | 'separate' => 'bottom', |
||
81 | ] |
||
82 | ), |
||
83 | new CodeSample( |
||
84 | '<?php |
||
85 | declare(strict_types=1); |
||
86 | |||
87 | namespace A\B; |
||
88 | |||
89 | echo 1; |
||
90 | ', |
||
91 | [ |
||
92 | 'header' => 'Made with love.', |
||
93 | 'comment_type' => 'comment', |
||
94 | 'location' => 'after_declare_strict', |
||
95 | ] |
||
96 | ), |
||
97 | new CodeSample( |
||
98 | '<?php |
||
99 | declare(strict_types=1); |
||
100 | |||
101 | /* |
||
102 | * Comment is not wanted here. |
||
103 | */ |
||
104 | |||
105 | namespace A\B; |
||
106 | |||
107 | echo 1; |
||
108 | ', |
||
109 | [ |
||
110 | 'header' => '', |
||
111 | ] |
||
112 | ), |
||
113 | ] |
||
114 | ); |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * {@inheritdoc} |
||
119 | */ |
||
120 | public function isCandidate(Tokens $tokens): bool |
||
121 | { |
||
122 | return isset($tokens[0]) && $tokens[0]->isGivenKind(T_OPEN_TAG) && $tokens->isMonolithicPhp(); |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * {@inheritdoc} |
||
127 | * |
||
128 | * Must run before SingleLineCommentStyleFixer. |
||
129 | * Must run after DeclareStrictTypesFixer, NoBlankLinesAfterPhpdocFixer. |
||
130 | */ |
||
131 | public function getPriority(): int |
||
132 | { |
||
133 | // When this fixer is configured with ["separate" => "bottom", "comment_type" => "PHPDoc"] |
||
134 | // and the target file has no namespace or declare() construct, |
||
135 | // the fixed header comment gets trimmed by NoBlankLinesAfterPhpdocFixer if we run before it. |
||
136 | return -30; |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * {@inheritdoc} |
||
141 | */ |
||
142 | protected function applyFix(\SplFileInfo $file, Tokens $tokens): void |
||
143 | { |
||
144 | $location = $this->configuration['location']; |
||
145 | $locationIndexes = []; |
||
146 | |||
147 | foreach (['after_open', 'after_declare_strict'] as $possibleLocation) { |
||
148 | $locationIndex = $this->findHeaderCommentInsertionIndex($tokens, $possibleLocation); |
||
149 | |||
150 | if (!isset($locationIndexes[$locationIndex]) || $possibleLocation === $location) { |
||
151 | $locationIndexes[$locationIndex] = $possibleLocation; |
||
152 | |||
153 | continue; |
||
154 | } |
||
155 | } |
||
156 | |||
157 | foreach (array_values($locationIndexes) as $possibleLocation) { |
||
158 | // figure out where the comment should be placed |
||
159 | $headerNewIndex = $this->findHeaderCommentInsertionIndex($tokens, $possibleLocation); |
||
160 | |||
161 | // check if there is already a comment |
||
162 | $headerCurrentIndex = $this->findHeaderCommentCurrentIndex($tokens, $headerNewIndex - 1); |
||
163 | |||
164 | if (null === $headerCurrentIndex) { |
||
165 | if ('' === $this->configuration['header'] || $possibleLocation !== $location) { |
||
166 | continue; |
||
167 | } |
||
168 | |||
169 | $this->insertHeader($tokens, $headerNewIndex); |
||
170 | |||
171 | continue; |
||
172 | } |
||
173 | |||
174 | $sameComment = $this->getHeaderAsComment() === $tokens[$headerCurrentIndex]->getContent(); |
||
175 | $expectedLocation = $possibleLocation === $location; |
||
176 | |||
177 | if (!$sameComment || !$expectedLocation) { |
||
178 | if ($expectedLocation ^ $sameComment) { |
||
179 | $this->removeHeader($tokens, $headerCurrentIndex); |
||
180 | } |
||
181 | |||
182 | if ('' === $this->configuration['header']) { |
||
183 | continue; |
||
184 | } |
||
185 | |||
186 | if ($possibleLocation === $location) { |
||
187 | $this->insertHeader($tokens, $headerNewIndex); |
||
188 | } |
||
189 | |||
190 | continue; |
||
191 | } |
||
192 | |||
193 | $this->fixWhiteSpaceAroundHeader($tokens, $headerCurrentIndex); |
||
194 | } |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * {@inheritdoc} |
||
199 | */ |
||
200 | protected function createConfigurationDefinition(): FixerConfigurationResolverInterface |
||
201 | { |
||
202 | $fixerName = $this->getName(); |
||
203 | |||
204 | return new FixerConfigurationResolver([ |
||
205 | (new FixerOptionBuilder('header', 'Proper header content.')) |
||
206 | ->setAllowedTypes(['string']) |
||
207 | ->setNormalizer(static function (Options $options, $value) use ($fixerName) { |
||
208 | if ('' === trim($value)) { |
||
209 | return ''; |
||
210 | } |
||
211 | |||
212 | if (false !== strpos($value, '*/')) { |
||
213 | throw new InvalidFixerConfigurationException($fixerName, 'Cannot use \'*/\' in header.'); |
||
214 | } |
||
215 | |||
216 | return $value; |
||
217 | }) |
||
218 | ->getOption(), |
||
219 | (new FixerOptionBuilder('comment_type', 'Comment syntax type.')) |
||
220 | ->setAllowedValues([self::HEADER_PHPDOC, self::HEADER_COMMENT]) |
||
221 | ->setDefault(self::HEADER_COMMENT) |
||
222 | ->getOption(), |
||
223 | (new FixerOptionBuilder('location', 'The location of the inserted header.')) |
||
224 | ->setAllowedValues(['after_open', 'after_declare_strict']) |
||
225 | ->setDefault('after_declare_strict') |
||
226 | ->getOption(), |
||
227 | (new FixerOptionBuilder('separate', 'Whether the header should be separated from the file content with a new line.')) |
||
228 | ->setAllowedValues(['both', 'top', 'bottom', 'none']) |
||
229 | ->setDefault('both') |
||
230 | ->getOption(), |
||
231 | ]); |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * Enclose the given text in a comment block. |
||
236 | */ |
||
237 | private function getHeaderAsComment(): string |
||
238 | { |
||
239 | $lineEnding = $this->whitespacesConfig->getLineEnding(); |
||
240 | $comment = (self::HEADER_COMMENT === $this->configuration['comment_type'] ? '/*' : '/**').$lineEnding; |
||
241 | $lines = explode("\n", str_replace("\r", '', $this->configuration['header'])); |
||
242 | |||
243 | foreach ($lines as $line) { |
||
244 | $comment .= rtrim(' * '.$line).$lineEnding; |
||
245 | } |
||
246 | |||
247 | return $comment.' */'; |
||
248 | } |
||
249 | |||
250 | private function findHeaderCommentCurrentIndex(Tokens $tokens, int $headerNewIndex): ?int |
||
251 | { |
||
252 | $index = $tokens->getNextNonWhitespace($headerNewIndex); |
||
253 | |||
254 | if (null === $index || !$tokens[$index]->isComment()) { |
||
255 | return null; |
||
256 | } |
||
257 | |||
258 | $next = $index + 1; |
||
259 | |||
260 | if (!isset($tokens[$next]) || \in_array($this->configuration['separate'], ['top', 'none'], true) || !$tokens[$index]->isGivenKind(T_DOC_COMMENT)) { |
||
261 | return $index; |
||
262 | } |
||
263 | |||
264 | if ($tokens[$next]->isWhitespace()) { |
||
265 | if (!Preg::match('/^\h*\R\h*$/D', $tokens[$next]->getContent())) { |
||
266 | return $index; |
||
267 | } |
||
268 | |||
269 | ++$next; |
||
270 | } |
||
271 | |||
272 | if (!isset($tokens[$next]) || !$tokens[$next]->isClassy() && !$tokens[$next]->isGivenKind(T_FUNCTION)) { |
||
273 | return $index; |
||
274 | } |
||
275 | |||
276 | return $this->getHeaderAsComment() === $tokens[$index]->getContent() ? $index : null; |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Find the index where the header comment must be inserted. |
||
281 | */ |
||
282 | private function findHeaderCommentInsertionIndex(Tokens $tokens, string $location): int |
||
283 | { |
||
284 | if ('after_open' === $location) { |
||
285 | return 1; |
||
286 | } |
||
287 | |||
288 | $index = $tokens->getNextMeaningfulToken(0); |
||
289 | |||
290 | if (null === $index) { |
||
291 | return 1; // file without meaningful tokens but an open tag, comment should always be placed directly after the open tag |
||
292 | } |
||
293 | |||
294 | if (!$tokens[$index]->isGivenKind(T_DECLARE)) { |
||
295 | return 1; |
||
296 | } |
||
297 | |||
298 | $next = $tokens->getNextMeaningfulToken($index); |
||
299 | |||
300 | if (null === $next || !$tokens[$next]->equals('(')) { |
||
301 | return 1; |
||
302 | } |
||
303 | |||
304 | $next = $tokens->getNextMeaningfulToken($next); |
||
305 | |||
306 | if (null === $next || !$tokens[$next]->equals([T_STRING, 'strict_types'], false)) { |
||
307 | return 1; |
||
308 | } |
||
309 | |||
310 | $next = $tokens->getNextMeaningfulToken($next); |
||
311 | |||
312 | if (null === $next || !$tokens[$next]->equals('=')) { |
||
313 | return 1; |
||
314 | } |
||
315 | |||
316 | $next = $tokens->getNextMeaningfulToken($next); |
||
317 | |||
318 | if (null === $next || !$tokens[$next]->isGivenKind(T_LNUMBER)) { |
||
319 | return 1; |
||
320 | } |
||
321 | |||
322 | $next = $tokens->getNextMeaningfulToken($next); |
||
323 | |||
324 | if (null === $next || !$tokens[$next]->equals(')')) { |
||
325 | return 1; |
||
326 | } |
||
327 | |||
328 | $next = $tokens->getNextMeaningfulToken($next); |
||
329 | |||
330 | if (null === $next || !$tokens[$next]->equals(';')) { // don't insert after close tag |
||
331 | return 1; |
||
332 | } |
||
333 | |||
334 | return $next + 1; |
||
335 | } |
||
336 | |||
337 | private function fixWhiteSpaceAroundHeader(Tokens $tokens, int $headerIndex): void |
||
338 | { |
||
339 | $lineEnding = $this->whitespacesConfig->getLineEnding(); |
||
340 | |||
341 | // fix lines after header comment |
||
342 | if ( |
||
343 | ('both' === $this->configuration['separate'] || 'bottom' === $this->configuration['separate']) |
||
344 | && null !== $tokens->getNextMeaningfulToken($headerIndex) |
||
345 | ) { |
||
346 | $expectedLineCount = 2; |
||
347 | } else { |
||
348 | $expectedLineCount = 1; |
||
349 | } |
||
350 | |||
351 | if ($headerIndex === \count($tokens) - 1) { |
||
352 | $tokens->insertAt($headerIndex + 1, new Token([T_WHITESPACE, str_repeat($lineEnding, $expectedLineCount)])); |
||
353 | } else { |
||
354 | $lineBreakCount = $this->getLineBreakCount($tokens, $headerIndex, 1); |
||
355 | |||
356 | if ($lineBreakCount < $expectedLineCount) { |
||
357 | $missing = str_repeat($lineEnding, $expectedLineCount - $lineBreakCount); |
||
358 | |||
359 | if ($tokens[$headerIndex + 1]->isWhitespace()) { |
||
360 | $tokens[$headerIndex + 1] = new Token([T_WHITESPACE, $missing.$tokens[$headerIndex + 1]->getContent()]); |
||
361 | } else { |
||
362 | $tokens->insertAt($headerIndex + 1, new Token([T_WHITESPACE, $missing])); |
||
363 | } |
||
364 | } elseif ($lineBreakCount > $expectedLineCount && $tokens[$headerIndex + 1]->isWhitespace()) { |
||
365 | $newLinesToRemove = $lineBreakCount - $expectedLineCount; |
||
366 | $tokens[$headerIndex + 1] = new Token([ |
||
367 | T_WHITESPACE, |
||
368 | Preg::replace("/^\\R{{$newLinesToRemove}}/", '', $tokens[$headerIndex + 1]->getContent()), |
||
369 | ]); |
||
370 | } |
||
371 | } |
||
372 | |||
373 | // fix lines before header comment |
||
374 | $expectedLineCount = 'both' === $this->configuration['separate'] || 'top' === $this->configuration['separate'] ? 2 : 1; |
||
375 | $prev = $tokens->getPrevNonWhitespace($headerIndex); |
||
376 | |||
377 | $regex = '/\h$/'; |
||
378 | |||
379 | if ($tokens[$prev]->isGivenKind(T_OPEN_TAG) && Preg::match($regex, $tokens[$prev]->getContent())) { |
||
380 | $tokens[$prev] = new Token([T_OPEN_TAG, Preg::replace($regex, $lineEnding, $tokens[$prev]->getContent())]); |
||
381 | } |
||
382 | |||
383 | $lineBreakCount = $this->getLineBreakCount($tokens, $headerIndex, -1); |
||
384 | |||
385 | if ($lineBreakCount < $expectedLineCount) { |
||
386 | // because of the way the insert index was determined for header comment there cannot be an empty token here |
||
387 | $tokens->insertAt($headerIndex, new Token([T_WHITESPACE, str_repeat($lineEnding, $expectedLineCount - $lineBreakCount)])); |
||
388 | } |
||
389 | } |
||
390 | |||
391 | private function getLineBreakCount(Tokens $tokens, int $index, int $direction): int |
||
392 | { |
||
393 | $whitespace = ''; |
||
394 | |||
395 | for ($index += $direction; isset($tokens[$index]); $index += $direction) { |
||
396 | $token = $tokens[$index]; |
||
397 | |||
398 | if ($token->isWhitespace()) { |
||
399 | $whitespace .= $token->getContent(); |
||
400 | |||
401 | continue; |
||
402 | } |
||
403 | |||
404 | if (-1 === $direction && $token->isGivenKind(T_OPEN_TAG)) { |
||
405 | $whitespace .= $token->getContent(); |
||
406 | } |
||
407 | |||
408 | if ('' !== $token->getContent()) { |
||
409 | break; |
||
410 | } |
||
411 | } |
||
412 | |||
413 | return substr_count($whitespace, "\n"); |
||
414 | } |
||
415 | |||
416 | private function removeHeader(Tokens $tokens, int $index): void |
||
452 | } |
||
453 | |||
454 | private function insertHeader(Tokens $tokens, int $index): void |
||
455 | { |
||
456 | $tokens->insertAt($index, new Token([self::HEADER_COMMENT === $this->configuration['comment_type'] ? T_COMMENT : T_DOC_COMMENT, $this->getHeaderAsComment()])); |
||
457 | $this->fixWhiteSpaceAroundHeader($tokens, $index); |
||
458 | } |
||
459 | } |
||
460 |