Skip to content

Commit

Permalink
Merge pull request #244 from PHPCSStandards/feature/new-no-useless-al…
Browse files Browse the repository at this point in the history
…iases-sniff

✨ New `Universal.UseStatements.NoUselessAliases` sniff
  • Loading branch information
jrfnl authored Jun 18, 2023
2 parents 289f035 + 0d8aef9 commit 1d70047
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 1d70047

Please sign in to comment.