Skip to content

Commit

Permalink
Revert "fix: do not enable AlphabeticallyOrderedConstantsSniff by def…
Browse files Browse the repository at this point in the history
…ault (tmp sniff removal) (#124)" (#130)

This reverts commit 74ad7ea.
  • Loading branch information
simPod authored Jan 9, 2024
1 parent 381abd0 commit 7c60c78
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 0 deletions.
249 changes: 249 additions & 0 deletions src/Cdn77/Sniffs/Ordering/AlphabeticallyOrderedConstantsSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\Ordering;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\FixerHelper;

use function array_key_first;
use function array_map;
use function implode;
use function in_array;
use function sort;
use function sprintf;
use function strtolower;
use function ucfirst;
use function usort;

use const T_CONST;
use const T_EQUAL;
use const T_OPEN_TAG;
use const T_PRIVATE;
use const T_PROTECTED;
use const T_PUBLIC;
use const T_SEMICOLON;
use const T_STRING;
use const T_WHITESPACE;

/**
* @phpstan-type NameWithValueShape array{
* name: NameShape,
* value: ValueShape
* }
* @phpstan-type NameShape array{
* content: string,
* lowercaseContent: string,
* ptr: int
* }
* @phpstan-type ValueShape array{
* content: string,
* startPtr: int,
* endPtr: int
* }
*/
final class AlphabeticallyOrderedConstantsSniff implements Sniff
{
public const CodeIncorrectConstantOrder = 'IncorrectConstantOrder';

public function register(): array
{
return [T_OPEN_TAG];
}

public function process(File $phpcsFile, mixed $stackPtr): void
{
$namesWithValuesByVisibility = $this->findConstantNamesWithValuesByVisibility($phpcsFile);

if ($namesWithValuesByVisibility === []) {
return;
}

foreach ($namesWithValuesByVisibility as $visibility => $namesWithValues) {
$constantNames = array_map(
static fn (array $nameWithValue): string => $nameWithValue['name']['lowercaseContent'],
$namesWithValues,
);
$sortedConstantNames = $constantNames;
sort($sortedConstantNames);

if ($sortedConstantNames === $constantNames) {
continue;
}

$firstNameWithValue = $namesWithValues[array_key_first($namesWithValues)];
$fix = $phpcsFile->addFixableError(
sprintf('%s constant names are not alphabetically ordered.', ucfirst($visibility)),
$firstNameWithValue['name']['ptr'],
self::CodeIncorrectConstantOrder,
);

if (! $fix) {
continue;
}

$this->fix($phpcsFile, $namesWithValues);
}
}

/** @param list<NameWithValueShape> $namesWithValues */
private function fix(File $file, array $namesWithValues): void
{
$fixer = $file->fixer;
$sortedNameAndValueTokens = $namesWithValues;
usort(
$sortedNameAndValueTokens,
static fn (array $a, array $b): int => $a['name']['lowercaseContent'] <=> $b['name']['lowercaseContent'],
);

$fixer->beginChangeset();

foreach ($namesWithValues as $key => $nameWithValue) {
$sortedNameAndValueToken = $sortedNameAndValueTokens[$key];

$namePointer = $nameWithValue['name']['ptr'];
FixerHelper::removeBetweenIncluding($file, $namePointer, $namePointer);
$fixer->addContent($namePointer, $sortedNameAndValueToken['name']['content']);

$value = $nameWithValue['value'];
FixerHelper::removeBetweenIncluding($file, $value['startPtr'], $value['endPtr']);
$fixer->addContent($value['startPtr'], $sortedNameAndValueToken['value']['content']);
}

$fixer->endChangeset();
}

/** @return array<string, list<NameWithValueShape>> */
private function findConstantNamesWithValuesByVisibility(File $phpcsFile): array
{
$constantNamesWithValues = [];
$tokens = $phpcsFile->getTokens();

foreach ($tokens as $stackPtr => $token) {
if ($token['code'] !== T_CONST) {
continue;
}

$visibility = $this->getVisibility($phpcsFile, $stackPtr);
$constantName = $this->findConstantName($phpcsFile, $stackPtr);

if ($constantName === null) {
continue;
}

$equalsTokenPointer = $this->findEqualsPointer($phpcsFile, $constantName['ptr']);

if ($equalsTokenPointer === null) {
continue;
}

$value = $this->findValue($phpcsFile, $equalsTokenPointer);

if ($value === null) {
continue;
}

$constantNamesWithValues[$visibility][] = [
'name' => $constantName,
'value' => $value,
];
}

return $constantNamesWithValues;
}

private function getVisibility(File $phpcsFile, int $constStackPtr): string
{
$tokens = $phpcsFile->getTokens();
$visibilityTokenPointer = $phpcsFile->findPrevious(
types: Tokens::$emptyTokens,
start: $constStackPtr - 1,
exclude: true,
local: true,
);

return in_array($tokens[$visibilityTokenPointer]['code'], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true)
? (string) $tokens[$visibilityTokenPointer]['content']
: 'public';
}

/** @phpstan-return NameShape|null */
private function findConstantName(File $phpcsFile, int $constStackPtr): array|null
{
$tokens = $phpcsFile->getTokens();
$constantNameTokenPointer = $phpcsFile->findNext(
types: Tokens::$emptyTokens,
start: $constStackPtr + 1,
exclude: true,
local: true,
);

if ($constantNameTokenPointer === false || $tokens[$constantNameTokenPointer]['code'] !== T_STRING) {
return null;
}

return [
'content' => $tokens[$constantNameTokenPointer]['content'],
'lowercaseContent' => strtolower($tokens[$constantNameTokenPointer]['content']),
'ptr' => $constantNameTokenPointer,
];
}

private function findEqualsPointer(File $phpcsFile, int $constNameStackPtr): int|null
{
$tokens = $phpcsFile->getTokens();
$equalsTokenPointer = $phpcsFile->findNext(
types: Tokens::$emptyTokens,
start: $constNameStackPtr + 1,
exclude: true,
local: true,
);

if ($equalsTokenPointer === false || $tokens[$equalsTokenPointer]['code'] !== T_EQUAL) {
return null;
}

return $equalsTokenPointer;
}

/** @phpstan-return ValueShape|null */
private function findValue(File $phpcsFile, int $equalsTokenPointer): array|null
{
$tokens = $phpcsFile->getTokens();
$startValueTokenPointer = $phpcsFile->findNext(
types: Tokens::$emptyTokens,
start: $equalsTokenPointer + 1,
exclude: true,
local: true,
);

if ($startValueTokenPointer === false) {
return null;
}

$endValueTokenPointer = $startValueTokenPointer;
$valueToken = $tokens[$endValueTokenPointer];
$values = [];

while ($valueToken['code'] !== T_SEMICOLON) {
if (
$valueToken['code'] === T_WHITESPACE
&& in_array($valueToken['content'], ["\n", "\r\n", "\r"], true)
) {
return null;
}

$values[] = $valueToken['content'];
$valueToken = $tokens[++$endValueTokenPointer];
}

return [
'content' => implode('', $values),
'startPtr' => $startValueTokenPointer,
'endPtr' => $endValueTokenPointer - 1,
];
}
}
44 changes: 44 additions & 0 deletions tests/Sniffs/Ordering/AlphabeticallyOrderedConstantsSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\Ordering;

use Cdn77\TestCase;

use function array_keys;
use function json_encode;

use const JSON_THROW_ON_ERROR;

final class AlphabeticallyOrderedConstantsSniffTest extends TestCase
{
public function testErrors(): void
{
$file = self::checkFile(__DIR__ . '/data/AlphabeticallyOrderedConstantsSniffTest.inc');
$expectedErrors = [
9 => AlphabeticallyOrderedConstantsSniff::CodeIncorrectConstantOrder,
19 => AlphabeticallyOrderedConstantsSniff::CodeIncorrectConstantOrder,
25 => AlphabeticallyOrderedConstantsSniff::CodeIncorrectConstantOrder,
];
$possibleLines = array_keys($expectedErrors);
$errors = $file->getErrors();

foreach ($errors as $line => $error) {
self::assertContains($line, $possibleLines, json_encode($error, JSON_THROW_ON_ERROR));

$expectedError = $expectedErrors[$line];
self::assertSniffError($file, $line, $expectedError);
}

self::assertSame(3, $file->getErrorCount());

$file->disableCaching();
$file->fixer->fixFile();

self::assertStringEqualsFile(
__DIR__ . '/data/AlphabeticallyOrderedConstantsSniffTest.fixed.inc',
$file->fixer->getContents(),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\Ordering\data;

final class TestClass
{
public const A = 'a';
public const B = 'b';

public const
IgnoredMultiline1 = 1,
IgnoredMultiline2 = 2;

public const C = 'c' . PHP_EOL;
public const D = [123, 'test'];

protected const E = 'e';
protected const F = 'f';
protected const G = 'g';
protected const Ha = 'h';
protected const HB = 'h';

private const I = 'i';
private const J = 'j';
private const K = 'k';
private const L = 'l';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\Ordering\data;

final class TestClass
{
public const C = 'c' . PHP_EOL;
public const A = 'a';

public const
IgnoredMultiline1 = 1,
IgnoredMultiline2 = 2;

public const D = [123, 'test'];
public const B = 'b';

protected const E = 'e';
protected const G = 'g';
protected const F = 'f';
protected const HB = 'h';
protected const Ha = 'h';

private const K = 'k';
private const J = 'j';
private const I = 'i';
private const L = 'l';
}

0 comments on commit 7c60c78

Please sign in to comment.