Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a sniff to ensure constants being typed by native types #1702

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading