Skip to content

Commit

Permalink
Merge branch 'ordered_overrides_fixer'
Browse files Browse the repository at this point in the history
  • Loading branch information
erickskrauch committed Jan 23, 2024
2 parents 4f379ec + 8dce272 commit 55364f4
Show file tree
Hide file tree
Showing 17 changed files with 900 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
->in(__DIR__);
->in(__DIR__)
->exclude('tests/ClassNotation/_data')
;

return Ely\CS\Config::create([
// Disable "parameters" and "match" to keep compatibility with PHP 7.4
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Enh #4: Introduce `ErickSkrauch\ordered_overrides` fixer.

## [1.2.4] - 2024-01-15
### Fixed
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ return (new \PhpCsFixer\Config())

## Fixers

Table of contents:

* [`ErickSkrauch/align_multiline_parameters`](#erickskrauchalign_multiline_parameters) - Align multiline function params (or remove alignment).
* [`ErickSkrauch/blank_line_around_class_body`](#erickskrauchblank_line_around_class_body) - Add space inside class body.
* [`ErickSkrauch/blank_line_before_return`](#erickskrauchblank_line_before_return) - Add blank line before `return`.
* [`ErickSkrauch/line_break_after_statements`](#erickskrauchline_break_after_statements) - Add blank line after control structures.
* [`ErickSkrauch/multiline_if_statement_braces`](#erickskrauchmultiline_if_statement_braces) - Fix brace position for multiline `if` statements.
* [`ErickSkrauch/ordered_overrides`](#erickskrauchordered_overrides) - Sort overridden methods.
* [`ErickSkrauch/remove_class_name_method_usages`](#erickskrauchremove_class_name_method_usages-yii2) - Replace `::className()` with `:class` (Yii2).

### `ErickSkrauch/align_multiline_parameters`

Forces aligned or not aligned multiline function parameters:
Expand Down Expand Up @@ -154,6 +164,34 @@ Ensures that multiline if statement body curly brace placed on the right line.
* `keep_on_own_line` - should this place closing bracket on its own line? If it's set to `false`, than
curly bracket will be placed right after the last condition statement. **Default**: `true`.

### `ErickSkrauch/ordered_overrides`

Overridden and implemented methods must be sorted in the same order as they are defined in parent classes.

```diff
--- Original
+++ New
@@ @@
<?php
class Foo implements Serializable {

- public function unserialize($data) {}
+ public function serialize() {}

- public function serialize() {}
+ public function unserialize($data) {}

}
```

**Caveats:**

* This fixer is implemented against the PHP-CS-Fixer principle and relies on runtime, classes autoloading and reflection. If dependencies are missing or the autoloader isn't configured correctly, the fixer will not be able to discover the order of methods in parents.

* Fixer prioritizes `extends` and applies `implements` afterwards. It searches for the deepest parents of classes and takes them as the basis for sorting, ignoring later reordering.

* This fixer runs BEFORE the `ordered_interfaces` fixer, so you might need to run PHP-CS-Fixer twice when you're using this fixer to get proper result. See [this discussion](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7760) for more info.

### `ErickSkrauch/remove_class_name_method_usages` (Yii2)

Replaces Yii2 [`BaseObject::className()`](https://github.com/yiisoft/yii2/blob/e53fc0ded1/framework/base/BaseObject.php#L84)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"phpspec/prophecy": "^1.15",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan": "^1.11.x-dev",
"phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "^9.5",
Expand Down
17 changes: 10 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ parameters:
- src
- tests
tipsOfTheDay: false
ignoreErrors:
# https://github.com/phpstan/phpstan-strict-rules/issues/36
- message: '#Dynamic call to static method PHPUnit\\Framework\\.*#'
path: tests/*
132 changes: 132 additions & 0 deletions src/Analyzer/ClassElementsAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);

namespace ErickSkrauch\PhpCsFixer\Analyzer;

use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;

/**
* Taken from the \PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer and simplified
*
* @phpstan-type AnalyzedClassElementType 'use_trait'|'case'|'constant'|'property'|'method'
* @phpstan-type AnalyzedClassElement array{
* start: int,
* visibility: string,
* abstract: bool,
* static: bool,
* readonly: bool,
* type: AnalyzedClassElementType,
* name: string,
* end: int,
* }
*/
final class ClassElementsAnalyzer {

/**
* @return list<AnalyzedClassElement>
*/
public function getClassElements(Tokens $tokens, int $classOpenBraceIndex): array {
static $elementTokenKinds = [CT::T_USE_TRAIT, T_CASE, T_CONST, T_VARIABLE, T_FUNCTION];

$startIndex = $classOpenBraceIndex + 1;
$elements = [];

while (true) {
$element = [
'start' => $startIndex,
'visibility' => 'public',
'abstract' => false,
'static' => false,
'readonly' => false,
];

for ($i = $startIndex; ; ++$i) {
$token = $tokens[$i];

// class end
if ($token->equals('}')) {
return $elements; // @phpstan-ignore return.type
}

if ($token->isGivenKind(T_ABSTRACT)) {
$element['abstract'] = true;

continue;
}

if ($token->isGivenKind(T_STATIC)) {
$element['static'] = true;

continue;
}

if (defined('T_READONLY') && $token->isGivenKind(T_READONLY)) {
$element['readonly'] = true;
}

if ($token->isGivenKind([T_PROTECTED, T_PRIVATE])) {
$element['visibility'] = strtolower($token->getContent());

continue;
}

if (!$token->isGivenKind($elementTokenKinds)) {
continue;
}

$element['type'] = $this->detectElementType($tokens, $i);
if ($element['type'] === 'property') {
$element['name'] = $tokens[$i]->getContent();
} elseif (in_array($element['type'], ['use_trait', 'case', 'constant', 'method', 'magic', 'construct', 'destruct'], true)) {
$element['name'] = $tokens[$tokens->getNextMeaningfulToken($i)]->getContent();
}

$element['end'] = $this->findElementEnd($tokens, $i);

break;
}

$elements[] = $element;
$startIndex = $element['end'] + 1; // @phpstan-ignore offsetAccess.notFound
}
}

/**
* @phpstan-return AnalyzedClassElementType
*/
private function detectElementType(Tokens $tokens, int $index): string {
$token = $tokens[$index];
if ($token->isGivenKind(CT::T_USE_TRAIT)) {
return 'use_trait';
}

if ($token->isGivenKind(T_CASE)) {
return 'case';
}

if ($token->isGivenKind(T_CONST)) {
return 'constant';
}

if ($token->isGivenKind(T_VARIABLE)) {
return 'property';
}

return 'method';
}

private function findElementEnd(Tokens $tokens, int $index): int {
$endIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
if ($tokens[$endIndex]->equals('{')) {
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $endIndex);
}

for (++$endIndex; $tokens[$endIndex]->isWhitespace(" \t") || $tokens[$endIndex]->isComment(); ++$endIndex);

--$endIndex;

return $tokens[$endIndex]->isWhitespace() ? $endIndex - 1 : $endIndex;
}

}
65 changes: 65 additions & 0 deletions src/Analyzer/ClassNameAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);

namespace ErickSkrauch\PhpCsFixer\Analyzer;

use LogicException;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;

final class ClassNameAnalyzer {

private NamespacesAnalyzer $namespacesAnalyzer;

private NamespaceUsesAnalyzer $namespacesUsesAnalyzer;

public function __construct() {
$this->namespacesAnalyzer = new NamespacesAnalyzer();
$this->namespacesUsesAnalyzer = new NamespaceUsesAnalyzer();
}

/**
* @see https://www.php.net/manual/en/language.namespaces.rules.php
*
* @phpstan-return class-string
*/
public function getFqn(Tokens $tokens, int $classNameIndex): string {
$firstPart = $tokens[$classNameIndex];
if (!$firstPart->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
throw new LogicException(sprintf('No T_STRING or T_NS_SEPARATOR at given index %d, got "%s".', $classNameIndex, $firstPart->getName()));
}

$relativeClassName = $this->readClassName($tokens, $classNameIndex);
if (str_starts_with($relativeClassName, '\\')) {
return $relativeClassName; // @phpstan-ignore return.type
}

$namespace = $this->namespacesAnalyzer->getNamespaceAt($tokens, $classNameIndex);
$uses = $this->namespacesUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);
$parts = explode('\\', $relativeClassName, 2);
/** @var \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis $use */
foreach ($uses as $use) {
if ($use->getShortName() !== $parts[0]) {
continue;
}

// @phpstan-ignore return.type
return '\\' . $use->getFullName() . (isset($parts[1]) ? ('\\' . $parts[1]) : '');
}

// @phpstan-ignore return.type
return ($namespace->getFullName() !== '' ? '\\' : '') . $namespace->getFullName() . '\\' . $relativeClassName;
}

private function readClassName(Tokens $tokens, int $classNameStart): string {
$className = '';
$index = $classNameStart;
do {
$className .= $tokens[$index]->getContent();
} while ($tokens[++$index]->isGivenKind([T_STRING, T_NS_SEPARATOR]));

return $className;
}

}
Loading

0 comments on commit 55364f4

Please sign in to comment.