Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
DocumentationTypeTrait
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 6
992
0.00% covered (danger)
0.00%
0 / 1
 splitTypeAndComment
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
132
 fixShortTypes
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 fixTrailingPunctuation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 fixWrappedParenthesis
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 maybeAddObjectTypehintError
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 maybeAddTypeTypehintError
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
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;
24
25/**
26 * Share code between FunctionCommentSniff and PropertyDocumentationSniff
27 */
28trait DocumentationTypeTrait {
29
30    /**
31     * Mapping for swap short types
32     * @var string[]
33     */
34    private static $SHORT_TYPE_MAPPING = [
35        // @phan-suppress-previous-line PhanReadOnlyPrivateProperty Traits cannot have constants
36        'boolean' => 'bool',
37        'boolean[]' => 'bool[]',
38        'integer' => 'int',
39        'integer[]' => 'int[]',
40    ];
41
42    /**
43     * Mapping for primitive types to case correct
44     * Cannot just detect case due to classes being uppercase
45     *
46     * @var string[]
47     */
48    private static $PRIMITIVE_TYPE_MAPPING = [
49        // @phan-suppress-previous-line PhanReadOnlyPrivateProperty Traits cannot have constants
50        'Array' => 'array',
51        'Array[]' => 'array[]',
52        'Bool' => 'bool',
53        'Bool[]' => 'bool[]',
54        'Float' => 'float',
55        'Float[]' => 'float[]',
56        'Int' => 'int',
57        'Int[]' => 'int[]',
58        'Mixed' => 'mixed',
59        'Mixed[]' => 'mixed[]',
60        'Null' => 'null',
61        'NULL' => 'null',
62        'Null[]' => 'null[]',
63        'NULL[]' => 'null[]',
64        'Object' => 'object',
65        'Object[]' => 'object[]',
66        'String' => 'string',
67        'String[]' => 'string[]',
68        'Callable' => 'callable',
69        'Callable[]' => 'callable[]',
70    ];
71
72    /**
73     * Split PHPDoc comment strings like "bool[] Comment" into type and comment, while respecting
74     * types like `array<int, array<string, bool>>` and `array{id: int, name: string}`.
75     *
76     * @param string $str
77     *
78     * @return array [ string $type, int|null $separatorLength, string $comment ]
79     */
80    private function splitTypeAndComment( string $str ): array {
81        $brackets = 0;
82        $curly = 0;
83        $len = strlen( $str );
84        for ( $i = 0; $i < $len; $i++ ) {
85            $char = $str[$i];
86            // Stop at the first space that is not part of a valid pair of brackets
87            if ( $char === ' ' && !$brackets && !$curly ) {
88                $separatorLength = strspn( $str, ' ', $i );
89                return [ substr( $str, 0, $i ), $separatorLength, substr( $str, $i + $separatorLength ) ];
90            } elseif ( $char === '>' && $brackets ) {
91                $brackets--;
92            } elseif ( $char === '<' ) {
93                $brackets++;
94            } elseif ( $char === '}' && $curly ) {
95                $curly--;
96            } elseif ( $char === '{' ) {
97                $curly++;
98            }
99        }
100        return [ $str, null, '' ];
101    }
102
103    /**
104     * @param File $phpcsFile
105     * @param int $stackPtr
106     * @param string $typesString
107     * @param bool &$fix Set when autofix is needed
108     * @param string $annotation Either "param" or "return" or "var"
109     * @return string Updated $typesString
110     */
111    private function fixShortTypes(
112        File $phpcsFile,
113        int $stackPtr,
114        string $typesString,
115        bool &$fix,
116        string $annotation
117    ): string {
118        $typeList = explode( '|', $typesString );
119        foreach ( $typeList as &$type ) {
120            // Corrects long types from both upper and lowercase to lowercase shorttype
121            $key = lcfirst( $type );
122            if ( isset( self::$SHORT_TYPE_MAPPING[$key] ) ) {
123                $type = self::$SHORT_TYPE_MAPPING[$key];
124                $code = 'NotShort' . str_replace( '[]', 'Array', ucfirst( $type ) ) . ucfirst( $annotation );
125                $fix = $phpcsFile->addFixableError(
126                    'Short type of "%s" should be used for @%s tag',
127                    $stackPtr,
128                    $code,
129                    [ $type, $annotation ]
130                ) || $fix;
131            } elseif ( isset( self::$PRIMITIVE_TYPE_MAPPING[$type] ) ) {
132                $type = self::$PRIMITIVE_TYPE_MAPPING[$type];
133                $code = 'UppercasePrimitive' . str_replace( '[]', 'Array', ucfirst( $type ) ) . ucfirst( $annotation );
134                $fix = $phpcsFile->addFixableError(
135                    'Lowercase type of "%s" should be used for @%s tag',
136                    $stackPtr,
137                    $code,
138                    [ $type, $annotation ]
139                ) || $fix;
140            }
141        }
142        return implode( '|', $typeList );
143    }
144
145    /**
146     * @param File $phpcsFile
147     * @param int $stackPtr
148     * @param string $typesString
149     * @param bool &$fix Set when autofix is needed
150     * @param string $annotation Either "param" or "return" or "var" + "name" or "type"
151     * @return string Updated $typesString
152     */
153    private function fixTrailingPunctuation(
154        File $phpcsFile,
155        int $stackPtr,
156        string $typesString,
157        bool &$fix,
158        string $annotation
159    ): string {
160        if ( preg_match( '/^(.*)((?:(?![\[\]_{}()])\p{P})+)$/', $typesString, $matches ) ) {
161            $typesString = $matches[1];
162            $fix = $phpcsFile->addFixableError(
163                '%s should not end with punctuation "%s"',
164                $stackPtr,
165                'NotPunctuation' . str_replace( ' ', '', ucwords( $annotation ) ),
166                [ ucfirst( $annotation ), $matches[2] ]
167            ) || $fix;
168        }
169        return $typesString;
170    }
171
172    /**
173     * @param File $phpcsFile
174     * @param int $stackPtr
175     * @param string $typesString
176     * @param bool &$fix Set when autofix is needed
177     * @param string $annotation Either "param" or "return" or "var" + "name" or "type"
178     * @return string Updated $typesString
179     */
180    private function fixWrappedParenthesis(
181        File $phpcsFile,
182        int $stackPtr,
183        string $typesString,
184        bool &$fix,
185        string $annotation
186    ): string {
187        if ( preg_match( '/^([{\[]+)(.*)([\]}]+)$/', $typesString, $matches ) ) {
188            $typesString = $matches[2];
189            $fix = $phpcsFile->addFixableError(
190                '%s should not be wrapped in parenthesis; %s and %s found',
191                $stackPtr,
192                'NotParenthesis' . str_replace( ' ', '', ucwords( $annotation ) ),
193                [ ucfirst( $annotation ), $matches[1], $matches[3] ]
194            ) || $fix;
195        }
196        return $typesString;
197    }
198
199    /**
200     * @param File $phpcsFile
201     * @param int $stackPtr
202     * @param string $typesString
203     * @param string $annotation Either "param" or "return" or "var"
204     */
205    private function maybeAddObjectTypehintError(
206        File $phpcsFile,
207        int $stackPtr,
208        string $typesString,
209        string $annotation
210    ): void {
211        $typeList = explode( '|', $typesString );
212        foreach ( $typeList as $type ) {
213            if ( $type === 'object' || $type === 'object[]' ) {
214                $phpcsFile->addWarning(
215                    '`object` should rarely be used as a typehint. If more specific types are ' .
216                        'known, list them. If only plain anonymous objects are expected, use ' .
217                        '`stdClass`. If the intent is indeed to allow any object, mark it with a ' .
218                        '// phpcs:… comment or set this rule\'s <severity> to 0.',
219                    $stackPtr,
220                    'ObjectTypeHint' . ucfirst( $annotation )
221                );
222            }
223        }
224    }
225
226    /**
227     * Complain about `type` as a type, its likely to have been autogenerated and isn't
228     * informative (but we don't care about `Type`, since that might be a class name),
229     * see T273806
230     *
231     * @param File $phpcsFile
232     * @param int $stackPtr
233     * @param string $typesString
234     * @param string $annotation Either "param" or "return" or "var"
235     */
236    private function maybeAddTypeTypehintError(
237        File $phpcsFile,
238        int $stackPtr,
239        string $typesString,
240        string $annotation
241    ): void {
242        $typeList = explode( '|', $typesString );
243        foreach ( $typeList as $type ) {
244            if ( $type === 'type' || $type === 'type[]' ) {
245                $phpcsFile->addWarning(
246                    '`type` should not be used as a typehint, the actual type should be used',
247                    $stackPtr,
248                    'TypeTypeHint' . ucfirst( $annotation )
249                );
250            }
251        }
252    }
253}