Completed
Push — master ( ea568b...27f42e )
by Wim
06:01
created

NewInterfacesSniff::process()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 31
Code Lines 19

Duplication

Lines 31
Ratio 100 %

Importance

Changes 0
Metric Value
dl 31
loc 31
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 19
nc 8
nop 2
1
<?php
2
/**
3
 * \PHPCompatibility\Sniffs\PHP\NewInterfacesSniff.
4
 *
5
 * @category PHP
6
 * @package  PHPCompatibility
7
 * @author   Juliette Reinders Folmer <[email protected]>
8
 */
9
10
namespace PHPCompatibility\Sniffs\PHP;
11
12
use PHPCompatibility\AbstractNewFeatureSniff;
13
14
/**
15
 * \PHPCompatibility\Sniffs\PHP\NewInterfacesSniff.
16
 *
17
 * @category PHP
18
 * @package  PHPCompatibility
19
 * @author   Juliette Reinders Folmer <[email protected]>
20
 */
21
class NewInterfacesSniff extends AbstractNewFeatureSniff
22
{
23
24
    /**
25
     * A list of new interfaces, not present in older versions.
26
     *
27
     * The array lists : version number with false (not present) or true (present).
28
     * If's sufficient to list the first version where the interface appears.
29
     *
30
     * @var array(string => array(string => int|string|null))
31
     */
32
    protected $newInterfaces = array(
33
        'Traversable' => array(
34
            '4.4' => false,
35
            '5.0' => true,
36
        ),
37
        'Reflector' => array(
38
            '4.4' => false,
39
            '5.0' => true,
40
        ),
41
42
        'Countable' => array(
43
            '5.0' => false,
44
            '5.1' => true,
45
        ),
46
        'OuterIterator' => array(
47
            '5.0' => false,
48
            '5.1' => true,
49
        ),
50
        'RecursiveIterator' => array(
51
            '5.0' => false,
52
            '5.1' => true,
53
        ),
54
        'SeekableIterator' => array(
55
            '5.0' => false,
56
            '5.1' => true,
57
        ),
58
        'Serializable' => array(
59
            '5.0' => false,
60
            '5.1' => true,
61
        ),
62
        'SplObserver' => array(
63
            '5.0' => false,
64
            '5.1' => true,
65
        ),
66
        'SplSubject' => array(
67
            '5.0' => false,
68
            '5.1' => true,
69
        ),
70
71
        'JsonSerializable' => array(
72
            '5.3' => false,
73
            '5.4' => true,
74
        ),
75
        'SessionHandlerInterface' => array(
76
            '5.3' => false,
77
            '5.4' => true,
78
        ),
79
80
        'DateTimeInterface' => array(
81
            '5.4' => false,
82
            '5.5' => true,
83
        ),
84
85
        'Throwable' => array(
86
            '5.6' => false,
87
            '7.0' => true,
88
        ),
89
90
    );
91
92
    /**
93
     * A list of methods which cannot be used in combination with particular interfaces.
94
     *
95
     * @var array(string => array(string => string))
96
     */
97
    protected $unsupportedMethods = array(
98
        'Serializable' => array(
99
            '__sleep'  => 'http://php.net/serializable',
100
            '__wakeup' => 'http://php.net/serializable',
101
        ),
102
    );
103
104
    /**
105
     * Returns an array of tokens this test wants to listen for.
106
     *
107
     * @return array
108
     */
109
    public function register()
110
    {
111
        // Handle case-insensitivity of interface names.
112
        $this->newInterfaces      = $this->arrayKeysToLowercase($this->newInterfaces);
113
        $this->unsupportedMethods = $this->arrayKeysToLowercase($this->unsupportedMethods);
114
115
        $targets = array(
116
            T_CLASS,
117
            T_FUNCTION,
118
            T_CLOSURE,
119
        );
120
121
        if (defined('T_ANON_CLASS')) {
122
            $targets[] = constant('T_ANON_CLASS');
123
        }
124
125
        if (defined('T_RETURN_TYPE')) {
126
            $targets[] = constant('T_RETURN_TYPE');
127
        }
128
129
        return $targets;
130
131
    }//end register()
132
133
134
    /**
135
     * Processes this test, when one of its tokens is encountered.
136
     *
137
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
138
     * @param int                   $stackPtr  The position of the current token in
139
     *                                         the stack passed in $tokens.
140
     *
141
     * @return void
142
     */
143 View Code Duplication
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
144
    {
145
        $tokens = $phpcsFile->getTokens();
146
147
        switch ($tokens[$stackPtr]['type']) {
148
            case 'T_CLASS':
149
            case 'T_ANON_CLASS':
150
                $this->processClassToken($phpcsFile, $stackPtr);
151
                break;
152
153
            case 'T_FUNCTION':
154
            case 'T_CLOSURE':
155
                $this->processFunctionToken($phpcsFile, $stackPtr);
156
157
                // Deal with older PHPCS versions which don't recognize return type hints.
158
                $returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
159
                if ($returnTypeHint !== false) {
160
                    $this->processReturnTypeToken($phpcsFile, $returnTypeHint);
161
                }
162
                break;
163
164
            case 'T_RETURN_TYPE':
165
                $this->processReturnTypeToken($phpcsFile, $stackPtr);
166
                break;
167
168
            default:
169
                // Deliberately left empty.
170
                break;
171
        }
172
173
    }//end process()
174
175
176
    /**
177
     * Processes this test for when a class token is encountered.
178
     *
179
     * - Detect classes implementing the new interfaces.
180
     * - Detect classes implementing the new interfaces with unsupported functions.
181
     *
182
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
183
     * @param int                   $stackPtr  The position of the current token in
184
     *                                         the stack passed in $tokens.
185
     *
186
     * @return void
187
     */
188
    private function processClassToken(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
189
    {
190
        $interfaces = $this->findImplementedInterfaceNames($phpcsFile, $stackPtr);
191
192
        if (is_array($interfaces) === false || $interfaces === array()) {
193
            return;
194
        }
195
196
        $tokens       = $phpcsFile->getTokens();
197
        $checkMethods = false;
198
199
        if (isset($tokens[$stackPtr]['scope_closer'])) {
200
            $checkMethods = true;
201
            $scopeCloser  = $tokens[$stackPtr]['scope_closer'];
202
        }
203
204
        foreach ($interfaces as $interface) {
205
            $interfaceLc = strtolower($interface);
206
207
            if (isset($this->newInterfaces[$interfaceLc]) === true) {
208
                $itemInfo = array(
209
                    'name'   => $interface,
210
                    'nameLc' => $interfaceLc,
211
                );
212
                $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
213
            }
214
215
            if ($checkMethods === true && isset($this->unsupportedMethods[$interfaceLc]) === true) {
216
                $nextFunc = $stackPtr;
217
                while (($nextFunc = $phpcsFile->findNext(T_FUNCTION, ($nextFunc + 1), $scopeCloser)) !== false) {
0 ignored issues
show
Bug introduced by
The variable $scopeCloser does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
218
                    $funcName   = $phpcsFile->getDeclarationName($nextFunc);
219
                    $funcNameLc = strtolower($funcName);
220
                    if ($funcNameLc === '') {
221
                        continue;
222
                    }
223
224
                    if (isset($this->unsupportedMethods[$interfaceLc][$funcNameLc]) === true) {
225
                        $error     = 'Classes that implement interface %s do not support the method %s(). See %s';
226
                        $errorCode = $this->stringToErrorCode($interface).'UnsupportedMethod';
227
                        $data      = array(
228
                            $interface,
229
                            $funcName,
230
                            $this->unsupportedMethods[$interfaceLc][$funcNameLc],
231
                        );
232
233
                        $phpcsFile->addError($error, $nextFunc, $errorCode, $data);
234
                    }
235
                }
236
            }
237
        }
238
    }//end processClassToken()
239
240
241
    /**
242
     * Processes this test for when a function token is encountered.
243
     *
244
     * - Detect new interfaces when used as a type hint.
245
     *
246
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
247
     * @param int                   $stackPtr  The position of the current token in
248
     *                                         the stack passed in $tokens.
249
     *
250
     * @return void
251
     */
252 View Code Duplication
    private function processFunctionToken(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
253
    {
254
        $typeHints = $this->getTypeHintsFromFunctionDeclaration($phpcsFile, $stackPtr);
255
        if (empty($typeHints) || is_array($typeHints) === false) {
256
            return;
257
        }
258
259
        foreach ($typeHints as $hint) {
260
261
            $typeHintLc = strtolower($hint);
262
263
            if (isset($this->newInterfaces[$typeHintLc]) === true) {
264
                $itemInfo = array(
265
                    'name'   => $hint,
266
                    'nameLc' => $typeHintLc,
267
                );
268
                $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
269
            }
270
        }
271
    }
272
273
274
    /**
275
     * Processes this test for when a return type token is encountered.
276
     *
277
     * - Detect new interfaces when used as a return type declaration.
278
     *
279
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
280
     * @param int                   $stackPtr  The position of the current token in
281
     *                                         the stack passed in $tokens.
282
     *
283
     * @return void
284
     */
285 View Code Duplication
    private function processReturnTypeToken(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
    {
287
        $returnTypeHint   = $this->getReturnTypeHintName($phpcsFile, $stackPtr);
288
        $returnTypeHint   = ltrim($returnTypeHint, '\\');
289
        $returnTypeHintLc = strtolower($returnTypeHint);
290
291
        if (isset($this->newInterfaces[$returnTypeHintLc]) === false) {
292
            return;
293
        }
294
295
        // Still here ? Then this is a return type declaration using a new interface.
296
        $itemInfo = array(
297
            'name'   => $returnTypeHint,
298
            'nameLc' => $returnTypeHintLc,
299
        );
300
        $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
301
    }
302
303
304
    /**
305
     * Get the relevant sub-array for a specific item from a multi-dimensional array.
306
     *
307
     * @param array $itemInfo Base information about the item.
308
     *
309
     * @return array Version and other information about the item.
310
     */
311
    public function getItemArray(array $itemInfo)
312
    {
313
        return $this->newInterfaces[$itemInfo['nameLc']];
314
    }
315
316
317
    /**
318
     * Get the error message template for this sniff.
319
     *
320
     * @return string
321
     */
322
    protected function getErrorMsgTemplate()
323
    {
324
        return 'The built-in interface '.parent::getErrorMsgTemplate();
325
    }
326
327
328
}//end class
329