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

NexusPHP / cs-config / 5620798754

pending completion
5620798754

push

github

paulbalandan
Add more checked tokens in no_extra_blank_lines

24 of 24 new or added lines in 4 files covered. (100.0%)

36 existing lines in 1 file now uncovered.

2848 of 2888 relevant lines covered (98.61%)

24.3 hits per line

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

78.7
/src/Test/AbstractCustomFixerTestCase.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Nexus CS Config.
7
 *
8
 * (c) 2020 John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Nexus\CsConfig\Test;
15

16
use Nexus\CsConfig\Fixer\AbstractCustomFixer;
17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
21
use PhpCsFixer\FixerConfiguration\FixerOptionInterface;
22
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
23
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
24
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
25
use PhpCsFixer\FixerNameValidator;
26
use PhpCsFixer\Linter\CachingLinter;
27
use PhpCsFixer\Linter\LinterInterface;
28
use PhpCsFixer\Linter\ProcessLinter;
29
use PhpCsFixer\Preg;
30
use PhpCsFixer\Tokenizer\Token;
31
use PhpCsFixer\Tokenizer\Tokens;
32
use PHPUnit\Framework\TestCase;
33

34
/**
35
 * Used for testing the fixers.
36
 *
37
 * Most of the tests here are directly from `PhpCsFixer\Tests\Test\AbstractFixerTestCase`
38
 * with some modifications and additions, since the test case is not shipped to production.
39
 *
40
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
41
 * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
42
 */
43
abstract class AbstractCustomFixerTestCase extends TestCase
44
{
45
    protected AbstractCustomFixer $fixer;
46
    protected LinterInterface $linter;
47

48
    protected function setUp(): void
49
    {
50
        parent::setUp();
69✔
51

52
        $this->fixer = $this->createFixer();
69✔
53
        $this->linter = $this->getLinter();
69✔
54
    }
55

56
    final public function testIsRisky(): void
57
    {
58
        $riskyDescription = $this->fixer->getDefinition()->getRiskyDescription();
6✔
59

60
        if ($this->fixer->isRisky()) {
6✔
UNCOV
61
            self::assertIsString($riskyDescription);
×
UNCOV
62
            self::assertValidDescription($this->fixer->getName(), 'risky description', (string) $riskyDescription);
×
63
        } else {
64
            self::assertNull($riskyDescription, sprintf('[%s] Fixer is not risky so no description of it is expected.', $this->fixer->getName()));
6✔
65
        }
66

67
        $reflection = new \ReflectionMethod($this->fixer, 'isRisky');
6✔
68

69
        self::assertSame(
6✔
70
            ! $this->fixer->isRisky(),
6✔
71
            $reflection->getDeclaringClass()->getName() === AbstractFixer::class,
6✔
72
            sprintf(
6✔
73
                '[%s] Fixer is %s so the method "AbstractFixer::isRisky()" must be %s.',
6✔
74
                $this->fixer->getName(),
6✔
75
                $this->fixer->isRisky() ? 'risky' : 'not risky',
6✔
76
                $this->fixer->isRisky() ? 'overridden' : 'used',
6✔
77
            ),
6✔
78
        );
6✔
79
    }
80

81
    final public function testNameIsValid(): void
82
    {
83
        $nameValidator = new FixerNameValidator();
6✔
84
        $customFixerName = $this->fixer->getName();
6✔
85

86
        self::assertTrue(
6✔
87
            $nameValidator->isValid($customFixerName, true),
6✔
88
            sprintf('Fixer name "%s" is not valid.', $customFixerName),
6✔
89
        );
6✔
90
    }
91

92
    final public function testFixerIsFinal(): void
93
    {
94
        self::assertTrue(
6✔
95
            (new \ReflectionClass($this->fixer))->isFinal(),
6✔
96
            sprintf('Fixer "%s" must be declared "final".', $this->fixer->getName()),
6✔
97
        );
6✔
98
    }
99

100
    final public function testDeprecatedFixersHaveCorrectSummary(): void
101
    {
102
        self::assertStringNotContainsString(
6✔
103
            'DEPRECATED',
6✔
104
            $this->fixer->getDefinition()->getSummary(),
6✔
105
            'Fixer cannot contain word "DEPRECATED" in summary',
6✔
106
        );
6✔
107

108
        $comment = (new \ReflectionClass($this->fixer))->getDocComment();
6✔
109
        self::assertIsString($comment, sprintf('[%s] Fixer is missing a class-level PHPDoc.', $this->fixer->getName()));
6✔
110
        $comment = (string) $comment;
6✔
111

112
        if ($this->fixer instanceof DeprecatedFixerInterface) {
6✔
113
            self::assertStringContainsString('@deprecated', $comment);
3✔
114
        } else {
115
            self::assertStringNotContainsString('@deprecated', $comment);
3✔
116
        }
117
    }
118

119
    final public function testFixerConfigurationDefinitions(): void
120
    {
121
        if (! $this->fixer instanceof ConfigurableFixerInterface) {
6✔
122
            $this->addToAssertionCount(1); // not applied to the fixer without configuration
6✔
123

124
            return;
6✔
125
        }
126

UNCOV
127
        $configurationDefinition = $this->fixer->getConfigurationDefinition();
×
128

UNCOV
129
        self::assertInstanceOf(FixerConfigurationResolverInterface::class, $configurationDefinition);
×
130

UNCOV
131
        foreach ($configurationDefinition->getOptions() as $option) {
×
UNCOV
132
            self::assertInstanceOf(FixerOptionInterface::class, $option);
×
UNCOV
133
            self::assertNotEmpty($option->getDescription());
×
134

UNCOV
135
            self::assertTrue(
×
UNCOV
136
                $option->hasDefault(),
×
UNCOV
137
                sprintf(
×
UNCOV
138
                    'Option `%s` of fixer `%s` should have a default value.',
×
UNCOV
139
                    $option->getName(),
×
UNCOV
140
                    $this->fixer->getName(),
×
UNCOV
141
                ),
×
UNCOV
142
            );
×
143

UNCOV
144
            self::assertStringNotContainsString(
×
UNCOV
145
                'DEPRECATED',
×
UNCOV
146
                $option->getDescription(),
×
UNCOV
147
                'Option description cannot contain word "DEPRECATED"',
×
UNCOV
148
            );
×
149
        }
150
    }
151

152
    final public function testFixerDefinitions(): void
153
    {
154
        $fixerName = $this->fixer->getName();
6✔
155
        $definition = $this->fixer->getDefinition();
6✔
156
        $fixerIsConfigurable = $this->fixer instanceof ConfigurableFixerInterface;
6✔
157

158
        self::assertValidDescription($fixerName, 'summary', $definition->getSummary());
6✔
159

160
        $samples = $definition->getCodeSamples();
6✔
161
        self::assertNotEmpty($samples, sprintf('[%s] Code samples are required.', $fixerName));
6✔
162

163
        $configSamplesProvided = [];
6✔
164
        $dummyFileInfo = new \SplFileInfo(__FILE__);
6✔
165

166
        foreach ($samples as $counter => $sample) {
6✔
167
            self::assertIsInt($counter);
6✔
168

169
            ++$counter;
6✔
170
            self::assertInstanceOf(CodeSampleInterface::class, $sample, sprintf('[%s] Sample #%d must be an instance of "%s".', $fixerName, $counter, CodeSampleInterface::class));
6✔
171

172
            $code = $sample->getCode();
6✔
173
            self::assertNotEmpty($code, sprintf('[%s] Code provided by sample #%d must not be empty.', $fixerName, $counter));
6✔
174
            self::assertSame("\n", substr($code, -1), sprintf('[%s] Sample #%d must end with linebreak', $fixerName, $counter));
6✔
175

176
            $config = $sample->getConfiguration();
6✔
177

178
            if (null !== $config) {
6✔
UNCOV
179
                self::assertTrue($fixerIsConfigurable, sprintf('[%s] Sample #%d has configuration, but the fixer is not configurable.', $fixerName, $counter));
×
UNCOV
180
                self::assertIsArray($config, sprintf('[%s] Sample #%d configuration must be an array or null.', $fixerName, $counter));
×
181

UNCOV
182
                $configSamplesProvided[$counter] = $config;
×
183
            } elseif ($fixerIsConfigurable) {
6✔
UNCOV
184
                if (! $sample instanceof VersionSpecificCodeSampleInterface) {
×
UNCOV
185
                    self::assertArrayNotHasKey('default', $configSamplesProvided, sprintf('[%s] Multiple non-versioned samples with default configuration.', $fixerName));
×
186
                }
187

UNCOV
188
                $configSamplesProvided['default'] = true;
×
189
            }
190

191
            if ($sample instanceof VersionSpecificCodeSampleInterface && ! $sample->isSuitableFor(\PHP_VERSION_ID)) {
6✔
UNCOV
192
                continue;
×
193
            }
194

195
            if ($fixerIsConfigurable) {
6✔
196
                // always re-configure as the fixer might have been configured with diff. configuration from previous sample
UNCOV
197
                $this->fixer->configure(null === $config ? [] : $config);
×
198
            }
199

200
            Tokens::clearCache();
6✔
201
            $tokens = Tokens::fromCode($code);
6✔
202

203
            $this->fixer->fix(
6✔
204
                $sample instanceof FileSpecificCodeSampleInterface ? $sample->getSplFileInfo() : $dummyFileInfo,
6✔
205
                $tokens,
6✔
206
            );
6✔
207

208
            self::assertTrue($tokens->isChanged(), sprintf('[%s] Sample #%d is not changed during fixing.', $fixerName, $counter));
6✔
209

210
            $duplicatedCodeSample = array_search(
6✔
211
                $sample,
6✔
212
                \array_slice($samples, 0, $counter - 1),
6✔
213
                false,
6✔
214
            );
6✔
215

216
            self::assertFalse(
6✔
217
                $duplicatedCodeSample,
6✔
218
                sprintf('[%s] Sample #%d duplicates #%d.', $fixerName, $counter, ++$duplicatedCodeSample),
6✔
219
            );
6✔
220
        }
221

222
        if ($fixerIsConfigurable) {
6✔
UNCOV
223
            if (isset($configSamplesProvided['default'])) {
×
UNCOV
224
                reset($configSamplesProvided);
×
UNCOV
225
                self::assertSame('default', key($configSamplesProvided), sprintf('[%s] First sample must be for the default configuration.', $fixerName));
×
226
            }
227

UNCOV
228
            if (\count($configSamplesProvided) < 2) {
×
UNCOV
229
                self::fail(sprintf('[%s] Configurable fixer only provides a default configuration sample and none for its configuration options.', $fixerName));
×
230
            }
231

UNCOV
232
            $options = $this->fixer->getConfigurationDefinition()->getOptions();
×
233

UNCOV
234
            foreach ($options as $option) {
×
UNCOV
235
                self::assertMatchesRegularExpression('/^[a-z_]+[a-z]$/', $option->getName(), sprintf('[%s] Option %s is not snake_case.', $fixerName, $option->getName()));
×
236
            }
237
        }
238
    }
239

240
    /**
241
     * Tests if a fixer fixes a given string to match the expected result.
242
     *
243
     * It is used both if you want to test if something is fixed or if it is not touched by the fixer.
244
     *
245
     * It also makes sure that the expected output does not change when run through the fixer. That means that you
246
     * do not need two test cases like [$expected] and [$expected, $input] (where $expected is the same in both cases)
247
     * as the latter covers both of them.
248
     *
249
     * This method throws an exception if $expected and $input are equal to prevent test cases that accidentally do
250
     * not test anything.
251
     *
252
     * @param string      $expected The expected fixer output
253
     * @param null|string $input    The fixer input, or null if it should intentionally be equal to the output
254
     */
255
    protected function doTest(string $expected, ?string $input = null): void
256
    {
257
        if ($expected === $input) {
33✔
258
            throw new \LogicException('Input parameter must not be equal to expected parameter.'); // @codeCoverageIgnore
259
        }
260

261
        $file = new \SplFileInfo(__FILE__);
33✔
262

263
        if (null !== $input) {
33✔
264
            self::assertNull($this->lintSource($input));
18✔
265

266
            Tokens::clearCache();
18✔
267
            $tokens = Tokens::fromCode($input);
18✔
268

269
            self::assertTrue($this->fixer->isCandidate($tokens), 'Fixer must be a candidate for input code.');
18✔
270
            self::assertFalse($tokens->isChanged(), 'Fixer must not touch Tokens on candidate check.');
18✔
271
            $this->fixer->fix($file, $tokens);
18✔
272

273
            self::assertSame(
18✔
274
                $expected,
18✔
275
                $tokens->generateCode(),
18✔
276
                'Code build on input code must match expected code.',
18✔
277
            );
18✔
278
            self::assertTrue($tokens->isChanged(), 'Tokens collection built on input code must be marked as changed after fixing.');
18✔
279

280
            $tokens->clearEmptyTokens();
18✔
281

282
            /** @var Token[] $tokensArray */
283
            $tokensArray = $tokens->toArray();
18✔
284

285
            self::assertSame(
18✔
286
                \count($tokens),
18✔
287
                \count(array_unique(array_map(static fn(Token $token): string => spl_object_hash($token), $tokensArray))),
18✔
288
                'Token items inside Tokens collection must be unique.',
18✔
289
            );
18✔
290

291
            unset($tokensArray);
18✔
292
            Tokens::clearCache();
18✔
293
            $expectedTokens = Tokens::fromCode($expected);
18✔
294
            self::assertTokens($expectedTokens, $tokens);
18✔
295
        }
296

297
        self::assertNull($this->lintSource($expected));
33✔
298

299
        Tokens::clearCache();
33✔
300
        $tokens = Tokens::fromCode($expected);
33✔
301
        $this->fixer->fix($file, $tokens);
33✔
302

303
        self::assertSame(
33✔
304
            $expected,
33✔
305
            $tokens->generateCode(),
33✔
306
            'Code build on expected code must not change.',
33✔
307
        );
33✔
308
        self::assertFalse($tokens->isChanged(), 'Tokens collection built on expected code must not be marked as changed after fixing.');
33✔
309
    }
310

311
    protected function createFixer(): AbstractCustomFixer
312
    {
313
        /** @phpstan-var class-string<AbstractCustomFixer> $customFixer */
314
        $customFixer = Preg::replace('/^(Nexus\\\\CsConfig)\\\\Tests(\\\\.+)Test$/', '$1$2', static::class);
69✔
315

316
        return new $customFixer();
69✔
317
    }
318

319
    /**
320
     * @codeCoverageIgnore
321
     */
322
    protected function lintSource(string $source): ?string
323
    {
324
        try {
325
            $this->linter->lintSource($source)->check();
326

327
            return null;
328
        } catch (\Throwable $e) {
329
            return sprintf('Linting "%s" failed with message: %s.', $source, $e->getMessage());
330
        }
331
    }
332

333
    private function getLinter(): LinterInterface
334
    {
335
        static $linter = null;
69✔
336

337
        if (null === $linter) {
69✔
338
            $linter = new CachingLinter(new ProcessLinter());
4✔
339
        }
340

341
        return $linter;
69✔
342
    }
343

344
    private static function assertTokens(Tokens $expectedTokens, Tokens $inputTokens): void
345
    {
346
        self::assertSame($expectedTokens->count(), $inputTokens->count(), 'Both Tokens collections should have the same size.');
18✔
347

348
        /** @var Token $expectedToken */
349
        foreach ($expectedTokens as $index => $expectedToken) {
18✔
350
            /** @var Token $inputToken */
351
            $inputToken = $inputTokens[$index];
18✔
352

353
            self::assertTrue(
18✔
354
                $expectedToken->equals($inputToken),
18✔
355
                sprintf("Token at index %d must be:\n%s,\ngot:\n%s.", $index, $expectedToken->toJson(), $inputToken->toJson()),
18✔
356
            );
18✔
357
        }
358
    }
359

360
    private static function assertValidDescription(string $fixerName, string $descriptionType, string $description): void
361
    {
362
        self::assertMatchesRegularExpression('/^[A-Z`][^"]+\.$/', $description, sprintf('[%s] The %s must start with capital letter or a ` and end with dot.', $fixerName, $descriptionType));
6✔
363
        self::assertStringNotContainsString('phpdocs', $description, sprintf('[%s] `PHPDoc` must not be in the plural in %s.', $fixerName, $descriptionType));
6✔
364
        self::assertCorrectCasing($description, 'PHPDoc', sprintf('[%s] `PHPDoc` must be in correct casing in %s.', $fixerName, $descriptionType));
6✔
365
        self::assertCorrectCasing($description, 'PHPUnit', sprintf('[%s] `PHPUnit` must be in correct casing in %s.', $fixerName, $descriptionType));
6✔
366
        self::assertFalse(strpos($descriptionType, '``'), sprintf('[%s] The %s must not contain sequential backticks.', $fixerName, $descriptionType));
6✔
367
    }
368

369
    private static function assertCorrectCasing(string $needle, string $haystack, string $message): void
370
    {
371
        self::assertSame(substr_count(strtolower($haystack), strtolower($needle)), substr_count($haystack, $needle), $message);
6✔
372
    }
373
}
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