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

keradus / PHP-CS-Fixer / 17678835382

12 Sep 2025 03:24PM UTC coverage: 94.69% (-0.06%) from 94.75%
17678835382

push

github

keradus
fix typo

1 of 1 new or added line in 1 file covered. (100.0%)

1042 existing lines in 177 files now uncovered.

28424 of 30018 relevant lines covered (94.69%)

45.5 hits per line

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

96.39
/src/Fixer/PhpTag/EchoTagSyntaxFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Fixer\PhpTag;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Tokenizer\Token;
27
use PhpCsFixer\Tokenizer\Tokens;
28

29
/**
30
 * @phpstan-type _AutogeneratedInputConfiguration array{
31
 *  format?: 'long'|'short',
32
 *  long_function?: 'echo'|'print',
33
 *  shorten_simple_statements_only?: bool,
34
 * }
35
 * @phpstan-type _AutogeneratedComputedConfiguration array{
36
 *  format: 'long'|'short',
37
 *  long_function: 'echo'|'print',
38
 *  shorten_simple_statements_only: bool,
39
 * }
40
 *
41
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
42
 *
43
 * @author Michele Locati <michele@locati.it>
44
 *
45
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
46
 */
47
final class EchoTagSyntaxFixer extends AbstractFixer implements ConfigurableFixerInterface
48
{
49
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
50
    use ConfigurableFixerTrait;
51

52
    /** @internal */
53
    public const OPTION_FORMAT = 'format';
54

55
    /** @internal */
56
    public const OPTION_SHORTEN_SIMPLE_STATEMENTS_ONLY = 'shorten_simple_statements_only';
57

58
    /** @internal */
59
    public const OPTION_LONG_FUNCTION = 'long_function';
60

61
    /** @internal */
62
    public const FORMAT_SHORT = 'short';
63

64
    /** @internal */
65
    public const FORMAT_LONG = 'long';
66

67
    /** @internal */
68
    public const LONG_FUNCTION_ECHO = 'echo';
69

70
    /** @internal */
71
    public const LONG_FUNCTION_PRINT = 'print';
72

73
    private const SUPPORTED_FORMAT_OPTIONS = [
74
        self::FORMAT_LONG,
75
        self::FORMAT_SHORT,
76
    ];
77

78
    private const SUPPORTED_LONGFUNCTION_OPTIONS = [
79
        self::LONG_FUNCTION_ECHO,
80
        self::LONG_FUNCTION_PRINT,
81
    ];
82

83
    public function getDefinition(): FixerDefinitionInterface
84
    {
85
        $sample = <<<'EOT'
3✔
86
            <?=1?>
87
            <?php print '2' . '3'; ?>
88
            <?php /* comment */ echo '2' . '3'; ?>
89
            <?php print '2' . '3'; someFunction(); ?>
90

91
            EOT;
3✔
92

93
        return new FixerDefinition(
3✔
94
            'Replaces short-echo `<?=` with long format `<?php echo`/`<?php print` syntax, or vice-versa.',
3✔
95
            [
3✔
96
                new CodeSample($sample),
3✔
97
                new CodeSample($sample, [self::OPTION_FORMAT => self::FORMAT_LONG]),
3✔
98
                new CodeSample($sample, [self::OPTION_FORMAT => self::FORMAT_LONG, self::OPTION_LONG_FUNCTION => self::LONG_FUNCTION_PRINT]),
3✔
99
                new CodeSample($sample, [self::OPTION_FORMAT => self::FORMAT_SHORT]),
3✔
100
                new CodeSample($sample, [self::OPTION_FORMAT => self::FORMAT_SHORT, self::OPTION_SHORTEN_SIMPLE_STATEMENTS_ONLY => false]),
3✔
101
            ],
3✔
102
            null
3✔
103
        );
3✔
104
    }
105

106
    /**
107
     * {@inheritdoc}
108
     *
109
     * Must run before NoMixedEchoPrintFixer.
110
     * Must run after NoUselessPrintfFixer.
111
     */
112
    public function getPriority(): int
113
    {
114
        return 0;
1✔
115
    }
116

117
    public function isCandidate(Tokens $tokens): bool
118
    {
119
        if (self::FORMAT_SHORT === $this->configuration[self::OPTION_FORMAT]) {
33✔
120
            return $tokens->isAnyTokenKindsFound([\T_ECHO, \T_PRINT]);
15✔
121
        }
122

123
        return $tokens->isTokenKindFound(\T_OPEN_TAG_WITH_ECHO);
19✔
124
    }
125

126
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
127
    {
128
        return new FixerConfigurationResolver([
42✔
129
            (new FixerOptionBuilder(self::OPTION_FORMAT, 'The desired language construct.'))
42✔
130
                ->setAllowedValues(self::SUPPORTED_FORMAT_OPTIONS)
42✔
131
                ->setDefault(self::FORMAT_LONG)
42✔
132
                ->getOption(),
42✔
133
            (new FixerOptionBuilder(self::OPTION_LONG_FUNCTION, 'The function to be used to expand the short echo tags.'))
42✔
134
                ->setAllowedValues(self::SUPPORTED_LONGFUNCTION_OPTIONS)
42✔
135
                ->setDefault(self::LONG_FUNCTION_ECHO)
42✔
136
                ->getOption(),
42✔
137
            (new FixerOptionBuilder(self::OPTION_SHORTEN_SIMPLE_STATEMENTS_ONLY, 'Render short-echo tags only in case of simple code.'))
42✔
138
                ->setAllowedTypes(['bool'])
42✔
139
                ->setDefault(true)
42✔
140
                ->getOption(),
42✔
141
        ]);
42✔
142
    }
143

144
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
145
    {
146
        if (self::FORMAT_SHORT === $this->configuration[self::OPTION_FORMAT]) {
33✔
147
            $this->longToShort($tokens);
15✔
148
        } else {
149
            $this->shortToLong($tokens);
19✔
150
        }
151
    }
152

153
    private function longToShort(Tokens $tokens): void
154
    {
155
        $count = $tokens->count();
15✔
156

157
        for ($index = 0; $index < $count; ++$index) {
15✔
158
            if (!$tokens[$index]->isGivenKind(\T_OPEN_TAG)) {
15✔
159
                continue;
15✔
160
            }
161

162
            $nextMeaningful = $tokens->getNextMeaningfulToken($index);
15✔
163

164
            if (null === $nextMeaningful) {
15✔
UNCOV
165
                return;
×
166
            }
167

168
            if (!$tokens[$nextMeaningful]->isGivenKind([\T_ECHO, \T_PRINT])) {
15✔
169
                $index = $nextMeaningful;
×
170

UNCOV
171
                continue;
×
172
            }
173

174
            if (true === $this->configuration[self::OPTION_SHORTEN_SIMPLE_STATEMENTS_ONLY] && $this->isComplexCode($tokens, $nextMeaningful + 1)) {
15✔
175
                $index = $nextMeaningful;
2✔
176

177
                continue;
2✔
178
            }
179

180
            $newTokens = $this->buildLongToShortTokens($tokens, $index, $nextMeaningful);
14✔
181
            $tokens->overrideRange($index, $nextMeaningful, $newTokens);
14✔
182
            $count = $tokens->count();
14✔
183
        }
184
    }
185

186
    private function shortToLong(Tokens $tokens): void
187
    {
188
        if (self::LONG_FUNCTION_PRINT === $this->configuration[self::OPTION_LONG_FUNCTION]) {
19✔
189
            $echoToken = [\T_PRINT, 'print'];
10✔
190
        } else {
191
            $echoToken = [\T_ECHO, 'echo'];
10✔
192
        }
193

194
        $index = -1;
19✔
195

196
        while (true) {
19✔
197
            $index = $tokens->getNextTokenOfKind($index, [[\T_OPEN_TAG_WITH_ECHO]]);
19✔
198

199
            if (null === $index) {
19✔
200
                return;
19✔
201
            }
202

203
            $replace = [new Token([\T_OPEN_TAG, '<?php ']), new Token($echoToken)];
19✔
204

205
            if (!$tokens[$index + 1]->isWhitespace()) {
19✔
206
                $replace[] = new Token([\T_WHITESPACE, ' ']);
9✔
207
            }
208

209
            $tokens->overrideRange($index, $index, $replace);
19✔
210
            ++$index;
19✔
211
        }
212
    }
213

214
    /**
215
     * Check if $tokens, starting at $index, contains "complex code", that is, the content
216
     * of the echo tag contains more than a simple "echo something".
217
     *
218
     * This is done by a very quick test: if the tag contains non-whitespace tokens after
219
     * a semicolon, we consider it as "complex".
220
     *
221
     * @example `<?php echo 1 ?>` is false (not complex)
222
     * @example `<?php echo 'hello' . 'world'; ?>` is false (not "complex")
223
     * @example `<?php echo 2; $set = 3 ?>` is true ("complex")
224
     */
225
    private function isComplexCode(Tokens $tokens, int $index): bool
226
    {
227
        $semicolonFound = false;
14✔
228

229
        for ($count = $tokens->count(); $index < $count; ++$index) {
14✔
230
            $token = $tokens[$index];
14✔
231

232
            if ($token->isGivenKind(\T_CLOSE_TAG)) {
14✔
233
                return false;
10✔
234
            }
235

236
            if (';' === $token->getContent()) {
14✔
237
                $semicolonFound = true;
11✔
238
            } elseif ($semicolonFound && !$token->isWhitespace()) {
14✔
239
                return true;
2✔
240
            }
241
        }
242

243
        return false;
3✔
244
    }
245

246
    /**
247
     * Builds the list of tokens that replace a long echo sequence.
248
     *
249
     * @return non-empty-list<Token>
250
     */
251
    private function buildLongToShortTokens(Tokens $tokens, int $openTagIndex, int $echoTagIndex): array
252
    {
253
        $result = [new Token([\T_OPEN_TAG_WITH_ECHO, '<?='])];
14✔
254

255
        $start = $tokens->getNextNonWhitespace($openTagIndex);
14✔
256

257
        if ($start === $echoTagIndex) {
14✔
258
            // No non-whitespace tokens between $openTagIndex and $echoTagIndex
259
            return $result;
10✔
260
        }
261

262
        // Find the last non-whitespace index before $echoTagIndex
263
        $end = $echoTagIndex - 1;
5✔
264

265
        while ($tokens[$end]->isWhitespace()) {
5✔
266
            --$end;
5✔
267
        }
268

269
        // Copy the non-whitespace tokens between $openTagIndex and $echoTagIndex
270
        for ($index = $start; $index <= $end; ++$index) {
5✔
271
            $result[] = clone $tokens[$index];
5✔
272
        }
273

274
        return $result;
5✔
275
    }
276
}
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