• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 14516416464

17 Apr 2025 01:09PM UTC coverage: 77.945% (+0.3%) from 77.666%
14516416464

push

github

web-flow
Merge pull request #1010 from PHPCSStandards/phpcs-4.0/feature/sq-1612-stdout-vs-stderr

(Nearly) All status, debug, and progress output is now sent to STDERR instead of STDOUT

63 of 457 new or added lines in 18 files covered. (13.79%)

1 existing line in 1 file now uncovered.

19455 of 24960 relevant lines covered (77.94%)

78.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.15
/src/Tokenizers/Comment.php
1
<?php
2
/**
3
 * Tokenizes doc block comments.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Tokenizers;
11

12
use PHP_CodeSniffer\Util\Common;
13
use PHP_CodeSniffer\Util\Writers\StatusWriter;
14

15
class Comment
16
{
17

18

19
    /**
20
     * Splits a single doc block comment token up into something that can be easily iterated over.
21
     *
22
     * @param string $string   The doc block comment string to parse.
23
     * @param string $eolChar  The EOL character to use for splitting strings.
24
     * @param int    $stackPtr The position of the token in the "new"/final token stream.
25
     *
26
     * @return array<int, array<string, string|int|array<int>>>
27
     */
28
    public function tokenizeString($string, $eolChar, $stackPtr)
201✔
29
    {
30
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
NEW
31
            StatusWriter::write('*** START COMMENT TOKENIZING ***', 2);
×
32
        }
33

34
        $tokens   = [];
201✔
35
        $numChars = strlen($string);
201✔
36

37
        /*
38
            Doc block comments start with /*, but typically contain an
39
            extra star when they are used for function and class comments.
40
        */
41

42
        $char      = ($numChars - strlen(ltrim($string, '/*')));
201✔
43
        $lastChars = substr($string, -2);
201✔
44
        if ($char === $numChars && $lastChars === '*/') {
201✔
45
            // Edge case: docblock without whitespace or contents.
46
            $openTag = substr($string, 0, -2);
39✔
47
            $string  = $lastChars;
39✔
48
        } else {
49
            $openTag = substr($string, 0, $char);
201✔
50
            $string  = ltrim($string, '/*');
201✔
51
        }
52

53
        $tokens[$stackPtr] = [
201✔
54
            'content'      => $openTag,
201✔
55
            'code'         => T_DOC_COMMENT_OPEN_TAG,
201✔
56
            'type'         => 'T_DOC_COMMENT_OPEN_TAG',
201✔
57
            'comment_tags' => [],
134✔
58
        ];
134✔
59

60
        $openPtr = $stackPtr;
201✔
61
        $stackPtr++;
201✔
62

63
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
64
            $content = Common::prepareForOutput($openTag);
×
NEW
65
            StatusWriter::write("Create comment token: T_DOC_COMMENT_OPEN_TAG => $content", 2);
×
66
        }
67

68
        /*
69
            Strip off the close tag so it doesn't interfere with any
70
            of our comment line processing. The token will be added to the
71
            stack just before we return it.
72
        */
73

74
        $closeTag = [
134✔
75
            'content'        => substr($string, strlen(rtrim($string, '/*'))),
201✔
76
            'code'           => T_DOC_COMMENT_CLOSE_TAG,
201✔
77
            'type'           => 'T_DOC_COMMENT_CLOSE_TAG',
201✔
78
            'comment_opener' => $openPtr,
201✔
79
        ];
134✔
80

81
        if ($closeTag['content'] === false) {
201✔
82
            // In PHP < 8.0 substr() can return `false` instead of always returning a string.
83
            $closeTag['content'] = '';
×
84
        }
85

86
        $string = rtrim($string, '/*');
201✔
87

88
        /*
89
            Process each line of the comment.
90
        */
91

92
        $lines    = explode($eolChar, $string);
201✔
93
        $numLines = count($lines);
201✔
94
        foreach ($lines as $lineNum => $string) {
201✔
95
            if ($lineNum !== ($numLines - 1)) {
201✔
96
                $string .= $eolChar;
156✔
97
            }
98

99
            $char     = 0;
201✔
100
            $numChars = strlen($string);
201✔
101

102
            // We've started a new line, so process the indent.
103
            $space = $this->collectWhitespace($string, $char, $numChars);
201✔
104
            if ($space !== null) {
201✔
105
                $tokens[$stackPtr] = $space;
195✔
106
                $stackPtr++;
195✔
107
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
195✔
108
                    $content = Common::prepareForOutput($space['content']);
×
NEW
109
                    StatusWriter::write("Create comment token: T_DOC_COMMENT_WHITESPACE => $content", 2);
×
110
                }
111

112
                $char += strlen($space['content']);
195✔
113
                if ($char === $numChars) {
195✔
114
                    break;
183✔
115
                }
116
            }
117

118
            if ($string === '') {
201✔
119
                continue;
51✔
120
            }
121

122
            if ($lineNum > 0 && $string[$char] === '*') {
195✔
123
                // This is a function or class doc block line.
124
                $char++;
156✔
125
                $tokens[$stackPtr] = [
156✔
126
                    'content' => '*',
156✔
127
                    'code'    => T_DOC_COMMENT_STAR,
156✔
128
                    'type'    => 'T_DOC_COMMENT_STAR',
156✔
129
                ];
104✔
130

131
                $stackPtr++;
156✔
132

133
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
156✔
NEW
134
                    StatusWriter::write('Create comment token: T_DOC_COMMENT_STAR => *', 2);
×
135
                }
136
            }
137

138
            // Now we are ready to process the actual content of the line.
139
            $lineTokens = $this->processLine($string, $eolChar, $char, $numChars);
195✔
140
            foreach ($lineTokens as $lineToken) {
195✔
141
                $tokens[$stackPtr] = $lineToken;
195✔
142
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
195✔
143
                    $content = Common::prepareForOutput($lineToken['content']);
×
144
                    $type    = $lineToken['type'];
×
NEW
145
                    StatusWriter::write("Create comment token: $type => $content", 2);
×
146
                }
147

148
                if ($lineToken['code'] === T_DOC_COMMENT_TAG) {
195✔
149
                    $tokens[$openPtr]['comment_tags'][] = $stackPtr;
177✔
150
                }
151

152
                $stackPtr++;
195✔
153
            }
154
        }//end foreach
155

156
        $tokens[$stackPtr] = $closeTag;
201✔
157
        $tokens[$openPtr]['comment_closer'] = $stackPtr;
201✔
158
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
159
            $content = Common::prepareForOutput($closeTag['content']);
×
NEW
160
            StatusWriter::write("Create comment token: T_DOC_COMMENT_CLOSE_TAG => $content", 2);
×
161
        }
162

163
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
NEW
164
            StatusWriter::write('*** END COMMENT TOKENIZING ***', 2);
×
165
        }
166

167
        return $tokens;
201✔
168

169
    }//end tokenizeString()
170

171

172
    /**
173
     * Process a single line of a comment.
174
     *
175
     * @param string $string  The comment string being tokenized.
176
     * @param string $eolChar The EOL character to use for splitting strings.
177
     * @param int    $start   The position in the string to start processing.
178
     * @param int    $end     The position in the string to end processing.
179
     *
180
     * @return array<int, array<string, string|int>>
181
     */
182
    private function processLine($string, $eolChar, $start, $end)
195✔
183
    {
184
        $tokens = [];
195✔
185

186
        // Collect content padding.
187
        $space = $this->collectWhitespace($string, $start, $end);
195✔
188
        if ($space !== null) {
195✔
189
            $tokens[] = $space;
156✔
190
            $start   += strlen($space['content']);
156✔
191
        }
192

193
        if (isset($string[$start]) === false) {
195✔
194
            return $tokens;
6✔
195
        }
196

197
        if ($string[$start] === '@') {
195✔
198
            // The content up until the first whitespace is the tag name.
199
            $matches = [];
177✔
200
            preg_match('/@[^\s]+/', $string, $matches, 0, $start);
177✔
201
            if (isset($matches[0]) === true
177✔
202
                && substr(strtolower($matches[0]), 0, 7) !== '@phpcs:'
177✔
203
            ) {
204
                $tagName  = $matches[0];
177✔
205
                $start   += strlen($tagName);
177✔
206
                $tokens[] = [
177✔
207
                    'content' => $tagName,
177✔
208
                    'code'    => T_DOC_COMMENT_TAG,
177✔
209
                    'type'    => 'T_DOC_COMMENT_TAG',
177✔
210
                ];
118✔
211

212
                // Then there will be some whitespace.
213
                $space = $this->collectWhitespace($string, $start, $end);
177✔
214
                if ($space !== null) {
177✔
215
                    $tokens[] = $space;
177✔
216
                    $start   += strlen($space['content']);
177✔
217
                }
218
            }
219
        }//end if
220

221
        // Process the rest of the line.
222
        $eol = strpos($string, $eolChar, $start);
195✔
223
        if ($eol === false) {
195✔
224
            $eol = $end;
177✔
225
        }
226

227
        if ($eol > $start) {
195✔
228
            $tokens[] = [
195✔
229
                'content' => substr($string, $start, ($eol - $start)),
195✔
230
                'code'    => T_DOC_COMMENT_STRING,
195✔
231
                'type'    => 'T_DOC_COMMENT_STRING',
195✔
232
            ];
130✔
233
        }
234

235
        if ($eol !== $end) {
195✔
236
            $tokens[] = [
156✔
237
                'content' => substr($string, $eol, strlen($eolChar)),
156✔
238
                'code'    => T_DOC_COMMENT_WHITESPACE,
156✔
239
                'type'    => 'T_DOC_COMMENT_WHITESPACE',
156✔
240
            ];
104✔
241
        }
242

243
        return $tokens;
195✔
244

245
    }//end processLine()
246

247

248
    /**
249
     * Collect consecutive whitespace into a single token.
250
     *
251
     * @param string $string The comment string being tokenized.
252
     * @param int    $start  The position in the string to start processing.
253
     * @param int    $end    The position in the string to end processing.
254
     *
255
     * @return array<string, string|int>|null
256
     */
257
    private function collectWhitespace($string, $start, $end)
201✔
258
    {
259
        $space = '';
201✔
260
        for ($start; $start < $end; $start++) {
201✔
261
            if ($string[$start] !== ' ' && $string[$start] !== "\t") {
195✔
262
                break;
195✔
263
            }
264

265
            $space .= $string[$start];
195✔
266
        }
267

268
        if ($space === '') {
201✔
269
            return null;
201✔
270
        }
271

272
        return [
130✔
273
            'content' => $space,
195✔
274
            'code'    => T_DOC_COMMENT_WHITESPACE,
195✔
275
            'type'    => 'T_DOC_COMMENT_WHITESPACE',
195✔
276
        ];
130✔
277

278
    }//end collectWhitespace()
279

280

281
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc