Complex classes like TypeCommentSniff 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 TypeCommentSniff, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
29 | class TypeCommentSniff implements Sniff |
||
30 | { |
||
31 | |||
32 | const TYPE_TAG = '@var'; |
||
33 | |||
34 | /** |
||
35 | * A list of tokenizers this sniff supports. |
||
36 | * |
||
37 | * @var array |
||
38 | */ |
||
39 | public $supportedTokenizers = array('PHP'); |
||
40 | |||
41 | |||
42 | /** |
||
43 | * Returns an array of tokens this test wants to listen for. |
||
44 | * |
||
45 | * @return array |
||
46 | */ |
||
47 | 1 | public function register() |
|
48 | { |
||
49 | return array( |
||
50 | 1 | T_COMMENT, |
|
51 | 1 | T_DOC_COMMENT_OPEN_TAG, |
|
52 | 1 | ); |
|
53 | }//end register() |
||
54 | |||
55 | |||
56 | /** |
||
57 | * Processes this test, when one of its tokens is encountered. |
||
58 | * |
||
59 | * @param File $phpcsFile The file being scanned. |
||
60 | * @param int $stackPtr The position of the current token in the |
||
61 | * stack passed in $tokens. |
||
62 | * |
||
63 | * @return void |
||
64 | */ |
||
65 | 1 | public function process(File $phpcsFile, $stackPtr) |
|
66 | { |
||
67 | 1 | $tokens = $phpcsFile->getTokens(); |
|
68 | |||
69 | 1 | if ($tokens[$stackPtr]['code'] === T_COMMENT) { |
|
70 | 1 | $this->processComment($phpcsFile, $stackPtr); |
|
71 | 1 | } elseif ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { |
|
72 | 1 | $this->processDocBlock($phpcsFile, $stackPtr); |
|
73 | 1 | } |
|
74 | 1 | }//end process() |
|
75 | |||
76 | |||
77 | /** |
||
78 | * Processes comment. |
||
79 | * |
||
80 | * @param File $phpcsFile The file being scanned. |
||
81 | * @param int $stackPtr The position of the current token |
||
82 | * in the stack passed in $tokens. |
||
83 | * |
||
84 | * @return void |
||
85 | */ |
||
86 | 1 | public function processComment(File $phpcsFile, $stackPtr) |
|
87 | { |
||
88 | 1 | $tokens = $phpcsFile->getTokens(); |
|
89 | 1 | $commentText = $tokens[$stackPtr]['content']; |
|
90 | |||
91 | // Multi-line block comment. |
||
92 | 1 | if (substr($commentText, 0, 2) !== '/*' || substr($commentText, -2) !== '*/') { |
|
93 | 1 | return; |
|
94 | } |
||
95 | |||
96 | // Not a type comment. |
||
97 | 1 | if ($this->isTypeComment($commentText) === false) { |
|
98 | 1 | return; |
|
99 | } |
||
100 | |||
101 | // The "/**@var ...*/" comment isn't parsed as DocBlock and is fixed here. |
||
102 | 1 | $error = 'Type comment must be in "/** %s ClassName $variable_name */" format'; |
|
103 | 1 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle', array(self::TYPE_TAG)); |
|
104 | 1 | if ($fix === true) { |
|
105 | 1 | $phpcsFile->fixer->replaceToken($stackPtr, '/** '.trim($commentText, ' /*').' */'); |
|
106 | 1 | } |
|
107 | 1 | }//end processComment() |
|
108 | |||
109 | |||
110 | /** |
||
111 | * Processes DocBlock. |
||
112 | * |
||
113 | * @param File $phpcsFile The file being scanned. |
||
114 | * @param int $stackPtr The position of the current token |
||
115 | * in the stack passed in $tokens. |
||
116 | * |
||
117 | * @return void |
||
118 | */ |
||
119 | 1 | public function processDocBlock(File $phpcsFile, $stackPtr) |
|
120 | { |
||
121 | 1 | $tokens = $phpcsFile->getTokens(); |
|
122 | 1 | $commentStart = $stackPtr; |
|
123 | 1 | $commentEnd = $tokens[$stackPtr]['comment_closer']; |
|
124 | |||
125 | // Multi-line DocBlock. |
||
126 | 1 | if ($tokens[$commentEnd]['line'] !== $tokens[$commentStart]['line']) { |
|
127 | 1 | return; |
|
128 | } |
||
129 | |||
130 | 1 | $commentTags = $tokens[$stackPtr]['comment_tags']; |
|
131 | |||
132 | // Single-line DocBlock without tags inside. |
||
133 | 1 | if (empty($commentTags) === true) { |
|
134 | 1 | return; |
|
135 | } |
||
136 | |||
137 | // First tag will always exist. |
||
138 | 1 | $firstTagPtr = $commentTags[0]; |
|
139 | |||
140 | // Not a type comment. |
||
141 | 1 | if ($this->isTypeComment($tokens[$firstTagPtr]['content']) === false) { |
|
142 | 1 | return; |
|
143 | } |
||
144 | |||
145 | 1 | $structure = new TypeCommentStructure($phpcsFile, $stackPtr); |
|
146 | |||
147 | 1 | if ($structure->className === null) { |
|
148 | 1 | $error = 'Type comment must be in "/** %s ClassName $variable_name */" format'; |
|
149 | 1 | $leadingWhitespace = $tokens[$stackPtr + 1]['code'] === T_DOC_COMMENT_WHITESPACE; |
|
150 | 1 | $trailingWhitespace = $tokens[$commentEnd - 1]['code'] === T_DOC_COMMENT_WHITESPACE; |
|
151 | |||
152 | 1 | if ($leadingWhitespace === false || $trailingWhitespace === false) { |
|
153 | 1 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle', array(self::TYPE_TAG)); |
|
154 | 1 | if ($fix === true) { |
|
155 | 1 | if ($leadingWhitespace === false) { |
|
156 | 1 | $phpcsFile->fixer->addContentBefore($stackPtr + 1, ' '); |
|
157 | 1 | } |
|
158 | |||
159 | 1 | if ($trailingWhitespace === false) { |
|
160 | 1 | $phpcsFile->fixer->addContent($commentEnd - 1, ' '); |
|
161 | 1 | } |
|
162 | 1 | } |
|
163 | 1 | } else { |
|
164 | 1 | $phpcsFile->addError($error, $firstTagPtr, 'WrongStyle', array(self::TYPE_TAG)); |
|
165 | } |
||
166 | |||
167 | 1 | return; |
|
168 | }//end if |
||
169 | |||
170 | 1 | $this->processDocBlockContent($phpcsFile, $stackPtr, $structure); |
|
171 | 1 | $this->processVariableAssociation($phpcsFile, $stackPtr, $structure); |
|
172 | 1 | }//end processDocBlock() |
|
173 | |||
174 | |||
175 | /** |
||
176 | * Processes DocBlock content. |
||
177 | * |
||
178 | * @param File $phpcsFile The file being scanned. |
||
179 | * @param int $stackPtr The position of the current token |
||
180 | * in the stack passed in $tokens. |
||
181 | * @param TypeCommentStructure $structure Type comment structure. |
||
182 | * |
||
183 | * @return void |
||
184 | */ |
||
185 | 1 | public function processDocBlockContent( |
|
186 | File $phpcsFile, |
||
187 | $stackPtr, |
||
188 | TypeCommentStructure $structure |
||
189 | ) { |
||
190 | 1 | $tokens = $phpcsFile->getTokens(); |
|
191 | 1 | $commentTags = $tokens[$stackPtr]['comment_tags']; |
|
192 | 1 | $firstTagPtr = $commentTags[0]; |
|
193 | |||
194 | // Check correct tag usage. |
||
195 | 1 | if ($tokens[$firstTagPtr]['content'] !== self::TYPE_TAG) { |
|
196 | 1 | $fix = $phpcsFile->addFixableError( |
|
197 | 1 | 'Type comment must use "%s" tag; "%s" used', |
|
198 | 1 | $firstTagPtr, |
|
199 | 1 | 'WrongTag', |
|
200 | array( |
||
201 | 1 | self::TYPE_TAG, |
|
202 | 1 | $tokens[$firstTagPtr]['content'], |
|
203 | ) |
||
204 | 1 | ); |
|
205 | 1 | if ($fix === true) { |
|
206 | 1 | $phpcsFile->fixer->replaceToken($firstTagPtr, self::TYPE_TAG); |
|
207 | 1 | } |
|
208 | 1 | } |
|
209 | |||
210 | // Check spacing around the tag. |
||
211 | 1 | $spaceBeforeTagPtr = ($stackPtr + 1); |
|
212 | 1 | if ($tokens[$spaceBeforeTagPtr]['length'] !== 1) { |
|
213 | 1 | $fix = $phpcsFile->addFixableError( |
|
214 | 1 | 'There must be 1 space before "%s" tag; %d found', |
|
215 | 1 | $firstTagPtr, |
|
216 | 1 | 'SpaceBeforeTag', |
|
217 | array( |
||
218 | 1 | $tokens[$firstTagPtr]['content'], |
|
219 | 1 | $tokens[$spaceBeforeTagPtr]['length'], |
|
220 | ) |
||
221 | 1 | ); |
|
222 | 1 | if ($fix === true) { |
|
223 | 1 | $phpcsFile->fixer->replaceToken($spaceBeforeTagPtr, ' '); |
|
224 | 1 | } |
|
225 | 1 | } |
|
226 | |||
227 | 1 | $spaceAfterTagPtr = ($stackPtr + 3); |
|
228 | 1 | if ($tokens[$spaceAfterTagPtr]['length'] !== 1) { |
|
229 | 1 | $fix = $phpcsFile->addFixableError( |
|
230 | 1 | 'There must be 1 space between "%s" tag and class name; %d found', |
|
231 | 1 | $firstTagPtr, |
|
232 | 1 | 'SpaceAfterTag', |
|
233 | array( |
||
234 | 1 | $tokens[$firstTagPtr]['content'], |
|
235 | 1 | $tokens[$spaceAfterTagPtr]['length'], |
|
236 | ) |
||
237 | 1 | ); |
|
238 | 1 | if ($fix === true) { |
|
239 | 1 | $phpcsFile->fixer->replaceToken($spaceAfterTagPtr, ' '); |
|
240 | 1 | } |
|
241 | 1 | } |
|
242 | |||
243 | // Check presence of both type & variable. |
||
244 | 1 | if ($structure->variableName === null) { |
|
245 | 1 | if ($structure->isVariable($structure->className) === false) { |
|
246 | 1 | $phpcsFile->addError( |
|
247 | 1 | 'Type comment missing variable: /** %s %s ______ */', |
|
248 | 1 | $structure->tagContentPtr, |
|
249 | 1 | 'VariableMissing', |
|
250 | array( |
||
251 | 1 | self::TYPE_TAG, |
|
252 | 1 | $structure->className, |
|
253 | ) |
||
254 | 1 | ); |
|
255 | 1 | } else { |
|
256 | 1 | $phpcsFile->addError( |
|
257 | 1 | 'Type comment missing type: /** %s ______ %s */', |
|
258 | 1 | $structure->tagContentPtr, |
|
259 | 1 | 'TypeMissing', |
|
260 | array( |
||
261 | 1 | self::TYPE_TAG, |
|
262 | 1 | $structure->className, |
|
263 | ) |
||
264 | 1 | ); |
|
265 | }//end if |
||
266 | |||
267 | 1 | return; |
|
268 | }//end if |
||
269 | |||
270 | 1 | $classPositionCorrect = $structure->isVariable($structure->className) === false; |
|
271 | 1 | $variablePositionCorrect = $structure->isVariable($structure->variableName); |
|
272 | |||
273 | // Malformed type definition. |
||
274 | 1 | if (($classPositionCorrect === false && $variablePositionCorrect === true) |
|
275 | 1 | || ($classPositionCorrect === true && $variablePositionCorrect === false) |
|
276 | 1 | ) { |
|
277 | 1 | $error = 'Type comment must be in "/** %s ClassName $variable_name */" format'; |
|
278 | 1 | $phpcsFile->addError($error, $firstTagPtr, 'WrongStyle', array(self::TYPE_TAG)); |
|
279 | |||
280 | 1 | return; |
|
281 | } |
||
282 | |||
283 | 1 | if ($classPositionCorrect === true || $variablePositionCorrect === true) { |
|
284 | 1 | $expectedTagContent = $structure->className.' '.$structure->variableName.' '.$structure->description; |
|
285 | 1 | } else { |
|
286 | 1 | $expectedTagContent = $structure->variableName.' '.$structure->className.' '.$structure->description; |
|
287 | } |
||
288 | |||
289 | 1 | if ($structure->tagContent !== $expectedTagContent) { |
|
290 | 1 | $fix = $phpcsFile->addFixableError( |
|
291 | 1 | 'Wrong type and variable spacing/order. Expected: "%s"', |
|
292 | 1 | $structure->tagContentPtr, |
|
293 | 1 | 'WrongOrder', |
|
294 | 1 | array($expectedTagContent) |
|
295 | 1 | ); |
|
296 | 1 | if ($fix === true) { |
|
297 | 1 | $phpcsFile->fixer->replaceToken($structure->tagContentPtr, $expectedTagContent); |
|
298 | 1 | } |
|
299 | 1 | } |
|
300 | 1 | }//end processDocBlockContent() |
|
301 | |||
302 | |||
303 | /** |
||
304 | * Processes variable around DocBlock. |
||
305 | * |
||
306 | * @param File $phpcsFile The file being scanned. |
||
307 | * @param int $stackPtr The position of the current token |
||
308 | * in the stack passed in $tokens. |
||
309 | * @param TypeCommentStructure $structure Type comment structure. |
||
310 | * |
||
311 | * @return void |
||
312 | */ |
||
313 | 1 | public function processVariableAssociation( |
|
326 | |||
327 | |||
328 | /** |
||
329 | * Processes variable before DocBlock. |
||
330 | * |
||
331 | * @param File $phpcsFile The file being scanned. |
||
332 | * @param int $stackPtr The position of the current token |
||
333 | * in the stack passed in $tokens. |
||
334 | * @param TypeCommentStructure $structure Type comment structure. |
||
335 | * |
||
336 | * @return void |
||
337 | */ |
||
338 | 1 | public function processVariableBeforeDocBlock( |
|
339 | File $phpcsFile, |
||
340 | $stackPtr, |
||
341 | TypeCommentStructure $structure |
||
342 | ) { |
||
343 | 1 | $tokens = $phpcsFile->getTokens(); |
|
344 | |||
345 | 1 | $prevStatementEnd = $phpcsFile->findPrevious( |
|
346 | 1 | T_WHITESPACE, |
|
347 | 1 | ($stackPtr - 1), |
|
348 | 1 | null, |
|
349 | true |
||
350 | 1 | ); |
|
351 | |||
352 | if ($prevStatementEnd === false |
||
353 | 1 | || $tokens[$prevStatementEnd]['code'] !== T_SEMICOLON |
|
354 | 1 | ) { |
|
355 | 1 | return; |
|
356 | } |
||
357 | |||
358 | 1 | $assignmentTokenPtr = $phpcsFile->findPrevious( |
|
359 | 1 | T_EQUAL, |
|
360 | 1 | ($prevStatementEnd - 1), |
|
361 | 1 | null, |
|
362 | 1 | false, |
|
363 | 1 | null, |
|
364 | true |
||
365 | 1 | ); |
|
366 | |||
367 | // Not an assignment. |
||
368 | 1 | if ($assignmentTokenPtr === false) { |
|
369 | return; |
||
370 | } |
||
371 | |||
372 | 1 | $variableTokenPtr = $phpcsFile->findPrevious( |
|
373 | 1 | T_WHITESPACE, |
|
374 | 1 | ($assignmentTokenPtr - 1), |
|
375 | 1 | null, |
|
376 | true |
||
377 | 1 | ); |
|
378 | |||
379 | // Assignment not to a variable, mentioned in type comment. |
||
380 | if ($variableTokenPtr === false |
||
381 | 1 | || $tokens[$variableTokenPtr]['code'] !== T_VARIABLE |
|
382 | 1 | || $tokens[$variableTokenPtr]['content'] !== $structure->variableName |
|
383 | 1 | ) { |
|
384 | 1 | return; |
|
385 | } |
||
386 | |||
387 | 1 | $fix = $phpcsFile->addFixableError( |
|
388 | 1 | 'Type comment must be placed before variable declaration', |
|
389 | 1 | $stackPtr, |
|
390 | 'AfterVariable' |
||
391 | 1 | ); |
|
392 | 1 | if ($fix === true) { |
|
393 | 1 | $move_content = ''; |
|
394 | 1 | $copyStartPtr = $this->findFirstOnLine($phpcsFile, $stackPtr); |
|
395 | 1 | $copyEndPtr = $this->findLastOnLine($phpcsFile, $stackPtr); |
|
396 | |||
397 | 1 | $phpcsFile->fixer->beginChangeset(); |
|
398 | |||
399 | 1 | for ($i = $copyStartPtr; $i <= $copyEndPtr; $i++) { |
|
400 | 1 | $move_content .= $phpcsFile->fixer->getTokenContent($i); |
|
|
|||
401 | 1 | $phpcsFile->fixer->replaceToken($i, ''); |
|
402 | 1 | } |
|
403 | |||
404 | 1 | $phpcsFile->fixer->addContentBefore( |
|
405 | 1 | $this->findFirstOnLine($phpcsFile, $variableTokenPtr), |
|
406 | $move_content |
||
407 | 1 | ); |
|
408 | 1 | $phpcsFile->fixer->endChangeset(); |
|
409 | 1 | }//end if |
|
410 | 1 | }//end processVariableBeforeDocBlock() |
|
411 | |||
412 | |||
413 | /** |
||
414 | * Processes variable before DocBlock. |
||
415 | * |
||
416 | * @param File $phpcsFile The file being scanned. |
||
417 | * @param int $stackPtr The position of the current token |
||
418 | * in the stack passed in $tokens. |
||
419 | * @param TypeCommentStructure $structure Type comment structure. |
||
420 | * |
||
421 | * @return void |
||
422 | */ |
||
423 | 1 | public function processVariableAfterDocBlock( |
|
424 | File $phpcsFile, |
||
425 | $stackPtr, |
||
426 | TypeCommentStructure $structure |
||
427 | ) { |
||
428 | 1 | $tokens = $phpcsFile->getTokens(); |
|
429 | 1 | $variableTokenPtr = $phpcsFile->findNext( |
|
430 | 1 | T_WHITESPACE, |
|
431 | 1 | ($tokens[$stackPtr]['comment_closer'] + 1), |
|
432 | 1 | null, |
|
433 | true |
||
434 | 1 | ); |
|
435 | |||
436 | // No variable placed on next line after type comment. |
||
437 | if ($variableTokenPtr === false |
||
438 | 1 | || $tokens[$variableTokenPtr]['code'] !== T_VARIABLE |
|
439 | 1 | || $tokens[$variableTokenPtr]['line'] !== ($tokens[$stackPtr]['line'] + 1) |
|
440 | 1 | ) { |
|
441 | 1 | return; |
|
442 | } |
||
443 | |||
444 | 1 | if ($tokens[$variableTokenPtr]['content'] !== $structure->variableName) { |
|
445 | 1 | $phpcsFile->addError( |
|
446 | 1 | 'Type comment variable mismatch, expected "%s"; found: "%s"', |
|
447 | 1 | $structure->tagContentPtr, |
|
448 | 1 | 'VariableMismatch', |
|
449 | array( |
||
450 | 1 | $tokens[$variableTokenPtr]['content'], |
|
451 | 1 | $structure->variableName, |
|
452 | ) |
||
453 | 1 | ); |
|
454 | |||
455 | // Don't apply more checks, unless type comment is in sync with variable. |
||
456 | 1 | return; |
|
457 | } |
||
458 | |||
459 | 1 | $prevStatementEnd = $phpcsFile->findPrevious( |
|
460 | 1 | T_WHITESPACE, |
|
461 | 1 | ($stackPtr - 1), |
|
462 | 1 | null, |
|
463 | true |
||
464 | 1 | ); |
|
465 | |||
466 | // Previous statement is absent or placed correctly. |
||
467 | if ($prevStatementEnd === false |
||
468 | 1 | || $tokens[$prevStatementEnd]['code'] !== T_SEMICOLON |
|
469 | 1 | || $tokens[$prevStatementEnd]['line'] < ($tokens[$stackPtr]['line'] - 1) |
|
470 | 1 | ) { |
|
471 | 1 | return; |
|
472 | } |
||
473 | |||
474 | 1 | $fix = $phpcsFile->addFixableError( |
|
475 | 1 | 'There must be at least 1 empty line above type comment', |
|
476 | 1 | $stackPtr, |
|
477 | 'EmptyLineAbove' |
||
478 | 1 | ); |
|
479 | 1 | if ($fix === true) { |
|
480 | 1 | $phpcsFile->fixer->addNewline($prevStatementEnd); |
|
481 | 1 | } |
|
482 | 1 | }//end processVariableAfterDocBlock() |
|
483 | |||
484 | |||
485 | /** |
||
486 | * Checks if comment is type comment. |
||
487 | * |
||
488 | * @param string $commentText Comment text. |
||
489 | * |
||
490 | * @return bool |
||
491 | */ |
||
492 | 1 | protected function isTypeComment($commentText) |
|
496 | |||
497 | |||
498 | /** |
||
499 | * Finds first token on a line. |
||
500 | * |
||
501 | * @param File $phpcsFile All the tokens found in the document. |
||
502 | * @param int $start Start from token. |
||
503 | * |
||
504 | * @return int | bool |
||
505 | */ |
||
506 | 1 | public function findFirstOnLine(File $phpcsFile, $start) |
|
520 | |||
521 | |||
522 | /** |
||
523 | * Finds last token on a line. |
||
524 | * |
||
525 | * @param File $phpcsFile All the tokens found in the document. |
||
526 | * @param int $start Start from token. |
||
527 | * |
||
528 | * @return int | bool |
||
529 | */ |
||
530 | 1 | public function findLastOnLine(File $phpcsFile, $start) |
|
544 | }//end class |
||
545 | |||
546 | |||
547 | /** |
||
548 | * Type comment structure |
||
549 | */ |
||
550 | class TypeCommentStructure |
||
551 | { |
||
651 |
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.