Chris@0
|
1 <?php
|
Chris@0
|
2 /**
|
Chris@0
|
3 * Parses and verifies the doc comments for files.
|
Chris@0
|
4 *
|
Chris@0
|
5 * @category PHP
|
Chris@0
|
6 * @package PHP_CodeSniffer
|
Chris@0
|
7 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
8 */
|
Chris@0
|
9
|
Chris@17
|
10 namespace Drupal\Sniffs\Commenting;
|
Chris@17
|
11
|
Chris@17
|
12 use PHP_CodeSniffer\Files\File;
|
Chris@17
|
13 use PHP_CodeSniffer\Sniffs\Sniff;
|
Chris@17
|
14
|
Chris@0
|
15 /**
|
Chris@0
|
16 * Parses and verifies the doc comments for files.
|
Chris@0
|
17 *
|
Chris@0
|
18 * Verifies that :
|
Chris@0
|
19 * <ul>
|
Chris@0
|
20 * <li>A doc comment exists.</li>
|
Chris@0
|
21 * <li>There is a blank newline after the @file statement.</li>
|
Chris@0
|
22 * </ul>
|
Chris@0
|
23 *
|
Chris@0
|
24 * @category PHP
|
Chris@0
|
25 * @package PHP_CodeSniffer
|
Chris@0
|
26 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
27 */
|
Chris@0
|
28
|
Chris@17
|
29 class FileCommentSniff implements Sniff
|
Chris@0
|
30 {
|
Chris@0
|
31
|
Chris@0
|
32
|
Chris@0
|
33 /**
|
Chris@0
|
34 * A list of tokenizers this sniff supports.
|
Chris@0
|
35 *
|
Chris@0
|
36 * @var array
|
Chris@0
|
37 */
|
Chris@0
|
38 public $supportedTokenizers = array(
|
Chris@0
|
39 'PHP',
|
Chris@0
|
40 'JS',
|
Chris@0
|
41 );
|
Chris@0
|
42
|
Chris@0
|
43
|
Chris@0
|
44 /**
|
Chris@0
|
45 * Returns an array of tokens this test wants to listen for.
|
Chris@0
|
46 *
|
Chris@0
|
47 * @return array
|
Chris@0
|
48 */
|
Chris@0
|
49 public function register()
|
Chris@0
|
50 {
|
Chris@0
|
51 return array(T_OPEN_TAG);
|
Chris@0
|
52
|
Chris@0
|
53 }//end register()
|
Chris@0
|
54
|
Chris@0
|
55
|
Chris@0
|
56 /**
|
Chris@0
|
57 * Processes this test, when one of its tokens is encountered.
|
Chris@0
|
58 *
|
Chris@17
|
59 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
Chris@17
|
60 * @param int $stackPtr The position of the current token
|
Chris@17
|
61 * in the stack passed in $tokens.
|
Chris@0
|
62 *
|
Chris@0
|
63 * @return int
|
Chris@0
|
64 */
|
Chris@17
|
65 public function process(File $phpcsFile, $stackPtr)
|
Chris@0
|
66 {
|
Chris@0
|
67 $this->currentFile = $phpcsFile;
|
Chris@0
|
68
|
Chris@0
|
69 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
70 $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
|
Chris@0
|
71
|
Chris@0
|
72 // Files containing exactly one class, interface or trait are allowed to
|
Chris@0
|
73 // ommit a file doc block. If a namespace is used then the file comment must
|
Chris@0
|
74 // be omitted.
|
Chris@0
|
75 $oopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr);
|
Chris@0
|
76 if ($oopKeyword !== false) {
|
Chris@0
|
77 $namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr);
|
Chris@0
|
78 // Check if the file contains multiple classes/interfaces/traits - then a
|
Chris@0
|
79 // file doc block is allowed.
|
Chris@0
|
80 $secondOopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($oopKeyword + 1));
|
Chris@0
|
81 // Namespaced classes, interfaces and traits should not have an @file doc
|
Chris@0
|
82 // block.
|
Chris@0
|
83 if (($tokens[$commentStart]['code'] === T_DOC_COMMENT_OPEN_TAG
|
Chris@0
|
84 || $tokens[$commentStart]['code'] === T_COMMENT)
|
Chris@0
|
85 && $secondOopKeyword === false
|
Chris@0
|
86 && $namespace !== false
|
Chris@0
|
87 ) {
|
Chris@0
|
88 $fix = $phpcsFile->addFixableError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc');
|
Chris@0
|
89 if ($fix === true) {
|
Chris@0
|
90 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
91
|
Chris@0
|
92 for ($i = $commentStart; $i <= ($tokens[$commentStart]['comment_closer'] + 1); $i++) {
|
Chris@0
|
93 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
94 }
|
Chris@0
|
95
|
Chris@0
|
96 // If, after removing the comment, there are two new lines
|
Chris@0
|
97 // remove them.
|
Chris@0
|
98 if ($tokens[($commentStart - 1)]['content'] === "\n" && $tokens[$i]['content'] === "\n") {
|
Chris@0
|
99 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
100 }
|
Chris@0
|
101
|
Chris@0
|
102 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
103 }
|
Chris@0
|
104 }
|
Chris@0
|
105
|
Chris@0
|
106 if ($namespace !== false) {
|
Chris@0
|
107 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
108 }
|
Chris@0
|
109
|
Chris@0
|
110 // Search for global functions before and after the class.
|
Chris@0
|
111 $function = $phpcsFile->findPrevious(T_FUNCTION, ($oopKeyword - 1));
|
Chris@0
|
112 if ($function === false) {
|
Chris@0
|
113 $function = $phpcsFile->findNext(T_FUNCTION, ($tokens[$oopKeyword]['scope_closer'] + 1));
|
Chris@0
|
114 }
|
Chris@0
|
115
|
Chris@0
|
116 $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), null, false, '@file');
|
Chris@0
|
117
|
Chris@0
|
118 // No other classes, no other global functions and no explicit @file tag
|
Chris@0
|
119 // anywhere means it is ok to skip the file comment.
|
Chris@0
|
120 if ($secondOopKeyword === false && $function === false && $fileTag === false) {
|
Chris@0
|
121 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
122 }
|
Chris@0
|
123 }//end if
|
Chris@0
|
124
|
Chris@0
|
125 if ($tokens[$commentStart]['code'] === T_COMMENT) {
|
Chris@0
|
126 $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a file comment', $commentStart, 'WrongStyle');
|
Chris@0
|
127 if ($fix === true) {
|
Chris@0
|
128 $content = $tokens[$commentStart]['content'];
|
Chris@0
|
129
|
Chris@0
|
130 // If the comment starts with something like "/**" then we just
|
Chris@0
|
131 // insert a space after the stars.
|
Chris@0
|
132 if (strpos($content, '/**') === 0) {
|
Chris@0
|
133 $phpcsFile->fixer->replaceToken($commentStart, str_replace('/**', '/** ', $content));
|
Chris@0
|
134 } else if (strpos($content, '/*') === 0) {
|
Chris@0
|
135 // Just turn the /* ... */ style comment into a /** ... */ style
|
Chris@0
|
136 // comment.
|
Chris@0
|
137 $phpcsFile->fixer->replaceToken($commentStart, str_replace('/*', '/**', $content));
|
Chris@0
|
138 } else {
|
Chris@0
|
139 $content = trim(ltrim($tokens[$commentStart]['content'], '/# '));
|
Chris@0
|
140 $phpcsFile->fixer->replaceToken($commentStart, "/**\n * @file\n * $content\n */\n");
|
Chris@0
|
141 }
|
Chris@0
|
142 }
|
Chris@0
|
143
|
Chris@0
|
144 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
145 } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) {
|
Chris@0
|
146 $fix = $phpcsFile->addFixableError('Missing file doc comment', 0, 'Missing');
|
Chris@0
|
147 if ($fix === true) {
|
Chris@0
|
148 // Only PHP has a real opening tag, additional newline at the
|
Chris@0
|
149 // beginning here.
|
Chris@0
|
150 if ($phpcsFile->tokenizerType === 'PHP') {
|
Chris@0
|
151 // In templates add the file doc block to the very beginning of
|
Chris@0
|
152 // the file.
|
Chris@0
|
153 if ($tokens[0]['code'] === T_INLINE_HTML) {
|
Chris@0
|
154 $phpcsFile->fixer->addContentBefore(0, "<?php\n\n/**\n * @file\n */\n?>\n");
|
Chris@0
|
155 } else {
|
Chris@0
|
156 $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
|
Chris@0
|
157 }
|
Chris@0
|
158 } else {
|
Chris@0
|
159 $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
|
Chris@0
|
160 }
|
Chris@0
|
161 }
|
Chris@0
|
162
|
Chris@0
|
163 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
164 }//end if
|
Chris@0
|
165
|
Chris@0
|
166 $commentEnd = $tokens[$commentStart]['comment_closer'];
|
Chris@0
|
167 $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), $commentEnd, false, '@file');
|
Chris@0
|
168 $next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
|
Chris@0
|
169
|
Chris@0
|
170 // If there is no @file tag and the next line is a function or class
|
Chris@0
|
171 // definition then the file docblock is mising.
|
Chris@0
|
172 if ($tokens[$next]['line'] === ($tokens[$commentEnd]['line'] + 1)
|
Chris@0
|
173 && $tokens[$next]['code'] === T_FUNCTION
|
Chris@0
|
174 ) {
|
Chris@0
|
175 if ($fileTag === false) {
|
Chris@0
|
176 $fix = $phpcsFile->addFixableError('Missing file doc comment', $stackPtr, 'Missing');
|
Chris@0
|
177 if ($fix === true) {
|
Chris@0
|
178 // Only PHP has a real opening tag, additional newline at the
|
Chris@0
|
179 // beginning here.
|
Chris@0
|
180 if ($phpcsFile->tokenizerType === 'PHP') {
|
Chris@0
|
181 $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
|
Chris@0
|
182 } else {
|
Chris@0
|
183 $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
|
Chris@0
|
184 }
|
Chris@0
|
185 }
|
Chris@0
|
186
|
Chris@0
|
187 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
188 }
|
Chris@0
|
189 }//end if
|
Chris@0
|
190
|
Chris@0
|
191 if ($fileTag === false || $tokens[$fileTag]['line'] !== ($tokens[$commentStart]['line'] + 1)) {
|
Chris@0
|
192 $second_line = $phpcsFile->findNext(array(T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG), ($commentStart + 1), $commentEnd);
|
Chris@0
|
193 $fix = $phpcsFile->addFixableError('The second line in the file doc comment must be "@file"', $second_line, 'FileTag');
|
Chris@0
|
194 if ($fix === true) {
|
Chris@0
|
195 if ($fileTag === false) {
|
Chris@0
|
196 $phpcsFile->fixer->addContent($commentStart, "\n * @file");
|
Chris@0
|
197 } else {
|
Chris@0
|
198 // Delete the @file tag at its current position and insert one
|
Chris@0
|
199 // after the beginning of the comment.
|
Chris@0
|
200 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
201 $phpcsFile->fixer->addContent($commentStart, "\n * @file");
|
Chris@0
|
202 $phpcsFile->fixer->replaceToken($fileTag, '');
|
Chris@0
|
203 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
204 }
|
Chris@0
|
205 }
|
Chris@0
|
206
|
Chris@0
|
207 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
208 }
|
Chris@0
|
209
|
Chris@0
|
210 // Exactly one blank line after the file comment.
|
Chris@0
|
211 if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)
|
Chris@0
|
212 && $next !== false && $tokens[$next]['code'] !== T_CLOSE_TAG
|
Chris@0
|
213 ) {
|
Chris@0
|
214 $error = 'There must be exactly one blank line after the file comment';
|
Chris@0
|
215 $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfterComment');
|
Chris@0
|
216 if ($fix === true) {
|
Chris@0
|
217 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
218 $uselessLine = ($commentEnd + 1);
|
Chris@0
|
219 while ($uselessLine < $next) {
|
Chris@0
|
220 $phpcsFile->fixer->replaceToken($uselessLine, '');
|
Chris@0
|
221 $uselessLine++;
|
Chris@0
|
222 }
|
Chris@0
|
223
|
Chris@0
|
224 $phpcsFile->fixer->addContent($commentEnd, "\n\n");
|
Chris@0
|
225 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
226 }
|
Chris@0
|
227
|
Chris@0
|
228 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
229 }
|
Chris@0
|
230
|
Chris@0
|
231 // Template file: no blank line after the file comment.
|
Chris@0
|
232 if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 1)
|
Chris@0
|
233 && $tokens[$next]['line'] > $tokens[$commentEnd]['line']
|
Chris@0
|
234 && $tokens[$next]['code'] === T_CLOSE_TAG
|
Chris@0
|
235 ) {
|
Chris@0
|
236 $error = 'There must be no blank line after the file comment in a template';
|
Chris@0
|
237 $fix = $phpcsFile->addFixableError($error, $commentEnd, 'TeamplateSpacingAfterComment');
|
Chris@0
|
238 if ($fix === true) {
|
Chris@0
|
239 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
240 $uselessLine = ($commentEnd + 1);
|
Chris@0
|
241 while ($uselessLine < $next) {
|
Chris@0
|
242 $phpcsFile->fixer->replaceToken($uselessLine, '');
|
Chris@0
|
243 $uselessLine++;
|
Chris@0
|
244 }
|
Chris@0
|
245
|
Chris@0
|
246 $phpcsFile->fixer->addContent($commentEnd, "\n");
|
Chris@0
|
247 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
248 }
|
Chris@0
|
249 }
|
Chris@0
|
250
|
Chris@0
|
251 // Ignore the rest of the file.
|
Chris@0
|
252 return ($phpcsFile->numTokens + 1);
|
Chris@0
|
253
|
Chris@0
|
254 }//end process()
|
Chris@0
|
255
|
Chris@0
|
256
|
Chris@0
|
257 }//end class
|