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

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

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

90.51
/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✔
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_opener' => $stackPtr,
201✔
58
            'comment_tags'   => [],
134✔
59
        ];
134✔
60

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

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

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

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

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

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

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

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

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

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

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

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

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

134
                $stackPtr++;
156✔
135

136
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
156✔
137
                    StatusWriter::write('Create comment token: T_DOC_COMMENT_STAR => *', 2);
×
138
                }
139
            }
140

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

152
                if ($lineToken['code'] === T_DOC_COMMENT_TAG) {
195✔
153
                    $tokens[$openPtr]['comment_tags'][] = $stackPtr;
177✔
154
                }
155

156
                $stackPtr++;
195✔
157
            }
158
        }//end foreach
159

160
        $tokens[$stackPtr] = $closeTag;
201✔
161
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
162
            $content = Common::prepareForOutput($closeTag['content']);
×
163
            StatusWriter::write("Create comment token: T_DOC_COMMENT_CLOSE_TAG => $content", 2);
×
164
        }
165

166
        // Only now do we know the stack pointer to the docblock closer,
167
        // so add it to all previously created comment tokens.
168
        foreach ($tokens as $ptr => $token) {
201✔
169
            $tokens[$ptr]['comment_closer'] = $stackPtr;
201✔
170
        }
171

172
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
201✔
173
            StatusWriter::write('*** END COMMENT TOKENIZING ***', 2);
×
174
        }
175

176
        return $tokens;
201✔
177

178
    }//end tokenizeString()
179

180

181
    /**
182
     * Process a single line of a comment.
183
     *
184
     * @param string $string  The comment string being tokenized.
185
     * @param string $eolChar The EOL character to use for splitting strings.
186
     * @param int    $start   The position in the string to start processing.
187
     * @param int    $end     The position in the string to end processing.
188
     *
189
     * @return array<int, array<string, string|int>>
190
     */
191
    private function processLine($string, $eolChar, $start, $end)
195✔
192
    {
193
        $tokens = [];
195✔
194

195
        // Collect content padding.
196
        $space = $this->collectWhitespace($string, $start, $end);
195✔
197
        if ($space !== null) {
195✔
198
            $tokens[] = $space;
156✔
199
            $start   += strlen($space['content']);
156✔
200
        }
201

202
        if (isset($string[$start]) === false) {
195✔
203
            return $tokens;
6✔
204
        }
205

206
        if ($string[$start] === '@') {
195✔
207
            // The content up until the first whitespace is the tag name.
208
            $matches = [];
177✔
209
            preg_match('/@[^\s]+/', $string, $matches, 0, $start);
177✔
210
            if (isset($matches[0]) === true
177✔
211
                && substr(strtolower($matches[0]), 0, 7) !== '@phpcs:'
177✔
212
            ) {
213
                $tagName  = $matches[0];
177✔
214
                $start   += strlen($tagName);
177✔
215
                $tokens[] = [
177✔
216
                    'content' => $tagName,
177✔
217
                    'code'    => T_DOC_COMMENT_TAG,
177✔
218
                    'type'    => 'T_DOC_COMMENT_TAG',
177✔
219
                ];
118✔
220

221
                // Then there will be some whitespace.
222
                $space = $this->collectWhitespace($string, $start, $end);
177✔
223
                if ($space !== null) {
177✔
224
                    $tokens[] = $space;
177✔
225
                    $start   += strlen($space['content']);
177✔
226
                }
227
            }
228
        }//end if
229

230
        // Process the rest of the line.
231
        $eol = strpos($string, $eolChar, $start);
195✔
232
        if ($eol === false) {
195✔
233
            $eol = $end;
177✔
234
        }
235

236
        if ($eol > $start) {
195✔
237
            $tokens[] = [
195✔
238
                'content' => substr($string, $start, ($eol - $start)),
195✔
239
                'code'    => T_DOC_COMMENT_STRING,
195✔
240
                'type'    => 'T_DOC_COMMENT_STRING',
195✔
241
            ];
130✔
242
        }
243

244
        if ($eol !== $end) {
195✔
245
            $tokens[] = [
156✔
246
                'content' => substr($string, $eol, strlen($eolChar)),
156✔
247
                'code'    => T_DOC_COMMENT_WHITESPACE,
156✔
248
                'type'    => 'T_DOC_COMMENT_WHITESPACE',
156✔
249
            ];
104✔
250
        }
251

252
        return $tokens;
195✔
253

254
    }//end processLine()
255

256

257
    /**
258
     * Collect consecutive whitespace into a single token.
259
     *
260
     * @param string $string The comment string being tokenized.
261
     * @param int    $start  The position in the string to start processing.
262
     * @param int    $end    The position in the string to end processing.
263
     *
264
     * @return array<string, string|int>|null
265
     */
266
    private function collectWhitespace($string, $start, $end)
201✔
267
    {
268
        $space = '';
201✔
269
        for ($start; $start < $end; $start++) {
201✔
270
            if ($string[$start] !== ' ' && $string[$start] !== "\t") {
195✔
271
                break;
195✔
272
            }
273

274
            $space .= $string[$start];
195✔
275
        }
276

277
        if ($space === '') {
201✔
278
            return null;
201✔
279
        }
280

281
        return [
130✔
282
            'content' => $space,
195✔
283
            'code'    => T_DOC_COMMENT_WHITESPACE,
195✔
284
            'type'    => 'T_DOC_COMMENT_WHITESPACE',
195✔
285
        ];
130✔
286

287
    }//end collectWhitespace()
288

289

290
}//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

© 2025 Coveralls, Inc