Completed
Push — master ( aa59dc...71690e )
by Wim
10s
created

NewFlexibleHeredocNowdocSniff   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 2
dl 0
loc 216
rs 9.68
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A register() 0 14 2
A process() 0 16 5
C detectIndentedNonStandAloneClosingMarker() 0 76 11
C detectClosingMarkerInBody() 0 68 16
1
<?php
2
/**
3
 * \PHPCompatibility\Sniffs\Syntax\NewFlexibleHeredocNowdocSniff.
4
 *
5
 * PHP version 7.3
6
 *
7
 * @category PHP
8
 * @package  PHPCompatibility
9
 * @author   Juliette Reinders Folmer <[email protected]>
10
 */
11
12
namespace PHPCompatibility\Sniffs\Syntax;
13
14
use PHPCompatibility\Sniff;
15
use PHPCompatibility\PHPCSHelper;
16
17
/**
18
 * New Flexible Heredoc Nowdoc.
19
 *
20
 * As of PHP 7.3:
21
 * - The body and the closing marker of a Heredoc/nowdoc can be indented;
22
 * - The closing marker no longer needs to be on a line by itself;
23
 * - The heredoc/nowdoc body may no longer contain the closing marker at the
24
 *   start of any of its lines.
25
 *
26
 * PHP version 7.3
27
 *
28
 * @category PHP
29
 * @package  PHPCompatibility
30
 * @author   Juliette Reinders Folmer <[email protected]>
31
 */
32
class NewFlexibleHeredocNowdocSniff extends Sniff
33
{
34
35
    /**
36
     * Returns an array of tokens this test wants to listen for.
37
     *
38
     * @return array
39
     */
40
    public function register()
41
    {
42
        $targets = array(
43
            T_END_HEREDOC,
44
            T_END_NOWDOC,
45
        );
46
47
        if (version_compare(PHP_VERSION_ID, '70299', '>') === false) {
48
            // Start identifier of a PHP 7.3 flexible heredoc/nowdoc.
49
            $targets[] = T_STRING;
50
        }
51
52
        return $targets;
53
    }
54
55
56
    /**
57
     * Processes this test, when one of its tokens is encountered.
58
     *
59
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
60
     * @param int                   $stackPtr  The position of the current token in the
61
     *                                         stack passed in $tokens.
62
     *
63
     * @return void
64
     */
65
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
66
    {
67
        /*
68
         * Due to a tokenizer bug which gets hit when the PHP 7.3 heredoc/nowdoc syntax
69
         * is used, this part of the sniff cannot possibly work on PHPCS < 2.6.0.
70
         * See upstream issue #928.
71
         */
72
        if ($this->supportsBelow('7.2') === true && version_compare(PHPCSHelper::getVersion(), '2.6.0', '>=')) {
73
            $this->detectIndentedNonStandAloneClosingMarker($phpcsFile, $stackPtr);
74
        }
75
76
        $tokens = $phpcsFile->getTokens();
77
        if ($this->supportsAbove('7.3') === true && $tokens[$stackPtr]['code'] !== T_STRING) {
78
            $this->detectClosingMarkerInBody($phpcsFile, $stackPtr);
79
        }
80
    }
81
82
83
    /**
84
     * Detect indented and/or non-stand alone closing markers.
85
     *
86
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
87
     * @param int                   $stackPtr  The position of the current token in the
88
     *                                         stack passed in $tokens.
89
     *
90
     * @return void
91
     */
92
    protected function detectIndentedNonStandAloneClosingMarker(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
93
    {
94
        $tokens            = $phpcsFile->getTokens();
95
        $indentError       = 'Heredoc/nowdoc with an indented closing marker is not supported in PHP 7.2 or earlier.';
96
        $indentErrorCode   = 'IndentedClosingMarker';
97
        $trailingError     = 'Having code - other than a semi-colon or new line - after the closing marker of a heredoc/nowdoc is not supported in PHP 7.2 or earlier.';
98
        $trailingErrorCode = 'ClosingMarkerNoNewLine';
99
100
        if (version_compare(PHP_VERSION_ID, '70299', '>') === true) {
101
102
            /*
103
             * Check for indented closing marker.
104
             */
105
            if (ltrim($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) {
106
                $phpcsFile->addError($indentError, $stackPtr, $indentErrorCode);
107
            }
108
109
            /*
110
             * Check for tokens after the closing marker.
111
             */
112
            $nextNonWhitespace = $phpcsFile->findNext(array(T_WHITESPACE, T_SEMICOLON), ($stackPtr + 1), null, true);
113
            if ($tokens[$stackPtr]['line'] === $tokens[$nextNonWhitespace]['line']) {
114
                $phpcsFile->addError($trailingError, $stackPtr, $trailingErrorCode);
115
            }
116
        } else {
117
            // For PHP < 7.3, we're only interested in T_STRING tokens.
118
            if ($tokens[$stackPtr]['code'] !== T_STRING) {
119
                return;
120
            }
121
122
            if (preg_match('`^<<<([\'"]?)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\1[\r\n]+`', $tokens[$stackPtr]['content'], $matches) !== 1) {
123
                // Not the start of a PHP 7.3 flexible heredoc/nowdoc.
124
                return;
125
            }
126
127
            $identifier = $matches[2];
128
129
            for ($i = ($stackPtr + 1); $i <= $phpcsFile->numTokens; $i++) {
130
                if ($tokens[$i]['code'] !== T_ENCAPSED_AND_WHITESPACE) {
131
                    continue;
132
                }
133
134
                $trimmed = ltrim($tokens[$i]['content']);
135
136
                if (strpos($trimmed, $identifier) !== 0) {
137
                    continue;
138
                }
139
140
                // OK, we've found the PHP 7.3 flexible heredoc/nowdoc closing marker.
141
142
                /*
143
                 * Check for indented closing marker.
144
                 */
145
                if ($trimmed !== $tokens[$i]['content']) {
146
                    // Indent found before closing marker.
147
                    $phpcsFile->addError($indentError, $i, $indentErrorCode);
148
                }
149
150
                /*
151
                 * Check for tokens after the closing marker.
152
                 */
153
                // Remove the identifier.
154
                $afterMarker = substr($trimmed, strlen($identifier));
155
                // Remove a potential semi-colon at the beginning of what's left of the string.
156
                $afterMarker = ltrim($afterMarker, ';');
157
                // Remove new line characters at the end of the string.
158
                $afterMarker = rtrim($afterMarker, "\r\n");
159
160
                if ($afterMarker !== '') {
161
                    $phpcsFile->addError($trailingError, $i, $trailingErrorCode);
162
                }
163
164
                break;
165
            }
166
        }
167
    }
168
169
170
    /**
171
     * Detect heredoc/nowdoc identifiers at the start of lines in the heredoc/nowdoc body.
172
     *
173
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
174
     * @param int                   $stackPtr  The position of the current token in the
175
     *                                         stack passed in $tokens.
176
     *
177
     * @return void
178
     */
179
    protected function detectClosingMarkerInBody(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
180
    {
181
        $tokens    = $phpcsFile->getTokens();
182
        $error     = 'The body of a heredoc/nowdoc can not contain the heredoc/nowdoc closing marker as text at the start of a line since PHP 7.3.';
183
        $errorCode = 'ClosingMarkerNoNewLine';
184
185
        if (version_compare(PHP_VERSION_ID, '70299', '>') === true) {
186
            $nextNonWhitespace = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true, null, true);
187
            if ($nextNonWhitespace === false
188
                || $tokens[$nextNonWhitespace]['code'] === T_SEMICOLON
189
                || (($tokens[$nextNonWhitespace]['code'] === T_COMMA
190
                    || $tokens[$nextNonWhitespace]['code'] === T_STRING_CONCAT)
191
                    && $tokens[$nextNonWhitespace]['line'] !== $tokens[$stackPtr]['line'])
192
            ) {
193
                // This is most likely a correctly identified closing marker.
194
                return;
195
            }
196
197
            // The real closing tag has to be before the next heredoc/nowdoc.
198
            $nextHereNowDoc = $phpcsFile->findNext(array(T_START_HEREDOC, T_START_NOWDOC), ($stackPtr + 1));
199
            if ($nextHereNowDoc === false) {
200
                $nextHereNowDoc = null;
201
            }
202
203
            $identifier        = trim($tokens[$stackPtr]['content']);
204
            $realClosingMarker = $stackPtr;
205
206
            while (($realClosingMarker = $phpcsFile->findNext(T_STRING, ($realClosingMarker + 1), $nextHereNowDoc, false, $identifier)) !== false) {
207
208
                $prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($realClosingMarker - 1), null, true);
209
                if ($prevNonWhitespace === false
210
                    || $tokens[$prevNonWhitespace]['line'] === $tokens[$realClosingMarker]['line']
211
                ) {
212
                    // Marker text found, but not at the start of the line.
213
                    continue;
214
                }
215
216
                // The original T_END_HEREDOC/T_END_NOWDOC was most likely incorrect as we've found
217
                // a possible alternative closing marker.
218
                $phpcsFile->addError($error, $stackPtr, $errorCode);
219
220
                break;
221
            }
222
223
        } else {
224
            if (isset($tokens[$stackPtr]['scope_closer'], $tokens[$stackPtr]['scope_opener']) === true
225
                && $tokens[$stackPtr]['scope_closer'] === $stackPtr
226
            ) {
227
                $opener = $tokens[$stackPtr]['scope_opener'];
228
            } else {
229
                // PHPCS < 3.0.2 did not add scope_* values for Nowdocs.
230
                $opener = $phpcsFile->findPrevious(T_START_NOWDOC, ($stackPtr - 1));
231
                if ($opener === false) {
232
                    return;
233
                }
234
            }
235
236
            $identifier       = $tokens[$stackPtr]['content'];
0 ignored issues
show
Unused Code introduced by
$identifier is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
237
            $quotedIdentifier = preg_quote($tokens[$stackPtr]['content'], '`');
238
239
            // Throw an error for each line in the body which starts with the identifier.
240
            for ($i = ($opener + 1); $i < $stackPtr; $i++) {
241
                if (preg_match('`^[ \t]*' . $quotedIdentifier . '\b`', $tokens[$i]['content']) === 1) {
242
                    $phpcsFile->addError($error, $i, $errorCode);
243
                }
244
            }
245
        }
246
    }
247
}
248