Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 85 additions & 4 deletions PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function process(File $phpcsFile, $stackPtr): void
}

// Get the expected indentation based on scope
$expectedIndent = $this->getExpectedIndent($tokens[$nextToken]);
$expectedIndent = $this->getExpectedIndent($phpcsFile, $nextToken, $tokens);

// Skip anything that could be intentional (most things)
if ($this->isInsideClosure($phpcsFile, $nextToken, $tokens)) {
Expand Down Expand Up @@ -180,15 +180,18 @@ protected function getIndentLevel(File $phpcsFile, array $token): int
/**
* Get the expected indentation level based on scope.
*
* @param array<string, mixed> $token
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $stackPtr
* @param array<int, array<string, mixed>> $tokens
*
* @return int
*/
protected function getExpectedIndent(array $token): int
protected function getExpectedIndent(File $phpcsFile, int $stackPtr, array $tokens): int
{
$token = $tokens[$stackPtr];
$conditions = $token['conditions'];

return count($conditions);
return count($conditions) + $this->getUnscopedBraceIndent($phpcsFile, $stackPtr, $tokens);
}

/**
Expand Down Expand Up @@ -327,6 +330,17 @@ protected function isValidContinuation(int $prevToken, array $tokens): bool
*/
private static array $arrowFunctionScopesCache = [];

/**
* Per-file cache of paired curly-brace ranges that phpcs did not model as scopes.
*
* PHP 8.4 property hooks are one example: their braces have bracket
* opener/closer metadata, but they are not propagated through the
* `conditions` map. These ranges still affect block indentation.
*
* @var array<string, array{count: int, scopes: array<int, array{0: int, 1: int}>}>
*/
private static array $unscopedBraceScopesCache = [];

/**
* Check if the current position is inside a closure or arrow function.
*
Expand Down Expand Up @@ -403,6 +417,73 @@ protected function getArrowFunctionScopes(File $phpcsFile, array $tokens): array
return $scopes;
}

/**
* Count unscoped brace ranges enclosing the current token.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $stackPtr
* @param array<int, array<string, mixed>> $tokens
*
* @return int
*/
protected function getUnscopedBraceIndent(File $phpcsFile, int $stackPtr, array $tokens): int
{
$indent = 0;
foreach ($this->getUnscopedBraceScopes($phpcsFile, $tokens) as $range) {
if ($stackPtr > $range[0] && $stackPtr < $range[1]) {
$indent++;
}
}

return $indent;
}

/**
* Build (and cache per file) unscoped paired curly-brace ranges.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param array<int, array<string, mixed>> $tokens
*
* @return array<int, array{0: int, 1: int}>
*/
protected function getUnscopedBraceScopes(File $phpcsFile, array $tokens): array
{
$cacheKey = $phpcsFile->getFilename();
$tokenCount = count($tokens);
if (
isset(self::$unscopedBraceScopesCache[$cacheKey])
&& self::$unscopedBraceScopesCache[$cacheKey]['count'] === $tokenCount
) {
return self::$unscopedBraceScopesCache[$cacheKey]['scopes'];
}

$scopeOpeners = [];
foreach ($tokens as $token) {
if (isset($token['scope_opener'])) {
$scopeOpeners[$token['scope_opener']] = true;
}
}

$scopes = [];
foreach ($tokens as $stackPtr => $token) {
if ($token['code'] !== T_OPEN_CURLY_BRACKET) {
continue;
}
if (!isset($token['bracket_closer']) || isset($scopeOpeners[$stackPtr])) {
continue;
}

$scopes[] = [$stackPtr, $token['bracket_closer']];
}

self::$unscopedBraceScopesCache[$cacheKey] = [
'count' => $tokenCount,
'scopes' => $scopes,
];

return $scopes;
}

/**
* Check if the current position is inside an array where indentation tracking may be unreliable.
* This includes multi-dimensional arrays and arrays with closures as values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ConsistentIndentSniffTest extends TestCase
*/
public function testConsistentIndentSniffer(): void
{
$this->assertSnifferFindsErrors(new ConsistentIndentSniff(), 2);
$this->assertSnifferFindsErrors(new ConsistentIndentSniff(), 3);
}

/**
Expand Down
16 changes: 16 additions & 0 deletions tests/_data/ConsistentIndent/after.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@

class FixMe
{
public string $label = 'x' {
get {
return $this->label;
}
set {
$this->label = $value;
}
}

public string $misindentedLabel = 'x' {
set {
$this->misindentedLabel = $value;
$this->misindentedLabel = trim($this->misindentedLabel);
}
}

public function orphanedIndentExample($params): array
{
if (!isset($params['url']['unresolved'])) {
Expand Down
16 changes: 16 additions & 0 deletions tests/_data/ConsistentIndent/before.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@

class FixMe
{
public string $label = 'x' {
get {
return $this->label;
}
set {
$this->label = $value;
}
}

public string $misindentedLabel = 'x' {
set {
$this->misindentedLabel = $value;
$this->misindentedLabel = trim($this->misindentedLabel);
}
}

public function orphanedIndentExample($params): array
{
if (!isset($params['url']['unresolved'])) {
Expand Down
Loading