Skip to content
Snippets Groups Projects
Unverified Commit e8aa87a5 authored by Mitch Portier's avatar Mitch Portier Committed by GitHub
Browse files

feat(GlobalClass): Add FQN based class check and increase list of disallowed...

feat(GlobalClass): Add FQN based class check and increase list of disallowed classes (#3137530 by Arkener)
parent 24b80764
Branches
Tags 8.3.12
No related merge requests found
......@@ -25,19 +25,67 @@ class GlobalClassSniff implements Sniff
{
/**
* Class names that should not be called statically, mostly entity classes.
* Core class names that should not be called statically, mostly entity
* classes.
*
* @var string[]
*/
protected $classes = [
'File',
'Node',
'NodeType',
'Role',
'Term',
'User',
protected $coreClasses = [
'Drupal\Core\Datetime\Entity\DateFormat',
'Drupal\Core\Entity\Entity\EntityFormDisplay',
'Drupal\Core\Entity\Entity\EntityFormMode',
'Drupal\Core\Entity\Entity\EntityViewDisplay',
'Drupal\Core\Entity\Entity\EntityViewMode',
'Drupal\Core\Field\Entity\BaseFieldOverride',
'Drupal\aggregator\Entity\Feed',
'Drupal\aggregator\Entity\Item',
'Drupal\block\Entity\Block',
'Drupal\block_content\Entity\BlockContent',
'Drupal\block_content\Entity\BlockContentType',
'Drupal\comment\Entity\Comment',
'Drupal\comment\Entity\CommentType',
'Drupal\contact\Entity\ContactForm',
'Drupal\contact\Entity\Message',
'Drupal\content_moderation\Entity\ContentModerationState',
'Drupal\editor\Entity\Editor',
'Drupal\field\Entity\FieldConfig',
'Drupal\field\Entity\FieldStorageConfig',
'Drupal\file\Entity\File',
'Drupal\filter\Entity\FilterFormat',
'Drupal\image\Entity\ImageStyle',
'Drupal\language\Entity\ConfigurableLanguage',
'Drupal\language\Entity\ContentLanguageSettings',
'Drupal\media\Entity\Media',
'Drupal\media\Entity\MediaType',
'Drupal\menu_link_content\Entity\MenuLinkContent',
'Drupal\node\Entity\Node',
'Drupal\node\Entity\NodeType',
'Drupal\path_alias\Entity\PathAlias',
'Drupal\rdf\Entity\RdfMapping',
'Drupal\responsive_image\Entity\ResponsiveImageStyle',
'Drupal\rest\Entity\RestResourceConfig',
'Drupal\search\Entity\SearchPage',
'Drupal\shortcut\Entity\Shortcut',
'Drupal\shortcut\Entity\ShortcutSet',
'Drupal\system\Entity\Action',
'Drupal\system\Entity\Menu',
'Drupal\taxonomy\Entity\Term',
'Drupal\taxonomy\Entity\Vocabulary',
'Drupal\tour\Entity\Tour',
'Drupal\user\Entity\Role',
'Drupal\user\Entity\User',
'Drupal\views\Entity\View',
'Drupal\workflows\Entity\Workflow',
'Drupal\workspaces\Entity\Workspace',
];
/**
* Class names that should not be called statically.
*
* @var string[]
*/
public $classes = [];
/**
* Returns an array of tokens this test wants to listen for.
......@@ -58,7 +106,7 @@ class GlobalClassSniff implements Sniff
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
* @return void|int
*/
public function process(File $phpcsFile, $stackPtr)
{
......@@ -66,8 +114,7 @@ class GlobalClassSniff implements Sniff
// We are only interested in static class method calls, not in the global
// scope.
if (in_array($tokens[$stackPtr]['content'], $this->classes) === false
|| $tokens[($stackPtr + 1)]['code'] !== T_DOUBLE_COLON
if ($tokens[($stackPtr + 1)]['code'] !== T_DOUBLE_COLON
|| isset($tokens[($stackPtr + 2)]) === false
|| $tokens[($stackPtr + 2)]['code'] !== T_STRING
|| in_array($tokens[($stackPtr + 2)]['content'], ['load', 'loadMultiple']) === false
......@@ -85,21 +132,31 @@ class GlobalClassSniff implements Sniff
}
}
$fullName = $this->getFullyQualifiedName($phpcsFile, $tokens[$stackPtr]['content']);
if (in_array($fullName, $this->coreClasses) === false && in_array($fullName, $this->classes) === false) {
return;
}
// Check if the class extends another class and get the name of the class
// that is extended.
$classPtr = key($tokens[$stackPtr]['conditions']);
$extendsName = $phpcsFile->findExtendedClassName($classPtr);
// Check if the class implements ContainerInjectionInterface.
// Check if the class implements a container injection interface.
$containerInterfaces = [
'ContainerInjectionInterface',
'ContainerFactoryPluginInterface',
'ContainerDeriverInterface',
];
$implementedInterfaceNames = $phpcsFile->findImplementedInterfaceNames($classPtr);
$canAccessContainer = !empty($implementedInterfaceNames) && in_array('ContainerInjectionInterface', $implementedInterfaceNames);
$canAccessContainer = !empty($implementedInterfaceNames) && !empty(array_intersect($containerInterfaces, $implementedInterfaceNames));
if (($extendsName === false
|| in_array($extendsName, GlobalDrupalSniff::$baseClasses) === false)
&& Project::isServiceClass($phpcsFile, $classPtr) === false
&& $canAccessContainer === false
) {
return;
return ($phpcsFile->numTokens + 1);
}
$warning = '%s::%s calls should be avoided in classes, use dependency injection instead';
......@@ -112,4 +169,53 @@ class GlobalClassSniff implements Sniff
}//end process()
/**
* Retrieve the fully qualified name of the given classname.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param string $className The classname for which to retrieve the FQN.
*
* @return string
*/
protected function getFullyQualifiedName(File $phpcsFile, $className)
{
$useStatement = $phpcsFile->findNext(T_USE, 0);
while ($useStatement !== false) {
$endPtr = $phpcsFile->findEndOfStatement($useStatement);
$useEnd = ($phpcsFile->findNext([T_STRING, T_NS_SEPARATOR, T_WHITESPACE], ($useStatement + 1), null, true) - 1);
$useFullName = trim($phpcsFile->getTokensAsString(($useStatement + 1), ($useEnd - $useStatement)), '\\ ');
// Check if use statement contains an alias.
$asPtr = $phpcsFile->findNext(T_AS, ($useEnd + 1), $endPtr);
if ($asPtr !== false) {
$aliasName = trim($phpcsFile->getTokensAsString(($asPtr + 1), ($endPtr - 1 - $asPtr)));
if ($aliasName === $className) {
return $useFullName;
}
}
$parts = explode('\\', $useFullName);
$useClassName = end($parts);
// Check if the resulting classname is the classname we're looking
// for.
if ($useClassName === $className) {
return $useFullName;
}
// Check if we're currently in a multi-use statement.
$tokens = $phpcsFile->getTokens();
if ($tokens[$endPtr]['code'] === T_COMMA) {
$useStatement = $endPtr;
continue;
}
$useStatement = $phpcsFile->findNext(T_USE, ($useStatement + 1));
}//end while
return $className;
}//end getFullyQualifiedName()
}//end class
<?php
use \Drupal\node\Entity\NodeType;
use Drupal\node\NotEntity\User;
class TestForm extends FormBase {
......@@ -13,4 +14,9 @@ class TestForm extends FormBase {
return NodeType::load('article');
}
public function notentity() {
// This is not a user entity.
return User::load(1);
}
}
......@@ -39,7 +39,7 @@ class GlobalClassUnitTest extends CoderSniffUnitTest
{
switch ($testFile) {
case 'GlobalClassUnitTest.inc':
return [8 => 1];
return [9 => 1];
case 'ExampleClassWithDependencyInjection.php':
return [24 => 1];
case 'ExampleService.php':
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment