Skip to content

Commit

Permalink
Create a sniff to ensure constants being typed by native types.
Browse files Browse the repository at this point in the history
  • Loading branch information
DaDeather committed Oct 14, 2024
1 parent c72b104 commit e1fecd4
Show file tree
Hide file tree
Showing 8 changed files with 526 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\TypeHints;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\ClassHelper;
use SlevomatCodingStandard\Helpers\NamespaceHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\SuppressHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_keys;
use function count;
use function enum_exists;
use function in_array;
use function sprintf;
use const T_CONST;
use const T_EQUAL;
use const T_FALSE;
use const T_MINUS;
use const T_NULL;
use const T_OPEN_SHORT_ARRAY;
use const T_TRUE;

class MissingNativeConstantTypeSniff implements Sniff
{

public const CODE_MISSING_CONSTANT_TYPE = 'MissingConstantType';
private const NAME = 'SlevomatCodingStandard.TypeHints.MissingConstantType';

private const T_LNUMBER = 311;
private const T_DNUMBER = 312;
private const T_STRING = 313;
private const T_CONSTANT_ENCAPSED_STRING = 320;

private const TOKEN_TO_TYPE_MAP = [
self::T_DNUMBER => 'float',
self::T_LNUMBER => 'int',
T_NULL => 'null',
T_TRUE => 'true',
T_FALSE => 'false',
T_OPEN_SHORT_ARRAY => 'array',
self::T_CONSTANT_ENCAPSED_STRING => 'string',
];

/** @var bool */
public $enable = true;

/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_CONST,
];
}

/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param int $stackPtr
*/
public function process(File $phpcsFile, $stackPtr): void
{
$this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80300);

if (!$this->enable) {
return;
}

if (SuppressHelper::isSniffSuppressed($phpcsFile, $stackPtr, self::NAME)) {
return;
}

$tokens = $phpcsFile->getTokens();

/** @var int $classPointer */
$classPointer = array_keys($tokens[$stackPtr]['conditions'])[count($tokens[$stackPtr]['conditions']) - 1];
$typePointer = TokenHelper::findNextEffective($phpcsFile, $stackPtr + 1);
if (in_array($tokens[$typePointer]['code'], [T_NULL, T_TRUE, T_FALSE], true)) {
return;
}

if (
$tokens[$typePointer]['code'] === self::T_STRING
&& in_array($tokens[$typePointer]['content'], ['int', 'string', 'float', 'double', 'array', 'object'], true)
) {
return;
}

$equalSignPointer = TokenHelper::findNext($phpcsFile, T_EQUAL, $stackPtr + 1);
$namePointer = TokenHelper::findPreviousEffective($phpcsFile, $equalSignPointer - 1);

if (
$tokens[$typePointer]['code'] === self::T_STRING
&& $namePointer !== $typePointer
) {
$className = NamespaceHelper::resolveClassName($phpcsFile, $tokens[$typePointer]['content'], $typePointer);
if (enum_exists($className)) {
return;
}
}

$assignedValuePointer = TokenHelper::findNextEffective($phpcsFile, $equalSignPointer + 1);
if ($tokens[$assignedValuePointer]['code'] === T_MINUS) {
$assignedValuePointer = TokenHelper::findNextEffective($phpcsFile, $assignedValuePointer + 1);
}

$fixableType = self::TOKEN_TO_TYPE_MAP[$tokens[$assignedValuePointer]['code']] ?? null;
if ($fixableType === null) {
$className = NamespaceHelper::resolveClassName($phpcsFile, $tokens[$assignedValuePointer]['content'], $assignedValuePointer);
if (enum_exists($className)) {
$fixableType = $tokens[$assignedValuePointer]['content'];
}
}

if ($fixableType !== null) {
$message = sprintf(
'Constant %s::%s is missing a type (%s).',
ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer),
$tokens[$namePointer]['content'],
$fixableType
);

$fix = $phpcsFile->addFixableError($message, $typePointer, self::CODE_MISSING_CONSTANT_TYPE);
if ($fix) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContentBefore($typePointer, $fixableType . ' ');
$phpcsFile->fixer->endChangeset();
}

return;
}

$message = sprintf(
'Constant %s::%s is missing a type.',
ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer),
$tokens[$namePointer]['content']
);

$phpcsFile->addError($message, $stackPtr, self::CODE_MISSING_CONSTANT_TYPE);
}

}
68 changes: 68 additions & 0 deletions tests/Sniffs/TypeHints/MissingNativeConstantTypeSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\TypeHints;

use SlevomatCodingStandard\Sniffs\TestCase;

class MissingNativeConstantTypeSniffTest extends TestCase
{

public function testNoErrors(): void
{
$report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintNoErrors.php');
self::assertNoSniffErrorInFile($report);
}

public function testErrors(): void
{
$report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintErrors.php');

self::assertSame(36, $report->getErrorCount());

for ($i = 8; $i < 16; $i++) {
self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE);
}

for ($i = 23; $i < 31; $i++) {
self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE);
}

for ($i = 38; $i < 46; $i++) {
self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE);
}

for ($i = 53; $i < 61; $i++) {
self::assertSniffError($report, $i, MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE);
}

self::assertAllFixedInFile($report);
}

public function testIgnoredBySuppress(): void
{
$report = self::checkFile(__DIR__ . '/data/missingConstantTypeHintIgnoreErrors.php');

self::assertSame(0, $report->getErrorCount());
}

public function testWithEnableConfigEnabled(): void
{
$report = self::checkFile(
__DIR__ . '/data/missingConstantTypeHintErrors.php',
['enable' => true],
[MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE]
);
self::assertAllFixedInFile($report);
}

public function testWithEnableConfigDisabled(): void
{
$report = self::checkFile(
__DIR__ . '/data/missingConstantTypeHintDisabled.php',
['enable' => false],
[MissingNativeConstantTypeSniff::CODE_MISSING_CONSTANT_TYPE]
);
self::assertAllFixedInFile($report);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace PersonalHomePage;

class A
{

const AA = null;
const AAA = true;
const AAAA = false;
const AAAAA = 'aa';
const AAAAAA = 123;
const AAAAAAA = 123.456;
const AAAAAAAA = ['php'];

}

interface B
{

public const BB = null;
public const BBB = true;
public const BBBB = false;
public const BBBBB = 'aa';
public const BBBBBB = 123;
public const BBBBBBB = 123.456;
public const BBBBBBBB = ['php'];

}

new class implements B
{

const CC = null;
const CCC = true;
const CCCC = false;
const CCCCC = 'aa';
const CCCCCC = 123;
const CCCCCCC = 123.456;
const CCCCCCCC = ['php'];

};

abstract class C
{

const DD = null;
const DDD = true;
const DDDD = false;
const DDDDD = 'aa';
const DDDDDD = 123;
const DDDDDDD = 123.456;
const DDDDDDDD = ['php'];

}
55 changes: 55 additions & 0 deletions tests/Sniffs/TypeHints/data/missingConstantTypeHintDisabled.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace PersonalHomePage;

class A
{

const AA = null;
const AAA = true;
const AAAA = false;
const AAAAA = 'aa';
const AAAAAA = 123;
const AAAAAAA = 123.456;
const AAAAAAAA = ['php'];

}

interface B
{

public const BB = null;
public const BBB = true;
public const BBBB = false;
public const BBBBB = 'aa';
public const BBBBBB = 123;
public const BBBBBBB = 123.456;
public const BBBBBBBB = ['php'];

}

new class implements B
{

const CC = null;
const CCC = true;
const CCCC = false;
const CCCCC = 'aa';
const CCCCCC = 123;
const CCCCCCC = 123.456;
const CCCCCCCC = ['php'];

};

abstract class C
{

const DD = null;
const DDD = true;
const DDDD = false;
const DDDDD = 'aa';
const DDDDDD = 123;
const DDDDDDD = 123.456;
const DDDDDDDD = ['php'];

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php // lint >= 8.3

namespace PersonalHomePage;

class A
{

const null AA = null;
const true AAA = true;
const false AAAA = false;
const string AAAAA = 'aa';
const int AAAAAA = 123;
const float AAAAAAA = 123.456;
const array AAAAAAAA = ['php'];
const int AAAAAAAAA = -123;
const float AAAAAAAAAA = -123.456;

}

interface B
{

public const null BB = null;
public const true BBB = true;
public const false BBBB = false;
public const string BBBBB = 'aa';
public const int BBBBBB = 123;
public const float BBBBBBB = 123.456;
public const array BBBBBBBB = ['php'];
public const int BBBBBBBBB = -123;
public const float BBBBBBBBBB = -123.456;

}

new class implements B
{

const null CC = null;
const true CCC = true;
const false CCCC = false;
const string CCCCC = 'aa';
const int CCCCCC = 123;
const float CCCCCCC = 123.456;
const array CCCCCCCC = ['php'];
const int CCCCCCCCC = -123;
const float CCCCCCCCCC = -123.456;

};

abstract class C
{

const null DD = null;
const true DDD = true;
const false DDDD = false;
const string DDDDD = 'aa';
const int DDDDDD = 123;
const float DDDDDDD = 123.456;
const array DDDDDDDD = ['php'];
const int DDDDDDDDD = -123;
const float DDDDDDDDDD = -123.456;

}
Loading

0 comments on commit e1fecd4

Please sign in to comment.