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
24 changes: 24 additions & 0 deletions data/MaxLineLengthLongDocBlockClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App;

/**
* This is a very long docblock comment that definitely exceeds the eighty character maximum line length limit.
* And here is another very long line in the docblock that also exceeds the eighty character limit significantly.
*/
class MaxLineLengthLongDocBlockClass
{
public function shortMethod(): void
{
$variable = 'short';
}

/**
* This method has a very long docblock description that definitely exceeds the eighty character maximum line length.
*/
public function methodWithVeryLongLineInsideThatDefinitelyExceedsTheEightyCharacterMaximumLineLengthLimit(): void
{
$thisVariableNameIsAlsoExtremelyLongAndWillDefinitelyExceedTheMaximumLineLengthLimitOf80Characters = true;
}
}

17 changes: 17 additions & 0 deletions data/MaxLineLengthLongNamespaceClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Some\Very\Long\Namespace\That\Definitely\Exceeds\Eighty\Characters\And\Goes\On\Forever;

class MaxLineLengthLongNamespaceClass
{
public function shortMethod(): void
{
$variable = 'short';
}

public function methodWithVeryLongLineInsideThatDefinitelyExceedsTheEightyCharacterMaximumLineLengthLimit(): void
{
$thisVariableNameIsAlsoExtremelyLongAndWillDefinitelyExceedTheMaximumLineLengthLimitOf80Characters = true;
}
}

58 changes: 53 additions & 5 deletions docs/rules/Max-Line-Length-Rule.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Max Line Length Rule

Checks that lines do not exceed a specified maximum length. Provides options to exclude files by pattern and ignore use statements.
Checks that lines do not exceed a specified maximum length. Provides options to exclude files by pattern and ignore specific types of lines (use statements, namespace declarations, docblocks).

## Configuration Example
## Configuration Examples

### Using the new array API (recommended)

```neon
-
Expand All @@ -11,13 +13,59 @@ Checks that lines do not exceed a specified maximum length. Provides options to
maxLineLength: 80
excludePatterns: ['/.*\.generated\.php$/', '/.*vendor.*/']
ignoreUseStatements: false
ignoreLineTypes:
useStatements: true
namespaceDeclaration: true
docBlocks: true
tags:
- phpstan.rules.rule
```

### Using the legacy parameter (backward compatible)

```neon
-
class: Phauthentic\PHPStanRules\CleanCode\MaxLineLengthRule
arguments:
maxLineLength: 80
excludePatterns: []
ignoreUseStatements: true
tags:
- phpstan.rules.rule
```

## Parameters

- `maxLineLength`: Maximum allowed line length in characters (default: 80).
- `excludePatterns`: Array of regex patterns to exclude files from checking (optional).
- `ignoreUseStatements`: Whether to ignore use statement lines (default: false).
- `maxLineLength`: Maximum allowed line length in characters (required).
- `excludePatterns`: Array of regex patterns to exclude files from checking (optional, default: `[]`).
- `ignoreUseStatements`: Whether to ignore use statement lines (optional, default: `false`). **Note:** This parameter is maintained for backward compatibility. When set to `true`, it takes precedence over the `ignoreLineTypes` array.
- `ignoreLineTypes`: Array of line types to ignore when checking line length (optional, default: `[]`). Available options:
- `useStatements`: Ignore lines containing `use` statements
- `namespaceDeclaration`: Ignore lines containing `namespace` declarations
- `docBlocks`: Ignore lines that are part of docblock comments (`/** ... */`)

## Examples

### Ignore only use statements

```neon
ignoreLineTypes:
useStatements: true
```

### Ignore namespace declarations and docblocks

```neon
ignoreLineTypes:
namespaceDeclaration: true
docBlocks: true
```

### Ignore all supported line types

```neon
ignoreLineTypes:
useStatements: true
namespaceDeclaration: true
docBlocks: true
```
138 changes: 126 additions & 12 deletions src/CleanCode/MaxLineLengthRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace Phauthentic\PHPStanRules\CleanCode;

use PhpParser\Node;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
use PHPStan\Analyser\Scope;
use PHPStan\Node\FileNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
Expand All @@ -17,7 +19,9 @@
*
* - Checks if a line exceeds the configured maximum line length.
* - Optionally excludes files matching specific patterns.
* - Optionally ignores use statements.
* - Optionally ignores use statements (BC: via ignoreUseStatements parameter or 'useStatements' in ignoreLineTypes).
* - Optionally ignores namespace declaration (via 'namespaceDeclaration' in ignoreLineTypes).
* - Optionally ignores docblock comments (via 'docBlocks' in ignoreLineTypes).
*
* @implements Rule<Node>
*/
Expand All @@ -36,6 +40,10 @@ class MaxLineLengthRule implements Rule

private bool $ignoreUseStatements;

private bool $ignoreNamespaceDeclaration;

private bool $ignoreDocBlocks;

/**
* @var array<string, array<int, int>>
*/
Expand All @@ -52,14 +60,35 @@ class MaxLineLengthRule implements Rule
*/
private array $useStatementLines = [];

/**
* @var array<string, array<int, bool>>
* Cache of which lines contain namespace statements per file
*/
private array $namespaceLines = [];

/**
* @var array<string, array<int, bool>>
* Cache of which lines contain docblock comments per file
*/
private array $docBlockLines = [];

/**
* @param string[] $excludePatterns
* @param array<string, bool> $ignoreLineTypes Array of line types to ignore (e.g., ['useStatements' => true, 'namespaceDeclaration' => true, 'docBlocks' => true])
*/
public function __construct(int $maxLineLength, array $excludePatterns = [], bool $ignoreUseStatements = false)
{
public function __construct(
int $maxLineLength,
array $excludePatterns = [],
bool $ignoreUseStatements = false,
array $ignoreLineTypes = []
) {
$this->maxLineLength = $maxLineLength;
$this->excludePatterns = $excludePatterns;
$this->ignoreUseStatements = $ignoreUseStatements;

// BC: ignoreUseStatements parameter takes precedence over array when both are set
$this->ignoreUseStatements = $ignoreUseStatements ?: ($ignoreLineTypes['useStatements'] ?? false);
$this->ignoreNamespaceDeclaration = $ignoreLineTypes['namespaceDeclaration'] ?? false;
$this->ignoreDocBlocks = $ignoreLineTypes['docBlocks'] ?? false;
}

public function getNodeType(): string
Expand All @@ -77,13 +106,18 @@ public function getNodeType(): string
*/
public function processNode(Node $node, Scope $scope): array
{
// Skip PHPStan-specific nodes that don't represent actual PHP code
if ($node instanceof FileNode) {
return [];
}

// Skip if file should be excluded
if ($this->shouldExcludeFile($scope)) {
return [];
}

$filePath = $scope->getFile();
$lineNumber = $node->getLine();
$lineNumber = $node->getStartLine();

// Track use statement lines for this file
if ($node instanceof Use_) {
Expand All @@ -95,11 +129,65 @@ public function processNode(Node $node, Scope $scope): array
}
}

// Track namespace statement lines for this file
if ($node instanceof Namespace_) {
// Only mark the start line where the namespace declaration appears
$namespaceLine = $node->getStartLine();
$this->markLineAsNamespace($filePath, $namespaceLine);

// If ignoring namespaces and this is the namespace declaration line, skip it
if ($this->ignoreNamespaceDeclaration && $lineNumber === $namespaceLine) {
return [];
}
}

// Handle docblock lines for this node
$errors = [];
$docComment = $node->getDocComment();
if ($docComment !== null) {
$startLine = $docComment->getStartLine();
$endLine = $docComment->getEndLine();

// Mark all docblock lines
for ($line = $startLine; $line <= $endLine; $line++) {
$this->markLineAsDocBlock($filePath, $line);
}

// If not ignoring docblocks, check each line in the docblock
if (!$this->ignoreDocBlocks) {
for ($line = $startLine; $line <= $endLine; $line++) {
// Skip if we've already processed this line
if ($this->isLineProcessed($filePath, $line)) {
continue;
}

$lineLength = $this->getLineLength($filePath, $line);
if ($lineLength > $this->maxLineLength) {
$this->markLineAsProcessed($filePath, $line);
$errors[] = RuleErrorBuilder::message($this->buildErrorMessage($line, $lineLength))
->identifier(self::IDENTIFIER)
->line($line)
->build();
}
}
}
}

// Skip if this line is a use statement and we're ignoring them
if ($this->ignoreUseStatements && $this->isUseStatementLine($filePath, $lineNumber)) {
return [];
}

// Skip if this line is a namespace and we're ignoring them
if ($this->ignoreNamespaceDeclaration && $this->isNamespaceLine($filePath, $lineNumber)) {
return [];
}

// Skip if this line is a docblock and we're ignoring them
if ($this->ignoreDocBlocks && $this->isDocBlockLine($filePath, $lineNumber)) {
return [];
}

// Skip if we've already processed this line
if ($this->isLineProcessed($filePath, $lineNumber)) {
return [];
Expand All @@ -111,15 +199,13 @@ public function processNode(Node $node, Scope $scope): array
if ($lineLength > $this->maxLineLength) {
$this->markLineAsProcessed($filePath, $lineNumber);

return [
RuleErrorBuilder::message($this->buildErrorMessage($lineNumber, $lineLength))
->identifier(self::IDENTIFIER)
->line($lineNumber)
->build()
];
$errors[] = RuleErrorBuilder::message($this->buildErrorMessage($lineNumber, $lineLength))
->identifier(self::IDENTIFIER)
->line($lineNumber)
->build();
}

return [];
return $errors;
}

private function shouldExcludeFile(Scope $scope): bool
Expand Down Expand Up @@ -167,6 +253,34 @@ private function markLineAsUseStatement(string $filePath, int $lineNumber): void
$this->useStatementLines[$filePath][$lineNumber] = true;
}

private function isNamespaceLine(string $filePath, int $lineNumber): bool
{
return isset($this->namespaceLines[$filePath][$lineNumber]);
}

private function markLineAsNamespace(string $filePath, int $lineNumber): void
{
if (!isset($this->namespaceLines[$filePath])) {
$this->namespaceLines[$filePath] = [];
}

$this->namespaceLines[$filePath][$lineNumber] = true;
}

private function isDocBlockLine(string $filePath, int $lineNumber): bool
{
return isset($this->docBlockLines[$filePath][$lineNumber]);
}

private function markLineAsDocBlock(string $filePath, int $lineNumber): void
{
if (!isset($this->docBlockLines[$filePath])) {
$this->docBlockLines[$filePath] = [];
}

$this->docBlockLines[$filePath][$lineNumber] = true;
}

private function getLineLength(string $filePath, int $lineNumber): int
{
// Cache file line lengths to avoid reading the same file multiple times
Expand Down
44 changes: 44 additions & 0 deletions tests/TestCases/CleanCode/MaxLineLengthRuleArrayApiTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Phauthentic\PHPStanRules\Tests\TestCases\CleanCode;

use Phauthentic\PHPStanRules\CleanCode\MaxLineLengthRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* Test the new array API for ignore options
*
* @extends RuleTestCase<MaxLineLengthRule>
*/
class MaxLineLengthRuleArrayApiTest extends RuleTestCase
{
protected function getRule(): Rule
{
// Use the new array API to ignore use statements via the array
return new MaxLineLengthRule(80, [], false, ['useStatements' => true]);
}

/**
* Test that the new array API works for useStatements
*/
public function testArrayApiForUseStatements(): void
{
// Lines 5, 6, 7 have use statements that exceed 80 characters - should be ignored
// Line 16 has a method signature that exceeds 80 characters - should be detected
// Line 18 has a variable assignment that exceeds 80 characters - should be detected
$this->analyse([__DIR__ . '/../../../data/MaxLineLengthLongUseStatementsClass.php'], [
[
'Line 16 exceeds the maximum length of 80 characters (found 117 characters).',
16,
],
[
'Line 18 exceeds the maximum length of 80 characters (found 114 characters).',
18,
],
]);
}
}

Loading