Skip to content

Commit

Permalink
✨ New Universal.UseStatements.NoUselessAliases sniff
Browse files Browse the repository at this point in the history
New sniff to detect useless aliases in import use statements.

Aliasing something to the same name as the original construct is considered useless (though allowed in PHP).
Note: as OO and function names in PHP are case-insensitive, aliasing to the same name, using a different case is also considered useless.

Includes unit tests.
Includes documentation.

Fixes 233
  • Loading branch information
jrfnl committed Jun 18, 2023
1 parent 289f035 commit 0d8aef9
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 0 deletions.
30 changes: 30 additions & 0 deletions Universal/Docs/UseStatements/NoUselessAliasesStandard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
title="No Useless Aliases"
>
<standard>
<![CDATA[
Detects useless aliases for import use statements.
Aliasing something to the same name as the original construct is considered useless.
Note: as OO and function names in PHP are case-insensitive, aliasing to the same name, using a different case is also considered useless.
]]>
</standard>
<code_comparison>
<code title="Valid: Import use statement with an alias to a different name.">
<![CDATA[
use Vendor\Package\ClassName as AnotherName;
use function functionName as my_function;
use const SOME_CONSTANT as MY_CONSTANT;
]]>
</code>
<code title="Invalid: Import use statement with an alias to the same name.">
<![CDATA[
use Vendor\Package\ClassName as ClassName;
use function functionName as FunctionName;
use const SOME_CONSTANT as SOME_CONSTANT;
]]>
</code>
</code_comparison>
</documentation>
164 changes: 164 additions & 0 deletions Universal/Sniffs/UseStatements/NoUselessAliasesSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php
/**
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
*
* @package PHPCSExtra
* @copyright 2023 PHPCSExtra Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSExtra
*/

namespace PHPCSExtra\Universal\Sniffs\UseStatements;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\NamingConventions;
use PHPCSUtils\Utils\UseStatements;

/**
* Detects useless aliases for import use statements.
*
* Aliasing something to the same name as the original construct is considered useless.
* Note: as OO and function names in PHP are case-insensitive, aliasing to the same name,
* using a different case is also considered useless.
*
* @since 1.1.0
*/
final class NoUselessAliasesSniff implements Sniff
{

/**
* Name of the "Use import source" metric.
*
* @since 1.1.0
*
* @var string
*/
const METRIC_NAME = 'Import use statement type';

/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.1.0
*
* @return array
*/
public function register()
{
return [\T_USE];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.1.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
if (UseStatements::isImportUse($phpcsFile, $stackPtr) === false) {
// Closure or trait use statement. Bow out.
return;
}

$endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
if ($endOfStatement === false) {
// Parse error or live coding.
return;
}

$hasAliases = $phpcsFile->findNext(\T_AS, ($stackPtr + 1), $endOfStatement);
if ($hasAliases === false) {
// This use import statement does not alias anything, bow out.
return;
}

$useStatements = UseStatements::splitImportUseStatement($phpcsFile, $stackPtr);
if (\count($useStatements, \COUNT_RECURSIVE) <= 3) {
// No statements found. Shouldn't be possible, but still. Bow out.
return;
}

$tokens = $phpcsFile->getTokens();

// Collect all places where aliases are used in this use statement.
$aliasPtrs = [];
$currentAs = $hasAliases;
do {
$aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($currentAs + 1), null, true);
if ($aliasPtr !== false && $tokens[$aliasPtr]['code'] === \T_STRING) {
$aliasPtrs[$currentAs] = $aliasPtr;
}

$currentAs = $phpcsFile->findNext(\T_AS, ($currentAs + 1), $endOfStatement);
} while ($currentAs !== false);

// Now check the names in each use statement for useless aliases.
foreach ($useStatements as $type => $statements) {
foreach ($statements as $alias => $fqName) {
$unqualifiedName = \ltrim(\substr($fqName, \strrpos($fqName, '\\')), '\\');

$uselessAlias = false;
if ($type === 'const') {
// Do a case-sensitive comparison for constants.
if ($unqualifiedName === $alias) {
$uselessAlias = true;
}
} elseif (NamingConventions::isEqual($unqualifiedName, $alias)) {
$uselessAlias = true;
}

if ($uselessAlias === false) {
continue;
}

// Now check if this is actually used as an alias or just the actual name.
foreach ($aliasPtrs as $asPtr => $aliasPtr) {
if ($tokens[$aliasPtr]['content'] !== $alias) {
continue;
}

// Make sure this is really the right one.
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($asPtr - 1), null, true);
if ($tokens[$prev]['code'] !== \T_STRING
|| $tokens[$prev]['content'] !== $unqualifiedName
) {
continue;
}

$error = 'Useless alias "%s" found for import of "%s"';
$code = 'Found';
$data = [$alias, $fqName];

// Okay, so this is the one which should be flagged.
$hasComments = $phpcsFile->findNext(Tokens::$commentTokens, ($prev + 1), $aliasPtr);
if ($hasComments !== false) {
// Don't auto-fix if there are comments.
$phpcsFile->addError($error, $aliasPtr, $code, $data);
break;
}

$fix = $phpcsFile->addFixableError($error, $aliasPtr, $code, $data);

if ($fix === true) {
$phpcsFile->fixer->beginChangeset();

for ($i = ($prev + 1); $i <= $aliasPtr; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}

$phpcsFile->fixer->endChangeset();
}

break;
}
}
}
}
}
84 changes: 84 additions & 0 deletions Universal/Tests/UseStatements/NoUselessAliasesUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

// Ignore as not import use.
$closure = function () use ($bar) {
return $bar;
};

class Foo {
use MyNamespace\Bar;
}

// Ignore, no aliases.
use MyNamespace\MyClass;
use function MyNamespace\MyFunction;
use const MyNamespace\MyConst;

// Ignore, aliased to different name.
use MyNamespace\MyClass as YourClass;
use function MyNamespace\MyFunction as YourFunction;
use const MyNamespace\MyConst as YourConst;

// Ignore, constant aliased to same name, but different case.
use const MyNamespace\MyConst as MYCONST;

// These should be flagged.
use MyNamespace\NotAutoFixable /*comment*/ as NotAutoFixable;
use MyNamespace\NotAutoFixableEither
as
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
notAutofixableEither;

use MyNamespace\MyClass as MyClass;
use MyNamespace\MyClass as MYCLASS;
use MyNamespace\MyClass as myclass;

use function MyNamespace\MyFunction as MyFunction;
use function MyNamespace\MyFunction
as
myfunction;

use const MyNamespace\MyConst as MyConst;

// Verify that the error is thrown on the correct token/line for multi and group use statements.
use function foo\math\sin,
foo\math\cos as Cos, // Error.
foo\math\cosh;

use some\namespacing\{
SomeClassA as SomeOtherClass, // OK.
deeper\level\SomeClassB,
another\level\SomeClassC as SomeClassC // Error.
};

use const foo\math\PI,
// Comment.
foo\math\GOLDEN_RATIO as GOLDEN_RATIO;

use Some\NS\ {
ClassName as className, // Error.
function SubLevel\functionName as FunctionName, // Error.
const Constants\MYCONSTANT as MYCONSTANT, // Error.
const Constants\CONSTANT_NAME as Constant_Name, // OK.
};

// Verify handling of non-ascii names.
use Vendor\Package\Déjàvü as Dejavu; // OK.
use Vendor\Package\Déjàvü as DÉJÀVÜ; // Ok.
use Vendor\Package\Déjàvü as Déjàvü; // Error.
use Vendor\Package\Déjàvü as déJàVü; // Error.

use function 💩💩 as 💩; // OK.
use function 💩💩 as 💩💩; // Error.

// Verify handing with (illegal) duplicate aliases.
use function foo\math\sin as Cos, // OK.
foo\math\cos as Cos; // Error.

// Intentional parse error.
use function as ;

// Intentional parse error.
// This has to be the last test in the file.
use MyNS\Level\{
Something,
82 changes: 82 additions & 0 deletions Universal/Tests/UseStatements/NoUselessAliasesUnitTest.inc.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

// Ignore as not import use.
$closure = function () use ($bar) {
return $bar;
};

class Foo {
use MyNamespace\Bar;
}

// Ignore, no aliases.
use MyNamespace\MyClass;
use function MyNamespace\MyFunction;
use const MyNamespace\MyConst;

// Ignore, aliased to different name.
use MyNamespace\MyClass as YourClass;
use function MyNamespace\MyFunction as YourFunction;
use const MyNamespace\MyConst as YourConst;

// Ignore, constant aliased to same name, but different case.
use const MyNamespace\MyConst as MYCONST;

// These should be flagged.
use MyNamespace\NotAutoFixable /*comment*/ as NotAutoFixable;
use MyNamespace\NotAutoFixableEither
as
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
notAutofixableEither;

use MyNamespace\MyClass;
use MyNamespace\MyClass;
use MyNamespace\MyClass;

use function MyNamespace\MyFunction;
use function MyNamespace\MyFunction;

use const MyNamespace\MyConst;

// Verify that the error is thrown on the correct token/line for multi and group use statements.
use function foo\math\sin,
foo\math\cos, // Error.
foo\math\cosh;

use some\namespacing\{
SomeClassA as SomeOtherClass, // OK.
deeper\level\SomeClassB,
another\level\SomeClassC // Error.
};

use const foo\math\PI,
// Comment.
foo\math\GOLDEN_RATIO;

use Some\NS\ {
ClassName, // Error.
function SubLevel\functionName, // Error.
const Constants\MYCONSTANT, // Error.
const Constants\CONSTANT_NAME as Constant_Name, // OK.
};

// Verify handling of non-ascii names.
use Vendor\Package\Déjàvü as Dejavu; // OK.
use Vendor\Package\Déjàvü as DÉJÀVÜ; // Ok.
use Vendor\Package\Déjàvü; // Error.
use Vendor\Package\Déjàvü; // Error.

use function 💩💩 as 💩; // OK.
use function 💩💩; // Error.

// Verify handing with (illegal) duplicate aliases.
use function foo\math\sin as Cos, // OK.
foo\math\cos; // Error.

// Intentional parse error.
use function as ;

// Intentional parse error.
// This has to be the last test in the file.
use MyNS\Level\{
Something,
Loading

0 comments on commit 0d8aef9

Please sign in to comment.