Completed
Pull Request — master (#390)
by Juliette
02:38
created

NewNowdocQuotedHeredocSniff::register()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 8
nop 0
1
<?php
2
/**
3
 * PHPCompatibility_Sniffs_PHP_NewNowdocQuotedHeredocSniff.
4
 *
5
 * PHP version 5.3
6
 *
7
 * @category  PHP
8
 * @package   PHPCompatibility
9
 * @author    Juliette Reinders Folmer <[email protected]>
10
 */
11
12
/**
13
 * PHPCompatibility_Sniffs_PHP_NewNowdocQuotedHeredocSniff.
14
 *
15
 * PHP 5.3 introduces Nowdoc syntax and (double) quoted identifiers for heredocs.
16
 *
17
 * @category  PHP
18
 * @package   PHPCompatibility
19
 * @author    Juliette Reinders Folmer <[email protected]>
20
 */
21
class PHPCompatibility_Sniffs_PHP_NewNowdocQuotedHeredocSniff extends PHPCompatibility_Sniff
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
22
{
23
24
    /**
25
     * Returns an array of tokens this test wants to listen for.
26
     *
27
     * @return array
28
     */
29
    public function register()
30
    {
31
        $targets = array();
32
33
        if (version_compare(PHP_VERSION, '5.3', '<') === true) {
34
            $targets[] = T_SL;
35
        }
36
37
        if (defined('T_START_NOWDOC')) {
38
            $targets[] = constant('T_START_NOWDOC');
39
        }
40
        if (defined('T_END_NOWDOC')) {
41
            $targets[] = constant('T_END_NOWDOC');
42
        }
43
44
        $targets[] = T_START_HEREDOC;
45
46
        return $targets;
47
48
    }//end register()
49
50
51
    /**
52
     * Processes this test, when one of its tokens is encountered.
53
     *
54
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
55
     * @param int                  $stackPtr  The position of the current token in
56
     *                                        the stack passed in $tokens.
57
     *
58
     * @return int|void On older PHP versions passes a pointer to the nowdoc/heredoc closer
59
     *                  to skip passed anything in between in regards to processing
60
     *                  the file for this sniff.
61
     */
62
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
63
    {
64
        if ($this->supportsBelow('5.2') === false) {
65
            return;
66
        }
67
68
        $tokens    = $phpcsFile->getTokens();
69
        $isNowdoc  = false;
70
        $isHeredoc = false;
71
72
        switch ($tokens[$stackPtr]['type']) {
73
            case 'T_START_NOWDOC':
74
            case 'T_END_NOWDOC':
75
                $isNowdoc = true;
76
                break;
77
78
            case 'T_START_HEREDOC':
79
                /*
80
                 * If we have a heredoc opener, make sure we only report on double quoted identifiers.
81
                 * A double quoted identifier will have the opening quote on position 3 in the string:
82
                 * `<<<"ID"`
83
                 */
84
                if ($tokens[$stackPtr]['content'][3] !== '"') {
85
                    return;
86
                }
87
88
                $isHeredoc = true;
89
                break;
90
        }
91
92
        /*
93
         * In PHP 5.2 the T_NOWDOC and the quoted T_HEREDOC tokens aren't recognized yet
94
         * and PHPCS does not backfill for it, so we have to sniff for a specific
95
         * combination of tokens.
96
         */
97
        if ($tokens[$stackPtr]['code'] === T_SL) {
98
            /*
99
             * Check for the right token combination.
100
             */
101 View Code Duplication
            if (isset($tokens[($stackPtr + 1)]) === false || $tokens[($stackPtr + 1)]['code'] !== T_LESS_THAN) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across 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...
102
                return;
103
            }
104
105 View Code Duplication
            if (isset($tokens[($stackPtr + 2)]) === false
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across 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...
106
                || $tokens[($stackPtr + 2)]['code'] !== T_CONSTANT_ENCAPSED_STRING) {
107
                return;
108
            }
109
110
            /*
111
             * Heredoc and nowdoc naming rules:
112
             * "it must contain only alphanumeric characters and underscores and
113
             *  must start with a non-digit character or underscore"
114
             * @link http://php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc
115
             */
116
            if (preg_match('`^(["\'])([a-z][a-z0-9_]*)\1$`iD', $tokens[($stackPtr + 2)]['content'], $matches) < 1) {
117
                return;
118
            }
119
120
            // Remember whether we found a nowdoc or a heredoc.
121
            switch ($matches[1]) {
122
                case "'":
123
                    $isNowdoc = true;
124
                    break;
125
126
                case '"':
127
                    $isHeredoc = true;
128
                    break;
129
130
            }
131
132
133
            /*
134
             * See if we can find the nowdoc/heredoc closer.
135
             */
136
            $closer = null;
137
138
            for ($i = ($stackPtr + 3); $i < $phpcsFile->numTokens; $i++) {
139
                $maybeCloser = $phpcsFile->findNext(T_STRING, $i, null, false, $matches[2]);
140
                if ($maybeCloser === false) {
141
                    return;
142
                }
143
144
                // The closing identifier must begin in the first column of the line.
145
                if ($tokens[$maybeCloser]['column'] !== 1) {
146
                    continue;
147
                }
148
149
                // The closing identifier must be the only content on that line, except for maybe a semi-colon.
150
                $next = $phpcsFile->findNext(array(T_WHITESPACE, T_SEMICOLON), ($maybeCloser + 1), null, true);
151
                if ($tokens[$maybeCloser]['line'] === $tokens[$next]['line']) {
152
                    continue;
153
                }
154
155
                $closer = $maybeCloser;
156
                break;
157
            }
158
159
            if (isset($closer) === false) {
160
                // No valid closer found.
161
                return;
162
            }
163
        }
164
165
        if ($isNowdoc === true) {
166
            $error = 'Nowdocs are not present in PHP version 5.2 or earlier.';
167
            $phpcsFile->addError($error, $stackPtr, 'Found');
168
169
            if (isset($closer) !== false) {
170
                // Also throw an error for the closer on older PHP versions and skip forward past it.
171
                // Not needed for newer PHP versions as those will trigger this sniff for the closer token itself.
172
                $phpcsFile->addError($error, $closer, 'Found');
0 ignored issues
show
Bug introduced by
The variable $closer 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...
173
                return ($closer + 1);
174
            }
175
        }
176
177
        if ($isHeredoc === true) {
178
            // Only throw an error for the opener.
179
            $phpcsFile->addError('The Heredoc identifier may not be enclosed in (double) quotes in PHP version 5.2 or earlier.', $stackPtr, 'Found');
180
181
            if (isset($closer) !== false) {
182
                return ($closer + 1);
183
            }
184
        }
185
186
    }//end process()
187
188
}//end class
189