Skip to content

Commit

Permalink
ClassStructureSniff: Support for shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed Mar 23, 2020
1 parent dc48e2b commit c4af7e2
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 24 deletions.
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,39 @@ Checks that class/trait/interface members are in the correct order.

Sniff provides the following settings:

* `groups`: order of groups. Use multiple groups in one `<element value="">` to not differentiate among them. Groups without defined order are sorted to the end.
* `groups`: order of groups. Use multiple groups in one `<element value="">` to not differentiate among them. You can use specific groups or shortcuts.
* `enableFinalMethods`: enables groups for `final` methods

List of supported groups: uses, public constants, protected constants, private constants,
**List of supported groups**:
uses,
public constants, protected constants, private constants,
public properties, public static properties, protected properties, protected static properties, private properties, private static properties,
constructor, static constructors, destructor, magic methods
public methods, public abstract methods, public static methods, public static abstract methods,
protected methods, protected abstract methods, protected static methods, protected static abstract methods,
private methods, private static methods
constructor, static constructors, destructor, magic methods,
public methods, protected methods, private methods,
public final methods, public static final methods, protected final methods, protected static final methods,
public abstract methods, public static abstract methods, protected abstract methods, protected static abstract methods,
public static methods, protected static methods, private static methods,
private methods

* `enableFinalMethods`: enables groups for `final` methods - public final methods, public static final methods, protected final methods, protected static final methods
**List of supported shortcuts**:
constants, properties, static properties, methods, static methods, final methods, abstract methods

```xml
<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
<properties>
<property name="groups" type="array">
<element value="uses"/>

<!-- Public constants are first but you don't care about the order of protected or private constants -->
<element value="public constants"/>
<element value="protected constants"/>
<element value="private constants"/>
<element value="constants"/>

<!-- You don't care about the order among the properties. The same can be done with "properties" shortcut -->
<element value="public properties, protected properties, private properties"/>

<!-- Constructor is first, then all other methods and magic methods are last -->
<element value="constructor"/>
<element value="public methods, protected methods, private methods"/>
<element value="methods"/>
<element value="magic methods"/>
</property>
</properties>
Expand Down
135 changes: 122 additions & 13 deletions SlevomatCodingStandard/Sniffs/Classes/ClassStructureSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use function preg_split;
use function sprintf;
use function str_repeat;
use function strpos;
use function strtolower;
use const T_ABSTRACT;
use const T_CLOSE_CURLY_BRACKET;
Expand Down Expand Up @@ -74,6 +75,66 @@ class ClassStructureSniff implements Sniff
private const GROUP_PRIVATE_METHODS = 'private methods';
private const GROUP_PRIVATE_STATIC_METHODS = 'private static methods';

private const GROUP_SHORTCUT_CONSTANTS = 'constants';
private const GROUP_SHORTCUT_PROPERTIES = 'properties';
private const GROUP_SHORTCUT_STATIC_PROPERTIES = 'static properties';
private const GROUP_SHORTCUT_METHODS = 'methods';
private const GROUP_SHORTCUT_STATIC_METHODS = 'static methods';
private const GROUP_SHORTCUT_ABSTRACT_METHODS = 'abstract methods';
private const GROUP_SHORTCUT_FINAL_METHODS = 'final methods';

private const SHORTCUTS = [
self::GROUP_SHORTCUT_CONSTANTS => [
self::GROUP_PUBLIC_CONSTANTS,
self::GROUP_PROTECTED_CONSTANTS,
self::GROUP_PRIVATE_CONSTANTS,
],
self::GROUP_SHORTCUT_STATIC_PROPERTIES => [
self::GROUP_PUBLIC_STATIC_PROPERTIES,
self::GROUP_PROTECTED_STATIC_PROPERTIES,
self::GROUP_PRIVATE_STATIC_PROPERTIES,
],
self::GROUP_SHORTCUT_PROPERTIES => [
self::GROUP_SHORTCUT_STATIC_PROPERTIES,
self::GROUP_PUBLIC_PROPERTIES,
self::GROUP_PROTECTED_PROPERTIES,
self::GROUP_PRIVATE_PROPERTIES,
],
self::GROUP_SHORTCUT_FINAL_METHODS => [
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
],
self::GROUP_SHORTCUT_ABSTRACT_METHODS => [
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
],
self::GROUP_SHORTCUT_STATIC_METHODS => [
self::GROUP_STATIC_CONSTRUCTORS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PRIVATE_STATIC_METHODS,
],
self::GROUP_SHORTCUT_METHODS => [
self::GROUP_SHORTCUT_FINAL_METHODS,
self::GROUP_SHORTCUT_ABSTRACT_METHODS,
self::GROUP_SHORTCUT_STATIC_METHODS,
self::GROUP_CONSTRUCTOR,
self::GROUP_DESTRUCTOR,
self::GROUP_PUBLIC_METHODS,
self::GROUP_PROTECTED_METHODS,
self::GROUP_PRIVATE_METHODS,
self::GROUP_MAGIC_METHODS,
],
];

private const SPECIAL_METHODS = [
'__construct' => self::GROUP_CONSTRUCTOR,
'__destruct' => self::GROUP_DESTRUCTOR,
Expand Down Expand Up @@ -474,9 +535,13 @@ private function getNormalizedGroups(): array
self::GROUP_PROTECTED_STATIC_PROPERTIES,
self::GROUP_PRIVATE_PROPERTIES,
self::GROUP_PRIVATE_STATIC_PROPERTIES,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_CONSTRUCTOR,
self::GROUP_STATIC_CONSTRUCTORS,
Expand All @@ -490,31 +555,55 @@ private function getNormalizedGroups(): array
self::GROUP_MAGIC_METHODS,
];

if ($this->enableFinalMethods) {
$supportedGroups[] = self::GROUP_PUBLIC_FINAL_METHODS;
$supportedGroups[] = self::GROUP_PUBLIC_STATIC_FINAL_METHODS;
$supportedGroups[] = self::GROUP_PROTECTED_FINAL_METHODS;
$supportedGroups[] = self::GROUP_PROTECTED_STATIC_FINAL_METHODS;
if (!$this->enableFinalMethods) {
foreach ($supportedGroups as $supportedGroupNo => $supportedGroupName) {
if (strpos($supportedGroupName, ' final ') === false) {
continue;
}

unset($supportedGroups[$supportedGroupNo]);
}
}

$normalizedGroups = [];
$normalizedGroupsWithShortcuts = [];
$order = 1;
foreach (SniffSettingsHelper::normalizeArray($this->groups) as $groupsString) {
/** @var string[] $groups */
$groups = preg_split('~\\s*,\\s*~', $groupsString);
foreach ($groups as $group) {
$group = preg_replace('~\\s+~', ' ', $group);

if (!in_array($group, $supportedGroups, true)) {
throw new UnsupportedClassGroupException($group);
$groups = preg_split('~\\s*,\\s*~', strtolower($groupsString));
foreach ($groups as $groupOrShortcut) {
$groupOrShortcut = preg_replace('~\\s+~', ' ', $groupOrShortcut);

if (
!in_array($groupOrShortcut, $supportedGroups, true)
&& !array_key_exists($groupOrShortcut, self::SHORTCUTS)
) {
throw new UnsupportedClassGroupException($groupOrShortcut);
}

$normalizedGroups[$group] = $order;
$normalizedGroupsWithShortcuts[$groupOrShortcut] = $order;
}

$order++;
}

$normalizedGroups = [];
foreach ($normalizedGroupsWithShortcuts as $groupOrShortcut => $groupOrder) {
if (in_array($groupOrShortcut, $supportedGroups, true)) {
$normalizedGroups[$groupOrShortcut] = $groupOrder;
} else {
foreach ($this->unpackShortcut($groupOrShortcut, $supportedGroups) as $group) {
if (
array_key_exists($group, $normalizedGroupsWithShortcuts)
|| array_key_exists($group, $normalizedGroups)
) {
continue;
}

$normalizedGroups[$group] = $groupOrder;
}
}
}

if ($normalizedGroups === []) {
$normalizedGroups = array_flip($supportedGroups);
} else {
Expand All @@ -530,4 +619,24 @@ private function getNormalizedGroups(): array
return $this->normalizedGroups;
}

/**
* @param string $shortcut
* @param array<int, string> $supportedGroups
* @return array<int, string>
*/
private function unpackShortcut(string $shortcut, array $supportedGroups): array
{
$groups = [];

foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) {
if (in_array($groupOrShortcut, $supportedGroups, true)) {
$groups[] = $groupOrShortcut;
} else {
$groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups));
}
}

return $groups;
}

}
31 changes: 30 additions & 1 deletion tests/Sniffs/Classes/ClassStructureSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,35 @@ public function testErrorsWithFinalMethodsEnabled(): void
self::assertAllFixedInFile($report);
}

public function testErrorsWithShortcuts(): void
{
$report = self::checkFile(
__DIR__ . '/data/classStructureWithShortcutsErrors.php',
[
'groups' => [
'uses',
'constants',
'private properties',
'static properties',
'properties',
'constructor',
'public methods',
'final methods',
'public abstract methods',
'abstract methods',
'public static methods',
'static methods',
'methods',
'magic methods',
],
'enableFinalMethods' => true,
]
);

self::assertSame(9, $report->getErrorCount());
self::assertAllFixedInFile($report);
}

public function testThrowExceptionForUnsupportedGroup(): void
{
try {
Expand Down Expand Up @@ -160,7 +189,7 @@ public function testThrowExceptionForMissingGroupsWithFinalMethodsEnabled(): voi
);
self::fail();
} catch (MissingClassGroupsException $e) {
self::assertStringContainsString(': public final methods, public static final methods, protected final methods, protected static final methods.', $e->getMessage());
self::assertStringContainsString(': public static final methods, protected static final methods, public final methods, protected final methods.', $e->getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

abstract class Whatever
{
public const PUBLIC_CONSTANT = 1;

private const PRIVATE_CONSTANT = 0;
private $privateProperty;

public static $publicStaticProperty;

private static $privateStaticProperty;

public $nonStaticProperty;

public function __construct()
{
}

public function publicMethod()
{
}

final protected function finalProtectedMethod()
{
}

public abstract function abstractPublicMethod();

abstract protected function abstractProtectedMethod();

public static function publicStaticMethod()
{
}

private static function privateStaticMethod()
{
}

public function __destruct()
{
}

protected function protectedMethod()
{
}

private function privateMethod()
{
}

public function __get($name)
{
return null;
}

}
57 changes: 57 additions & 0 deletions tests/Sniffs/Classes/data/classStructureWithShortcutsErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

abstract class Whatever
{
public static $publicStaticProperty;

public const PUBLIC_CONSTANT = 1;
private const PRIVATE_CONSTANT = 0;

private static $privateStaticProperty;

public $nonStaticProperty;

private $privateProperty;

public function __construct()
{
}

public function __destruct()
{
}

protected function protectedMethod()
{
}

private function privateMethod()
{
}

final protected function finalProtectedMethod()
{
}

abstract protected function abstractProtectedMethod();

public abstract function abstractPublicMethod();

public static function publicStaticMethod()
{
}

public function publicMethod()
{
}

private static function privateStaticMethod()
{
}

public function __get($name)
{
return null;
}

}

0 comments on commit c4af7e2

Please sign in to comment.