Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrefixedGlobalFunctionsSniff
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 3
306
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tokenIsNamespaced
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 process
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * Verify that global functions start with a valid prefix
4 *
5 * For MediaWiki code, the valid prefixes are `wf` (or `ef` in some legacy
6 * extension code, per https://www.mediawiki.org/wiki/Manual:Coding_conventions/PHP#Naming),
7 * by default the sniff only allows `wf`, but repositories
8 * can configure this via the `allowedPrefixes` property.
9 */
10
11namespace MediaWiki\Sniffs\NamingConventions;
12
13use PHP_CodeSniffer\Files\File;
14use PHP_CodeSniffer\Sniffs\Sniff;
15
16class PrefixedGlobalFunctionsSniff implements Sniff {
17
18    /** @var string[] */
19    public array $ignoreList = [];
20
21    /**
22     * A list of global function prefixes allowed.
23     *
24     * @var string[]
25     */
26    public array $allowedPrefixes = [ 'wf' ];
27
28    /**
29     * @inheritDoc
30     */
31    public function register(): array {
32        return [ T_FUNCTION ];
33    }
34
35    /**
36     * @var int[] array containing the first locations of namespaces in files that we have seen so far.
37     */
38    private array $firstNamespaceLocations = [];
39
40    /**
41     * @param File $phpcsFile
42     * @param int $ptr The current token index.
43     *
44     * @return bool Does a namespace statement exist before this position in the file?
45     */
46    private function tokenIsNamespaced( File $phpcsFile, int $ptr ): bool {
47        $fileName = $phpcsFile->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}