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

NexusPHP / cs-config / 14655342589

23 Feb 2025 03:16PM UTC coverage: 95.644%. Remained the same
14655342589

push

github

paulbalandan
Update build badge

1076 of 1125 relevant lines covered (95.64%)

63.51 hits per line

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

76.4
/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\FixerOptionInterface;
21
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
22
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
23
use PhpCsFixer\FixerNameValidator;
24
use PhpCsFixer\Linter\CachingLinter;
25
use PhpCsFixer\Linter\LinterInterface;
26
use PhpCsFixer\Linter\ProcessLinter;
27
use PhpCsFixer\Preg;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30
use PHPUnit\Framework\TestCase;
31

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

46
    protected function setUp(): void
47
    {
48
        parent::setUp();
92✔
49

50
        $this->fixer = $this->createFixer();
92✔
51
        $this->linter = $this->getLinter();
92✔
52
    }
53

54
    final public function testIsRisky(): void
55
    {
56
        $riskyDescription = $this->fixer->getDefinition()->getRiskyDescription();
8✔
57

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

65
        $reflection = new \ReflectionMethod($this->fixer, 'isRisky');
8✔
66

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

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

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

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

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

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

109
        if ($this->fixer instanceof DeprecatedFixerInterface) {
8✔
110
            self::assertStringContainsString('@deprecated', $comment);
4✔
111
        } else {
112
            self::assertStringNotContainsString('@deprecated', $comment);
4✔
113
        }
114
    }
115

116
    final public function testFixerConfigurationDefinitions(): void
117
    {
118
        if (! $this->fixer instanceof ConfigurableFixerInterface) {
8✔
119
            $this->addToAssertionCount(1); // not applied to the fixer without configuration
8✔
120

121
            return;
8✔
122
        }
123

124
        $configurationDefinition = $this->fixer->getConfigurationDefinition();
×
125

126
        foreach ($configurationDefinition->getOptions() as $option) {
×
127
            self::assertInstanceOf(FixerOptionInterface::class, $option);
×
128
            self::assertNotEmpty($option->getDescription());
×
129

130
            self::assertTrue(
×
131
                $option->hasDefault(),
×
132
                \sprintf(
×
133
                    'Option `%s` of fixer `%s` should have a default value.',
×
134
                    $option->getName(),
×
135
                    $this->fixer->getName(),
×
136
                ),
×
137
            );
×
138

139
            self::assertStringNotContainsString(
×
140
                'DEPRECATED',
×
141
                $option->getDescription(),
×
142
                'Option description cannot contain word "DEPRECATED"',
×
143
            );
×
144
        }
145
    }
146

147
    final public function testFixerDefinitions(): void
148
    {
149
        $fixerName = $this->fixer->getName();
8✔
150
        $definition = $this->fixer->getDefinition();
8✔
151

152
        self::assertValidDescription($fixerName, 'summary', $definition->getSummary());
8✔
153

154
        $samples = $definition->getCodeSamples();
8✔
155
        self::assertNotEmpty($samples, \sprintf('[%s] Code samples are required.', $fixerName));
8✔
156

157
        $configSamplesProvided = [];
8✔
158
        $dummyFileInfo = new \SplFileInfo(__FILE__);
8✔
159

160
        foreach ($samples as $counter => $sample) {
8✔
161
            self::assertIsInt($counter);
8✔
162

163
            ++$counter;
8✔
164
            $code = $sample->getCode();
8✔
165
            self::assertNotEmpty($code, \sprintf('[%s] Code provided by sample #%d must not be empty.', $fixerName, $counter));
8✔
166
            self::assertSame("\n", substr($code, -1), \sprintf('[%s] Sample #%d must end with linebreak', $fixerName, $counter));
8✔
167

168
            $config = $sample->getConfiguration();
8✔
169

170
            if (null !== $config) {
8✔
171
                self::assertInstanceOf(
×
172
                    ConfigurableFixerInterface::class,
×
173
                    $this->fixer,
×
174
                    \sprintf('[%s] Sample #%d has configuration, but the fixer is not configurable.', $fixerName, $counter),
×
175
                );
×
176

177
                $configSamplesProvided[$counter] = $config;
×
178
            } elseif ($this->fixer instanceof ConfigurableFixerInterface) {
8✔
179
                if (! $sample instanceof VersionSpecificCodeSampleInterface) {
×
180
                    self::assertArrayNotHasKey(
×
181
                        'default',
×
182
                        $configSamplesProvided,
×
183
                        \sprintf('[%s] Multiple non-versioned samples with default configuration.', $fixerName),
×
184
                    );
×
185
                }
186

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

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

194
            if ($this->fixer instanceof ConfigurableFixerInterface) {
8✔
195
                // always re-configure as the fixer might have been configured with diff. configuration from previous sample
196
                $this->fixer->configure($config ?? []);
×
197
            }
198

199
            Tokens::clearCache();
8✔
200
            $tokens = Tokens::fromCode($code);
8✔
201

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

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

209
            $duplicatedCodeSample = array_search(
8✔
210
                $sample,
8✔
211
                \array_slice($samples, 0, $counter - 1),
8✔
212
                true,
8✔
213
            );
8✔
214

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

221
        if ($this->fixer instanceof ConfigurableFixerInterface) {
8✔
222
            if (isset($configSamplesProvided['default'])) {
×
223
                reset($configSamplesProvided);
×
224
                self::assertSame('default', key($configSamplesProvided), \sprintf('[%s] First sample must be for the default configuration.', $fixerName));
×
225
            }
226

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

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

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

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

260
        $file = new \SplFileInfo(__FILE__);
44✔
261

262
        if (null !== $input) {
44✔
263
            self::assertNull($this->lintSource($input));
24✔
264

265
            Tokens::clearCache();
24✔
266
            $tokens = Tokens::fromCode($input);
24✔
267

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

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

279
            $tokens->clearEmptyTokens();
24✔
280

281
            /** @var list<Token> $tokensArray */
282
            $tokensArray = $tokens->toArray();
24✔
283

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

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

296
        self::assertNull($this->lintSource($expected));
44✔
297

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

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

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

315
        return new $customFixer();
92✔
316
    }
317

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

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

332
    private function getLinter(): LinterInterface
333
    {
334
        /** @var null|CachingLinter $linter */
335
        static $linter = null;
92✔
336

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

341
        return $linter;
92✔
342
    }
343

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

348
        foreach ($expectedTokens as $index => $expectedToken) {
24✔
349
            $inputToken = $inputTokens[$index];
24✔
350

351
            self::assertInstanceOf(Token::class, $expectedToken, 'Expected token is null.');
24✔
352
            self::assertInstanceOf(Token::class, $inputToken, 'Input token is null.');
24✔
353
            self::assertTrue(
24✔
354
                $expectedToken->equals($inputToken),
24✔
355
                \sprintf("Token at index %d must be:\n%s,\ngot:\n%s.", $index, $expectedToken->toJson(), $inputToken->toJson()),
24✔
356
            );
24✔
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));
8✔
363
        self::assertStringNotContainsString('phpdocs', $description, \sprintf('[%s] `PHPDoc` must not be in the plural in %s.', $fixerName, $descriptionType));
8✔
364
        self::assertCorrectCasing($description, 'PHPDoc', \sprintf('[%s] `PHPDoc` must be in correct casing in %s.', $fixerName, $descriptionType));
8✔
365
        self::assertCorrectCasing($description, 'PHPUnit', \sprintf('[%s] `PHPUnit` must be in correct casing in %s.', $fixerName, $descriptionType));
8✔
366
        self::assertFalse(strpos($descriptionType, '``'), \sprintf('[%s] The %s must not contain sequential backticks.', $fixerName, $descriptionType));
8✔
367
    }
368

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