├── COPYRIGHT ├── Doxyfile ├── HISTORY.md ├── LICENSE ├── MediaWiki ├── Sniffs │ ├── AlternativeSyntax │ │ ├── LeadingZeroInFloatSniff.php │ │ └── UnicodeEscapeSniff.php │ ├── Arrays │ │ ├── AlphabeticArraySortSniff.php │ │ └── TrailingCommaSniff.php │ ├── Classes │ │ ├── FullQualifiedClassNameSniff.php │ │ ├── UnsortedUseStatementsSniff.php │ │ └── UnusedUseStatementSniff.php │ ├── Commenting │ │ ├── ClassLevelLicenseSniff.php │ │ ├── DocCommentSniff.php │ │ ├── DocumentationTypeTrait.php │ │ ├── EmptyTagSniff.php │ │ ├── FunctionAnnotationsSniff.php │ │ ├── FunctionCommentSniff.php │ │ ├── IllegalSingleLineCommentSniff.php │ │ ├── LicenseCommentSniff.php │ │ ├── MissingCoversSniff.php │ │ ├── PhpunitAnnotationsSniff.php │ │ ├── PropertyDocumentationSniff.php │ │ ├── RedundantVarNameSniff.php │ │ └── VariadicArgumentSniff.php │ ├── ControlStructures │ │ └── MissingElseBetweenBracketsSniff.php │ ├── ExtraCharacters │ │ └── ParenthesesAroundKeywordSniff.php │ ├── Files │ │ └── ClassMatchesFilenameSniff.php │ ├── NamingConventions │ │ ├── LowerCamelFunctionsNameSniff.php │ │ ├── PrefixedGlobalFunctionsSniff.php │ │ └── ValidGlobalNameSniff.php │ ├── PHPUnit │ │ ├── AssertCountSniff.php │ │ ├── AssertEmptySniff.php │ │ ├── AssertEqualsSniff.php │ │ ├── AssertionOrderSniff.php │ │ ├── DeprecatedPHPUnitMethodsSniff.php │ │ ├── MockBoilerplateSniff.php │ │ ├── PHPUnitClassUsageSniff.php │ │ ├── PHPUnitTestTrait.php │ │ ├── PHPUnitTypeHintsSniff.php │ │ ├── SetMethodsSniff.php │ │ └── SpecificAssertionsSniff.php │ ├── Usage │ │ ├── AssignmentInReturnSniff.php │ │ ├── DbrQueryUsageSniff.php │ │ ├── DeprecatedConstantUsageSniff.php │ │ ├── DeprecatedGlobalVariablesSniff.php │ │ ├── DirUsageSniff.php │ │ ├── ExtendClassUsageSniff.php │ │ ├── FinalPrivateSniff.php │ │ ├── ForbiddenFunctionsSniff.php │ │ ├── InArrayUsageSniff.php │ │ ├── IsNullSniff.php │ │ ├── MagicConstantClosureSniff.php │ │ ├── NestedInlineTernarySniff.php │ │ ├── NullableTypeSniff.php │ │ ├── PlusStringConcatSniff.php │ │ ├── ReferenceThisSniff.php │ │ ├── StaticClosureSniff.php │ │ └── SuperGlobalsUsageSniff.php │ ├── Utils │ │ └── ExtensionInfo.php │ ├── VariableAnalysis │ │ ├── MisleadingGlobalNamesSniff.php │ │ └── UnusedGlobalVariablesSniff.php │ └── WhiteSpace │ │ ├── EmptyLinesBetweenUseSniff.php │ │ ├── MultipleEmptyLinesSniff.php │ │ ├── OpeningKeywordParenthesisSniff.php │ │ ├── SpaceAfterClosureSniff.php │ │ ├── SpaceBeforeBracketSniff.php │ │ ├── SpaceBeforeClassBraceSniff.php │ │ ├── SpaceBeforeSingleLineCommentSniff.php │ │ ├── SpaceyParenthesisSniff.php │ │ ├── UnaryMinusSpacingSniff.php │ │ └── WhiteSpaceBeforeFunctionSniff.php └── ruleset.xml ├── README.md └── utils └── bootstrap-ci.php /COPYRIGHT: -------------------------------------------------------------------------------- 1 | This code is copyrighted: 2 | 3 | Copyright (c) 2012-2013 Antoine "hashar Musso 4 | Copyirght (c) 2013 Wikimedia Foundation 5 | 6 | Code made by Antoine "hashar" Musso since October 2012 are under a joint 7 | copyright with the "Wikimedia Foundation". 8 | 9 | See LICENSE for the actual license. 10 | -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | # Doxyfile 2 | PROJECT_NAME = "MediaWiki-CodeSniffer" 3 | PROJECT_BRIEF = "MediaWiki CodeSniffer Standards" 4 | 5 | OUTPUT_DIRECTORY = doc 6 | JAVADOC_AUTOBRIEF = YES 7 | QT_AUTOBRIEF = YES 8 | QUIET = YES 9 | WARNINGS = NO 10 | WARN_IF_UNDOCUMENTED = NO 11 | WARN_IF_DOC_ERROR = NO 12 | INPUT = README.md COPYRIGHT LICENSE MediaWiki/ 13 | FILE_PATTERNS = *.php 14 | RECURSIVE = YES 15 | USE_MDFILE_AS_MAINPAGE = README.md 16 | HTML_COLORSTYLE = LIGHT 17 | HTML_DYNAMIC_SECTIONS = YES 18 | GENERATE_TREEVIEW = YES 19 | TREEVIEW_WIDTH = 250 20 | GENERATE_LATEX = NO 21 | GENERATE_TAGFILE = doc/html/tagfile.xml 22 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/AlternativeSyntax/LeadingZeroInFloatSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 49 | $content = $tokens[$stackPtr]['content']; 50 | if ( $content[0] === '.' ) { 51 | // Starts with a ., needs a leading 0. 52 | $fix = $phpcsFile->addFixableWarning( 53 | 'Floats should have a leading 0', 54 | $stackPtr, 55 | 'Found' 56 | ); 57 | if ( $fix ) { 58 | $phpcsFile->fixer->addContentBefore( $stackPtr, '0' ); 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/AlternativeSyntax/UnicodeEscapeSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 46 | 47 | // Find the end of the string. 48 | $endPtr = $phpcsFile->findNext( 49 | /* types */ [ $tokens[$stackPtr]['code'], T_HEREDOC, T_END_HEREDOC ], 50 | /* start */ $stackPtr + 1, 51 | /* end */ null, 52 | /* exclude */ true 53 | ) ?: $phpcsFile->numTokens; 54 | 55 | if ( $tokens[$endPtr - 1]['code'] === T_END_HEREDOC ) { 56 | if ( isset( $tokens[$endPtr] ) && $tokens[$endPtr]['code'] === T_SEMICOLON ) { 57 | ++$endPtr; 58 | } 59 | if ( isset( $tokens[$endPtr] ) && $tokens[$endPtr]['code'] === T_WHITESPACE ) { 60 | ++$endPtr; 61 | } 62 | } 63 | 64 | // If this is a single-quoted string, skip it. 65 | if ( $tokens[$stackPtr]['code'] === T_CONSTANT_ENCAPSED_STRING && 66 | $tokens[$stackPtr]['content'][0] === "'" 67 | ) { 68 | return $endPtr; 69 | } 70 | 71 | // If the string takes up multiple lines, PHP_CodeSniffer would 72 | // have split some of its tokens. Recombine the string's tokens 73 | // so the next step will work. 74 | $content = $phpcsFile->getTokensAsString( $stackPtr, $endPtr - $stackPtr ); 75 | 76 | // If the string contains braced expressions, PHP_CodeSniffer 77 | // would have combined these and surrounding tokens, which could 78 | // lead to false matches. Avoid this by retokenizing the string. 79 | $origTokens = token_get_all( ' $origToken ) { 83 | // Skip the PHP opening tag we added. 84 | if ( $i === 0 ) { 85 | continue; 86 | } 87 | 88 | // Don't check tokens that cannot contain escape sequences. 89 | $origToken = (array)$origToken; 90 | if ( !( 91 | $origToken[0] === T_ENCAPSED_AND_WHITESPACE || 92 | ( $origToken[0] === T_CONSTANT_ENCAPSED_STRING && $origToken[1][0] !== "'" ) 93 | ) ) { 94 | $content .= $origToken[1] ?? $origToken[0]; 95 | continue; 96 | } 97 | 98 | // Check for Unicode escape sequences in the token, explicitly 99 | // skipping escaped backslashes to prevent false matches. 100 | $content .= preg_replace_callback( 101 | '/\\\\(?:u\{([0-9A-Fa-f]+)\}|\\\\(*SKIP)(*FAIL))/', 102 | static function ( array $m ) use ( &$warn ) { 103 | // Decode the codepoint-digits. 104 | $cp = hexdec( $m[1] ); 105 | if ( $cp > 0x10FFFF ) { 106 | // This is a parse error. Don't offer to fix it. 107 | return $m[0]; 108 | } 109 | 110 | // Check the codepoint-digits against the expected format. 111 | $hex = sprintf( '%04X', $cp ); 112 | if ( $m[1] === $hex ) { 113 | // Keep the conforming escape sequence as-is. 114 | return $m[0]; 115 | } 116 | 117 | // Print a warning for the token containing the nonconforming 118 | // escape sequence and replace it with a conforming one. 119 | $warn = true; 120 | return '\u{' . $hex . '}'; 121 | }, 122 | $origToken[1] 123 | ); 124 | } 125 | 126 | if ( $warn ) { 127 | $fix = $phpcsFile->addFixableWarning( 128 | 'Unicode code points should be expressed using four to six uppercase hex ' . 129 | 'digits, with leading zeros used only as necessary for \u{0FFF} and below', 130 | $stackPtr, 131 | 'DigitsNotNormalized' 132 | ); 133 | if ( $fix ) { 134 | $phpcsFile->fixer->beginChangeset(); 135 | $phpcsFile->fixer->replaceToken( $stackPtr, $content ); 136 | for ( $i = $stackPtr + 1; $i < $endPtr; ++$i ) { 137 | $phpcsFile->fixer->replaceToken( $i, '' ); 138 | } 139 | $phpcsFile->fixer->endChangeset(); 140 | } 141 | } 142 | 143 | return $endPtr; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Arrays/AlphabeticArraySortSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 49 | $end = $tokens[$stackPtr]['comment_closer']; 50 | foreach ( $tokens[$stackPtr]['comment_tags'] as $tag ) { 51 | if ( $tokens[$tag]['content'] === self::ANNOTATION_NAME ) { 52 | $this->processDocTag( $phpcsFile, $tokens, $tag, $end ); 53 | break; 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * @param File $phpcsFile 60 | * @param array[] $tokens 61 | * @param int $tagPtr Token position of the tag 62 | * @param int $docEnd Token position of the end of the doc comment 63 | */ 64 | private function processDocTag( File $phpcsFile, array $tokens, int $tagPtr, int $docEnd ): void { 65 | $arrayToken = $phpcsFile->findNext( [ T_OPEN_SHORT_ARRAY, T_ARRAY ], $docEnd + 1 ); 66 | if ( $arrayToken === false || ( 67 | // On the same line or one line after the doc block 68 | $tokens[$docEnd]['line'] !== $tokens[$arrayToken]['line'] && 69 | $tokens[$docEnd]['line'] !== $tokens[$arrayToken]['line'] - 1 ) 70 | ) { 71 | $phpcsFile->addWarning( 72 | 'No array found after %s', 73 | $tagPtr, 74 | 'Unsupported', 75 | [ self::ANNOTATION_NAME, $tokens[$arrayToken]['content'] ] 76 | ); 77 | return; 78 | } 79 | 80 | if ( !isset( $tokens[$arrayToken]['bracket_opener'] ) ) { 81 | // Live coding 82 | return; 83 | } 84 | 85 | $endArray = $tokens[$arrayToken]['bracket_closer'] - 1; 86 | $startArray = $phpcsFile->findNext( 87 | Tokens::$emptyTokens, 88 | $tokens[$arrayToken]['bracket_opener'] + 1, 89 | $endArray, 90 | true 91 | ); 92 | if ( $startArray === false ) { 93 | // Empty array 94 | return; 95 | } 96 | $endArray = $phpcsFile->findPrevious( Tokens::$emptyTokens, $endArray, $startArray, true ); 97 | if ( $tokens[$endArray]['code'] === T_COMMA ) { 98 | // Ignore trailing commas 99 | $endArray--; 100 | } 101 | 102 | $keys = []; 103 | $duplicateCounter = 0; 104 | $next = $startArray; 105 | while ( $next <= $endArray ) { 106 | $endStatement = $phpcsFile->findEndOfStatement( $next, [ T_DOUBLE_ARROW ] ); 107 | if ( $endStatement >= $endArray ) { 108 | // Not going ahead on our own end 109 | $endStatement = $endArray; 110 | $endItem = $endArray; 111 | } else { 112 | // Do not track comma 113 | $endItem = $endStatement - 1; 114 | } 115 | 116 | $keyStart = $phpcsFile->findNext( Tokens::$emptyTokens, $next, $endItem + 1, true ); 117 | $keyEnd = $phpcsFile->findNext( [ T_DOUBLE_ARROW ], $keyStart, $endItem + 1 ); 118 | 119 | // Determine if it's a key-value pair or just a value 120 | if ( $keyEnd !== false ) { 121 | $arrayKey = trim( $phpcsFile->getTokensAsString( $keyStart, $keyEnd - $keyStart ) ); 122 | } else { 123 | $arrayKey = trim( $phpcsFile->getTokensAsString( $keyStart, $endItem - $keyStart + 1 ) ); 124 | } 125 | 126 | if ( isset( $keys[$arrayKey] ) ) { 127 | $phpcsFile->addWarning( 128 | 'Found duplicate key "%s" on array required sorting', 129 | $keyStart, 130 | 'Duplicate', 131 | [ $arrayKey ] 132 | ); 133 | $duplicateCounter++; 134 | // Make the key unique to get a stable sort result and to handle this token as well 135 | $arrayKey .= "\0" . $duplicateCounter; 136 | } 137 | 138 | $keys[$arrayKey] = [ 139 | 'key' => $keyStart, 140 | 'end' => $endItem, 141 | 'startLocation' => $next, 142 | 'endLocation' => $endStatement, 143 | ]; 144 | $next = $endStatement + 1; 145 | } 146 | 147 | $sortedKeys = $this->sortStatements( $keys ); 148 | if ( $sortedKeys === array_keys( $keys ) ) { 149 | return; 150 | } 151 | 152 | $fix = $phpcsFile->addFixableWarning( 153 | 'Array is not sorted alphabetically', 154 | $tagPtr, 155 | 'Unsorted' 156 | ); 157 | 158 | if ( $fix ) { 159 | $this->rebuildSortedArray( $phpcsFile, $sortedKeys, $keys, $startArray ); 160 | } else { 161 | $this->warnOnFirstMismatch( $phpcsFile, $sortedKeys, $keys ); 162 | } 163 | } 164 | 165 | /** 166 | * Add a warning on first mismatched key to make it easier found the wrong key in the array. 167 | * On each key could make warning on all keys, when the first is already out of order 168 | * 169 | * @param File $phpcsFile 170 | * @param string[] $sorted 171 | * @param array[] $unsorted 172 | */ 173 | private function warnOnFirstMismatch( File $phpcsFile, array $sorted, array $unsorted ): void { 174 | $iteratorUnsorted = new ArrayIterator( $unsorted ); 175 | foreach ( $sorted as $sortedKey ) { 176 | $unsortedKey = $iteratorUnsorted->key(); 177 | if ( $sortedKey !== $unsortedKey ) { 178 | $unsortedToken = $iteratorUnsorted->current(); 179 | $phpcsFile->addFixableWarning( 180 | 'This key is out of order (Needs %s, got %s)', 181 | $unsortedToken['key'], 182 | 'UnsortedHint', 183 | [ $sortedKey, $unsortedKey ] 184 | ); 185 | break; 186 | } 187 | $iteratorUnsorted->next(); 188 | } 189 | } 190 | 191 | /** 192 | * When autofix is wanted, rebuild the content of the array and use it 193 | * Get the comma and line indents between each items from the current order. 194 | * Add the key and values in sorted order. 195 | * 196 | * @param File $phpcsFile 197 | * @param string[] $sorted 198 | * @param array[] $unsorted 199 | * @param int $stackPtr 200 | */ 201 | private function rebuildSortedArray( File $phpcsFile, array $sorted, array $unsorted, int $stackPtr ): void { 202 | $phpcsFile->fixer->beginChangeset(); 203 | $iteratorSorted = new ArrayIterator( $sorted ); 204 | $newArray = ''; 205 | $lastEnd = false; 206 | foreach ( $unsorted as $values ) { 207 | // Add comma and indent between the items 208 | if ( $lastEnd !== false ) { 209 | $newArray .= $phpcsFile->getTokensAsString( 210 | $lastEnd + 1, 211 | $values['key'] - $lastEnd - 1, 212 | // keep tabs on multiline statements 213 | true 214 | ); 215 | } 216 | $lastEnd = $values['end']; 217 | 218 | // Add the array item 219 | $sortedKey = $iteratorSorted->current(); 220 | $unsortedToken = $unsorted[$sortedKey]; 221 | $newArray .= $phpcsFile->getTokensAsString( 222 | $unsortedToken['key'], 223 | $unsortedToken['end'] - $unsortedToken['key'] + 1, 224 | // keep tabs on multiline statements 225 | true 226 | ); 227 | $iteratorSorted->next(); 228 | 229 | // remove at old location including comma and indent 230 | for ( $i = $unsortedToken['startLocation']; $i <= $unsortedToken['endLocation']; $i++ ) { 231 | $phpcsFile->fixer->replaceToken( $i, '' ); 232 | } 233 | } 234 | $phpcsFile->fixer->addContent( $stackPtr, $newArray ); 235 | $phpcsFile->fixer->endChangeset(); 236 | } 237 | 238 | /** 239 | * This sorts the array keys 240 | * 241 | * @param array[] $statementList Array mapping keys to tokens 242 | * @return string[] Sorted list of keys 243 | */ 244 | private function sortStatements( array $statementList ): array { 245 | $map = []; 246 | foreach ( $statementList as $key => $_ ) { 247 | $map[$key] = trim( $key, "'\"" ); 248 | } 249 | natcasesort( $map ); 250 | // @phan-suppress-next-line PhanTypeMismatchReturn False positive as array_keys can return list 251 | return array_keys( $map ); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Arrays/TrailingCommaSniff.php: -------------------------------------------------------------------------------- 1 | 35 | * 36 | * 37 | * 38 | * 39 | * 40 | */ 41 | class TrailingCommaSniff implements Sniff { 42 | 43 | /** 44 | * Enforce the presence (true) or absence (false) of trailing commas in single-line arrays. 45 | * By default (null), do nothing. 46 | * @var bool|null 47 | */ 48 | public ?bool $singleLine = null; 49 | 50 | /** 51 | * Enforce the presence (true) or absence (false) of trailing commas in multi-line arrays. 52 | * By default (null), do nothing. 53 | * @var bool|null 54 | */ 55 | public ?bool $multiLine = null; 56 | 57 | public function register(): array { 58 | return [ T_CLOSE_SHORT_ARRAY ]; 59 | } 60 | 61 | /** 62 | * @param File $phpcsFile 63 | * @param int $stackPtr The current token index. 64 | * @return void|int 65 | */ 66 | public function process( File $phpcsFile, $stackPtr ) { 67 | if ( $this->singleLine === null && $this->multiLine === null ) { 68 | // not configured to do anything, skip to end of file 69 | return $phpcsFile->numTokens; 70 | } 71 | 72 | $tokens = $phpcsFile->getTokens(); 73 | 74 | $lastContentToken = $phpcsFile->findPrevious( 75 | Tokens::$emptyTokens, 76 | $stackPtr - 1, 77 | null, 78 | true 79 | ); 80 | 81 | $isEmptyArray = $tokens[$lastContentToken]['code'] === T_OPEN_SHORT_ARRAY; 82 | if ( $isEmptyArray ) { 83 | // PHP syntax doesn't allow [,] so we can stop early 84 | return; 85 | } 86 | 87 | $isMultiline = false; 88 | for ( $token = $lastContentToken + 1; $token < $stackPtr; $token++ ) { 89 | if ( str_contains( $tokens[$token]['content'], "\n" ) ) { 90 | $isMultiline = true; 91 | break; 92 | } 93 | } 94 | 95 | if ( $isMultiline ) { 96 | $wantTrailingComma = $this->multiLine; 97 | } else { 98 | $wantTrailingComma = $this->singleLine; 99 | } 100 | if ( $wantTrailingComma === null ) { 101 | return; 102 | } 103 | 104 | $hasTrailingComma = $tokens[$lastContentToken]['code'] === T_COMMA; 105 | 106 | $this->checkWarnAndFix( 107 | $phpcsFile, 108 | $lastContentToken, 109 | $hasTrailingComma, 110 | $wantTrailingComma, 111 | $isMultiline 112 | ); 113 | } 114 | 115 | /** 116 | * Check whether a warning should be emitted, 117 | * emit one if necessary, and fix it if requested. 118 | * 119 | * @param File $phpcsFile 120 | * @param int $token 121 | * @param bool $hasTrailingComma Whether the trailing comma *is* present. 122 | * @param bool $wantTrailingComma Whether the trailing comma *should* be present. 123 | * @param bool $isMultiline Whether the array is multi-line or single-line. 124 | * (Only used for the warning message and code at this point.) 125 | */ 126 | private function checkWarnAndFix( 127 | File $phpcsFile, 128 | int $token, 129 | bool $hasTrailingComma, 130 | bool $wantTrailingComma, 131 | bool $isMultiline 132 | ): void { 133 | if ( $hasTrailingComma === $wantTrailingComma ) { 134 | return; 135 | } 136 | 137 | // possible messages (for grepping): 138 | // Multi-line array with trailing comma 139 | // Multi-line array without trailing comma 140 | // Single-line array with trailing comma 141 | // Single-line array without trailing comma 142 | $fix = $phpcsFile->addFixableWarning( 143 | '%s array %s trailing comma', 144 | $token, 145 | $isMultiline ? 'MultiLine' : 'SingleLine', 146 | [ 147 | $isMultiline ? 'Multi-line' : 'Single-line', 148 | $wantTrailingComma ? 'without' : 'with', 149 | ] 150 | ); 151 | if ( !$fix ) { 152 | return; 153 | } 154 | 155 | // adding/removing the trailing comma works the same regardless of $isMultiline 156 | if ( $wantTrailingComma ) { 157 | $phpcsFile->fixer->addContent( $token, ',' ); 158 | } else { 159 | $phpcsFile->fixer->replaceToken( $token, '' ); 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Classes/FullQualifiedClassNameSniff.php: -------------------------------------------------------------------------------- 1 | 26 | * 5 27 | * 28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * Note this sniff currently does not check class names mentioned in PHPDoc comments. 35 | * 36 | * @author Thiemo Kreuz 37 | */ 38 | class FullQualifiedClassNameSniff implements Sniff { 39 | 40 | /** 41 | * @var bool Allows full qualified class names in the main namespace, e.g. \Title 42 | */ 43 | public bool $allowMainNamespace = true; 44 | 45 | /** 46 | * @var bool Allows to use full qualified class names in "extends" and "implements" 47 | */ 48 | public bool $allowInheritance = true; 49 | 50 | /** 51 | * @var bool Allows function calls like \Wikimedia\suppressWarnings() 52 | */ 53 | public bool $allowFunctions = true; 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function register(): array { 59 | return [ T_NS_SEPARATOR ]; 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function process( File $phpcsFile, $stackPtr ) { 66 | $tokens = $phpcsFile->getTokens(); 67 | 68 | if ( !isset( $tokens[$stackPtr + 2] ) 69 | // The current backslash is not the last one in the full qualified class name 70 | || $tokens[$stackPtr + 2]['code'] === T_NS_SEPARATOR 71 | // Some unexpected backslash that's not part of a class name 72 | || $tokens[$stackPtr + 1]['code'] !== T_STRING 73 | ) { 74 | return; 75 | } 76 | 77 | if ( $this->allowMainNamespace 78 | && $tokens[$stackPtr - 1]['code'] !== T_STRING 79 | ) { 80 | return; 81 | } 82 | 83 | if ( $this->allowFunctions 84 | && $tokens[$stackPtr + 2]['code'] === T_OPEN_PARENTHESIS 85 | ) { 86 | return; 87 | } 88 | 89 | $skip = Tokens::$emptyTokens; 90 | $skip[] = T_STRING; 91 | $skip[] = T_NS_SEPARATOR; 92 | $prev = $phpcsFile->findPrevious( $skip, $stackPtr - 2, null, true ); 93 | if ( !$prev ) { 94 | return; 95 | } 96 | $prev = $tokens[$prev]['code']; 97 | 98 | // "namespace" and "use" statements must use full qualified class names 99 | if ( $prev === T_NAMESPACE || $prev === T_USE ) { 100 | return; 101 | } 102 | 103 | if ( $this->allowInheritance 104 | && ( $prev === T_EXTENDS || $prev === T_IMPLEMENTS ) 105 | ) { 106 | return; 107 | } 108 | 109 | $phpcsFile->addError( 110 | 'Full qualified class name "%s\\%s" found, please utilize "use …"', 111 | $stackPtr, 112 | 'Found', 113 | [ 114 | $tokens[$stackPtr - 1]['code'] === T_STRING ? '…' : '', 115 | $tokens[$stackPtr + 1]['content'], 116 | ] 117 | ); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/ClassLevelLicenseSniff.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * 16 | * 17 | * 18 | * 19 | * Doing so makes the LicenseComment sniff obsolete. You might want to disable it: 20 | * 21 | * 22 | * @license GPL-2.0-or-later 23 | * @author Thiemo Kreuz 24 | */ 25 | class ClassLevelLicenseSniff implements Sniff { 26 | 27 | /** 28 | * @var string Typically "GPL-2.0-or-later", empty by default 29 | */ 30 | public string $license = ''; 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | public function register(): array { 36 | return [ T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM ]; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function process( File $phpcsFile, $stackPtr ) { 43 | // This sniff requires you to set a in your .phpcs.xml 44 | if ( !$this->license ) { 45 | return $phpcsFile->numTokens; 46 | } 47 | 48 | $tokens = $phpcsFile->getTokens(); 49 | 50 | // All auto-fixes below assume we are on the top level 51 | if ( $tokens[$stackPtr]['level'] !== 0 ) { 52 | // @codeCoverageIgnoreStart 53 | return; 54 | // @codeCoverageIgnoreEnd 55 | } 56 | 57 | $skip = Tokens::$methodPrefixes; 58 | $skip[] = T_WHITESPACE; 59 | $closer = $phpcsFile->findPrevious( $skip, $stackPtr - 1, null, true ); 60 | 61 | if ( $tokens[$closer]['code'] !== T_DOC_COMMENT_CLOSE_TAG ) { 62 | $fix = $phpcsFile->addFixableError( 63 | 'All code in this codebase should have a @license tag', 64 | $stackPtr, 65 | 'Missing' 66 | ); 67 | if ( $fix ) { 68 | $phpcsFile->fixer->addContentBefore( 69 | $stackPtr, 70 | "/**\n * @license $this->license\n */\n" 71 | ); 72 | } 73 | return; 74 | } 75 | 76 | if ( !isset( $tokens[$closer]['comment_opener'] ) ) { 77 | // @codeCoverageIgnoreStart 78 | // Live coding 79 | return; 80 | // @codeCoverageIgnoreEnd 81 | } 82 | 83 | $opener = $tokens[$closer]['comment_opener']; 84 | foreach ( $tokens[$opener]['comment_tags'] as $ptr ) { 85 | $tag = $tokens[$ptr]['content']; 86 | if ( strncasecmp( $tag, '@licen', 6 ) !== 0 ) { 87 | continue; 88 | } 89 | 90 | if ( !isset( $tokens[$ptr + 2] ) 91 | || $tokens[$ptr + 2]['code'] !== T_DOC_COMMENT_STRING 92 | ) { 93 | $fix = $phpcsFile->addFixableError( 94 | 'All code in this codebase should be tagged with "%s %s", found empty "%s" instead', 95 | $ptr, 96 | 'Empty', 97 | [ $tag, $this->license, $tag ] 98 | ); 99 | if ( $fix ) { 100 | $phpcsFile->fixer->addContent( $ptr, " $this->license" ); 101 | } 102 | } elseif ( $tokens[$ptr + 2]['content'] !== $this->license ) { 103 | $fix = $phpcsFile->addFixableError( 104 | 'All code in this codebase should be tagged with "%s %s", found "%s %s" instead', 105 | $ptr + 2, 106 | 'WrongLicense', 107 | [ $tag, $this->license, $tag, $tokens[$ptr + 2]['content'] ] 108 | ); 109 | if ( $fix ) { 110 | $phpcsFile->fixer->replaceToken( $ptr + 2, $this->license ); 111 | } 112 | } 113 | 114 | // This sniff intentionally checks the first @license tag only 115 | return; 116 | } 117 | 118 | $fix = $phpcsFile->addFixableError( 119 | 'All code in this codebase should have a @license tag', 120 | $opener, 121 | 'Missing' 122 | ); 123 | if ( $fix ) { 124 | $phpcsFile->fixer->addContentBefore( $closer, "* @license $this->license\n " ); 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/DocumentationTypeTrait.php: -------------------------------------------------------------------------------- 1 | '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>` 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 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 | } 254 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/EmptyTagSniff.php: -------------------------------------------------------------------------------- 1 | '@access', 38 | '@author' => '@author', 39 | '@dataProvider' => '@dataProvider', 40 | '@depends' => '@depends', 41 | '@group' => '@group', 42 | '@license' => '@license', 43 | '@link' => '@link', 44 | '@see' => '@see', 45 | '@since' => '@since', 46 | '@suppress' => '@suppress', 47 | ]; 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function register(): array { 53 | return [ T_DOC_COMMENT_OPEN_TAG ]; 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 stack passed in $tokens. 61 | * 62 | * @return void 63 | */ 64 | public function process( File $phpcsFile, $stackPtr ) { 65 | $tokens = $phpcsFile->getTokens(); 66 | // Delay this because we typically (when there are no errors) don't need it 67 | $where = null; 68 | 69 | foreach ( $tokens[$stackPtr]['comment_tags'] as $tag ) { 70 | $content = $tokens[$tag]['content']; 71 | 72 | if ( !isset( self::DISALLOWED_EMPTY_TAGS[$content] ) || 73 | !isset( $tokens[$tag + 2] ) || 74 | // The tag is "not empty" only when it's followed by something on the same line 75 | ( $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING && 76 | $tokens[$tag + 2]['line'] === $tokens[$tag]['line'] ) 77 | ) { 78 | continue; 79 | } 80 | 81 | if ( !$where ) { 82 | $where = $this->findContext( $phpcsFile, $tokens[$stackPtr]['comment_closer'] + 1 ); 83 | } 84 | 85 | $phpcsFile->addError( 86 | 'Content missing for %s tag in %s comment', 87 | $tag, 88 | ucfirst( $where ) . ucfirst( substr( $content, 1 ) ), 89 | [ $content, $where ] 90 | ); 91 | } 92 | } 93 | 94 | /** 95 | * @param File $phpcsFile 96 | * @param int $start 97 | * 98 | * @return string Either "property" or "function" 99 | */ 100 | private function findContext( File $phpcsFile, int $start ): string { 101 | $tokens = $phpcsFile->getTokens(); 102 | $skip = array_merge( 103 | Tokens::$emptyTokens, 104 | Tokens::$methodPrefixes, 105 | [ 106 | // Skip outdated `var` keywords as well 107 | T_VAR, 108 | // Skip type hints, e.g. in `public ?Foo\Bar $var` 109 | T_STRING, 110 | T_NS_SEPARATOR, 111 | T_NULLABLE, 112 | ] 113 | ); 114 | $next = $phpcsFile->findNext( $skip, $start, null, true ); 115 | return $tokens[$next]['code'] === T_VARIABLE ? 'property' : $tokens[$next]['content']; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/IllegalSingleLineCommentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | $currentToken = $tokens[$stackPtr]; 33 | 34 | if ( str_starts_with( $currentToken['content'], '/*' ) ) { 35 | // Possible inline comment 36 | if ( !str_ends_with( $currentToken['content'], '*/' ) ) { 37 | // Whether it's a comment across multiple lines 38 | $numOfTokens = $phpcsFile->numTokens; 39 | for ( $i = $stackPtr + 1; $i < $numOfTokens; $i++ ) { 40 | $token = $tokens[$i]; 41 | if ( ( $token['code'] !== T_COMMENT && $token['code'] !== T_WHITESPACE ) || ( 42 | !str_starts_with( $token['content'], '/*' ) && 43 | str_ends_with( $token['content'], '*/' ) 44 | ) ) { 45 | return; 46 | } 47 | } 48 | $fix = $phpcsFile->addFixableError( 49 | 'Missing proper ending of a single line comment', 50 | $stackPtr, 51 | 'MissingCommentEnding' 52 | ); 53 | if ( $fix ) { 54 | $phpcsFile->fixer->replaceToken( 55 | $stackPtr, 56 | rtrim( $currentToken['content'] ) . ' */' . $phpcsFile->eolChar 57 | ); 58 | } 59 | } else { 60 | // Determine whether multiple "*" appears right before the "*/" 61 | if ( preg_match( '/[^\/*](\*){2,}\//', $currentToken['content'] ) !== 0 ) { 62 | $fix = $phpcsFile->addFixableWarning( 63 | 'Invalid end of a single line comment', 64 | $stackPtr, 65 | 'IllegalSingleLineCommentEnd' 66 | ); 67 | if ( $fix ) { 68 | $phpcsFile->fixer->replaceToken( 69 | $stackPtr, 70 | preg_replace( '/(\*){2,}\//', '*/', $currentToken['content'] ) 71 | ); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/LicenseCommentSniff.php: -------------------------------------------------------------------------------- 1 | 'GPL-2.0-or-later', 37 | 'GNU GPL v2\+' => 'GPL-2.0-or-later', 38 | ]; 39 | 40 | /** 41 | * Returns an array of tokens this test wants to listen for. 42 | * 43 | * @return array 44 | */ 45 | public function register(): array { 46 | return [ T_DOC_COMMENT_OPEN_TAG ]; 47 | } 48 | 49 | /** 50 | * Processes this test, when one of its tokens is encountered. 51 | * 52 | * @param File $phpcsFile The file being scanned. 53 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 54 | * @return void 55 | */ 56 | public function process( File $phpcsFile, $stackPtr ) { 57 | $tokens = $phpcsFile->getTokens(); 58 | $end = $tokens[$stackPtr]['comment_closer']; 59 | foreach ( $tokens[$stackPtr]['comment_tags'] as $tag ) { 60 | $this->processDocTag( $phpcsFile, $tokens, $tag, $end ); 61 | } 62 | } 63 | 64 | /** 65 | * @param File $phpcsFile 66 | * @param array[] $tokens 67 | * @param int $tag Token position of the tag 68 | * @param int $end Token position of the end of the doc comment 69 | */ 70 | private function processDocTag( File $phpcsFile, array $tokens, int $tag, int $end ): void { 71 | $tagText = $tokens[$tag]['content']; 72 | 73 | if ( $tagText === '@licence' ) { 74 | $fix = $phpcsFile->addFixableWarning( 75 | 'Incorrect wording of @license', $tag, 'LicenceTag' 76 | ); 77 | if ( $fix ) { 78 | $phpcsFile->fixer->replaceToken( $tag, '@license' ); 79 | } 80 | } elseif ( $tagText !== '@license' ) { 81 | return; 82 | } 83 | 84 | if ( $tokens[$tag]['level'] !== 0 ) { 85 | $phpcsFile->addWarning( 86 | '@license should only be used on the top level', 87 | $tag, 'LicenseTagNonFileComment' 88 | ); 89 | } 90 | 91 | // It is okay to have more than one @license 92 | 93 | // Validate text behind @license 94 | $next = $phpcsFile->findNext( [ T_DOC_COMMENT_WHITESPACE ], $tag + 1, $end, true ); 95 | if ( $tokens[$next]['code'] !== T_DOC_COMMENT_STRING ) { 96 | $phpcsFile->addWarning( 97 | '@license not followed by a license', 98 | $tag, 'LicenseTagEmpty' 99 | ); 100 | return; 101 | } 102 | $license = rtrim( $tokens[$next]['content'] ); 103 | 104 | // @license can contain a url, use the text behind it 105 | $m = []; 106 | if ( preg_match( '/^https?:\/\/[^\s]+\s+(.*)/', $license, $m ) ) { 107 | $license = $m[1]; 108 | } 109 | 110 | $licenseValidator = self::getLicenseValidator(); 111 | if ( !$licenseValidator->validate( $license ) ) { 112 | $fixable = null; 113 | foreach ( self::REPLACEMENTS as $regex => $identifier ) { 114 | // Make sure the entire license matches the regex, and 115 | // then check that the new replacement is valid too 116 | if ( preg_match( "/^$regex$/", $license ) === 1 117 | && $licenseValidator->validate( $identifier ) 118 | ) { 119 | $fixable = $identifier; 120 | break; 121 | } 122 | } 123 | if ( $fixable !== null ) { 124 | $fix = $phpcsFile->addFixableWarning( 125 | 'Invalid SPDX license identifier "%s", see ', 126 | $tag, 'InvalidLicenseTag', [ $license ] 127 | ); 128 | if ( $fix ) { 129 | $phpcsFile->fixer->replaceToken( $next, $fixable ); 130 | } 131 | } else { 132 | $phpcsFile->addWarning( 133 | 'Invalid SPDX license identifier "%s", see ', 134 | $tag, 'InvalidLicenseTag', [ $license ] 135 | ); 136 | } 137 | } else { 138 | // Split list to check each license for deprecation 139 | $singleLicenses = preg_split( '/\s+(?:AND|OR)\s+/i', $license ); 140 | foreach ( $singleLicenses as $singleLicense ) { 141 | // Check if the split license is known to the validator - T195429 142 | if ( !is_array( $licenseValidator->getLicenseByIdentifier( $singleLicense ) ) ) { 143 | // @codeCoverageIgnoreStart 144 | $phpcsFile->addWarning( 145 | 'An error occurred during processing SPDX license identifier "%s"', 146 | $tag, 'ErrorLicenseTag', [ $license ] 147 | ); 148 | break; 149 | // @codeCoverageIgnoreEnd 150 | } 151 | if ( $licenseValidator->isDeprecatedByIdentifier( $singleLicense ) ) { 152 | $phpcsFile->addWarning( 153 | 'Deprecated SPDX license identifier "%s", see ', 154 | $tag, 'DeprecatedLicenseTag', [ $singleLicense ] 155 | ); 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * @return SpdxLicenses 163 | */ 164 | private static function getLicenseValidator(): SpdxLicenses { 165 | if ( self::$licenses === null ) { 166 | self::$licenses = new SpdxLicenses(); 167 | } 168 | return self::$licenses; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/MissingCoversSniff.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | */ 20 | 21 | namespace MediaWiki\Sniffs\Commenting; 22 | 23 | use PHP_CodeSniffer\Files\File; 24 | use PHP_CodeSniffer\Sniffs\Sniff; 25 | use PHP_CodeSniffer\Util\Tokens; 26 | 27 | /** 28 | * Identify Test classes that do not have 29 | * any @covers tags 30 | */ 31 | class MissingCoversSniff implements Sniff { 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function register(): array { 37 | return [ T_CLASS ]; 38 | } 39 | 40 | /** 41 | * @param File $phpcsFile 42 | * @param int $stackPtr Position of T_CLASS 43 | * @return void 44 | */ 45 | public function process( File $phpcsFile, $stackPtr ) { 46 | $name = $phpcsFile->getDeclarationName( $stackPtr ); 47 | if ( !str_ends_with( $name, 'Test' ) ) { 48 | // Only want to validate classes that end in test 49 | return; 50 | } 51 | $props = $phpcsFile->getClassProperties( $stackPtr ); 52 | if ( $props['is_abstract'] ) { 53 | // No point in requiring @covers from an abstract class 54 | return; 55 | } 56 | 57 | $classCovers = $this->hasCoversTags( $phpcsFile, $stackPtr ); 58 | if ( $classCovers ) { 59 | // The class has a @covers tag, awesome. 60 | return; 61 | } 62 | 63 | // Check each individual test function 64 | $tokens = $phpcsFile->getTokens(); 65 | $classCloser = $tokens[$stackPtr]['scope_closer']; 66 | $funcPtr = $stackPtr; 67 | while ( true ) { 68 | $funcPtr = $phpcsFile->findNext( [ T_FUNCTION ], $funcPtr + 1, $classCloser ); 69 | if ( !$funcPtr ) { 70 | // No more 71 | break; 72 | } 73 | 74 | $name = $phpcsFile->getDeclarationName( $funcPtr ); 75 | if ( !str_starts_with( $name, 'test' ) ) { 76 | // If it doesn't start with "test", skip 77 | continue; 78 | } 79 | 80 | $hasCovers = $this->hasCoversTags( $phpcsFile, $funcPtr ); 81 | if ( !$hasCovers ) { 82 | $phpcsFile->addWarning( 83 | 'The %s test method has no @covers tags', 84 | $funcPtr, 'MissingCovers', [ $name ] 85 | ); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Whether the statement has @covers tags 92 | * 93 | * @param File $phpcsFile 94 | * @param int $stackPtr Position of T_CLASS/T_FUNCTION 95 | * 96 | * @return bool 97 | */ 98 | protected function hasCoversTags( File $phpcsFile, int $stackPtr ): bool { 99 | $exclude = array_merge( 100 | Tokens::$methodPrefixes, 101 | [ T_WHITESPACE ] 102 | ); 103 | $closer = $phpcsFile->findPrevious( $exclude, $stackPtr - 1, 0, true ); 104 | if ( $closer === false ) { 105 | return false; 106 | } 107 | $tokens = $phpcsFile->getTokens(); 108 | $token = $tokens[$closer]; 109 | if ( $token['code'] !== T_DOC_COMMENT_CLOSE_TAG ) { 110 | // No doc comment 111 | return false; 112 | } 113 | 114 | $opener = $tokens[$closer]['comment_opener']; 115 | $tags = $tokens[$opener]['comment_tags']; 116 | foreach ( $tags as $tag ) { 117 | $name = $tokens[$tag]['content']; 118 | if ( $name === '@covers' || $name === '@coversNothing' ) { 119 | return true; 120 | } 121 | } 122 | 123 | return false; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/PropertyDocumentationSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 49 | 50 | // Only for class properties 51 | $scopes = array_keys( $tokens[$stackPtr]['conditions'] ); 52 | $scope = array_pop( $scopes ); 53 | if ( isset( $tokens[$stackPtr]['nested_parenthesis'] ) 54 | || $scope === null 55 | || ( $tokens[$scope]['code'] !== T_CLASS && $tokens[$scope]['code'] !== T_TRAIT ) 56 | ) { 57 | return; 58 | } 59 | 60 | $find = Tokens::$emptyTokens; 61 | $find[] = T_STATIC; 62 | $find[] = T_NULLABLE; 63 | $find[] = T_STRING; 64 | $visibilityPtr = $phpcsFile->findPrevious( $find, $stackPtr - 1, null, true ); 65 | if ( !$visibilityPtr || ( $tokens[$visibilityPtr]['code'] !== T_VAR && 66 | !isset( Tokens::$scopeModifiers[ $tokens[$visibilityPtr]['code'] ] ) ) 67 | ) { 68 | return; 69 | } 70 | $commentEnd = $phpcsFile->findPrevious( [ T_WHITESPACE ], $visibilityPtr - 1, null, true ); 71 | if ( $tokens[$commentEnd]['code'] === T_COMMENT ) { 72 | // Inline comments might just be closing comments for 73 | // control structures or functions instead of function comments 74 | // using the wrong comment type. If there is other code on the line, 75 | // assume they relate to that code. 76 | $prev = $phpcsFile->findPrevious( $find, $commentEnd - 1, null, true ); 77 | if ( $prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line'] ) { 78 | $commentEnd = $prev; 79 | } 80 | } 81 | if ( $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG ) { 82 | $memberProps = $phpcsFile->getMemberProperties( $stackPtr ); 83 | if ( $memberProps['type'] === '' ) { 84 | if ( $tokens[$commentEnd]['code'] === T_COMMENT ) { 85 | $phpcsFile->addError( 86 | 'You must use "/**" style comments for a class property comment', 87 | $stackPtr, 88 | 'WrongStyle' 89 | ); 90 | return; 91 | } 92 | $phpcsFile->addError( 93 | 'Missing class property doc comment', 94 | $stackPtr, 95 | // Messages used: MissingDocumentationPublic, MissingDocumentationProtected, 96 | // MissingDocumentationPrivate 97 | 'MissingDocumentation' . ucfirst( $memberProps['scope'] ) 98 | ); 99 | } 100 | return; 101 | } 102 | if ( $tokens[$commentEnd]['line'] !== $tokens[$visibilityPtr]['line'] - 1 ) { 103 | $error = 'There must be no blank lines after the class property comment'; 104 | $phpcsFile->addError( $error, $commentEnd, 'SpacingAfter' ); 105 | } 106 | $commentStart = $tokens[$commentEnd]['comment_opener']; 107 | foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) { 108 | $tagText = $tokens[$tag]['content']; 109 | if ( strcasecmp( $tagText, '@inheritDoc' ) === 0 || $tagText === '@deprecated' ) { 110 | // No need to validate deprecated properties or those that inherit 111 | // their documentation 112 | return; 113 | } 114 | } 115 | 116 | $this->processVar( $phpcsFile, $commentStart, $stackPtr ); 117 | } 118 | 119 | /** 120 | * Process the var doc comments. 121 | * 122 | * @param File $phpcsFile The file being scanned. 123 | * @param int $commentStart The position in the stack where the comment started. 124 | * @param int $stackPtr The position in the stack where the property itself started (T_VARIABLE) 125 | */ 126 | private function processVar( File $phpcsFile, int $commentStart, int $stackPtr ): void { 127 | $tokens = $phpcsFile->getTokens(); 128 | $var = null; 129 | foreach ( $tokens[$commentStart]['comment_tags'] as $ptr ) { 130 | $tag = $tokens[$ptr]['content']; 131 | if ( $tag !== '@var' ) { 132 | continue; 133 | } 134 | if ( $var ) { 135 | $error = 'Only 1 @var tag is allowed in a class property comment'; 136 | $phpcsFile->addError( $error, $ptr, 'DuplicateVar' ); 137 | return; 138 | } 139 | $var = $ptr; 140 | } 141 | if ( $var !== null ) { 142 | $varTypeSpacing = $var + 1; 143 | // Check spaces before var 144 | if ( $tokens[$varTypeSpacing]['code'] === T_DOC_COMMENT_WHITESPACE ) { 145 | $expectedSpaces = 1; 146 | $currentSpaces = strlen( $tokens[$varTypeSpacing]['content'] ); 147 | if ( $currentSpaces !== $expectedSpaces ) { 148 | $fix = $phpcsFile->addFixableWarning( 149 | 'Expected %s spaces before var type; %s found', 150 | $varTypeSpacing, 151 | 'SpacingBeforeVarType', 152 | [ $expectedSpaces, $currentSpaces ] 153 | ); 154 | if ( $fix ) { 155 | $phpcsFile->fixer->replaceToken( $varTypeSpacing, ' ' ); 156 | } 157 | } 158 | } 159 | $varType = $var + 2; 160 | $content = ''; 161 | if ( $tokens[$varType]['code'] === T_DOC_COMMENT_STRING ) { 162 | $content = $tokens[$varType]['content']; 163 | } 164 | if ( $content === '' ) { 165 | $error = 'Var type missing for @var tag in class property comment'; 166 | $phpcsFile->addError( $error, $var, 'MissingVarType' ); 167 | return; 168 | } 169 | [ $type, $separatorLength, $comment ] = $this->splitTypeAndComment( $content ); 170 | $fixType = false; 171 | // Check for unneeded punctuation 172 | $type = $this->fixTrailingPunctuation( 173 | $phpcsFile, 174 | $varType, 175 | $type, 176 | $fixType, 177 | 'var type' 178 | ); 179 | $type = $this->fixWrappedParenthesis( 180 | $phpcsFile, 181 | $varType, 182 | $type, 183 | $fixType, 184 | 'var type' 185 | ); 186 | // Check the type for short types 187 | $type = $this->fixShortTypes( $phpcsFile, $varType, $type, $fixType, 'var' ); 188 | $this->maybeAddObjectTypehintError( 189 | $phpcsFile, 190 | $varType, 191 | $type, 192 | 'var' 193 | ); 194 | $this->maybeAddTypeTypehintError( 195 | $phpcsFile, 196 | $varType, 197 | $type, 198 | 'var' 199 | ); 200 | // Check spacing after type 201 | if ( $comment !== '' ) { 202 | $expectedSpaces = 1; 203 | if ( $separatorLength !== $expectedSpaces ) { 204 | $fix = $phpcsFile->addFixableWarning( 205 | 'Expected %s spaces after var type; %s found', 206 | $varType, 207 | 'SpacingAfterVarType', 208 | [ $expectedSpaces, $separatorLength ] 209 | ); 210 | if ( $fix ) { 211 | $fixType = true; 212 | $separatorLength = $expectedSpaces; 213 | } 214 | } 215 | } 216 | if ( $fixType ) { 217 | $phpcsFile->fixer->replaceToken( 218 | $varType, 219 | $type . ( $comment !== '' ? str_repeat( ' ', $separatorLength ) . $comment : '' ) 220 | ); 221 | } 222 | } elseif ( $phpcsFile->getMemberProperties( $stackPtr )['type'] === '' ) { 223 | $error = 'Missing type or @var tag in class property comment'; 224 | $phpcsFile->addError( $error, $tokens[$commentStart]['comment_closer'], 'MissingVar' ); 225 | } 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/RedundantVarNameSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 30 | 31 | if ( $tokens[$stackPtr]['level'] !== 1 || $tokens[$stackPtr]['content'] !== '@var' ) { 32 | return; 33 | } 34 | 35 | $docPtr = $phpcsFile->findNext( T_DOC_COMMENT_WHITESPACE, $stackPtr + 1, null, true ); 36 | if ( !$docPtr || $tokens[$docPtr]['code'] !== T_DOC_COMMENT_STRING ) { 37 | return; 38 | } 39 | 40 | // This assumes there is always a variable somewhere after a @var, which should be the case 41 | $variablePtr = $phpcsFile->findNext( T_VARIABLE, $docPtr + 1 ); 42 | if ( !$variablePtr ) { 43 | return; 44 | } 45 | 46 | $visibilityPtr = $phpcsFile->findPrevious( 47 | // This is already compatible with `public int $var;` available since PHP 7.4 48 | // Skip over `static` in the declaration too, T278471 49 | Tokens::$emptyTokens + [ T_NULLABLE, T_STRING, T_STATIC ], 50 | $variablePtr - 1, 51 | $docPtr + 1, 52 | true 53 | ); 54 | if ( !$visibilityPtr || ( $tokens[$visibilityPtr]['code'] !== T_VAR && 55 | !isset( Tokens::$scopeModifiers[ $tokens[$visibilityPtr]['code'] ] ) ) 56 | ) { 57 | return; 58 | } 59 | 60 | $variableName = $tokens[$variablePtr]['content']; 61 | if ( !preg_match( 62 | '{^([^\s$]+\s)?\s*' . preg_quote( $variableName ) . '\b\s*(.*)}is', 63 | $tokens[$docPtr]['content'], 64 | $matches 65 | ) ) { 66 | return; 67 | } 68 | 69 | $fix = $phpcsFile->addFixableError( 70 | 'Found redundant variable name %s in @var', 71 | $docPtr, 72 | 'Found', 73 | [ $variableName ] 74 | ); 75 | if ( $fix ) { 76 | $phpcsFile->fixer->replaceToken( $docPtr, trim( $matches[1] . $matches[2] ) ); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Commenting/VariadicArgumentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 33 | if ( !isset( $tokens[$stackPtr]['parenthesis_opener'] ) ) { 34 | // Live coding 35 | return; 36 | } 37 | 38 | $end = $tokens[$stackPtr]['parenthesis_closer']; 39 | $commentPos = $phpcsFile->findNext( T_COMMENT, $tokens[$stackPtr]['parenthesis_opener'] + 1, $end ); 40 | while ( $commentPos !== false ) { 41 | $comment = $tokens[$commentPos]['content']; 42 | if ( str_starts_with( $comment, '/*' ) ) { 43 | $content = substr( $comment, 2, -2 ); 44 | if ( preg_match( '/^[,\s]*\.\.\.\s*$|\.\.\.\$|\$[a-z_][a-z0-9_]*,\.\.\./i', $content ) ) { 45 | // An autofix would be trivial to write, but we shouldn't offer that. Removing the 46 | // comment is not enough, because people should also add the actual variadic parameter. 47 | // For some methods, variadic parameters are only documented via this inline comment, 48 | // hence an autofixer would effectively remove any documentation about them. 49 | $phpcsFile->addError( 50 | 'Comments indicating variadic arguments are superfluous and should be replaced ' . 51 | 'with actual variadic arguments', 52 | $commentPos, 53 | 'SuperfluousVariadicArgComment' 54 | ); 55 | } 56 | } 57 | $commentPos = $phpcsFile->findNext( T_COMMENT, $commentPos + 1, $end ); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/ControlStructures/MissingElseBetweenBracketsSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | $next = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 33 | 34 | if ( $tokens[$next]['code'] === T_OPEN_CURLY_BRACKET ) { 35 | $phpcsFile->addError( 36 | 'Missing `else` between closing an opening and closing bracket', 37 | $next, 38 | 'Missing' 39 | ); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/ExtraCharacters/ParenthesesAroundKeywordSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 46 | 47 | $opener = $phpcsFile->findNext( [ T_WHITESPACE ], $stackPtr + 1, null, true ); 48 | if ( $opener === false || 49 | $tokens[$opener]['code'] !== T_OPEN_PARENTHESIS || 50 | !isset( $tokens[$opener]['parenthesis_closer'] ) 51 | ) { 52 | // not a whitespace and parenthesis after the keyword, possible a comment or live coding 53 | return; 54 | } 55 | 56 | $fix = $phpcsFile->addFixableWarning( 57 | '%s keyword must not be used as a function.', 58 | $opener, 59 | 'ParenthesesAroundKeywords', 60 | [ $tokens[$stackPtr]['content'] ] 61 | ); 62 | 63 | if ( $fix ) { 64 | $phpcsFile->fixer->beginChangeset(); 65 | if ( $tokens[$stackPtr + 1]['code'] !== T_WHITESPACE ) { 66 | // Ensure the both tokens are not mangled together without space 67 | $phpcsFile->fixer->addContent( $stackPtr, ' ' ); 68 | } 69 | 70 | $phpcsFile->fixer->replaceToken( $opener, '' ); 71 | // remove whitespace after the opener 72 | if ( $tokens[$opener + 1]['code'] === T_WHITESPACE ) { 73 | $phpcsFile->fixer->replaceToken( $opener + 1, '' ); 74 | } 75 | 76 | $closer = $tokens[$opener]['parenthesis_closer']; 77 | $phpcsFile->fixer->replaceToken( $closer, '' ); 78 | // remove whitespace before the closer 79 | if ( $tokens[$closer - 1]['code'] === T_WHITESPACE ) { 80 | $phpcsFile->fixer->replaceToken( $closer - 1, '' ); 81 | } 82 | $phpcsFile->fixer->endChangeset(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Files/ClassMatchesFilenameSniff.php: -------------------------------------------------------------------------------- 1 | getFilename(); 45 | if ( $fname === 'STDIN' ) { 46 | return $phpcsFile->numTokens; 47 | } 48 | 49 | $base = basename( $fname ); 50 | $name = $phpcsFile->getDeclarationName( $stackPtr ); 51 | if ( $base !== "$name.php" ) { 52 | $wrongCase = strcasecmp( $base, "$name.php" ) === 0; 53 | if ( $wrongCase && $this->isMaintenanceScript( $phpcsFile ) ) { 54 | // Maintenance scripts follow the class name, but the first 55 | // letter is lowercase. 56 | $expected = lcfirst( $name ); 57 | if ( $base === "$expected.php" ) { 58 | // OK! 59 | return $phpcsFile->numTokens; 60 | } 61 | } 62 | $phpcsFile->addError( 63 | 'Class name \'%s\' does not match filename \'%s\'', 64 | $stackPtr, 65 | $wrongCase ? 'WrongCase' : 'NotMatch', 66 | [ $name, $base ] 67 | ); 68 | } 69 | 70 | return $phpcsFile->numTokens; 71 | } 72 | 73 | /** 74 | * Figure out whether the file is a MediaWiki maintenance script 75 | * 76 | * @param File $phpcsFile 77 | * 78 | * @return bool 79 | */ 80 | private function isMaintenanceScript( File $phpcsFile ): bool { 81 | $tokens = $phpcsFile->getTokens(); 82 | 83 | // Per convention the line we are looking for is the last in all maintenance scripts 84 | for ( $i = $phpcsFile->numTokens; $i--; ) { 85 | if ( $tokens[$i]['level'] !== 0 ) { 86 | // Only look into the global scope 87 | return false; 88 | } 89 | if ( $tokens[$i]['code'] === T_STRING 90 | && $tokens[$i]['content'] === 'RUN_MAINTENANCE_IF_MAIN' 91 | ) { 92 | return true; 93 | } 94 | } 95 | 96 | return false; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/NamingConventions/LowerCamelFunctionsNameSniff.php: -------------------------------------------------------------------------------- 1 | true, 28 | '__destruct' => true, 29 | '__call' => true, 30 | '__callstatic' => true, 31 | '__get' => true, 32 | '__set' => true, 33 | '__isset' => true, 34 | '__unset' => true, 35 | '__sleep' => true, 36 | '__wakeup' => true, 37 | '__tostring' => true, 38 | '__set_state' => true, 39 | '__clone' => true, 40 | '__invoke' => true, 41 | '__serialize' => true, 42 | '__unserialize' => true, 43 | '__debuginfo' => true 44 | ]; 45 | 46 | // A list of non-magic methods with double underscore. 47 | private const METHOD_DOUBLE_UNDERSCORE = [ 48 | '__soapcall' => true, 49 | '__getlastrequest' => true, 50 | '__getlastresponse' => true, 51 | '__getlastrequestheaders' => true, 52 | '__getlastresponseheaders' => true, 53 | '__getfunctions' => true, 54 | '__gettypes' => true, 55 | '__dorequest' => true, 56 | '__setcookie' => true, 57 | '__setlocation' => true, 58 | '__setsoapheaders' => true 59 | ]; 60 | 61 | /** 62 | * @inheritDoc 63 | */ 64 | public function register(): array { 65 | return [ T_FUNCTION ]; 66 | } 67 | 68 | /** 69 | * @param File $phpcsFile 70 | * @param int $stackPtr The current token index. 71 | * @return void 72 | */ 73 | public function process( File $phpcsFile, $stackPtr ) { 74 | $originalFunctionName = $phpcsFile->getDeclarationName( $stackPtr ); 75 | if ( $originalFunctionName === null ) { 76 | return; 77 | } 78 | 79 | $lowerFunctionName = strtolower( $originalFunctionName ); 80 | if ( isset( self::METHOD_DOUBLE_UNDERSCORE[$lowerFunctionName] ) || 81 | isset( self::MAGIC_METHODS[$lowerFunctionName] ) 82 | ) { 83 | // Method is excluded from this sniff 84 | return; 85 | } 86 | 87 | $containsUnderscores = str_contains( $originalFunctionName, '_' ); 88 | if ( $originalFunctionName[0] === $lowerFunctionName[0] && 89 | ( !$containsUnderscores || $this->isTestFunction( $phpcsFile, $stackPtr ) ) 90 | ) { 91 | // Everything is ok when the first letter is lowercase and there are no underscores 92 | // (except in tests where they are allowed) 93 | return; 94 | } 95 | 96 | if ( $containsUnderscores ) { 97 | // Check for MediaWiki hooks 98 | // Only matters if there is an underscore, all hook handlers have methods beginning 99 | // with "on" and so start with lowercase 100 | if ( $this->shouldIgnoreHookHandler( $phpcsFile, $stackPtr, $originalFunctionName ) ) { 101 | return; 102 | } 103 | } 104 | 105 | $tokens = $phpcsFile->getTokens(); 106 | foreach ( $tokens[$stackPtr]['conditions'] as $code ) { 107 | if ( !isset( Tokens::$ooScopeTokens[$code] ) ) { 108 | continue; 109 | } 110 | 111 | $phpcsFile->addError( 112 | 'Method name "%s" should use lower camel case.', 113 | $stackPtr, 114 | 'FunctionName', 115 | [ $originalFunctionName ] 116 | ); 117 | } 118 | } 119 | 120 | /** 121 | * Check if the method should be ignored because it is a hook handler and the method 122 | * name is inherited from an interface 123 | * 124 | * @param File $phpcsFile 125 | * @param int $stackPtr 126 | * @param string $functionName 127 | * @return bool 128 | */ 129 | private function shouldIgnoreHookHandler( 130 | File $phpcsFile, 131 | int $stackPtr, 132 | string $functionName 133 | ): bool { 134 | $matches = []; 135 | if ( !( preg_match( '/^on([A-Z]\S+)$/', $functionName, $matches ) ) ) { 136 | return false; 137 | } 138 | 139 | // Method name looks like a hook handler, check if the class implements 140 | // a hook by that name 141 | 142 | $classToken = $this->getClassToken( $phpcsFile, $stackPtr ); 143 | if ( !$classToken ) { 144 | // Not within a class, don't skip 145 | return false; 146 | } 147 | 148 | $implementedInterfaces = $phpcsFile->findImplementedInterfaceNames( $classToken ); 149 | if ( !$implementedInterfaces ) { 150 | // Not implementing the hook interface 151 | return false; 152 | } 153 | 154 | $hookMethodName = $matches[1]; 155 | $hookInterfaceName = $hookMethodName . 'Hook'; 156 | 157 | // We need to account for the interface name in both the fully qualified form, 158 | // and just the interface name. If we have the fully qualified form, explode() 159 | // will return an array of the different namespaces and sub namespaces, with the 160 | // last entry being the actual interface name, and if we just have the interface 161 | // name, explode() will return an array of just that string 162 | foreach ( $implementedInterfaces as $interface ) { 163 | $parts = explode( '\\', $interface ); 164 | if ( end( $parts ) === $hookInterfaceName ) { 165 | return true; 166 | } 167 | } 168 | return false; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/NamingConventions/PrefixedGlobalFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | getFilename(); 48 | 49 | // Check if we already know if the token is namespaced or not 50 | if ( !isset( $this->firstNamespaceLocations[$fileName] ) ) { 51 | // If not scan the whole file at once looking for namespacing or lack of and set in the statics. 52 | $tokens = $phpcsFile->getTokens(); 53 | $numTokens = $phpcsFile->numTokens; 54 | for ( $tokenIndex = 0; $tokenIndex < $numTokens; $tokenIndex++ ) { 55 | $token = $tokens[$tokenIndex]; 56 | if ( $token['code'] === T_NAMESPACE && !isset( $token['scope_opener'] ) ) { 57 | // In the format of "namespace Foo;", which applies to everything below 58 | $this->firstNamespaceLocations[$fileName] = $tokenIndex; 59 | break; 60 | } 61 | 62 | if ( isset( $token['scope_closer'] ) ) { 63 | // Skip any non-zero level code as it can not contain a relevant namespace 64 | $tokenIndex = $token['scope_closer']; 65 | continue; 66 | } 67 | } 68 | 69 | // Nothing found, just save unreachable token index 70 | if ( !isset( $this->firstNamespaceLocations[$fileName] ) ) { 71 | $this->firstNamespaceLocations[$fileName] = $numTokens; 72 | } 73 | } 74 | 75 | // Return if the token was namespaced. 76 | return $ptr > $this->firstNamespaceLocations[$fileName]; 77 | } 78 | 79 | /** 80 | * @param File $phpcsFile 81 | * @param int $stackPtr The current token index. 82 | * @return int|void 83 | */ 84 | public function process( File $phpcsFile, $stackPtr ) { 85 | // If there are no prefixes specified, we have nothing to do for this file 86 | if ( $this->allowedPrefixes === [] ) { 87 | // @codeCoverageIgnoreStart 88 | return $phpcsFile->numTokens; 89 | // @codeCoverageIgnoreEnd 90 | } 91 | 92 | $tokens = $phpcsFile->getTokens(); 93 | 94 | // Check if function is global 95 | if ( $tokens[$stackPtr]['level'] !== 0 ) { 96 | return; 97 | } 98 | 99 | $name = $phpcsFile->getDeclarationName( $stackPtr ); 100 | if ( $name === null || in_array( $name, $this->ignoreList ) ) { 101 | return; 102 | } 103 | 104 | foreach ( $this->allowedPrefixes as $allowedPrefix ) { 105 | if ( str_starts_with( $name, $allowedPrefix ) ) { 106 | return; 107 | } 108 | } 109 | 110 | if ( $this->tokenIsNamespaced( $phpcsFile, $stackPtr ) ) { 111 | return; 112 | } 113 | 114 | // From ValidGlobalNameSniff 115 | if ( count( $this->allowedPrefixes ) === 1 ) { 116 | // Build message telling you the allowed prefix 117 | $allowedPrefix = '\'' . $this->allowedPrefixes[0] . '\''; 118 | 119 | // Forge a valid global function name 120 | $expected = $this->allowedPrefixes[0] . ucfirst( $name ) . "()"; 121 | } else { 122 | // Build message telling you which prefixes are allowed 123 | $allowedPrefix = 'one of \'' 124 | . implode( '\', \'', $this->allowedPrefixes ) 125 | . '\''; 126 | 127 | // Build a list of forged valid global function names 128 | $expected = 'one of "' 129 | . implode( ucfirst( $name ) . '()", "', $this->allowedPrefixes ) 130 | . ucfirst( $name ) 131 | . '()"'; 132 | } 133 | $phpcsFile->addError( 134 | 'Global function "%s()" is lacking a valid prefix (%s). It should be %s.', 135 | $stackPtr, 136 | 'allowedPrefix', 137 | [ $name, $allowedPrefix, $expected ] 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/NamingConventions/ValidGlobalNameSniff.php: -------------------------------------------------------------------------------- 1 | allowedPrefixes === [] ) { 59 | // @codeCoverageIgnoreStart 60 | return $phpcsFile->numTokens; 61 | // @codeCoverageIgnoreEnd 62 | } 63 | 64 | $tokens = $phpcsFile->getTokens(); 65 | 66 | $nameIndex = $phpcsFile->findNext( T_VARIABLE, $stackPtr + 1 ); 67 | if ( !$nameIndex ) { 68 | // Avoid possibly running in an endless loop below 69 | return; 70 | } 71 | 72 | // Note: This requires at least 1 character after the prefix 73 | $allowedPrefixesPattern = '/^\$(?:' . implode( '|', $this->allowedPrefixes ) . ')(.)/'; 74 | $semicolonIndex = $phpcsFile->findNext( T_SEMICOLON, $stackPtr + 1 ); 75 | 76 | while ( $nameIndex < $semicolonIndex ) { 77 | // Note, this skips dynamic identifiers. 78 | if ( $tokens[$nameIndex ]['code'] === T_VARIABLE && $tokens[$nameIndex - 1]['code'] !== T_DOLLAR ) { 79 | $globalName = $tokens[$nameIndex]['content']; 80 | 81 | if ( in_array( $globalName, $this->ignoreList ) || 82 | in_array( $globalName, self::PHP_RESERVED ) 83 | ) { 84 | // need to manually increment $nameIndex here since 85 | // we won't reach the line at the end that does it 86 | $nameIndex++; 87 | continue; 88 | } 89 | 90 | // Determine if a simple error message can be used 91 | 92 | if ( count( $this->allowedPrefixes ) === 1 ) { 93 | // Skip '$' and forge a valid global variable name 94 | $expected = '"$' . $this->allowedPrefixes[0] . ucfirst( substr( $globalName, 1 ) ) . '"'; 95 | 96 | // Build message telling you the allowed prefix 97 | $allowedPrefix = '\'' . $this->allowedPrefixes[0] . '\''; 98 | } else { 99 | // We already checked for an empty set of allowed prefixes earlier, 100 | // so if the count is not 1 them it must be multiple; 101 | // build a list of forged valid global variable names 102 | $expected = 'one of "$' 103 | . implode( ucfirst( substr( $globalName, 1 ) . '", "$' ), $this->allowedPrefixes ) 104 | . ucfirst( substr( $globalName, 1 ) ) 105 | . '"'; 106 | 107 | // Build message telling you which prefixes are allowed 108 | $allowedPrefix = 'one of \'' 109 | . implode( '\', \'', $this->allowedPrefixes ) 110 | . '\''; 111 | } 112 | 113 | // Verify global is prefixed with an allowed prefix 114 | $isAllowed = preg_match( $allowedPrefixesPattern, $globalName, $matches ); 115 | if ( !$isAllowed ) { 116 | $phpcsFile->addError( 117 | 'Global variable "%s" is lacking an allowed prefix (%s). Should be %s.', 118 | $stackPtr, 119 | 'allowedPrefix', 120 | [ $globalName, $allowedPrefix, $expected ] 121 | ); 122 | } elseif ( ctype_lower( $matches[1] ) ) { 123 | $phpcsFile->addError( 124 | 'Global variable "%s" should use CamelCase: %s', 125 | $stackPtr, 126 | 'CamelCase', 127 | [ $globalName, $expected ] 128 | ); 129 | } 130 | } 131 | $nameIndex++; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/AssertCountSniff.php: -------------------------------------------------------------------------------- 1 | true, 20 | 'assertSame' => true, 21 | 'assertCount' => true, 22 | ]; 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function register(): array { 28 | return [ T_STRING ]; 29 | } 30 | 31 | /** 32 | * @param File $phpcsFile 33 | * @param int $stackPtr 34 | * 35 | * @return void|int 36 | */ 37 | public function process( File $phpcsFile, $stackPtr ) { 38 | if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) { 39 | return $phpcsFile->numTokens; 40 | } 41 | 42 | $tokens = $phpcsFile->getTokens(); 43 | $assertion = $tokens[$stackPtr]['content']; 44 | 45 | // We don't care about stuff that's not in a method in a class 46 | if ( $tokens[$stackPtr]['level'] < 2 || !isset( self::ASSERTIONS[$assertion] ) ) { 47 | return; 48 | } 49 | 50 | $opener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 51 | if ( !isset( $tokens[$opener]['parenthesis_closer'] ) ) { 52 | // Looks like this string is not a method call 53 | return $opener; 54 | } 55 | $end = $tokens[$opener]['parenthesis_closer']; 56 | 57 | $firstCount = $this->parseCount( $phpcsFile, $opener ); 58 | if ( !$firstCount && $assertion === 'assertCount' ) { 59 | return $end; 60 | } 61 | 62 | // Jump over the expected parameter, whatever it is 63 | $searchTokens = [ 64 | T_OPEN_CURLY_BRACKET, 65 | T_OPEN_SQUARE_BRACKET, 66 | T_OPEN_PARENTHESIS, 67 | T_OPEN_SHORT_ARRAY, 68 | T_COMMA 69 | ]; 70 | $commaToken = false; 71 | $next = $phpcsFile->findNext( $searchTokens, $opener + 1, $end ); 72 | while ( $commaToken === false ) { 73 | if ( $next === false ) { 74 | // No token 75 | return; 76 | } 77 | switch ( $tokens[$next]['code'] ) { 78 | case T_OPEN_CURLY_BRACKET: 79 | case T_OPEN_SQUARE_BRACKET: 80 | case T_OPEN_PARENTHESIS: 81 | case T_OPEN_SHORT_ARRAY: 82 | if ( isset( $tokens[$next]['parenthesis_closer'] ) ) { 83 | // jump to closing parenthesis to ignore commas between opener and closer 84 | $next = $tokens[$next]['parenthesis_closer']; 85 | } elseif ( isset( $tokens[$next]['bracket_closer'] ) ) { 86 | // jump to closing bracket 87 | $next = $tokens[$next]['bracket_closer']; 88 | } 89 | break; 90 | case T_COMMA: 91 | $commaToken = $next; 92 | break; 93 | } 94 | $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end ); 95 | } 96 | 97 | $secondCount = $this->parseCount( $phpcsFile, $commaToken ); 98 | if ( !( $secondCount xor $assertion === 'assertCount' ) ) { 99 | return $end; 100 | } 101 | 102 | // T330008: Prefer assertSameSize when both part of comparison are count() 103 | $newAssert = $firstCount ? 'assertSameSize' : 'assertCount'; 104 | $fix = $phpcsFile->addFixableWarning( 105 | '%s can be used instead of manually using %s with the result of count()', 106 | $stackPtr, 107 | $newAssert === 'assertSameSize' ? 'AssertSameSize' : 'NotUsed', 108 | [ $newAssert, $assertion ] 109 | ); 110 | if ( !$fix ) { 111 | return; 112 | } 113 | 114 | $phpcsFile->fixer->replaceToken( $stackPtr, $newAssert ); 115 | if ( $firstCount ) { 116 | $this->replaceCountContent( $phpcsFile, $firstCount ); 117 | } 118 | if ( $secondCount ) { 119 | $this->replaceCountContent( $phpcsFile, $secondCount ); 120 | } 121 | 122 | // There is no way the next assertEquals() or assertSame() can be closer than this 123 | return $tokens[$opener]['parenthesis_closer'] + 4; 124 | } 125 | 126 | /** 127 | * @param File $phpcsFile 128 | * @param int $stackPtr 129 | * @return array|void 130 | */ 131 | private function parseCount( File $phpcsFile, int $stackPtr ) { 132 | $tokens = $phpcsFile->getTokens(); 133 | $countToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 134 | if ( $tokens[$countToken]['code'] !== T_STRING || 135 | $tokens[$countToken]['content'] !== 'count' 136 | ) { 137 | // Not `count` 138 | return; 139 | } 140 | 141 | $countOpen = $phpcsFile->findNext( T_WHITESPACE, $countToken + 1, null, true ); 142 | if ( !isset( $tokens[$countOpen]['parenthesis_closer'] ) ) { 143 | // Not a function 144 | return; 145 | } 146 | 147 | $countClose = $tokens[$countOpen]['parenthesis_closer']; 148 | $afterCount = $phpcsFile->findNext( T_WHITESPACE, $countClose + 1, null, true ); 149 | if ( !in_array( $tokens[$afterCount]['code'], [ T_COMMA, T_CLOSE_PARENTHESIS ] ) ) { 150 | // Not followed by a comma and a third parameter, or a closing parenthesis 151 | // something more complex is going on 152 | return; 153 | } 154 | 155 | return [ $countToken, $countOpen, $countClose ]; 156 | } 157 | 158 | /** 159 | * @param File $phpcsFile 160 | * @param int[] $parsed 161 | * @return void 162 | */ 163 | private function replaceCountContent( File $phpcsFile, array $parsed ) { 164 | [ $countToken, $countOpen, $countClose ] = $parsed; 165 | $countContentStart = $phpcsFile->findNext( T_WHITESPACE, $countOpen + 1, null, true ); 166 | $countContentEnd = $phpcsFile->findPrevious( T_WHITESPACE, $countClose - 1, null, true ); 167 | 168 | $phpcsFile->fixer->replaceToken( $countToken, '' ); 169 | $phpcsFile->fixer->replaceToken( $countOpen, '' ); 170 | for ( $i = $countOpen + 1; $i < $countContentStart; $i++ ) { 171 | // Whitespace between count( and the content 172 | $phpcsFile->fixer->replaceToken( $i, '' ); 173 | } 174 | for ( $i = $countContentEnd + 1; $i < $countClose; $i++ ) { 175 | // Whitespace between content and ) 176 | $phpcsFile->fixer->replaceToken( $i, '' ); 177 | } 178 | $phpcsFile->fixer->replaceToken( $countClose, '' ); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/AssertEmptySniff.php: -------------------------------------------------------------------------------- 1 | isTestFile( $phpcsFile, $stackPtr ) ) { 32 | return $phpcsFile->numTokens; 33 | } 34 | 35 | $tokens = $phpcsFile->getTokens(); 36 | 37 | // Only check code in a method in a class 38 | if ( $tokens[$stackPtr]['level'] < 2 ) { 39 | return; 40 | } 41 | 42 | // Ensure its the right method name 43 | if ( $tokens[$stackPtr]['content'] !== 'assertEmpty' ) { 44 | return; 45 | } 46 | 47 | $openParen = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 48 | 49 | // If the next non-whitespace token isn't parenthesis, its not a call to assertEmpty 50 | if ( !isset( $tokens[$openParen]['parenthesis_closer'] ) ) { 51 | return $openParen; 52 | } 53 | 54 | $phpcsFile->addWarning( 55 | 'assertEmpty performs loose comparisons and should not be used.', 56 | $stackPtr, 57 | 'AssertEmptyUsed' 58 | ); 59 | 60 | // Minimum number of tokens before the next possible assertEmpty 61 | return $tokens[$openParen]['parenthesis_closer'] + 4; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/AssertEqualsSniff.php: -------------------------------------------------------------------------------- 1 | true, 22 | 'assertNotEquals' => true, 23 | 'assertNotSame' => true, 24 | ]; 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public function register(): array { 30 | return [ T_STRING ]; 31 | } 32 | 33 | /** 34 | * @param File $phpcsFile 35 | * @param int $stackPtr 36 | * 37 | * @return void|int 38 | */ 39 | public function process( File $phpcsFile, $stackPtr ) { 40 | if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) { 41 | return $phpcsFile->numTokens; 42 | } 43 | 44 | $tokens = $phpcsFile->getTokens(); 45 | $assertion = $tokens[$stackPtr]['content']; 46 | 47 | // We don't care about stuff that's not in a method in a class 48 | if ( $tokens[$stackPtr]['level'] < 2 || !isset( self::ASSERTIONS[$assertion] ) ) { 49 | return; 50 | } 51 | 52 | $opener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 53 | // Looks like this string is not a method call 54 | if ( !isset( $tokens[$opener]['parenthesis_closer'] ) ) { 55 | return $opener; 56 | } 57 | 58 | $isAssertEquals = $assertion === 'assertEquals'; 59 | $expected = $phpcsFile->findNext( T_WHITESPACE, $opener + 1, null, true ); 60 | $msg = '%s accepts many non-%s values, please use strict alternatives like %s'; 61 | /** @var bool|string $fix */ 62 | $fix = false; 63 | 64 | switch ( $tokens[$expected]['code'] ) { 65 | case T_NULL: 66 | if ( !$isAssertEquals ) { 67 | break; 68 | } 69 | 70 | $msgParams = [ $assertion, 'null', 'assertNull' ]; 71 | if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Null', $msgParams ) ) { 72 | $fix = 'assertNull'; 73 | } 74 | break; 75 | 76 | case T_FALSE: 77 | $replacement = $isAssertEquals ? 'assertFalse' : 'assertTrue'; 78 | $msgParams = [ $assertion, $isAssertEquals ? 'false' : 'true', $replacement ]; 79 | if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'False', $msgParams ) ) { 80 | $fix = $replacement; 81 | } 82 | break; 83 | 84 | case T_TRUE: 85 | $replacement = $isAssertEquals ? 'assertTrue' : 'assertFalse'; 86 | $msgParams = [ $assertion, $isAssertEquals ? 'true' : 'false', $replacement ]; 87 | if ( $phpcsFile->addFixableWarning( $msg, $stackPtr, 'True', $msgParams ) ) { 88 | $fix = $replacement; 89 | } 90 | break; 91 | 92 | case T_LNUMBER: 93 | if ( !$isAssertEquals ) { 94 | break; 95 | } 96 | 97 | $number = (int)$tokens[$expected]['content']; 98 | if ( $number === 0 || $number === 1 ) { 99 | $msgParams = [ $assertion, $number ? 'numeric' : 'zero', 'assertSame' ]; 100 | $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Int', $msgParams ); 101 | } 102 | break; 103 | 104 | case T_DNUMBER: 105 | if ( !$isAssertEquals ) { 106 | break; 107 | } 108 | 109 | $number = (float)$tokens[$expected]['content']; 110 | if ( $number === 0.0 || $number === 1.0 ) { 111 | $msgParams = [ $assertion, $number ? 'numeric' : 'zero', 'assertSame' ]; 112 | $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'Float', $msgParams ); 113 | } 114 | break; 115 | 116 | case T_CONSTANT_ENCAPSED_STRING: 117 | if ( !$isAssertEquals ) { 118 | break; 119 | } 120 | 121 | $msgParams = [ $assertion, 'string', 'assertSame' ]; 122 | 123 | // The empty string as well as "0" are among PHP's "falsy" values 124 | if ( strlen( $tokens[$expected]['content'] ) <= 2 || 125 | $tokens[$expected]['content'][1] === '0' 126 | ) { 127 | $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'FalsyString', $msgParams ); 128 | break; 129 | } 130 | 131 | $string = trim( substr( $tokens[$expected]['content'], 1, -1 ) ); 132 | if ( ctype_digit( $string ) ) { 133 | $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'IntegerString', $msgParams ); 134 | } elseif ( is_numeric( $string ) ) { 135 | $fix = $phpcsFile->addFixableWarning( $msg, $stackPtr, 'NumericString', $msgParams ); 136 | } 137 | } 138 | 139 | $fixer = $phpcsFile->fixer; 140 | // Fall back to assertSame instead of blindly removing unknown tokens 141 | if ( is_string( $fix ) && $tokens[$expected + 1]['code'] === T_COMMA ) { 142 | $fixer->replaceToken( $stackPtr, $fix ); 143 | $fixer->replaceToken( $expected, '' ); 144 | $fixer->replaceToken( $expected + 1, '' ); 145 | if ( $tokens[$expected + 2]['code'] === T_WHITESPACE ) { 146 | $fixer->replaceToken( $expected + 2, '' ); 147 | } 148 | } elseif ( $fix ) { 149 | $fixer->replaceToken( $stackPtr, 'assertSame' ); 150 | } 151 | 152 | // There is no way the next assertEquals() can be closer than this 153 | return $tokens[$opener]['parenthesis_closer'] + 4; 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/PHPUnitClassUsageSniff.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | */ 19 | 20 | namespace MediaWiki\Sniffs\PHPUnit; 21 | 22 | use PHP_CodeSniffer\Files\File; 23 | use PHP_CodeSniffer\Sniffs\Sniff; 24 | use PHP_CodeSniffer\Util\Tokens; 25 | use PHPUnit\Framework\TestCase; 26 | 27 | /** 28 | * Converts PHPUnit_Framework_TestCase to the new 29 | * PHPUnit 6 namespaced PHPUnit\Framework\Testcase 30 | * 31 | * The namespaced classes were backported in 4.8.35, 32 | * so this is compatible with 4.8.35+ and 5.4.3+ 33 | */ 34 | class PHPUnitClassUsageSniff implements Sniff { 35 | /** 36 | * Only look for classes that extend something 37 | * 38 | * @inheritDoc 39 | */ 40 | public function register(): array { 41 | return [ T_EXTENDS ]; 42 | } 43 | 44 | /** 45 | * @param File $phpcsFile 46 | * @param int $stackPtr Position of extends token 47 | * @return void 48 | */ 49 | public function process( File $phpcsFile, $stackPtr ) { 50 | $tokens = $phpcsFile->getTokens(); 51 | // Skip the "extends" (1) and the class name (1 or more) surrounded by spaces (2). 52 | $classPtr = $phpcsFile->findPrevious( Tokens::$ooScopeTokens, $stackPtr - 4 ); 53 | if ( !$classPtr || $tokens[$classPtr]['code'] !== T_CLASS ) { 54 | // interface Foo extends .. which we don't care about 55 | return; 56 | } 57 | $phpunitPtr = $phpcsFile->findNext( T_STRING, $stackPtr ); 58 | $phpunitToken = $tokens[$phpunitPtr]; 59 | if ( $phpunitToken['content'] !== 'PHPUnit_Framework_TestCase' ) { 60 | return; 61 | } 62 | 63 | $fix = $phpcsFile->addFixableWarning( 64 | 'Namespaced PHPUnit TestCase class should be used instead', 65 | $phpunitPtr, 66 | 'NotNamespaced' 67 | ); 68 | if ( $fix ) { 69 | $new = TestCase::class; 70 | // If this file is namespaced, we need a leading \ 71 | $inANamespace = $phpcsFile->findPrevious( T_NAMESPACE, $classPtr ) !== false; 72 | $classNameWithSlash = $phpcsFile->findExtendedClassName( $classPtr ); 73 | // But make sure it doesn't already have a slash... 74 | $hashLeadingSlash = $classNameWithSlash[0] === '\\'; 75 | if ( $inANamespace && !$hashLeadingSlash ) { 76 | $new = '\\' . $new; 77 | } 78 | $phpcsFile->fixer->replaceToken( $phpunitPtr, $new ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/PHPUnitTestTrait.php: -------------------------------------------------------------------------------- 1 | 'MediaWikiTestCase', 24 | 'MediaWikiUnitTestCase' => 'MediaWikiUnitTestCase', 25 | 'MediaWikiIntegrationTestCase' => 'MediaWikiIntegrationTestCase', 26 | 'PHPUnit_Framework_TestCase' => 'PHPUnit_Framework_TestCase', 27 | // This class may be 'used', but checking for that would be complicated 28 | TestCase::class => TestCase::class, 29 | ]; 30 | 31 | /** 32 | * @var bool[] 33 | */ 34 | private static $isTestFile = []; 35 | 36 | /** 37 | * @param File $phpcsFile 38 | * @param int|false $stackPtr 39 | * 40 | * @return bool 41 | */ 42 | private function isTestFile( File $phpcsFile, $stackPtr = false ): bool { 43 | $fileName = $phpcsFile->getFilename(); 44 | 45 | if ( !isset( self::$isTestFile[$fileName] ) ) { 46 | $classToken = $this->getClassToken( $phpcsFile, $stackPtr ) ?: 47 | $phpcsFile->findNext( Tokens::$ooScopeTokens, 0 ); 48 | $isTestFile = $this->isTestClass( $phpcsFile, $classToken ); 49 | 50 | // There is no file but STDIN when Helper::runPhpCs() is used 51 | if ( $phpcsFile instanceof DummyFile ) { 52 | return $isTestFile; 53 | } 54 | 55 | self::$isTestFile[$fileName] = $isTestFile; 56 | } 57 | 58 | return self::$isTestFile[$fileName]; 59 | } 60 | 61 | /** 62 | * @param File $phpcsFile 63 | * @param int|false $classToken Must point at a T_CLASS token 64 | * 65 | * @return bool 66 | */ 67 | private function isTestClass( File $phpcsFile, $classToken ): bool { 68 | $tokens = $phpcsFile->getTokens(); 69 | if ( !$classToken || $tokens[$classToken]['code'] !== T_CLASS ) { 70 | return false; 71 | } 72 | 73 | $extendedClass = ltrim( $phpcsFile->findExtendedClassName( $classToken ), '\\' ); 74 | return array_key_exists( $extendedClass, self::$PHPUNIT_CLASSES ) || 75 | (bool)preg_match( 76 | '/(?:Test(?:Case)?(?:Base)?|Suite)$/', 77 | $phpcsFile->getDeclarationName( $classToken ) 78 | ); 79 | } 80 | 81 | /** 82 | * @param File $phpcsFile 83 | * @param int $functionToken Token position of the function declaration 84 | * @return bool 85 | */ 86 | private function isTestFunction( File $phpcsFile, $functionToken ): bool { 87 | return $this->isTestClass( $phpcsFile, $this->getClassToken( $phpcsFile, $functionToken ) ) 88 | && preg_match( '/^(?:test|provide)|Provider$/', $phpcsFile->getDeclarationName( $functionToken ) ); 89 | } 90 | 91 | /** 92 | * @param File $phpcsFile 93 | * @param int|false $stackPtr Should point at the T_CLASS token or a token in the class 94 | * 95 | * @return int|false 96 | */ 97 | private function getClassToken( File $phpcsFile, $stackPtr ) { 98 | if ( !$stackPtr ) { 99 | return false; 100 | } 101 | 102 | $tokens = $phpcsFile->getTokens(); 103 | if ( $tokens[$stackPtr]['code'] === T_CLASS ) { 104 | return $stackPtr; 105 | } 106 | 107 | foreach ( $tokens[$stackPtr]['conditions'] as $ptr => $type ) { 108 | if ( $type === T_CLASS ) { 109 | return $ptr; 110 | } 111 | } 112 | 113 | return false; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/PHPUnitTypeHintsSniff.php: -------------------------------------------------------------------------------- 1 | isTestFile( $phpcsFile, $stackPtr ) ) { 44 | return $phpcsFile->numTokens; 45 | } 46 | 47 | $tokens = $phpcsFile->getTokens(); 48 | $startTok = $tokens[$stackPtr]; 49 | if ( !isset( $startTok['scope_opener'] ) ) { 50 | // live coding 51 | return; 52 | } 53 | $cur = $startTok['scope_opener']; 54 | $end = $startTok['scope_closer']; 55 | 56 | $functions = [ 57 | 'setUp' => false, 58 | 'tearDown' => false, 59 | 'setUpBeforeClass' => false, 60 | 'tearDownAfterClass' => false, 61 | 'assertPreConditions' => false, 62 | 'assertPostConditions' => false, 63 | 'onNotSuccessfulTest' => false, 64 | ]; 65 | 66 | $cur = $phpcsFile->findNext( T_FUNCTION, $cur + 1, $end ); 67 | while ( $cur !== false && $functions ) { 68 | if ( $phpcsFile->hasCondition( $cur, [ T_ANON_CLASS ] ) ) { 69 | if ( isset( $tokens[$cur]['scope_closer'] ) ) { 70 | // Skip to the end of the inner function/anon class and continue 71 | $cur = $tokens[$cur]['scope_closer']; 72 | } 73 | $cur = $phpcsFile->findNext( T_FUNCTION, $cur + 1, $end ); 74 | continue; 75 | } 76 | $funcNamePos = $phpcsFile->findNext( T_STRING, $cur ); 77 | $funcName = $tokens[$funcNamePos]['content']; 78 | if ( isset( $functions[$funcName] ) ) { 79 | unset( $functions[$funcName] ); 80 | $props = $phpcsFile->getMethodProperties( $cur ); 81 | $retTypeHint = $props['return_type']; 82 | $retTypeTok = $props['return_type_token']; 83 | 84 | $err = 'The PHPUnit method %s() should have a return typehint of "void"'; 85 | if ( $retTypeHint !== 'void' ) { 86 | if ( $retTypeTok === false ) { 87 | // Easy case, no return type specified. Offer autofix 88 | $fix = $phpcsFile->addFixableError( 89 | $err, 90 | $cur, 91 | 'MissingTypehint', 92 | [ $funcName ] 93 | ); 94 | if ( $fix ) { 95 | $phpcsFile->fixer->addContent( $tokens[$cur]['parenthesis_closer'], ': void ' ); 96 | } 97 | } else { 98 | // There's already a return type hint. No autofix, as the method must be manually checked 99 | $phpcsFile->addError( $err, $cur, 'WrongTypehint', [ $funcName ] ); 100 | } 101 | } 102 | } 103 | $cur = $phpcsFile->findNext( T_FUNCTION, $cur + 1, $end ); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/PHPUnit/SetMethodsSniff.php: -------------------------------------------------------------------------------- 1 | isTestFile( $phpcsFile, $stackPtr ) ) { 33 | return $phpcsFile->numTokens; 34 | } 35 | 36 | $tokens = $phpcsFile->getTokens(); 37 | $tokContent = $tokens[$stackPtr]['content']; 38 | 39 | // We don't care about stuff that's not in a method in a class 40 | if ( $tokens[$stackPtr]['level'] < 2 || $tokContent !== 'setMethods' ) { 41 | return; 42 | } 43 | 44 | $parOpener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 45 | if ( $tokens[$parOpener]['code'] !== T_OPEN_PARENTHESIS ) { 46 | return; 47 | } 48 | 49 | $fix = $phpcsFile->addFixableWarning( 50 | 'setMethods is deprecated in PHPUnit 8 and should be replaced with onlyMethods ' . 51 | 'or addMethods', 52 | $stackPtr, 53 | 'SetMethods' 54 | ); 55 | if ( !$fix ) { 56 | return; 57 | } 58 | 59 | $phpcsFile->fixer->replaceToken( $stackPtr, 'onlyMethods' ); 60 | // Special case: onlyMethods() takes an empty array, not null. 61 | $firstArgToken = $phpcsFile->findNext( T_WHITESPACE, $parOpener + 1, null, true ); 62 | if ( $tokens[$firstArgToken]['code'] === T_NULL ) { 63 | $phpcsFile->fixer->replaceToken( $firstArgToken, '[]' ); 64 | } 65 | return $stackPtr + 1; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/AssignmentInReturnSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 33 | 34 | $searchToken = Tokens::$assignmentTokens + [ 35 | T_CLOSURE, 36 | T_FUNCTION, 37 | T_ANON_CLASS, 38 | T_SEMICOLON, 39 | ]; 40 | $next = $phpcsFile->findNext( $searchToken, $stackPtr + 1 ); 41 | while ( $next !== false ) { 42 | $code = $tokens[$next]['code']; 43 | if ( isset( $tokens[$next]['scope_closer'] ) ) { 44 | // Skip to the end of the closure/inner function and continue 45 | $next = $phpcsFile->findNext( $searchToken, $tokens[$next]['scope_closer'] + 1 ); 46 | continue; 47 | } 48 | if ( $code === T_SEMICOLON ) { 49 | // End of return statement found 50 | break; 51 | } 52 | // Check if any assignment operator was used. Allow T_DOUBLE_ARROW as that can 53 | // be used in an array like `return [ 'foo' => 'bar' ]` 54 | if ( array_key_exists( $code, Tokens::$assignmentTokens ) 55 | && $code !== T_DOUBLE_ARROW 56 | ) { 57 | $errorPtr = $stackPtr; 58 | $content = $tokens[$stackPtr]['content']; 59 | // "yield from" could have whitespace and comments in the middle. If we only got a `yield`, skip 60 | // forwards until we find the `from`. See https://github.com/PHPCSStandards/PHP_CodeSniffer/pull/647 61 | if ( $tokens[$stackPtr]['code'] === T_YIELD_FROM && $content === 'yield' ) { 62 | $nextNotNoop = $phpcsFile->findNext( [ T_COMMENT, T_WHITESPACE ], $stackPtr + 1, null, true ); 63 | if ( 64 | $nextNotNoop === false || 65 | $tokens[$nextNotNoop]['code'] !== T_YIELD_FROM || 66 | $tokens[$nextNotNoop]['content'] !== 'from' 67 | ) { 68 | // Should never happen. 69 | $stackPtr++; 70 | } else { 71 | $content .= ' from'; 72 | $stackPtr = $nextNotNoop + 1; 73 | } 74 | } else { 75 | $stackPtr++; 76 | } 77 | // Split by any whitespaces and build better looking content with one space 78 | $contentPieces = preg_split( '/\s+/', $content ); 79 | $phpcsFile->addError( 80 | 'Assignment expression not allowed within "%s".', 81 | $errorPtr, 82 | 'AssignmentIn' . implode( '', array_map( 'ucfirst', $contentPieces ) ), 83 | [ implode( ' ', $contentPieces ) ] 84 | ); 85 | break; 86 | } 87 | $next = $phpcsFile->findNext( $searchToken, $next + 1 ); 88 | } 89 | // Do not report multiline yield tokens twice 90 | return $stackPtr; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/DbrQueryUsageSniff.php: -------------------------------------------------------------------------------- 1 | query() is used instead of $dbr->select() 4 | */ 5 | 6 | namespace MediaWiki\Sniffs\Usage; 7 | 8 | use PHP_CodeSniffer\Files\File; 9 | use PHP_CodeSniffer\Sniffs\Sniff; 10 | use PHP_CodeSniffer\Util\Tokens; 11 | 12 | class DbrQueryUsageSniff implements Sniff { 13 | 14 | /** 15 | * @inheritDoc 16 | */ 17 | public function register(): array { 18 | return [ T_OBJECT_OPERATOR ]; 19 | } 20 | 21 | /** 22 | * @param File $phpcsFile 23 | * @param int $stackPtr The current token index. 24 | * @return void 25 | */ 26 | public function process( File $phpcsFile, $stackPtr ) { 27 | $tokens = $phpcsFile->getTokens(); 28 | 29 | $dbrPtr = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); 30 | if ( !$dbrPtr 31 | || $tokens[$dbrPtr]['code'] !== T_VARIABLE 32 | || $tokens[$dbrPtr]['content'] !== '$dbr' 33 | ) { 34 | return; 35 | } 36 | 37 | $methodPtr = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 38 | if ( $methodPtr 39 | && $tokens[$methodPtr]['code'] === T_STRING 40 | && $tokens[$methodPtr]['content'] === 'query' 41 | ) { 42 | $phpcsFile->addWarning( 43 | 'Call $dbr->select() wrapper instead of $dbr->query()', 44 | $stackPtr, 45 | 'DbrQueryFound' 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/DeprecatedConstantUsageSniff.php: -------------------------------------------------------------------------------- 1 | Replacement, and last 31 | * MW version that old constant should still be used 32 | */ 33 | private const DEPRECATED_CONSTANTS = [ 34 | 'DB_SLAVE' => [ 35 | 'replace' => 'DB_REPLICA', 36 | 'version' => '1.27.3', 37 | ], 38 | 'DB_MASTER' => [ 39 | 'replace' => 'DB_PRIMARY', 40 | 'version' => '1.35.2', 41 | ], 42 | 'NS_IMAGE' => [ 43 | 'replace' => 'NS_FILE', 44 | 'version' => '1.13', 45 | ], 46 | 'NS_IMAGE_TALK' => [ 47 | 'replace' => 'NS_FILE_TALK', 48 | 'version' => '1.13', 49 | ], 50 | 'DO_MAINTENANCE' => [ 51 | 'replace' => 'RUN_MAINTENANCE_IF_MAIN', 52 | 'version' => '1.16.3', 53 | ] 54 | ]; 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function register(): array { 60 | return [ 61 | T_STRING, 62 | ]; 63 | } 64 | 65 | /** 66 | * Check for any deprecated constants 67 | * 68 | * @param File $phpcsFile Current file 69 | * @param int $stackPtr Position 70 | * @return void 71 | */ 72 | public function process( File $phpcsFile, $stackPtr ) { 73 | $token = $phpcsFile->getTokens()[$stackPtr]; 74 | $current = $token['content']; 75 | if ( isset( self::DEPRECATED_CONSTANTS[$current] ) ) { 76 | $extensionInfo = ExtensionInfo::newFromFile( $phpcsFile ); 77 | if ( $extensionInfo->supportsMediaWiki( self::DEPRECATED_CONSTANTS[$current]['version'] ) ) { 78 | return; 79 | } 80 | $fix = $phpcsFile->addFixableWarning( 81 | 'Deprecated constant %s used', 82 | $stackPtr, 83 | $current, 84 | [ $current ] 85 | ); 86 | if ( $fix ) { 87 | $phpcsFile->fixer->replaceToken( $stackPtr, self::DEPRECATED_CONSTANTS[$current]['replace'] ); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/DeprecatedGlobalVariablesSniff.php: -------------------------------------------------------------------------------- 1 | '1.27', 38 | // Deprecation done (T160815) 39 | '$wgContLang' => '1.32', 40 | // Deprecation done (T160811) 41 | '$wgParser' => '1.32', 42 | // Deprecation done (T159284) 43 | '$wgTitle' => '1.19', 44 | // Deprecation done (no task) 45 | '$parserMemc' => '1.30', 46 | // Deprecation done (T160813) 47 | '$wgMemc' => '1.35', 48 | // Deprecation done (T159299) 49 | '$wgUser' => '1.35', 50 | // Deprecation done (T212738) 51 | '$wgVersion' => '1.35', 52 | // Deprecation done (T331602) 53 | '$wgHooks' => '1.40', 54 | // Deprecation done (T313841) 55 | '$wgCommandLineMode' => '1.42', 56 | 57 | // Deprecation planned (T212739) 58 | // '$wgConf' => '', 59 | // Deprecation planned (T160814) 60 | // '$wgLang' => '', 61 | // Deprecation planned (T160812) 62 | // '$wgOut' => '', 63 | // Deprecation planned (T160810) 64 | // '$wgRequest' => '', 65 | ]; 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function register(): array { 71 | return [ T_GLOBAL ]; 72 | } 73 | 74 | /** 75 | * @param File $phpcsFile 76 | * @param int $stackPtr The current token index. 77 | * @return void 78 | */ 79 | public function process( File $phpcsFile, $stackPtr ) { 80 | $tokens = $phpcsFile->getTokens(); 81 | 82 | $next = $stackPtr++; 83 | $endOfGlobal = $phpcsFile->findEndOfStatement( $next, T_COMMA ); 84 | $extensionInfo = ExtensionInfo::newFromFile( $phpcsFile ); 85 | 86 | for ( ; $next < $endOfGlobal; $next++ ) { 87 | if ( $tokens[$next]['code'] !== T_VARIABLE ) { 88 | continue; 89 | } 90 | 91 | $globalVar = $tokens[$next]['content']; 92 | if ( !isset( self::DEPRECATED_GLOBALS[$globalVar] ) || 93 | $extensionInfo->supportsMediaWiki( self::DEPRECATED_GLOBALS[$globalVar] ) 94 | ) { 95 | continue; 96 | } 97 | 98 | $phpcsFile->addWarning( 99 | "Deprecated global $globalVar used", 100 | $next, 101 | 'Deprecated' . $globalVar 102 | ); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/DirUsageSniff.php: -------------------------------------------------------------------------------- 1 | dirname( __FILE__ ) 10 | * Pass: parent::dirname( __FILE__ ) 11 | */ 12 | 13 | namespace MediaWiki\Sniffs\Usage; 14 | 15 | use PHP_CodeSniffer\Files\File; 16 | use PHP_CodeSniffer\Sniffs\Sniff; 17 | 18 | class DirUsageSniff implements Sniff { 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | public function register(): array { 24 | // As per https://www.mediawiki.org/wiki/Manual:Coding_conventions/PHP#Other 25 | return [ T_STRING ]; 26 | } 27 | 28 | /** 29 | * @param File $phpcsFile 30 | * @param int $stackPtr The current token index. 31 | * @return void 32 | */ 33 | public function process( File $phpcsFile, $stackPtr ) { 34 | $tokens = $phpcsFile->getTokens(); 35 | 36 | // Check if the function is dirname() 37 | if ( $tokens[$stackPtr]['content'] !== 'dirname' ) { 38 | return; 39 | } 40 | 41 | // Find the parenthesis for the function 42 | $nextToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 43 | if ( $nextToken === false 44 | || $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS 45 | ) { 46 | return; 47 | } 48 | 49 | // Check if __FILE__ is inside it 50 | $nextToken = $phpcsFile->findNext( T_WHITESPACE, $nextToken + 1, null, true ); 51 | if ( $nextToken == false 52 | || $tokens[$nextToken]['code'] !== T_FILE 53 | ) { 54 | return; 55 | } 56 | 57 | // Check if it's a PHP function 58 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 59 | if ( $prevToken === false 60 | || $tokens[$prevToken]['code'] === T_OBJECT_OPERATOR 61 | || $tokens[$prevToken]['code'] === T_NULLSAFE_OBJECT_OPERATOR 62 | || $tokens[$prevToken]['code'] === T_DOUBLE_COLON 63 | || $tokens[$prevToken]['code'] === T_FUNCTION 64 | || $tokens[$prevToken]['code'] === T_CONST 65 | ) { 66 | return; 67 | } 68 | 69 | // Find close parenthesis 70 | $nextToken = $phpcsFile->findNext( T_WHITESPACE, $nextToken + 1, null, true ); 71 | if ( $nextToken === false 72 | || $tokens[$nextToken]['code'] !== T_CLOSE_PARENTHESIS 73 | ) { 74 | return; 75 | } 76 | 77 | $fix = $phpcsFile->addFixableError( 78 | 'Use __DIR__ constant instead of calling dirname(__FILE__)', 79 | $stackPtr, 80 | 'FunctionFound' 81 | ); 82 | if ( $fix ) { 83 | $curToken = $stackPtr; 84 | while ( $curToken <= $nextToken ) { 85 | if ( $tokens[$curToken]['code'] === T_FILE ) { 86 | $phpcsFile->fixer->replaceToken( $curToken, '__DIR__' ); 87 | } else { 88 | $phpcsFile->fixer->replaceToken( $curToken, '' ); 89 | } 90 | $curToken++; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/FinalPrivateSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 35 | 36 | // Find the next non-empty token 37 | $next = $phpcsFile->findNext( 38 | Tokens::$emptyTokens, 39 | $stackPtr + 1, 40 | null, 41 | true 42 | ); 43 | if ( $next === false 44 | || $tokens[$next]['code'] !== T_PRIVATE 45 | ) { 46 | // Not a private function or nothing after this, must be live coding 47 | return; 48 | } 49 | 50 | $fix = $phpcsFile->addFixableError( 51 | 'The `final` modifier should not be used for private methods', 52 | $stackPtr, 53 | 'Found' 54 | ); 55 | if ( $fix ) { 56 | $nextNonWhitespace = $phpcsFile->findNext( 57 | T_WHITESPACE, 58 | $stackPtr + 1, 59 | null, 60 | true 61 | ); 62 | 63 | $phpcsFile->fixer->beginChangeset(); 64 | $phpcsFile->fixer->replaceToken( $stackPtr, '' ); 65 | for ( $i = $stackPtr + 1; $i < $nextNonWhitespace; $i++ ) { 66 | $phpcsFile->fixer->replaceToken( $i, '' ); 67 | } 68 | $phpcsFile->fixer->endChangeset(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/ForbiddenFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | */ 19 | 20 | namespace MediaWiki\Sniffs\Usage; 21 | 22 | use PHP_CodeSniffer\Files\File; 23 | use PHP_CodeSniffer\Sniffs\Sniff; 24 | use PHP_CodeSniffer\Util\Tokens; 25 | 26 | /** 27 | * Use e.g. in your .phpcs.xml to remove 28 | * a function from the predefined list of forbidden functions. 29 | * 30 | * You can also add entries or modify existing ones. Note that an empty `value=""` won't work. Use 31 | * "null" for forbidden functions and any other non-empty value for replacements. 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 38 | * 39 | * 40 | * 41 | */ 42 | class ForbiddenFunctionsSniff implements Sniff { 43 | 44 | /** 45 | * Predefined list of deprecated functions and their replacements, or any empty value for 46 | * forbidden functions. 47 | */ 48 | private const FORBIDDEN_FUNCTIONS = [ 49 | 'chop' => 'rtrim', 50 | 'diskfreespace' => 'disk_free_space', 51 | 'doubleval' => 'floatval', 52 | 'ini_alter' => 'ini_set', 53 | 'is_integer' => 'is_int', 54 | 'is_long' => 'is_int', 55 | 'is_double' => 'is_float', 56 | 'is_real' => 'is_float', 57 | 'is_writeable' => 'is_writable', 58 | 'join' => 'implode', 59 | 'key_exists' => 'array_key_exists', 60 | 'pos' => 'current', 61 | 'sizeof' => 'count', 62 | 'strchr' => 'strstr', 63 | 'assert' => false, 64 | 'eval' => false, 65 | 'extract' => false, 66 | 'compact' => false, 67 | // Deprecated in PHP 7.2 68 | 'create_function' => false, 69 | 'each' => false, 70 | 'parse_str' => false, 71 | 'mb_parse_str' => false, 72 | // MediaWiki wrappers for external program execution should be used, 73 | // forbid PHP's (https://secure.php.net/manual/en/ref.exec.php) 74 | 'escapeshellarg' => false, 75 | 'escapeshellcmd' => false, 76 | 'exec' => false, 77 | 'passthru' => false, 78 | 'popen' => false, 79 | 'proc_open' => false, 80 | 'shell_exec' => false, 81 | 'system' => false, 82 | 'isset' => false, 83 | // resource type is going away in PHP 8.0+ (T260735) 84 | 'is_resource' => false, 85 | // define third parameter is deprecated in 7.3 86 | 'define' => false, 87 | ]; 88 | 89 | /** 90 | * Functions that are forbidden (per above) but allowed with a specific number of arguments 91 | */ 92 | private const ALLOWED_ARG_COUNT = [ 93 | 'parse_str' => 2, 94 | 'mb_parse_str' => 2, 95 | 'isset' => 1, 96 | 'define' => 2, 97 | ]; 98 | 99 | /** 100 | * @var string[] Key-value pairs as provided via .phpcs.xml. Maps deprecated function names to 101 | * their replacement, or the literal string "null" for forbidden functions. 102 | */ 103 | public $forbiddenFunctions = []; 104 | 105 | /** 106 | * @inheritDoc 107 | */ 108 | public function register(): array { 109 | return [ T_STRING, T_EVAL, T_ISSET ]; 110 | } 111 | 112 | /** 113 | * @param File $phpcsFile 114 | * @param int $stackPtr The current token index. 115 | * @return void 116 | */ 117 | public function process( File $phpcsFile, $stackPtr ) { 118 | $tokens = $phpcsFile->getTokens(); 119 | 120 | $nextToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); 121 | if ( $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS || 122 | !isset( $tokens[$nextToken]['parenthesis_closer'] ) 123 | ) { 124 | return; 125 | } 126 | 127 | // Check if the function is one of the bad ones 128 | $funcName = $tokens[$stackPtr]['content']; 129 | if ( array_key_exists( $funcName, $this->forbiddenFunctions ) ) { 130 | $replacement = $this->forbiddenFunctions[$funcName]; 131 | if ( $replacement === $funcName ) { 132 | return; 133 | } 134 | } elseif ( array_key_exists( $funcName, self::FORBIDDEN_FUNCTIONS ) ) { 135 | $replacement = self::FORBIDDEN_FUNCTIONS[$funcName]; 136 | } else { 137 | return; 138 | } 139 | 140 | $ignore = [ 141 | T_DOUBLE_COLON => true, 142 | T_OBJECT_OPERATOR => true, 143 | T_NULLSAFE_OBJECT_OPERATOR => true, 144 | T_FUNCTION => true, 145 | T_CONST => true, 146 | ]; 147 | 148 | // Check to make sure it's a PHP function (not $this->, etc.) 149 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 150 | if ( isset( $ignore[$tokens[$prevToken]['code']] ) ) { 151 | return; 152 | } 153 | 154 | // Check argument count 155 | $allowedArgCount = self::ALLOWED_ARG_COUNT[$funcName] ?? null; 156 | if ( $allowedArgCount !== null && 157 | $this->argCount( $phpcsFile, $nextToken ) == $allowedArgCount 158 | ) { 159 | // Nothing to replace 160 | return; 161 | } 162 | 163 | // The hard-coded FORBIDDEN_FUNCTIONS can use false, but values from .phpcs.xml are always 164 | // strings. We use the same special string "null" as in the Generic.PHP.ForbiddenFunctions 165 | // sniff. 166 | if ( $replacement && $replacement !== 'null' ) { 167 | $fix = $phpcsFile->addFixableWarning( 168 | 'Use %s() instead of %s', 169 | $stackPtr, 170 | $funcName, 171 | [ $replacement, $funcName ] 172 | ); 173 | if ( $fix ) { 174 | $phpcsFile->fixer->replaceToken( $stackPtr, $replacement ); 175 | } 176 | } else { 177 | $phpcsFile->addWarning( 178 | $allowedArgCount !== null 179 | ? '%s should be used with %s argument(s)' 180 | : '%s should not be used', 181 | $stackPtr, 182 | $funcName, 183 | [ $funcName, $allowedArgCount ] 184 | ); 185 | } 186 | } 187 | 188 | /** 189 | * Return the number of arguments between the $parenthesis as opener and its closer 190 | * Ignoring commas between brackets to support nested argument lists 191 | * 192 | * @param File $phpcsFile 193 | * @param int $parenthesis The parenthesis token index. 194 | * @return int 195 | */ 196 | private function argCount( File $phpcsFile, int $parenthesis ): int { 197 | $tokens = $phpcsFile->getTokens(); 198 | $end = $tokens[$parenthesis]['parenthesis_closer']; 199 | $next = $phpcsFile->findNext( Tokens::$emptyTokens, $parenthesis + 1, $end, true ); 200 | $argCount = 0; 201 | 202 | if ( $next !== false ) { 203 | // Something found, there is at least one argument 204 | $argCount++; 205 | 206 | $searchTokens = [ 207 | T_OPEN_CURLY_BRACKET, 208 | T_OPEN_SQUARE_BRACKET, 209 | T_OPEN_SHORT_ARRAY, 210 | T_OPEN_PARENTHESIS, 211 | T_COMMA 212 | ]; 213 | while ( $next !== false ) { 214 | switch ( $tokens[$next]['code'] ) { 215 | case T_OPEN_CURLY_BRACKET: 216 | case T_OPEN_SQUARE_BRACKET: 217 | case T_OPEN_PARENTHESIS: 218 | if ( isset( $tokens[$next]['parenthesis_closer'] ) ) { 219 | // jump to closing parenthesis to ignore commas between opener and closer 220 | $next = $tokens[$next]['parenthesis_closer']; 221 | } 222 | break; 223 | case T_OPEN_SHORT_ARRAY: 224 | if ( isset( $tokens[$next]['bracket_closer'] ) ) { 225 | // jump to closing bracket to ignore commas between opener and closer 226 | $next = $tokens[$next]['bracket_closer']; 227 | } 228 | break; 229 | case T_COMMA: 230 | $argCount++; 231 | break; 232 | } 233 | 234 | $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end ); 235 | } 236 | } 237 | 238 | return $argCount; 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/InArrayUsageSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | 33 | // Continue only if the string we found is wrapped in at least one parenthesis 34 | if ( !isset( $tokens[$stackPtr]['nested_parenthesis'] ) ) { 35 | return; 36 | } 37 | 38 | if ( $tokens[$stackPtr]['content'] !== 'array_flip' 39 | && $tokens[$stackPtr]['content'] !== 'array_keys' 40 | ) { 41 | return; 42 | } 43 | 44 | $openParenthesisPtr = array_key_last( $tokens[$stackPtr]['nested_parenthesis'] ); 45 | 46 | // Continue only if the parenthesis belongs to an in_array() call 47 | if ( $tokens[$openParenthesisPtr - 1]['code'] !== T_STRING 48 | || strcasecmp( $tokens[$openParenthesisPtr - 1]['content'], 'in_array' ) !== 0 49 | ) { 50 | return; 51 | } 52 | 53 | $previous = $phpcsFile->findPrevious( 54 | T_WHITESPACE, 55 | $stackPtr - 1, 56 | $openParenthesisPtr + 1, 57 | true 58 | ); 59 | if ( $tokens[$previous]['code'] !== T_COMMA ) { 60 | return; 61 | } 62 | 63 | $phpcsFile->addError( 64 | 'Found slow in_array( …, %s() ), should be array_key_exists() or isset()', 65 | $stackPtr, 66 | 'Found', 67 | [ $tokens[$stackPtr]['content'] ] 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/IsNullSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | 33 | if ( $tokens[$stackPtr]['content'] !== 'is_null' ) { 34 | return; 35 | } 36 | 37 | $ignore = [ 38 | T_DOUBLE_COLON => true, 39 | T_OBJECT_OPERATOR => true, 40 | T_NULLSAFE_OBJECT_OPERATOR => true, 41 | T_FUNCTION => true, 42 | T_CONST => true, 43 | ]; 44 | 45 | // Check to make sure it's a function call to is_null (not $this->, etc.) 46 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 47 | if ( isset( $ignore[$tokens[$prevToken]['code']] ) ) { 48 | return; 49 | } 50 | $nextToken = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 51 | if ( $nextToken === false 52 | || $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS 53 | || !isset( $tokens[$nextToken]['parenthesis_closer'] ) 54 | ) { 55 | return; 56 | } 57 | 58 | $nsToken = null; 59 | 60 | if ( $tokens[$prevToken]['code'] === T_NS_SEPARATOR ) { 61 | $nsToken = $prevToken; 62 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $prevToken - 1, null, true ); 63 | if ( $tokens[$prevToken]['code'] === T_STRING ) { 64 | // Not in the global namespace. 65 | return; 66 | } 67 | } 68 | 69 | $hasBackslash = $nsToken === null; 70 | 71 | if ( $this->isComparisonWithIsNull( $phpcsFile, $stackPtr, $hasBackslash ) ) { 72 | $phpcsFile->addWarning( 73 | 'Use a comparison against null instead of is_null', 74 | $stackPtr, 75 | 'IsNull' 76 | ); 77 | return; 78 | } 79 | 80 | $fix = $phpcsFile->addFixableWarning( 81 | 'Use a comparison against null instead of is_null', 82 | $stackPtr, 83 | 'IsNull' 84 | ); 85 | 86 | if ( !$fix ) { 87 | return; 88 | } 89 | 90 | $stackPtrOpenParenthesis = $nextToken; 91 | $stackPtrCloseParenthesis = $tokens[$nextToken]['parenthesis_closer']; 92 | 93 | $phpcsFile->fixer->beginChangeset(); 94 | 95 | // remove the backslash, if in global namespace 96 | if ( $nsToken !== null ) { 97 | $phpcsFile->fixer->replaceToken( $nsToken, '' ); 98 | } 99 | 100 | // Remove the function name. 101 | $phpcsFile->fixer->replaceToken( $stackPtr, '' ); 102 | 103 | $notNullComparison = $tokens[$prevToken]['code'] === T_BOOLEAN_NOT; 104 | 105 | if ( $this->keepParentheses( $phpcsFile, $stackPtrOpenParenthesis, $stackPtrCloseParenthesis ) ) { 106 | if ( $notNullComparison ) { 107 | // Remove the boolean not operator, it will be moved to the comparison operator. 108 | $phpcsFile->fixer->replaceToken( $prevToken, '' ); 109 | $replacement = ') !== null'; 110 | } else { 111 | $replacement = ') === null'; 112 | } 113 | } else { 114 | // Remove opening parenthesis. 115 | $phpcsFile->fixer->replaceToken( $stackPtrOpenParenthesis, '' ); 116 | // Remove following whitespace, if any. 117 | while ( $tokens[$stackPtrOpenParenthesis + 1]['code'] === T_WHITESPACE ) { 118 | $stackPtrOpenParenthesis++; 119 | $phpcsFile->fixer->replaceToken( $stackPtrOpenParenthesis, '' ); 120 | } 121 | 122 | if ( $notNullComparison ) { 123 | // Remove the boolean not operator, it will be moved to the comparison operator. 124 | $phpcsFile->fixer->replaceToken( $prevToken, '' ); 125 | $replacement = ' !== null'; 126 | } else { 127 | $replacement = ' === null'; 128 | } 129 | 130 | $ptrBeforeCloseParenthesis = $stackPtrCloseParenthesis; 131 | 132 | // Remove whitespace preceding closing parenthesis, if any. 133 | while ( $tokens[$ptrBeforeCloseParenthesis - 1]['code'] === T_WHITESPACE ) { 134 | $ptrBeforeCloseParenthesis--; 135 | $phpcsFile->fixer->replaceToken( $ptrBeforeCloseParenthesis, '' ); 136 | } 137 | } 138 | 139 | $phpcsFile->fixer->replaceToken( $stackPtrCloseParenthesis, $replacement ); 140 | 141 | $phpcsFile->fixer->endChangeset(); 142 | } 143 | 144 | /** 145 | * Determines if the content between parenthesis warrants keeping the parenthesis for the null 146 | * comparison. 147 | * 148 | * @param File $phpcsFile 149 | * @param int $stackPtrOpenParenthesis 150 | * @param int $stackPtrCloseParenthesis 151 | * @return bool 152 | */ 153 | private function keepParentheses( 154 | File $phpcsFile, int $stackPtrOpenParenthesis, int $stackPtrCloseParenthesis 155 | ): bool { 156 | $tokens = $phpcsFile->getTokens(); 157 | 158 | // Skip first whitespace, if any. 159 | $stackPtrFirstExpressionToken = $stackPtrOpenParenthesis + 1; 160 | while ( $tokens[$stackPtrFirstExpressionToken]['code'] === T_WHITESPACE ) { 161 | $stackPtrFirstExpressionToken++; 162 | } 163 | 164 | // Skip last whitespace, if any. 165 | $stackPtrLastExpressionToken = $stackPtrCloseParenthesis - 1; 166 | while ( $tokens[$stackPtrLastExpressionToken]['code'] === T_WHITESPACE ) { 167 | $stackPtrLastExpressionToken--; 168 | } 169 | 170 | // Look for whitespace between the parentheses. 171 | $firstWhitespace = $phpcsFile->findNext( 172 | T_WHITESPACE, 173 | $stackPtrFirstExpressionToken, 174 | $stackPtrLastExpressionToken 175 | ); 176 | 177 | // Statements like is_null( $var ) or is_null( Class::method() ) are simple enough 178 | // not to require whitespace, so the parentheses can be dropped. 179 | // PHPCS will identify statements is_null($a?$b:$c) as missing whitespace before this 180 | // sniff is run. 181 | if ( !$firstWhitespace ) { 182 | return false; 183 | } 184 | 185 | $innerParenthesis = $phpcsFile->findNext( 186 | T_OPEN_PARENTHESIS, 187 | $stackPtrFirstExpressionToken, 188 | $stackPtrLastExpressionToken 189 | ); 190 | 191 | // Something has been wrapped in parentheses ending just before the ending parenthesis of 192 | // the is_null statement. 193 | if ( 194 | $innerParenthesis && 195 | $tokens[$innerParenthesis]['parenthesis_closer'] === $stackPtrLastExpressionToken 196 | ) { 197 | $previousWhiteSpace = $phpcsFile->findPrevious( 198 | T_WHITESPACE, 199 | $innerParenthesis, 200 | $stackPtrFirstExpressionToken 201 | ); 202 | 203 | // Obviously, statements such as is_null( $a ? $b : ( $c ) ) will trick this check. 204 | // They should retain their parenthesis, so see if there is any whitespace before 205 | // the opening parenthesis. 206 | if ( !$previousWhiteSpace ) { 207 | return false; 208 | } 209 | } 210 | 211 | // When in doubt, keep parenthesis. 212 | return true; 213 | } 214 | 215 | /** 216 | * Comparisons that compare a variable to the result of is_null or to the result of another 217 | * is_null, like $var === is_null( $var ) or is_null( $var ) === is_null( $var ). 218 | * 219 | * These can't be replaced by other constructions and should remain untouched. 220 | * 221 | * @param File $phpcsFile 222 | * @param int $stackPtr 223 | * @param bool $hasBackslash 224 | * @return bool 225 | */ 226 | private function isComparisonWithIsNull( File $phpcsFile, int $stackPtr, bool $hasBackslash ): bool { 227 | $prevOnStack = $hasBackslash ? 1 : 2; 228 | 229 | $tokens = $phpcsFile->getTokens(); 230 | $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - $prevOnStack, null, true ); 231 | $nextToken = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 232 | $nextToken = $phpcsFile->findNext( 233 | Tokens::$emptyTokens, 234 | $tokens[$nextToken]['parenthesis_closer'] + 1, 235 | null, 236 | true 237 | ); 238 | 239 | return $tokens[$prevToken]['code'] === T_IS_EQUAL || 240 | $tokens[$prevToken]['code'] === T_IS_IDENTICAL || 241 | $tokens[$prevToken]['code'] === T_IS_NOT_EQUAL || 242 | $tokens[$prevToken]['code'] === T_IS_NOT_IDENTICAL || 243 | $tokens[$nextToken]['code'] === T_IS_EQUAL || 244 | $tokens[$nextToken]['code'] === T_IS_IDENTICAL || 245 | $tokens[$nextToken]['code'] === T_IS_NOT_EQUAL || 246 | $tokens[$nextToken]['code'] === T_IS_NOT_IDENTICAL; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/MagicConstantClosureSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 54 | $constant = $tokens[$stackPtr]['content']; 55 | $phpcsFile->addWarning( 56 | 'Avoid use of %s magic constant in closure', 57 | $stackPtr, 58 | $this->createSniffCode( 'FoundConstant', $constant ), 59 | [ $constant ] 60 | ); 61 | } 62 | 63 | /** 64 | * @param File $phpcsFile 65 | * @param int $stackPtr The current token index. 66 | * @return void 67 | * @suppress PhanUnusedProtectedMethodParameter Inherit from parent class 68 | */ 69 | protected function processTokenOutsideScope( File $phpcsFile, $stackPtr ) { 70 | } 71 | 72 | /** 73 | * @param string $prefix 74 | * @param string $constant 75 | * 76 | * @return string 77 | */ 78 | private function createSniffCode( string $prefix, string $constant ): string { 79 | return $prefix . ucfirst( strtolower( trim( $constant, '_' ) ) ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/NestedInlineTernarySniff.php: -------------------------------------------------------------------------------- 1 | endTokens = Tokens::$assignmentTokens + Tokens::$includeTokens + [ 41 | // Operators having a lower precedence than the ternary operator, 42 | // or left associative operators having the same precedence, can 43 | // end inline ternary statements. This includes all assignment and 44 | // include statements. 45 | // 46 | // In the PHP source code, the order of precedence can be found 47 | // in the file Zend/zend_language_parser.y. To find the ternary 48 | // operator in the list, search for "%left '?' ':'". 49 | T_INLINE_THEN => T_INLINE_THEN, 50 | T_INLINE_ELSE => T_INLINE_ELSE, 51 | T_YIELD_FROM => T_YIELD_FROM, 52 | T_YIELD => T_YIELD, 53 | T_PRINT => T_PRINT, 54 | T_LOGICAL_AND => T_LOGICAL_AND, 55 | T_LOGICAL_XOR => T_LOGICAL_XOR, 56 | T_LOGICAL_OR => T_LOGICAL_OR, 57 | 58 | // Obviously, right brackets, right parentheses, commas, colons, 59 | // and semicolons can also end inline ternary statements. There is 60 | // a list of corresponding tokens in File::findEndOfStatement(), 61 | // which we duplicate here. 62 | T_COLON => T_COLON, 63 | T_COMMA => T_COMMA, 64 | T_SEMICOLON => T_SEMICOLON, 65 | T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS, 66 | T_CLOSE_SQUARE_BRACKET => T_CLOSE_SQUARE_BRACKET, 67 | T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET, 68 | T_CLOSE_SHORT_ARRAY => T_CLOSE_SHORT_ARRAY, 69 | 70 | // Less obviously, a foreach loop's array_expression can be 71 | // an inline ternary statement, and would be followed by "as". 72 | T_AS => T_AS, 73 | ]; 74 | 75 | return [ T_INLINE_THEN ]; 76 | } 77 | 78 | /** 79 | * @param File $phpcsFile File 80 | * @param int $stackPtr Location 81 | * @return void 82 | */ 83 | public function process( File $phpcsFile, $stackPtr ) { 84 | $tokens = $phpcsFile->getTokens(); 85 | $elsePtr = null; 86 | $thenNestingLevel = 1; 87 | $thenNestedPtr = null; 88 | $elseNestedPtr = null; 89 | for ( $i = $stackPtr + 1; $i < $phpcsFile->numTokens; ++$i ) { 90 | // Skip bracketed and parenthesized subexpressions. 91 | $inBrackets = isset( $tokens[$i]['bracket_closer'] ); 92 | if ( $inBrackets && $tokens[$i]['bracket_opener'] === $i ) { 93 | $i = $tokens[$i]['bracket_closer']; 94 | continue; 95 | } 96 | $inParentheses = isset( $tokens[$i]['parenthesis_closer'] ); 97 | if ( $inParentheses && $tokens[$i]['parenthesis_opener'] === $i ) { 98 | $i = $tokens[$i]['parenthesis_closer']; 99 | continue; 100 | } 101 | 102 | if ( $elsePtr === null ) { 103 | // In the "then" part of the inline ternary statement: 104 | if ( $tokens[$i]['code'] === T_INLINE_THEN ) { 105 | // Let $thenNestedPtr point to the T_INLINE_THEN token 106 | // of the outermost inline ternary statement forming the 107 | // "then" part of the current inline ternary statement. 108 | // Example: $a ? $b ? $c ? $d : $e : $f : $g 109 | // - ^ stackPtr 110 | // - ^ thenNestedPtr 111 | if ( ++$thenNestingLevel === 2 ) { 112 | $thenNestedPtr = $i; 113 | } 114 | } elseif ( $tokens[$i]['code'] === T_INLINE_ELSE ) { 115 | // Let $elsePtr point to the T_INLINE_ELSE token of the 116 | // current inline ternary statement. See below example. 117 | if ( --$thenNestingLevel === 0 ) { 118 | $elsePtr = $i; 119 | } 120 | } 121 | // Strictly speaking, checking if the entire "then" part 122 | // is an inline ternary statement would involve checking the 123 | // token, whenever $thenNestingLevel is 1, against the 124 | // list of operators of lower precedence. 125 | // 126 | // However, we omit this check in order to allow additional 127 | // cases to be flagged as needing parentheses for clarity. 128 | 129 | } else { 130 | // In the "else" part of the inline ternary statement: 131 | if ( isset( $this->endTokens[$tokens[$i]['code']] ) ) { 132 | if ( $tokens[$i]['code'] === T_INLINE_THEN ) { 133 | // Let $elseNestedPtr point to the T_INLINE_THEN token 134 | // of the inline ternary statement having the current 135 | // inline ternary statement as its "if" part. 136 | // Example: $a ? $b : $c ? $d : $e ? $f : $g 137 | // - ^ stackPtr 138 | // - ^ elsePtr 139 | // - ^ elseNestedPtr 140 | $elseNestedPtr = $i; 141 | } 142 | break; 143 | } 144 | } 145 | } 146 | 147 | // The "then" part of the current inline ternary statement should not 148 | // be another inline ternary statement, unless that other inline 149 | // ternary statement is parenthesized. 150 | if ( $thenNestedPtr !== null && $elsePtr !== null ) { 151 | $fix = $phpcsFile->addFixableWarning( 152 | 'Nested inline ternary statements can be difficult to read without parentheses', 153 | $thenNestedPtr, 154 | 'UnparenthesizedThen' 155 | ); 156 | if ( $fix ) { 157 | $phpcsFile->fixer->beginChangeset(); 158 | $phpcsFile->fixer->addContent( $stackPtr, ' (' ); 159 | $phpcsFile->fixer->addContentBefore( $elsePtr, ') ' ); 160 | $phpcsFile->fixer->endChangeset(); 161 | } 162 | } 163 | 164 | // The current inline ternary statement must not be the "if" part of 165 | // another inline ternary statement, unless the current inline 166 | // ternary statement is parenthesized. 167 | if ( $elseNestedPtr !== null && !( 168 | // Exception: Stacking is permitted when only the short form of 169 | // the ternary operator is used. In this case, the operator's 170 | // left associativity is unlikely to matter. 171 | $this->isShortTernary( $phpcsFile, $stackPtr ) && 172 | $this->isShortTernary( $phpcsFile, $elseNestedPtr ) 173 | ) ) { 174 | // Report this violation as an error, because it looks like a bug. 175 | // For the same reason, don't offer to fix it automatically. 176 | $phpcsFile->addError( 177 | 'Nested inline ternary statements, in PHP, may not behave as you intend ' . 178 | 'without parentheses', 179 | $stackPtr, 180 | 'UnparenthesizedTernary' 181 | ); 182 | } 183 | } 184 | 185 | /** 186 | * @param File $phpcsFile File 187 | * @param int $i Location of T_INLINE_THEN 188 | * @return bool 189 | */ 190 | private function isShortTernary( File $phpcsFile, int $i ): bool { 191 | $tokens = $phpcsFile->getTokens(); 192 | $i = $phpcsFile->findNext( Tokens::$emptyTokens, $i + 1, null, true ); 193 | return $i !== false && $tokens[$i]['code'] === T_INLINE_ELSE; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/NullableTypeSniff.php: -------------------------------------------------------------------------------- 1 | getMethodParameters( $stackPtr ); 44 | 45 | foreach ( $params as $param ) { 46 | if ( 47 | $param['type_hint'] && 48 | $param['type_hint'] !== 'mixed' && 49 | $param['nullable_type'] === false && 50 | array_key_exists( 'default', $param ) && 51 | $param['default'] === 'null' 52 | ) { 53 | $fix = $phpcsFile->addFixableError( 54 | 'Use PHP 8.4 compatible syntax for explicit nullable types ("?%s %s = %s")', 55 | $param['type_hint_token'], 56 | 'ExplicitNullableTypes', 57 | [ $param['type_hint'], $param['name'], $param['default'] ] 58 | ); 59 | if ( $fix ) { 60 | $phpcsFile->fixer->addContentBefore( $param['type_hint_token'], '?' ); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/PlusStringConcatSniff.php: -------------------------------------------------------------------------------- 1 | findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); 50 | $next = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 51 | if ( $prev === false || $next === false ) { 52 | // Live coding 53 | return; 54 | } 55 | $tokens = $phpcsFile->getTokens(); 56 | 57 | // The token + should not have a string before or after it 58 | if ( isset( Tokens::$stringTokens[$tokens[$prev]['code']] ) 59 | || isset( Tokens::$stringTokens[$tokens[$next]['code']] ) 60 | ) { 61 | $phpcsFile->addError( 62 | 'Use "%s" for string concat', 63 | $stackPtr, 64 | 'Found', 65 | [ $tokens[$stackPtr]['code'] === T_PLUS ? '.' : '.=' ] 66 | ); 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/ReferenceThisSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 49 | if ( !isset( $tokens[$stackPtr + 1] ) ) { 50 | // Syntax error or live coding, bow out. 51 | return; 52 | } 53 | 54 | $next = $tokens[$stackPtr + 1]; 55 | if ( $next['code'] === T_VARIABLE && $next['content'] === '$this' ) { 56 | $after = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 2, null, true ); 57 | if ( $after !== false && 58 | in_array( 59 | $tokens[$after]['code'], 60 | [ T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_OPEN_SQUARE_BRACKET, T_DOUBLE_COLON ] 61 | ) 62 | ) { 63 | return; 64 | } 65 | $phpcsFile->addError( 66 | 'The ampersand in "&$this" must be removed. If you plan to get back another ' . 67 | 'instance of this class, assign $this to a temporary variable.', 68 | $stackPtr, 69 | 'Found' 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/StaticClosureSniff.php: -------------------------------------------------------------------------------- 1 | findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); 45 | if ( $prevClosureToken === false ) { 46 | return; 47 | } 48 | 49 | $tokens = $phpcsFile->getTokens(); 50 | $containsNonStaticStatements = false; 51 | $unclearSituation = false; 52 | 53 | $searchToken = [ 54 | T_VARIABLE, 55 | T_DOUBLE_QUOTED_STRING, 56 | T_HEREDOC, 57 | T_PARENT, 58 | T_SELF, 59 | T_STATIC, 60 | T_CLOSURE, 61 | T_ANON_CLASS, 62 | ]; 63 | $end = $tokens[$stackPtr]['scope_closer']; 64 | 65 | // Search for tokens which indicates that this cannot be a static closure 66 | $next = $phpcsFile->findNext( $searchToken, $tokens[$stackPtr]['scope_opener'] + 1, $end ); 67 | while ( $next !== false ) { 68 | $code = $tokens[$next]['code']; 69 | switch ( $code ) { 70 | case T_VARIABLE: 71 | if ( $tokens[$next]['content'] === '$this' ) { 72 | $containsNonStaticStatements = true; 73 | } 74 | break; 75 | 76 | case T_DOUBLE_QUOTED_STRING: 77 | case T_HEREDOC: 78 | if ( preg_match( '/\${?this\b/', $tokens[$next]['content'] ) ) { 79 | $containsNonStaticStatements = true; 80 | } 81 | break; 82 | 83 | case T_PARENT: 84 | case T_SELF: 85 | case T_STATIC: 86 | // Use of consts are allowed in static closures 87 | $nextToken = $phpcsFile->findNext( Tokens::$emptyTokens, $next + 1, $end, true ); 88 | // In case of T_STATIC ignore the static keyword on closures 89 | if ( $nextToken !== false 90 | && $tokens[$nextToken]['code'] !== T_CLOSURE 91 | && !$this->isStaticClassProperty( $phpcsFile, $tokens, $nextToken, $end ) 92 | ) { 93 | $prevToken = $phpcsFile->findPrevious( Tokens::$emptyTokens, $next - 1, null, true ); 94 | // Okay on "new self" 95 | if ( $prevToken === false || $tokens[$prevToken]['code'] !== T_NEW ) { 96 | // php allows to call non-static method with self:: or parent:: or static:: 97 | // That is normally a static call, but keep it as is, because it is unclear 98 | // and can break when changing. 99 | // Also keep unknown token sequences or unclear syntax 100 | $unclearSituation = true; 101 | } 102 | } 103 | if ( $nextToken !== false ) { 104 | // Skip over analyzed tokens 105 | $next = $nextToken; 106 | } 107 | break; 108 | 109 | case T_CLOSURE: 110 | // Skip arguments and use parameter for closure, which can contains T_SELF as type hint 111 | // But search also inside nested closures for $this 112 | if ( isset( $tokens[$next]['scope_opener'] ) ) { 113 | $next = $tokens[$next]['scope_opener']; 114 | } 115 | break; 116 | 117 | case T_ANON_CLASS: 118 | if ( isset( $tokens[$next]['scope_closer'] ) ) { 119 | // Skip to the end of the anon class because $this in anon is not relevant for this sniff 120 | $next = $tokens[$next]['scope_closer']; 121 | } 122 | break; 123 | } 124 | $next = $phpcsFile->findNext( $searchToken, $next + 1, $end ); 125 | } 126 | 127 | if ( $unclearSituation ) { 128 | // Keep everything as is 129 | return; 130 | } 131 | 132 | if ( $tokens[$prevClosureToken]['code'] === T_STATIC ) { 133 | if ( $containsNonStaticStatements ) { 134 | $fix = $phpcsFile->addFixableError( 135 | 'Cannot not use static closure in class context', 136 | $stackPtr, 137 | 'NonStaticClosure' 138 | ); 139 | if ( $fix ) { 140 | $phpcsFile->fixer->beginChangeset(); 141 | 142 | do { 143 | $phpcsFile->fixer->replaceToken( $prevClosureToken, '' ); 144 | $prevClosureToken++; 145 | } while ( $prevClosureToken < $stackPtr ); 146 | 147 | $phpcsFile->fixer->endChangeset(); 148 | } 149 | } 150 | } elseif ( !$containsNonStaticStatements ) { 151 | $fix = $phpcsFile->addFixableWarning( 152 | 'Use static closure', 153 | $stackPtr, 154 | 'StaticClosure' 155 | ); 156 | if ( $fix ) { 157 | $phpcsFile->fixer->addContentBefore( $stackPtr, 'static ' ); 158 | } 159 | } 160 | 161 | // also check inner closure for static 162 | } 163 | 164 | /** 165 | * Check if this is a class property like const or static field of format self::const 166 | * @param File $phpcsFile 167 | * @param array $tokens 168 | * @param int &$stackPtr Non-empty token after self/parent/static 169 | * @param int $end 170 | * @return bool 171 | */ 172 | private function isStaticClassProperty( File $phpcsFile, array $tokens, int &$stackPtr, int $end ): bool { 173 | // No ::, no const 174 | if ( $tokens[$stackPtr]['code'] !== T_DOUBLE_COLON ) { 175 | return false; 176 | } 177 | 178 | // the const is a T_STRING, but also method calls are T_STRING 179 | // okay with (static) variables 180 | $stackPtr = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, $end, true ); 181 | if ( $stackPtr === false || $tokens[$stackPtr]['code'] !== T_STRING ) { 182 | return $stackPtr !== false && $tokens[$stackPtr]['code'] === T_VARIABLE; 183 | } 184 | 185 | // const is a T_STRING without parenthesis after it 186 | $stackPtr = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, $end, true ); 187 | return $stackPtr !== false && $tokens[$stackPtr]['code'] !== T_OPEN_PARENTHESIS; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Usage/SuperGlobalsUsageSniff.php: -------------------------------------------------------------------------------- 1 | true, 21 | '$_GET' => true, 22 | '$_FILES' => true, 23 | ]; 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | public function register(): array { 29 | return [ T_VARIABLE ]; 30 | } 31 | 32 | /** 33 | * @param File $phpcsFile 34 | * @param int $stackPtr The current token index. 35 | * @return void 36 | */ 37 | public function process( File $phpcsFile, $stackPtr ) { 38 | $tokens = $phpcsFile->getTokens(); 39 | $currentToken = $tokens[$stackPtr]; 40 | if ( isset( self::FORBIDDEN_SUPER_GLOBALS[$currentToken['content']] ) ) { 41 | $error = '"%s" superglobals should not be accessed.'; 42 | $phpcsFile->addError( $error, $stackPtr, 'SuperGlobals', [ $currentToken['content'] ] ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/Utils/ExtensionInfo.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | */ 19 | 20 | namespace MediaWiki\Sniffs\Utils; 21 | 22 | use Composer\Semver\Constraint\Constraint; 23 | use Composer\Semver\VersionParser; 24 | use PHP_CodeSniffer\Files\File; 25 | 26 | class ExtensionInfo { 27 | 28 | /** 29 | * @var string Extension root path 30 | */ 31 | private string $dir; 32 | 33 | /** 34 | * @var array|false|null Parsed extension.json 35 | */ 36 | private $info = null; 37 | 38 | /** @var bool[] */ 39 | private array $supportCache = []; 40 | 41 | /** 42 | * @param File $phpcsFile 43 | * 44 | * @return ExtensionInfo 45 | */ 46 | public static function newFromFile( File $phpcsFile ) { 47 | static $instances = []; 48 | // The first standard path will be .phpcs.xml in the extension root 49 | $dir = dirname( $phpcsFile->config->standards[0] ); 50 | if ( !isset( $instances[$dir] ) ) { 51 | $instances[$dir] = new self( $dir ); 52 | } 53 | 54 | return $instances[$dir]; 55 | } 56 | 57 | /** 58 | * @internal For tests only, use ExtensionInfo::newFromFile instead 59 | * @param string $dir Path of extension 60 | */ 61 | public function __construct( $dir ) { 62 | $this->dir = $dir; 63 | } 64 | 65 | /** 66 | * @param string $version Version to see if it is still supported 67 | * 68 | * @return bool 69 | */ 70 | public function supportsMediaWiki( $version ) { 71 | if ( isset( $this->supportCache[$version] ) ) { 72 | return $this->supportCache[$version]; 73 | } 74 | 75 | $info = $this->readInfo(); 76 | if ( !$info ) { 77 | // Default behavior is that we assume they're following master 78 | return false; 79 | } 80 | 81 | if ( !isset( $info['requires']['MediaWiki'] ) ) { 82 | return false; 83 | } 84 | 85 | $versionParser = new VersionParser(); 86 | $ourVersion = new Constraint( '==', $versionParser->normalize( $version ) ); 87 | $ourVersion->setPrettyString( $version ); 88 | $matches = $versionParser 89 | ->parseConstraints( $info['requires']['MediaWiki'] ) 90 | ->matches( $ourVersion ); 91 | $this->supportCache[$version] = $matches; 92 | return $matches; 93 | } 94 | 95 | /** @return array|false */ 96 | private function readInfo() { 97 | if ( $this->info !== null ) { 98 | return $this->info; 99 | } 100 | 101 | $found = false; 102 | foreach ( [ 'extension', 'skin' ] as $type ) { 103 | $path = "{$this->dir}/$type.json"; 104 | if ( file_exists( $path ) ) { 105 | $found = true; 106 | break; 107 | } 108 | } 109 | 110 | if ( !$found ) { 111 | $this->info = false; 112 | return $this->info; 113 | } 114 | 115 | $this->info = json_decode( file_get_contents( $path ), true ); 116 | return $this->info; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/VariableAnalysis/MisleadingGlobalNamesSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 50 | if ( !isset( $tokens[$stackPtr]['scope_opener'] ) ) { 51 | // An interface or abstract function which doesn't have a body 52 | return; 53 | } 54 | 55 | $scopeOpener = $tokens[$stackPtr]['scope_opener'] + 1; 56 | $scopeCloser = $tokens[$stackPtr]['scope_closer']; 57 | 58 | $endOfGlobal = 0; 59 | $globalVariables = []; 60 | $misleadingVariables = []; 61 | 62 | for ( $i = $scopeOpener; $i < $scopeCloser; $i++ ) { 63 | if ( $tokens[$i]['code'] === T_GLOBAL ) { 64 | $endOfGlobal = $phpcsFile->findEndOfStatement( $i, T_COMMA ); 65 | } elseif ( $tokens[$i]['code'] === T_VARIABLE ) { 66 | $variableName = $tokens[$i]['content']; 67 | if ( strncmp( $variableName, '$wg', 3 ) === 0 ) { 68 | if ( $i < $endOfGlobal ) { 69 | $globalVariables[$variableName] = null; 70 | } elseif ( !array_key_exists( $variableName, $globalVariables ) && 71 | !isset( $misleadingVariables[$variableName] ) && 72 | ctype_upper( substr( $variableName, 3, 1 ) ) 73 | ) { 74 | $misleadingVariables[$variableName] = $i; 75 | } 76 | } 77 | } 78 | } 79 | 80 | foreach ( $misleadingVariables as $variableName => $stackPtr ) { 81 | $phpcsFile->addWarning( 82 | "The 'wg' prefix should only be used with the 'global' keyword", 83 | $stackPtr, 84 | 'Misleading' . $variableName 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/VariableAnalysis/UnusedGlobalVariablesSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 28 | if ( !isset( $tokens[$stackPtr]['scope_opener'] ) ) { 29 | // An interface or abstract function which doesn't have a body 30 | return; 31 | } 32 | 33 | $scopeOpener = $tokens[$stackPtr]['scope_opener'] + 1; 34 | $scopeCloser = $tokens[$stackPtr]['scope_closer']; 35 | 36 | $endOfGlobal = 0; 37 | $globalVariables = []; 38 | $matches = []; 39 | $delayedSkip = []; 40 | 41 | for ( $i = $scopeOpener; $i < $scopeCloser; $i++ ) { 42 | // Process a delayed skip 43 | if ( isset( $delayedSkip[$i] ) ) { 44 | $i = $delayedSkip[$i]; 45 | continue; 46 | } 47 | $code = $tokens[$i]['code']; 48 | if ( ( $code === T_CLOSURE || $code === T_FUNCTION || $code === T_ANON_CLASS ) 49 | && isset( $tokens[$i]['scope_closer'] ) 50 | ) { 51 | if ( $code === T_CLOSURE && isset( $tokens[$i]['parenthesis_closer'] ) ) { 52 | // Cannot skip directly to the end of closure 53 | // The use statement needs to be processed 54 | $delayedSkip[$tokens[$i]['scope_opener']] = $tokens[$i]['scope_closer']; 55 | 56 | // Skip the argument list of the closure 57 | $i = $tokens[$i]['parenthesis_closer']; 58 | } else { 59 | // Skip to the end of the inner function/anon class and continue 60 | $i = $tokens[$i]['scope_closer']; 61 | } 62 | continue; 63 | } 64 | 65 | if ( $code === T_GLOBAL ) { 66 | $endOfGlobal = $phpcsFile->findEndOfStatement( $i, T_COMMA ); 67 | } elseif ( $code === T_VARIABLE && $tokens[$i - 1]['code'] !== T_DOLLAR ) { 68 | // Note, this skips dynamic variable names. 69 | $variableName = $tokens[$i]['content']; 70 | if ( $i < $endOfGlobal ) { 71 | $globalVariables[$variableName] = $i; 72 | } else { 73 | unset( $globalVariables[$variableName] ); 74 | } 75 | } elseif ( ( $code === T_DOUBLE_QUOTED_STRING || $code === T_HEREDOC ) 76 | // Avoid the regex below when there are no globals to look for anyway 77 | && $globalVariables 78 | ) { 79 | preg_match_all( '/\${?(\w+)/', $tokens[$i]['content'], $matches ); 80 | foreach ( $matches[1] as $variableName ) { 81 | unset( $globalVariables[ '$' . $variableName ] ); 82 | } 83 | } 84 | } 85 | 86 | foreach ( $globalVariables as $variableName => $variableIndex ) { 87 | $phpcsFile->addWarning( 88 | 'Global %s is never used.', 89 | $variableIndex, 90 | 'UnusedGlobal' . $variableName, 91 | [ $variableName ] 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/EmptyLinesBetweenUseSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 35 | 36 | // In case this is a `use` of a class (or constant or function) within 37 | // a bracketed namespace rather than in the global scope, update the end 38 | // accordingly 39 | $useScopeEnd = $phpcsFile->numTokens; 40 | 41 | if ( !empty( $tokens[$stackPtr]['conditions'] ) ) { 42 | // We only care about use statements in the global scope, or the 43 | // equivalent for bracketed namespace (use statements in the namespace 44 | // and not in any class, etc.) 45 | $scope = array_key_first( $tokens[$stackPtr]['conditions'] ); 46 | if ( count( $tokens[$stackPtr]['conditions'] ) === 1 47 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive 48 | && $tokens[$stackPtr]['conditions'][$scope] === T_NAMESPACE 49 | ) { 50 | $useScopeEnd = $tokens[$scope]['scope_closer']; 51 | } else { 52 | return $tokens[$scope]['scope_closer'] ?? $stackPtr; 53 | } 54 | } 55 | 56 | // Every `use` after here, if its for imports (rather than using a trait), should 57 | // be part of this block, so for each T_USE token 58 | $priorLine = $tokens[$stackPtr]['line']; 59 | 60 | // Tokens for use statements that are not on the subsequent line, to check 61 | // for issues (not all are due to empty lines, could have comments) 62 | $toCheck = []; 63 | 64 | $next = $phpcsFile->findNext( T_USE, $stackPtr + 1, $useScopeEnd ); 65 | while ( $next !== false ) { 66 | $nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, $next + 1, $useScopeEnd, true ); 67 | if ( $tokens[$stackPtr]['level'] !== $tokens[$next]['level'] 68 | || $nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] === T_OPEN_PARENTHESIS 69 | ) { 70 | // We are past the initial `use` statements for imports or closure `use`. 71 | break; 72 | } 73 | if ( $tokens[$next]['line'] !== $priorLine + 1 ) { 74 | $toCheck[] = $next; 75 | } 76 | $priorLine = $tokens[$next]['line']; 77 | $next = $phpcsFile->findNext( T_USE, $nextNonEmpty + 1, $useScopeEnd ); 78 | } 79 | 80 | if ( !$toCheck ) { 81 | // No need to process further 82 | return $useScopeEnd; 83 | } 84 | 85 | $linesToRemove = []; 86 | $fix = false; 87 | foreach ( $toCheck as $checking ) { 88 | $prior = $checking - 1; 89 | while ( isset( $tokens[$prior - 1] ) 90 | && $tokens[$prior]['code'] === T_WHITESPACE 91 | && $tokens[$prior]['line'] !== $tokens[$prior - 1]['line'] 92 | ) { 93 | $prior--; 94 | $linesToRemove[] = $prior; 95 | } 96 | if ( $prior !== $checking - 1 ) { 97 | // We moved back, so there were empty lines 98 | // $prior is the pointer for the first line break in the series, 99 | // show the warning on the first empty line 100 | $fix = $phpcsFile->addFixableWarning( 101 | 'There should not be empty lines between use statements', 102 | $prior + 1, 103 | 'Found' 104 | ); 105 | } 106 | } 107 | 108 | if ( !$fix ) { 109 | return $useScopeEnd; 110 | } 111 | 112 | $phpcsFile->fixer->beginChangeset(); 113 | foreach ( $linesToRemove as $linePtr ) { 114 | $phpcsFile->fixer->replaceToken( $linePtr, '' ); 115 | } 116 | $phpcsFile->fixer->endChangeset(); 117 | 118 | return $useScopeEnd; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/MultipleEmptyLinesSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 33 | 34 | // This sniff intentionally doesn't care about whitespace at the end of the file 35 | if ( !isset( $tokens[$stackPtr + 3] ) || 36 | $tokens[$stackPtr + 2]['line'] === $tokens[$stackPtr + 3]['line'] 37 | ) { 38 | return $stackPtr + 3; 39 | } 40 | 41 | if ( $tokens[$stackPtr + 1]['line'] === $tokens[$stackPtr + 2]['line'] ) { 42 | return $stackPtr + 2; 43 | } 44 | 45 | // Finally, check the assumption the current token is or ends with a newline 46 | if ( $tokens[$stackPtr]['line'] === $tokens[$stackPtr + 1]['line'] ) { 47 | return; 48 | } 49 | 50 | // Search for the next non-newline token 51 | $next = $stackPtr + 1; 52 | while ( isset( $tokens[$next + 1] ) && 53 | $tokens[$next]['code'] === T_WHITESPACE && 54 | $tokens[$next]['line'] !== $tokens[$next + 1]['line'] 55 | ) { 56 | $next++; 57 | } 58 | $count = $next - $stackPtr - 1; 59 | 60 | if ( $count > 1 && 61 | $phpcsFile->addFixableError( 62 | 'Multiple empty lines should not exist in a row; found %s consecutive empty lines', 63 | $stackPtr + 1, 64 | 'MultipleEmptyLines', 65 | [ $count ] 66 | ) 67 | ) { 68 | $phpcsFile->fixer->beginChangeset(); 69 | // Remove all newlines except the first two, i.e. keep one empty line 70 | for ( $i = $stackPtr + 2; $i < $next; $i++ ) { 71 | $phpcsFile->fixer->replaceToken( $i, '' ); 72 | } 73 | $phpcsFile->fixer->endChangeset(); 74 | } 75 | 76 | // Don't check the current sequence a second time 77 | return $next; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/OpeningKeywordParenthesisSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 50 | $next = $stackPtr + 1; 51 | 52 | $openParenthesis = $phpcsFile->findNext( T_WHITESPACE, $next, null, true ); 53 | if ( $openParenthesis === false || 54 | $tokens[$openParenthesis]['code'] !== T_OPEN_PARENTHESIS 55 | ) { 56 | // no parenthesis found 57 | return; 58 | } 59 | 60 | if ( $next === $openParenthesis ) { 61 | // no whitespaces found 62 | return; 63 | } 64 | 65 | $whitespaces = $phpcsFile->getTokensAsString( $next, $openParenthesis - $next ); 66 | $fix = $phpcsFile->addFixableError( 67 | 'Expected no space before opening parenthesis; found %s', 68 | $openParenthesis, 69 | 'WrongWhitespaceBeforeParenthesis', 70 | [ strlen( $whitespaces ) ] 71 | ); 72 | if ( $fix ) { 73 | $phpcsFile->fixer->beginChangeset(); 74 | for ( $i = $next; $i < $openParenthesis; $i++ ) { 75 | $phpcsFile->fixer->replaceToken( $i, '' ); 76 | } 77 | $phpcsFile->fixer->endChangeset(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/SpaceAfterClosureSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 44 | $next = $tokens[$stackPtr + 1]; 45 | if ( $next['code'] === T_WHITESPACE ) { 46 | if ( $next['content'] === ' ' ) { 47 | return; 48 | } 49 | // It's whitespace, but not a single space. 50 | $token = $tokens[$stackPtr]; 51 | $fix = $phpcsFile->addFixableError( 52 | 'A single space should be after the %s keyword in closures', 53 | $stackPtr + 1, 54 | $token['code'] === T_CLOSURE ? 'WrongWhitespaceAfterClosure' : 'WrongWhitespaceAfterArrow', 55 | [ $token['content'] ] 56 | ); 57 | if ( $fix ) { 58 | $phpcsFile->fixer->replaceToken( $stackPtr + 1, ' ' ); 59 | } 60 | } elseif ( $next['code'] === T_OPEN_PARENTHESIS ) { 61 | $token = $tokens[$stackPtr]; 62 | $fix = $phpcsFile->addFixableError( 63 | 'A single space should be after the %s keyword in closures', 64 | $stackPtr, 65 | $token['code'] === T_CLOSURE ? 'NoWhitespaceAfterClosure' : 'NoWhitespaceAfterArrow', 66 | [ $token['content'] ] 67 | ); 68 | if ( $fix ) { 69 | $phpcsFile->fixer->addContent( $stackPtr, ' ' ); 70 | } 71 | } 72 | // else invalid syntax? 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/SpaceBeforeBracketSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 48 | 49 | // Check if the open bracket is preceded by whitespace 50 | $priorToken = $tokens[$stackPtr - 1]; 51 | if ( $priorToken['code'] !== T_WHITESPACE ) { 52 | return; 53 | } 54 | 55 | // Skip newlines 56 | $lastNonWhitespace = $phpcsFile->findPrevious( 57 | T_WHITESPACE, 58 | $stackPtr - 1, 59 | null, 60 | true 61 | ); 62 | if ( $lastNonWhitespace && 63 | $tokens[$lastNonWhitespace]['line'] !== $tokens[$stackPtr]['line'] 64 | ) { 65 | return; 66 | } 67 | 68 | $fix = $phpcsFile->addFixableWarning( 69 | 'Do not include whitespace between variable expression and array offset', 70 | $stackPtr - 1, 71 | 'SpaceFound' 72 | ); 73 | 74 | if ( $fix ) { 75 | $phpcsFile->fixer->replaceToken( $stackPtr - 1, '' ); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/SpaceBeforeClassBraceSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | if ( !isset( $tokens[$stackPtr]['scope_opener'] ) ) { 33 | return; 34 | } 35 | $structKeyword = $this->getStructureKeyword( $tokens[$stackPtr]['code'] ); 36 | $openBrace = $tokens[$stackPtr]['scope_opener']; 37 | // Find previous non-whitespace token from the opening brace 38 | $pre = $phpcsFile->findPrevious( T_WHITESPACE, $openBrace - 1, null, true ); 39 | 40 | $afterClass = $stackPtr; 41 | if ( $tokens[$stackPtr]['code'] === T_ANON_CLASS && isset( $tokens[$stackPtr]['parenthesis_closer'] ) ) { 42 | // The anon class could be multi-line, start after the class definition 43 | $afterClass = $tokens[$stackPtr]['parenthesis_closer']; 44 | } 45 | 46 | if ( $tokens[$openBrace]['line'] - $tokens[$afterClass]['line'] >= 2 ) { 47 | // If the class ... { statement is more than two lines, then 48 | // the { should be on a line by itself. 49 | if ( $tokens[$pre]['line'] === $tokens[$openBrace]['line'] ) { 50 | $fix = $phpcsFile->addFixableWarning( 51 | "Expected $structKeyword open brace to be on a new line", 52 | $openBrace, 53 | 'BraceNotOnOwnLine' 54 | ); 55 | if ( $fix ) { 56 | $phpcsFile->fixer->addNewlineBefore( $openBrace ); 57 | } 58 | } 59 | return; 60 | } 61 | $spaceCount = 0; 62 | for ( $start = $pre + 1; $start < $openBrace; $start++ ) { 63 | $content = $tokens[$start]['content']; 64 | $contentSize = strlen( $content ); 65 | $spaceCount += $contentSize; 66 | } 67 | 68 | if ( $spaceCount !== 1 ) { 69 | $fix = $phpcsFile->addFixableWarning( 70 | "Expected 1 space before $structKeyword open brace. Found %s.", 71 | $openBrace, 72 | 'NoSpaceBeforeBrace', 73 | [ $spaceCount ] 74 | ); 75 | if ( $fix ) { 76 | $phpcsFile->fixer->beginChangeset(); 77 | $phpcsFile->fixer->replaceToken( $openBrace, '' ); 78 | $phpcsFile->fixer->addContent( $pre, ' {' ); 79 | $phpcsFile->fixer->endChangeset(); 80 | } 81 | } 82 | 83 | if ( $tokens[$openBrace]['line'] !== $tokens[$pre]['line'] ) { 84 | $fix = $phpcsFile->addFixableWarning( 85 | "Expected $structKeyword open brace to be on the same line as the `$structKeyword` keyword.", 86 | $openBrace, 87 | 'BraceNotOnSameLine' 88 | ); 89 | if ( $fix ) { 90 | $phpcsFile->fixer->beginChangeset(); 91 | for ( $i = $pre + 1; $i < $openBrace; $i++ ) { 92 | $phpcsFile->fixer->replaceToken( $i, '' ); 93 | } 94 | $phpcsFile->fixer->endChangeset(); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Returns the keyword used to define the OOP structure of the given type. 101 | * 102 | * @param int|string $tokenType 103 | * @return string 104 | */ 105 | private function getStructureKeyword( $tokenType ): string { 106 | switch ( $tokenType ) { 107 | case T_CLASS: 108 | case T_ANON_CLASS: 109 | return 'class'; 110 | case T_INTERFACE: 111 | return 'interface'; 112 | case T_TRAIT: 113 | return 'trait'; 114 | case T_ENUM: 115 | return 'enum'; 116 | default: 117 | throw new UnexpectedValueException( "Token $tokenType not handled" ); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/SpaceBeforeSingleLineCommentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 30 | $currToken = $tokens[$stackPtr]; 31 | $preToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 32 | if ( $preToken !== false && 33 | $tokens[$preToken]['line'] === $tokens[$stackPtr]['line'] 34 | ) { 35 | $phpcsFile->addWarning( 36 | 'Comments should start on new line.', 37 | $stackPtr, 38 | 'NewLineComment' 39 | ); 40 | } 41 | if ( $currToken['code'] === T_COMMENT ) { 42 | // Accounting for multiple line comments, as single line comments 43 | // use only '//' and '#' 44 | // Also ignoring PHPDoc comments starting with '///', 45 | // as there are no coding standards documented for these 46 | if ( str_starts_with( $currToken['content'], '/*' ) 47 | || str_starts_with( $currToken['content'], '///' ) 48 | ) { 49 | return; 50 | } 51 | 52 | // Checking whether the comment is an empty one 53 | if ( ( str_starts_with( $currToken['content'], '//' ) && 54 | rtrim( $currToken['content'] ) === '//' ) || 55 | ( $currToken['content'][0] === '#' && 56 | rtrim( $currToken['content'] ) === '#' ) 57 | ) { 58 | return; 59 | } 60 | 61 | // If the previous token is a comment, assume extra spaces are used for indenting 62 | // and thus are OK. 63 | if ( $preToken !== false && $tokens[$preToken]['code'] === T_COMMENT ) { 64 | return; 65 | } 66 | 67 | // Checking whether there is a space between the comment delimiter 68 | // and the comment 69 | if ( str_starts_with( $currToken['content'], '//' ) ) { 70 | $commentContent = substr( $currToken['content'], 2 ); 71 | $commentTrim = ltrim( $commentContent ); 72 | if ( 73 | strlen( $commentContent ) !== ( strlen( $commentTrim ) + 1 ) || 74 | $currToken['content'][2] !== ' ' 75 | ) { 76 | $fix = $phpcsFile->addFixableWarning( 77 | 'Single space expected between "//" and comment', 78 | $stackPtr, 79 | 'SingleSpaceBeforeSingleLineComment' 80 | ); 81 | if ( $fix ) { 82 | $phpcsFile->fixer->replaceToken( $stackPtr, '// ' . $commentTrim ); 83 | } 84 | } 85 | // Finding what the comment delimiter is and checking whether there is a space 86 | // between the comment delimiter and the comment. 87 | } elseif ( $currToken['content'][0] === '#' ) { 88 | // Find number of `#` used. 89 | $startComment = 0; 90 | while ( $currToken['content'][$startComment] === '#' ) { 91 | $startComment++; 92 | } 93 | if ( $currToken['content'][$startComment] !== ' ' ) { 94 | $fix = $phpcsFile->addFixableWarning( 95 | 'Single space expected between "#" and comment', 96 | $stackPtr, 97 | 'SingleSpaceBeforeSingleLineComment' 98 | ); 99 | if ( $fix ) { 100 | $content = $currToken['content']; 101 | $newContent = '# '; 102 | $tmpContent = substr( $content, 1 ); 103 | $newContent .= ltrim( $tmpContent ); 104 | $phpcsFile->fixer->replaceToken( $stackPtr, $newContent ); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/SpaceyParenthesisSniff.php: -------------------------------------------------------------------------------- 1 | foo( $arg, $arg2 ); 5 | * wfFoo( $arg, $arg2 ); 6 | * 7 | * But, wfFoo() is ok. 8 | * 9 | * Also disallow wfFoo( ) and wfFoo( $param ) 10 | */ 11 | 12 | namespace MediaWiki\Sniffs\WhiteSpace; 13 | 14 | use PHP_CodeSniffer\Files\File; 15 | use PHP_CodeSniffer\Sniffs\Sniff; 16 | use PHP_CodeSniffer\Util\Tokens; 17 | 18 | class SpaceyParenthesisSniff implements Sniff { 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | public function register(): array { 24 | return [ 25 | T_OPEN_PARENTHESIS, 26 | T_CLOSE_PARENTHESIS, 27 | T_OPEN_SHORT_ARRAY, 28 | T_CLOSE_SHORT_ARRAY 29 | ]; 30 | } 31 | 32 | /** 33 | * @param File $phpcsFile 34 | * @param int $stackPtr The current token index. 35 | * @return void|int 36 | */ 37 | public function process( File $phpcsFile, $stackPtr ) { 38 | $tokens = $phpcsFile->getTokens(); 39 | $currentToken = $tokens[$stackPtr]; 40 | $closer = $currentToken['parenthesis_closer'] ?? $currentToken['bracket_closer'] ?? null; 41 | 42 | if ( !$closer ) { 43 | // Syntax error or live coding, bow out. 44 | return; 45 | } 46 | 47 | if ( $closer === $stackPtr ) { 48 | $this->processCloseParenthesis( $phpcsFile, $stackPtr ); 49 | return; 50 | } 51 | 52 | if ( $tokens[$stackPtr - 1]['code'] === T_WHITESPACE ) { 53 | $prevNonSpace = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 54 | $prevNonSpaceCode = $tokens[$prevNonSpace]['code']; 55 | if ( $prevNonSpaceCode === T_STRING || $prevNonSpaceCode === T_ARRAY ) { 56 | // Should always be function or array. 57 | $bracketType = $prevNonSpaceCode === T_STRING 58 | ? 'function parenthesis' 59 | : 'bracket of array'; 60 | $fix = $phpcsFile->addFixableWarning( 61 | 'Space found before opening %s', 62 | $stackPtr - 1, 63 | 'SpaceBeforeOpeningParenthesis', 64 | [ $bracketType ] 65 | ); 66 | if ( $fix ) { 67 | $phpcsFile->fixer->beginChangeset(); 68 | for ( $replace = $stackPtr - 1; $replace > $prevNonSpace; $replace-- ) { 69 | $phpcsFile->fixer->replaceToken( $replace, '' ); 70 | } 71 | $phpcsFile->fixer->endChangeset(); 72 | } 73 | } 74 | } 75 | 76 | // Shorten out as early as possible on empty parenthesis 77 | if ( $closer === $stackPtr + 1 ) { 78 | // Intentionally do not process the closing parenthesis again 79 | return $closer + 1; 80 | } 81 | 82 | // Check for space between parentheses without any arguments. Empty arrays (long and short) are not checked if 83 | // they contain a newline, as that is sometimes used to facilitate adding new elements. 84 | // TODO: Ideally, in that case we would only allow a newline and closer indentation, not any other random 85 | // whitespace. This seems tricky as we need to figure out what the correct indentation would be. 86 | if ( $currentToken['code'] === T_OPEN_SHORT_ARRAY ) { 87 | $isArray = true; 88 | } else { 89 | $prevNonEmpty = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); 90 | $isArray = $tokens[$prevNonEmpty]['code'] === T_ARRAY; 91 | } 92 | 93 | if ( $isArray ) { 94 | $hasUnnecessarySpace = $closer === $stackPtr + 2 && $tokens[$stackPtr + 1]['code'] === T_WHITESPACE; 95 | } else { 96 | $nextNonSpace = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, $closer + 1, true ); 97 | $hasUnnecessarySpace = $nextNonSpace === $closer; 98 | } 99 | if ( $hasUnnecessarySpace ) { 100 | $bracketType = $currentToken['code'] === T_OPEN_PARENTHESIS ? 'parentheses' : 'brackets'; 101 | $fix = $phpcsFile->addFixableWarning( 102 | 'Unnecessary space found within %s', 103 | $stackPtr + 1, 104 | 'UnnecessarySpaceBetweenParentheses', 105 | [ $bracketType ] 106 | ); 107 | if ( $fix ) { 108 | $phpcsFile->fixer->beginChangeset(); 109 | for ( $replace = $stackPtr + 1; $replace < $closer; $replace++ ) { 110 | $phpcsFile->fixer->replaceToken( $replace, '' ); 111 | } 112 | $phpcsFile->fixer->endChangeset(); 113 | } 114 | 115 | // Intentionally do not process the closing parenthesis again 116 | return $closer + 1; 117 | } 118 | 119 | $this->processOpenParenthesis( $phpcsFile, $stackPtr ); 120 | } 121 | 122 | /** 123 | * @param File $phpcsFile 124 | * @param int $stackPtr The current token index. 125 | */ 126 | protected function processOpenParenthesis( File $phpcsFile, int $stackPtr ): void { 127 | $tokens = $phpcsFile->getTokens(); 128 | $nextToken = $tokens[$stackPtr + 1]; 129 | // No space or not single space 130 | if ( ( $nextToken['code'] === T_WHITESPACE && 131 | $nextToken['line'] === $tokens[$stackPtr + 2]['line'] 132 | && $nextToken['content'] !== ' ' ) 133 | || $nextToken['code'] !== T_WHITESPACE 134 | ) { 135 | $fix = $phpcsFile->addFixableWarning( 136 | 'Single space expected after opening parenthesis', 137 | $stackPtr + 1, 138 | 'SingleSpaceAfterOpenParenthesis' 139 | ); 140 | if ( $fix ) { 141 | if ( $nextToken['code'] === T_WHITESPACE 142 | && $nextToken['line'] === $tokens[$stackPtr + 2]['line'] 143 | && $nextToken['content'] !== ' ' 144 | ) { 145 | $phpcsFile->fixer->replaceToken( $stackPtr + 1, ' ' ); 146 | } else { 147 | $phpcsFile->fixer->addContent( $stackPtr, ' ' ); 148 | } 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * @param File $phpcsFile 155 | * @param int $stackPtr The current token index. 156 | */ 157 | protected function processCloseParenthesis( File $phpcsFile, int $stackPtr ): void { 158 | $tokens = $phpcsFile->getTokens(); 159 | $previousToken = $tokens[$stackPtr - 1]; 160 | 161 | if ( ( $previousToken['code'] === T_WHITESPACE 162 | && $previousToken['content'] === ' ' ) 163 | || ( isset( Tokens::$commentTokens[ $previousToken['code'] ] ) 164 | && str_ends_with( $previousToken['content'], "\n" ) ) 165 | ) { 166 | // If previous token was 167 | // '(' or ' ' or a comment ending with a newline 168 | return; 169 | } 170 | 171 | // If any of the whitespace tokens immediately before this token is a newline 172 | $ptr = $stackPtr - 1; 173 | while ( $tokens[$ptr]['code'] === T_WHITESPACE ) { 174 | if ( $tokens[$ptr]['content'] === $phpcsFile->eolChar ) { 175 | return; 176 | } 177 | $ptr--; 178 | } 179 | 180 | // If the comment before all the whitespaces immediately preceding the ')' ends with a newline 181 | if ( isset( Tokens::$commentTokens[ $tokens[$ptr]['code'] ] ) 182 | && str_ends_with( $tokens[$ptr]['content'], "\n" ) 183 | ) { 184 | return; 185 | } 186 | 187 | $fix = $phpcsFile->addFixableWarning( 188 | 'Single space expected before closing parenthesis', 189 | $stackPtr, 190 | 'SingleSpaceBeforeCloseParenthesis' 191 | ); 192 | if ( $fix ) { 193 | if ( $previousToken['code'] === T_WHITESPACE ) { 194 | $phpcsFile->fixer->replaceToken( $stackPtr - 1, ' ' ); 195 | } else { 196 | $phpcsFile->fixer->addContentBefore( $stackPtr, ' ' ); 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/UnaryMinusSpacingSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 69 | 70 | // Check if the minus is followed by a space 71 | $nextToken = $tokens[$stackPtr + 1]; 72 | if ( $nextToken['code'] !== T_WHITESPACE ) { 73 | return; 74 | } 75 | 76 | // Find the last non-whitespace token 77 | $lastToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); 78 | 79 | $tokensBeforeSubtraction = array_merge( 80 | self::TOKENS_BEFORE_SUBTRACTION, 81 | array_values( Tokens::$commentTokens ) 82 | ); 83 | if ( in_array( $tokens[$lastToken]['code'], $tokensBeforeSubtraction ) ) { 84 | // Not a unary minus 85 | return; 86 | } 87 | 88 | // This is a unary minus, remove the space 89 | $fix = $phpcsFile->addFixableWarning( 90 | 'Do not use a space after a unary minus', 91 | $stackPtr + 1, 92 | 'SpaceFound' 93 | ); 94 | 95 | if ( $fix ) { 96 | $phpcsFile->fixer->replaceToken( $stackPtr + 1, '' ); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /MediaWiki/Sniffs/WhiteSpace/WhiteSpaceBeforeFunctionSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 48 | 49 | $next = $stackPtr + 1; 50 | $nextToken = $tokens[ $next ]; 51 | 52 | // Must be followed by a single space and then the function name 53 | if ( $nextToken['content'] !== ' ' || $tokens[ $next + 1 ]['code'] === T_WHITESPACE ) { 54 | $funcName = $phpcsFile->findNext( T_WHITESPACE, $next + 1, null, true ); 55 | 56 | $fix = $phpcsFile->addFixableError( 57 | 'Extra whitespace found after the `function` keyword; only a single space should be used', 58 | $stackPtr + 1, 59 | 'ExtraWhiteSpaceFound' 60 | ); 61 | 62 | if ( $fix ) { 63 | $phpcsFile->fixer->beginChangeset(); 64 | 65 | // Ensure "function" is followed by one space 66 | $phpcsFile->fixer->replaceToken( $next, ' ' ); 67 | 68 | // Remove anything else before the function name 69 | for ( $i = $next + 1; $i < $funcName; $i++ ) { 70 | $phpcsFile->fixer->replaceToken( $i, '' ); 71 | } 72 | $phpcsFile->fixer->endChangeset(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MediaWiki coding conventions 2 | ============================ 3 | 4 | Abstract 5 | -------- 6 | This project implements a set of rules for use with [PHP CodeSniffer][0]. 7 | 8 | See [MediaWiki conventions][1] on our wiki for a detailed description of the 9 | coding conventions that are validated by these rules. :-) 10 | 11 | How to install 12 | -------------- 13 | 1. Create a composer.json which adds this project as a dependency: 14 | 15 | ``` 16 | { 17 | "require-dev": { 18 | "mediawiki/mediawiki-codesniffer": "40.0.1" 19 | }, 20 | "scripts": { 21 | "test": [ 22 | "phpcs -p -s" 23 | ], 24 | "fix": "phpcbf" 25 | } 26 | } 27 | ``` 28 | 2. Create a .phpcs.xml with our configuration: 29 | 30 | ``` 31 | 32 | 33 | 34 | . 35 | 36 | 37 | 38 | 39 | ``` 40 | 3. Install: `composer update` 41 | 4. Run: `composer test` 42 | 5. Run: `composer fix` to auto-fix some of the errors, others might need 43 | manual intervention. 44 | 6. Commit! 45 | 46 | Note that for most MediaWiki projects, we'd also recommend adding a PHP linter 47 | to your `composer.json` – see the [full documentation][2] for more details. 48 | 49 | Configuration 50 | ------------- 51 | Some of the sniffs provided by this codesniffer standard allow you to configure details of how they work. 52 | 53 | * `MediaWiki.Classes.FullQualifiedClassName`: This sniff is disabled by default. 54 | 55 | ``` 56 | 57 | 5 58 | 59 | 60 | 61 | 62 | 63 | 64 | ``` 65 | 66 | * `MediaWiki.Usage.ExtendClassUsage`: This sniff lets you exclude globals from being reported by the sniff, in case they 67 | cannot be replaced with a Config::getConfig() call. Examples that are already in the list include `$wgTitle` and 68 | `$wgUser`. 69 | 70 | ``` 71 | 72 | 73 | 74 | 75 | 76 | ``` 77 | 78 | * `MediaWiki.Commenting.ClassLevelLicense`: This sniff does nothing by default. 79 | 80 | ``` 81 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | * `MediaWiki.NamingConventions.PrefixedGlobalFunctions`: This sniff lets you define a list of ignored global 89 | functions and a list of allowed prefixes. By default the only allowed prefix is 'wf', corresponding 90 | to the global function `wf...()`. 91 | 92 | ``` 93 | 94 | 95 | 96 | 97 | 98 | 99 | ``` 100 | 101 | * `MediaWiki.NamingConventions.ValidGlobalName`: This sniff lets you define a list of ignored globals and a list of allowed 102 | prefixes. By default the only allowed prefix is 'wg', for global variables `$wg...`. 103 | 104 | ``` 105 | 106 | 107 | 108 | 109 | 110 | 111 | ``` 112 | 113 | * `MediaWiki.Arrays.TrailingComma`: This sniff lets you enforce that multi-line arrays have trailing commas, 114 | which makes Git diffs nicer. 115 | It can also enforce that single-line arrays have no trailing comma. 116 | By default, it does nothing. 117 | 118 | ``` 119 | 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | --- 128 | [0]: https://packagist.org/packages/squizlabs/php_codesniffer 129 | [1]: https://www.mediawiki.org/wiki/Manual:Coding_conventions/PHP 130 | [2]: https://www.mediawiki.org/wiki/Continuous_integration/Entry_points#PHP 131 | -------------------------------------------------------------------------------- /utils/bootstrap-ci.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This program is free software; you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation; either version 2 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License along 19 | * with this program; if not, write to the Free Software Foundation, Inc., 20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 21 | * http://www.gnu.org/copyleft/gpl.html 22 | * 23 | * @file 24 | * @author Antoine Musso 25 | */ 26 | 27 | // phpcs:disable MediaWiki.Usage.ForbiddenFunctions.exec 28 | 29 | # Only filter when running from cli and using Jenkins 30 | if ( !( PHP_SAPI === 'cli' && getenv( 'JENKINS_URL' ) !== false ) ) { 31 | return; 32 | } 33 | 34 | # From integration/jenkins.git bin/git-changed-in-head 35 | $_head_files = []; 36 | exec( 37 | 'git show HEAD --name-only --diff-filter=ACMR -m --first-parent --format=format:', 38 | $_head_files, $_return 39 | ); 40 | if ( $_return !== 0 ) { 41 | echo "'git show HEAD' returned exit code $_return - analyzing all files\n"; 42 | unset( $_head_files ); 43 | unset( $_return ); 44 | return; 45 | } 46 | 47 | # Changes to phpcs.xml or .phpcs.xml affect all files 48 | if ( in_array( 'phpcs.xml', $_head_files ) || in_array( '.phpcs.xml', $_head_files ) ) { 49 | echo "phpcs.xml and/or .phpcs.xml changed - analyzing all files\n"; 50 | unset( $_head_files ); 51 | unset( $_return ); 52 | return; 53 | } 54 | # composer.json might affect mediawiki/mediawiki-codesniffer version 55 | if ( in_array( 'composer.json', $_head_files ) ) { 56 | exec( 'git show HEAD^:composer.json', $_prev_composer, $_return ); 57 | if ( $_return !== 0 ) { 58 | echo "'git show HEAD^:composer.json' returned exit code $_return - analyzing all files\n"; 59 | unset( $_head_files ); 60 | unset( $_return ); 61 | return; 62 | } 63 | exec( 'git show HEAD:composer.json', $_cur_composer, $_return ); 64 | if ( $_return !== 0 ) { 65 | echo "'git show HEAD:composer.json' returned exit code $_return - analyzing all files\n"; 66 | unset( $_head_files ); 67 | unset( $_return ); 68 | return; 69 | } 70 | $_prev_composer = json_decode( implode( '', $_prev_composer ), true ); 71 | $_cur_composer = json_decode( implode( '', $_cur_composer ), true ); 72 | if ( $_prev_composer['require-dev']['mediawiki/mediawiki-codesniffer'] 73 | !== $_cur_composer['require-dev']['mediawiki/mediawiki-codesniffer'] 74 | ) { 75 | echo "'mediawiki/mediawiki-codesniffer' version changed - analyzing all files\n"; 76 | unset( $_head_files ); 77 | unset( $_return ); 78 | return; 79 | } 80 | } 81 | 82 | # Only keep files out of git head that matches phpcs.xml/.phpcs.xml extensions. 83 | echo "Only analyzing files changed in HEAD\n"; 84 | $_extensions = array_keys( $this->config->extensions ); 85 | $this->config->files = array_filter( 86 | $_head_files, 87 | static function ( $file ) use ( $_extensions ) { 88 | $pinfo = pathinfo( $file ); 89 | return in_array( strtolower( $pinfo['extension'] ), $_extensions ); 90 | } 91 | ); 92 | unset( $_extensions ); 93 | unset( $_head_files ); 94 | unset( $_return ); 95 | if ( empty( $this->config->files ) ) { 96 | echo "No files to process. Skipping run\n"; 97 | exit( 0 ); 98 | } 99 | 100 | // phpcs:enable 101 | --------------------------------------------------------------------------------