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

keradus / PHP-CS-Fixer / 16999983712

15 Aug 2025 09:42PM UTC coverage: 94.75% (-0.09%) from 94.839%
16999983712

push

github

keradus
ci: more self-fixing checks on lowest/highest PHP

28263 of 29829 relevant lines covered (94.75%)

45.88 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
final class EchoTagSyntaxFixer extends AbstractFixer implements ConfigurableFixerInterface
46
{
47
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
48
    use ConfigurableFixerTrait;
49

50
    /** @internal */
51
    public const OPTION_FORMAT = 'format';
52

53
    /** @internal */
54
    public const OPTION_SHORTEN_SIMPLE_STATEMENTS_ONLY = 'shorten_simple_statements_only';
55

56
    /** @internal */
57
    public const OPTION_LONG_FUNCTION = 'long_function';
58

59
    /** @internal */
60
    public const FORMAT_SHORT = 'short';
61

62
    /** @internal */
63
    public const FORMAT_LONG = 'long';
64

65
    /** @internal */
66
    public const LONG_FUNCTION_ECHO = 'echo';
67

68
    /** @internal */
69
    public const LONG_FUNCTION_PRINT = 'print';
70

71
    private const SUPPORTED_FORMAT_OPTIONS = [
72
        self::FORMAT_LONG,
73
        self::FORMAT_SHORT,
74
    ];
75

76
    private const SUPPORTED_LONGFUNCTION_OPTIONS = [
77
        self::LONG_FUNCTION_ECHO,
78
        self::LONG_FUNCTION_PRINT,
79
    ];
80

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

89
            EOT;
3✔
90

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

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

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

121
        return $tokens->isTokenKindFound(\T_OPEN_TAG_WITH_ECHO);
19✔
122
    }
123

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

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

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

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

160
            $nextMeaningful = $tokens->getNextMeaningfulToken($index);
15✔
161

162
            if (null === $nextMeaningful) {
15✔
163
                return;
×
164
            }
165

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

169
                continue;
×
170
            }
171

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

175
                continue;
2✔
176
            }
177

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

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

192
        $index = -1;
19✔
193

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

197
            if (null === $index) {
19✔
198
                return;
19✔
199
            }
200

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

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

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

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

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

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

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

241
        return false;
3✔
242
    }
243

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

253
        $start = $tokens->getNextNonWhitespace($openTagIndex);
14✔
254

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

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

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

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

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