Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FunctionAnnotationsSniff
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 4
342
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
72
 normalizeAnnotation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 handleAccessAnnotation
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Sniffs\Commenting;
22
23use PHP_CodeSniffer\Files\File;
24use PHP_CodeSniffer\Sniffs\Sniff;
25use PHP_CodeSniffer\Util\Tokens;
26
27class FunctionAnnotationsSniff implements Sniff {
28    /**
29     * Annotations allowed for functions. This includes bad annotations that we check for
30     * elsewhere.
31     */
32    private const ALLOWED_ANNOTATIONS = [
33        // Allowed all-lowercase tags
34        '@after' => true,
35        '@author' => true,
36        '@before' => true,
37        '@code' => true,
38        '@cover' => true,
39        '@covers' => true,
40        '@depends' => true,
41        '@deprecated' => true,
42        '@endcode' => true,
43        '@fixme' => true,
44        '@group' => true,
45        '@internal' => true,
46        '@license' => true,
47        '@link' => true,
48        '@note' => true,
49        '@par' => true,
50        '@param' => true,
51        '@requires' => true,
52        '@return' => true,
53        '@see' => true,
54        '@since' => true,
55        '@throws' => true,
56        '@todo' => true,
57        '@uses' => true,
58        '@warning' => true,
59
60        // Automatically replaced
61        '@param[in]' => '@param',
62        '@param[in,out]' => '@param',
63        '@param[out]' => '@param',
64        '@params' => '@param',
65        '@returns' => '@return',
66        '@throw' => '@throws',
67        '@exception' => '@throws',
68
69        // private and protected is needed when functions stay public
70        // for deprecation or backward compatibility reasons
71        // @see https://www.mediawiki.org/wiki/Deprecation_policy#Scope
72        '@private' => true,
73        '@protected' => true,
74
75        // Special handling
76        '@access' => true,
77
78        // Stable interface policy tags
79        // @see https://www.mediawiki.org/wiki/Stable_interface_policy
80        '@newable' => true,
81        '@stable' => true,
82        '@unstable' => true,
83
84        // phan
85        '@phan-param' => true,
86        '@phan-return' => true,
87        '@phan-suppress-next-line' => true,
88        '@phan-var' => true,
89        '@phan-assert' => true,
90        '@phan-assert-true-condition' => true,
91        '@phan-assert-false-condition' => true,
92        '@phan-side-effect-free' => true,
93        '@suppress' => true,
94        '@phan-template' => true,
95        '@phan-type' => true,
96        // No other consumers for now.
97        '@template' => '@phan-template',
98
99        // pseudo-tags from phan-taint-check-plugin
100        '@param-taint' => true,
101        '@return-taint' => true,
102
103        // psalm
104        '@psalm-template' => true,
105        '@psalm-param' => true,
106        '@psalm-return' => true,
107
108        // T263390
109        '@noinspection' => true,
110
111        // phpunit tags that are mixed-case - map lowercase to preferred mixed-case
112        // phpunit tags that are already all-lowercase, like @after and @before
113        // are listed above
114        '@afterclass' => '@afterClass',
115        '@beforeclass' => '@beforeClass',
116        '@codecoverageignore' => '@codeCoverageIgnore',
117        '@covernothing' => '@coverNothing',
118        '@coversnothing' => '@coversNothing',
119        '@dataprovider' => '@dataProvider',
120        '@doesnotperformassertions' => '@doesNotPerformAssertions',
121        '@testwith' => '@testWith',
122
123        // Other phpunit annotations that we recognize, even if PhpunitAnnotationsSniff
124        // complains about them. See T276971
125        '@small' => true,
126        '@medium' => true,
127        '@large' => true,
128        '@test' => true,
129        '@testdox' => true,
130        '@backupglobals' => '@backupGlobals',
131        '@backupstaticattributes' => '@backupStaticAttributes',
132        '@runinseparateprocess' => '@runInSeparateProcess',
133        '@expectedexception' => '@expectedException',
134        '@expectedexceptioncode' => '@expectedExceptionCode',
135        '@expectedexceptionmessage' => '@expectedExceptionMessage',
136        '@expectedexceptionmessageregexp' => '@expectedExceptionMessageRegExp',
137
138        '@inheritdoc' => '@inheritDoc',
139
140        // Tags to automatically fix
141        '@deprecate' => '@deprecated',
142        '@gropu' => '@group',
143        '@parma' => '@param',
144        '@warn' => '@warning',
145    ];
146
147    /**
148     * @inheritDoc
149     */
150    public function register(): array {
151        return [ T_FUNCTION ];
152    }
153
154    /**
155     * Processes this test, when one of its tokens is encountered.
156     *
157     * @param File $phpcsFile The file being scanned.
158     * @param int $stackPtr The position of the current token in the stack passed in $tokens.
159     *
160     * @return void
161     */
162    public function process( File $phpcsFile, $stackPtr ) {
163        $tokens = $phpcsFile->getTokens();
164
165        $tokensToSkip = array_merge( Tokens::$emptyTokens, Tokens::$methodPrefixes );
166        unset( $tokensToSkip[T_DOC_COMMENT_CLOSE_TAG] );
167
168        $commentEnd = $phpcsFile->findPrevious( $tokensToSkip, $stackPtr - 1, null, true );
169        if ( !$commentEnd || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG ) {
170            return;
171        }
172
173        $commentStart = $tokens[$commentEnd]['comment_opener'];
174
175        foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) {
176            $tagContent = $tokens[$tag]['content'];
177            $annotation = $this->normalizeAnnotation( $tagContent );
178            if ( $annotation === false ) {
179                $error = '%s is not a valid function annotation';
180                $phpcsFile->addError( $error, $tag, 'UnrecognizedAnnotation', [ $tagContent ] );
181            } elseif ( $annotation === '@access' ) {
182                $this->handleAccessAnnotation( $phpcsFile, $tokens, $tag, $tagContent );
183            } elseif ( $tagContent !== $annotation ) {
184                $fix = $phpcsFile->addFixableWarning(
185                    'Use %s annotation instead of %s',
186                    $tag,
187                    'NonNormalizedAnnotation',
188                    [ $annotation, $tagContent ]
189                );
190                if ( $fix ) {
191                    $phpcsFile->fixer->replaceToken( $tag, $annotation );
192                }
193            }
194        }
195    }
196
197    /**
198     * Normalizes an annotation
199     *
200     * @param string $anno
201     * @return string|false Tag or false if it's not canonical
202     */
203    private function normalizeAnnotation( string $anno ) {
204        $anno = rtrim( $anno, ':' );
205        $lower = mb_strtolower( $anno );
206        if ( array_key_exists( $lower, self::ALLOWED_ANNOTATIONS ) ) {
207            return is_string( self::ALLOWED_ANNOTATIONS[$lower] )
208                ? self::ALLOWED_ANNOTATIONS[$lower]
209                : $lower;
210        }
211
212        if ( preg_match( '/^@code{\W?([a-z]+)}$/', $lower, $matches ) ) {
213            return '@code{.' . $matches[1] . '}';
214        }
215
216        return false;
217    }
218
219    /**
220     * @param File $phpcsFile
221     * @param array[] $tokens
222     * @param int $tag Token position of the annotation tag
223     * @param string $tagContent Content of the annotation
224     */
225    private function handleAccessAnnotation( File $phpcsFile, array $tokens, int $tag, string $tagContent ): void {
226        if ( $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING ) {
227            $text = strtolower( $tokens[$tag + 2]['content'] );
228            if ( $text === 'protected' || $text === 'private' ) {
229                $replacement = '@' . $text;
230                $fix = $phpcsFile->addFixableWarning(
231                    'Use %s annotation instead of "%s"',
232                    $tag,
233                    'AccessAnnotationReplacement',
234                    [ $replacement, $phpcsFile->getTokensAsString( $tag, 3 ) ]
235                );
236                if ( $fix ) {
237                    $phpcsFile->fixer->beginChangeset();
238                    $phpcsFile->fixer->replaceToken( $tag, $replacement );
239                    $phpcsFile->fixer->replaceToken( $tag + 1, '' );
240                    $phpcsFile->fixer->replaceToken( $tag + 2, '' );
241                    $phpcsFile->fixer->endChangeset();
242                }
243                return;
244            }
245        }
246        $error = '%s is not a valid function annotation';
247        $phpcsFile->addError( $error, $tag, 'AccessAnnotationInvalid', [ $tagContent ] );
248    }
249}