Skip to content

Commit

Permalink
Add initial version of NoGlobalAliasesSniffTest
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeTowers committed Dec 16, 2024
1 parent 61b127f commit 33a1518
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 5 deletions.
180 changes: 180 additions & 0 deletions modules/system/Phpcs/Sniffs/NoGlobalAliasesSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

namespace System\Console\Sniffs;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class NoGlobalAliasesSniff implements Sniff
{
/**
* Global aliases loaded from the aliases.php file.
*
* @var array
*/
private $aliases = [];

/**
* Load aliases from the aliases.php file.
*/
public function __construct()
{
$aliasesFile = __DIR__ . '/../../aliases.php';
if (file_exists($aliasesFile)) {
$this->aliases = include $aliasesFile;
}
}

/**
* Register tokens to listen for.
*
* @return array
*/
public function register()
{
return [T_USE];
}

/**
* Process the file for global alias usage.
*
* @param File $phpcsFile
* @param int $stackPtr
*/
public function process(File $phpcsFile, $stackPtr)
{
// Determine if the use statement is at the top of the file or within a class
$isTopLevelUse = $this->isTopLevelUse($phpcsFile, $stackPtr);

if (!$isTopLevelUse) {
// This is a trait import, check if the alias is already imported
$this->processTraitUse($phpcsFile, $stackPtr);
return;
}

// Detect the full use statement
$endOfStatement = $phpcsFile->findEndOfStatement($stackPtr);

// Build the `use` statement without trailing semicolon
$useStatement = '';
for ($i = $stackPtr + 1; $i < $endOfStatement; $i++) {
$useStatement .= $phpcsFile->getTokens()[$i]['content'];
}

// Normalize the use statement and remove "as" aliases
$useStatement = trim(preg_replace('/\s+as\s+\w+$/i', '', $useStatement));

// $phpcsFile->addError(
// $useStatement . var_export(isset($this->aliases[$useStatement]), true),
// $stackPtr,
// 'NoGlobalAliases'
// );

// Check if the use statement matches a key in the aliases
foreach ($this->aliases as $alias => $fullyQualifiedName) {
if ($useStatement === $alias) {
$fix = $phpcsFile->addFixableWarning(
"Avoid using global class alias '{$alias}'. Use '{$fullyQualifiedName}' instead.",
$stackPtr,
'NoGlobalAliases'
);

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

// Check if the original `use` statement includes an alias (e.g., `as $alias`)
$asPosition = $phpcsFile->findNext(T_AS, $stackPtr, $endOfStatement);
if ($asPosition !== false) {
// Retain the alias by capturing everything after "as"
$alias = $phpcsFile->getTokensAsString($asPosition, $endOfStatement - $asPosition);
$replacement = ' ' . $fullyQualifiedName . ' ' . trim($alias) . ';';
} else {
// No alias, replace with the fully qualified name directly
$replacement = ' ' . $fullyQualifiedName . ';';
}

// Replace the `use` statement
$phpcsFile->fixer->replaceToken($stackPtr + 1, $replacement);

// Remove any extra tokens between `use` and the end of the statement
for ($i = $stackPtr + 2; $i <= $endOfStatement; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}

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

return;
}
}
}

/**
* Determine if the current use statement is at the top of the file.
*
* @param File $phpcsFile
* @param int $stackPtr
* @return bool
*/
private function isTopLevelUse(File $phpcsFile, int $stackPtr): bool
{
$prevClassToken = $phpcsFile->findPrevious([T_CLASS, T_TRAIT, T_INTERFACE], $stackPtr);
return $prevClassToken === false;
}

/**
* Process trait imports within a class.
*
* @param File $phpcsFile
* @param int $stackPtr
*/
private function processTraitUse(File $phpcsFile, int $stackPtr)
{
$endOfStatement = $phpcsFile->findEndOfStatement($stackPtr);
$traitUse = $phpcsFile->getTokensAsString($stackPtr + 1, $endOfStatement - $stackPtr);
$traitUse = trim($traitUse);

// Extract the short name of the trait
$classParts = explode('\\', $traitUse);
$alias = end($classParts);

// Check if the alias matches a key in the aliases
if (isset($this->aliases[$alias]) && !$this->isAlreadyImported($phpcsFile, $alias, $stackPtr)) {
$phpcsFile->addWarning(
"Trait '{$alias}' matches a global alias. Ensure it is explicitly imported at the top of the file.",
$stackPtr,
'TraitMatchesAlias'
);
}
}

/**
* Check if a short alias has already been imported at the top of the file.
*
* @param File $phpcsFile
* @param string $alias
* @param int $stackPtr
* @return bool
*/
private function isAlreadyImported(File $phpcsFile, string $alias, int $stackPtr): bool
{
// Iterate over previous `use` statements at the top of the file
$prevUse = $phpcsFile->findPrevious(T_USE, $stackPtr - 1);
while ($prevUse !== false) {
$endOfStatement = $phpcsFile->findEndOfStatement($prevUse);
$useStatement = $phpcsFile->getTokensAsString($prevUse + 1, $endOfStatement - $prevUse);

// Normalize and extract the alias
$classParts = explode('\\', trim(preg_replace('/\s+as\s+\w+$/i', '', $useStatement)));
$importedAlias = end($classParts);

if ($importedAlias === $alias) {
return true; // Found an existing import
}

$prevUse = $phpcsFile->findPrevious(T_USE, $prevUse - 1);
}

return false;
}
}
36 changes: 36 additions & 0 deletions modules/system/Phpcs/Tests/Sniffs/NoGlobalAliasesSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace System\Phpcs\Tests\Sniffs;

use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;

class NoGlobalAliasesSniffTest extends AbstractSniffUnitTest
{
/**
* Returns the lines where errors should occur.
*
* @return array<int, int> Line numbers as keys, number of errors as values.
*/
public function getErrorList(): array
{
return [
3 => 1, // Global alias Config
4 => 1, // Global alias Lang
5 => 1, // Global alias URL
9 => 0, // Fully-qualified import (no error)
10 => 1, // Global alias Yaml
14 => 0, // Trait imported (no error)
15 => 1, // Trait conflicts with alias
];
}

/**
* Returns the lines where warnings should occur.
*
* @return array<int, int> Line numbers as keys, number of warnings as values.
*/
public function getWarningList(): array
{
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use Winter\Storm\Support\Facades\Config as MyConfig;
use Illuminate\Support\Facades\Lang as MyLang;
use Winter\Storm\Support\Facades\URL;
use Winter\Storm\Support\Arr;
use Winter\Storm\Support\Facades\Yaml;

class Example
{
use Config;
}
12 changes: 12 additions & 0 deletions modules/system/Phpcs/Tests/Sniffs/NoGlobalAliasesSniffUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use Config as MyConfig;
use Lang as MyLang;
use URL;
use Winter\Storm\Support\Arr;
use Yaml;

class Example
{
use Config;
}
14 changes: 9 additions & 5 deletions modules/system/aliases.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
// 'DB' => Illuminate\Support\Facades\DB::class, // Replaced by Winter
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
// 'Event' => Illuminate\Support\Facades\Event::class, // Replaced by Winter
// 'File' => Illuminate\Support\Facades\File::class, // Replaced by Winter
// 'Gate' => Illuminate\Support\Facades\Gate::class, // Currently unsupported in Winter
'Hash' => Illuminate\Support\Facades\Hash::class,
Expand All @@ -41,7 +41,7 @@
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
// 'Str' => Illuminate\Support\Str::class, // Replaced by Winter
'URL' => Illuminate\Support\Facades\URL::class,
// 'URL' => Illuminate\Support\Facades\URL::class, // Replaced by Winter
// 'Validator' => Illuminate\Support\Facades\Validator::class, // Replaced by Winter
'View' => Illuminate\Support\Facades\View::class,

Expand All @@ -56,8 +56,10 @@
'BackendMenu' => Backend\Facades\BackendMenu::class,
'Block' => Winter\Storm\Support\Facades\Block::class,
'Cms' => Cms\Facades\Cms::class,
'DB' => Winter\Storm\Support\Facades\DB::class,
'Config' => Winter\Storm\Support\Facades\Config::class,
'DbDongle' => Winter\Storm\Support\Facades\DbDongle::class,
'Event' => Winter\Storm\Support\Facades\Event::class,
'File' => Winter\Storm\Support\Facades\File::class,
'Flash' => Winter\Storm\Support\Facades\Flash::class,
'Form' => Winter\Storm\Support\Facades\Form::class,
Expand All @@ -71,17 +73,19 @@
'Schema' => Winter\Storm\Support\Facades\Schema::class,
'Seeder' => Winter\Storm\Database\Updates\Seeder::class,
'Str' => Winter\Storm\Support\Str::class,
'Svg' => Winter\Storm\Support\Facades\Svg::class,
'SystemException' => Winter\Storm\Exception\SystemException::class,
'Twig' => Winter\Storm\Support\Facades\Twig::class,
'URL' => Winter\Storm\Support\Facades\Url::class,
'ValidationException' => Winter\Storm\Exception\ValidationException::class,
'Validator' => Winter\Storm\Support\Facades\Validator::class,
'Yaml' => Winter\Storm\Support\Facades\Yaml::class,

/*
* Backwards compatibility aliases
*/
'Db' => Illuminate\Support\Facades\DB::class,
'Url' => Illuminate\Support\Facades\URL::class,
'Db' => Winter\Storm\Support\Facades\DB::class,
'Url' => Winter\Storm\Support\Facades\URL::class,
'TestCase' => System\Tests\Bootstrap\TestCase::class,
'PluginTestCase' => System\Tests\Bootstrap\PluginTestCase::class,

Expand Down
2 changes: 2 additions & 0 deletions modules/system/phpcs.base.xml
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,6 @@
<property name="searchAnnotations" value="true" />
</properties>
</rule>
<!-- Don't allow importing global aliases -->
<rule ref="./modules/system/Phpcs/Sniffs/NoGlobalAliasesSniff.php"/>
</ruleset>

0 comments on commit 33a1518

Please sign in to comment.