Skip to content

Commit 2a0a26f

Browse files
floriankraemerFlorian Krämer
andauthored
Add Max Line Length Rule enhancements and tests (#23)
- Introduced new options for ignoring specific line types (use statements, namespace declarations, docblocks) in the Max Line Length Rule. - Updated the rule implementation to handle these new options effectively. - Added multiple test cases to ensure proper functionality of the new features, including backward compatibility for the ignoreUseStatements parameter. - Created tests to validate detection and ignoring of long lines in docblocks and namespaces based on the new configuration options. Co-authored-by: Florian Krämer <[email protected]>
1 parent 2e57a65 commit 2a0a26f

10 files changed

+502
-17
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App;
4+
5+
/**
6+
* This is a very long docblock comment that definitely exceeds the eighty character maximum line length limit.
7+
* And here is another very long line in the docblock that also exceeds the eighty character limit significantly.
8+
*/
9+
class MaxLineLengthLongDocBlockClass
10+
{
11+
public function shortMethod(): void
12+
{
13+
$variable = 'short';
14+
}
15+
16+
/**
17+
* This method has a very long docblock description that definitely exceeds the eighty character maximum line length.
18+
*/
19+
public function methodWithVeryLongLineInsideThatDefinitelyExceedsTheEightyCharacterMaximumLineLengthLimit(): void
20+
{
21+
$thisVariableNameIsAlsoExtremelyLongAndWillDefinitelyExceedTheMaximumLineLengthLimitOf80Characters = true;
22+
}
23+
}
24+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Some\Very\Long\Namespace\That\Definitely\Exceeds\Eighty\Characters\And\Goes\On\Forever;
4+
5+
class MaxLineLengthLongNamespaceClass
6+
{
7+
public function shortMethod(): void
8+
{
9+
$variable = 'short';
10+
}
11+
12+
public function methodWithVeryLongLineInsideThatDefinitelyExceedsTheEightyCharacterMaximumLineLengthLimit(): void
13+
{
14+
$thisVariableNameIsAlsoExtremelyLongAndWillDefinitelyExceedTheMaximumLineLengthLimitOf80Characters = true;
15+
}
16+
}
17+

docs/rules/Max-Line-Length-Rule.md

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Max Line Length Rule
22

3-
Checks that lines do not exceed a specified maximum length. Provides options to exclude files by pattern and ignore use statements.
3+
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).
44

5-
## Configuration Example
5+
## Configuration Examples
6+
7+
### Using the new array API (recommended)
68

79
```neon
810
-
@@ -11,13 +13,59 @@ Checks that lines do not exceed a specified maximum length. Provides options to
1113
maxLineLength: 80
1214
excludePatterns: ['/.*\.generated\.php$/', '/.*vendor.*/']
1315
ignoreUseStatements: false
16+
ignoreLineTypes:
17+
useStatements: true
18+
namespaceDeclaration: true
19+
docBlocks: true
20+
tags:
21+
- phpstan.rules.rule
22+
```
23+
24+
### Using the legacy parameter (backward compatible)
25+
26+
```neon
27+
-
28+
class: Phauthentic\PHPStanRules\CleanCode\MaxLineLengthRule
29+
arguments:
30+
maxLineLength: 80
31+
excludePatterns: []
32+
ignoreUseStatements: true
1433
tags:
1534
- phpstan.rules.rule
1635
```
1736

1837
## Parameters
1938

20-
- `maxLineLength`: Maximum allowed line length in characters (default: 80).
21-
- `excludePatterns`: Array of regex patterns to exclude files from checking (optional).
22-
- `ignoreUseStatements`: Whether to ignore use statement lines (default: false).
39+
- `maxLineLength`: Maximum allowed line length in characters (required).
40+
- `excludePatterns`: Array of regex patterns to exclude files from checking (optional, default: `[]`).
41+
- `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.
42+
- `ignoreLineTypes`: Array of line types to ignore when checking line length (optional, default: `[]`). Available options:
43+
- `useStatements`: Ignore lines containing `use` statements
44+
- `namespaceDeclaration`: Ignore lines containing `namespace` declarations
45+
- `docBlocks`: Ignore lines that are part of docblock comments (`/** ... */`)
46+
47+
## Examples
2348

49+
### Ignore only use statements
50+
51+
```neon
52+
ignoreLineTypes:
53+
useStatements: true
54+
```
55+
56+
### Ignore namespace declarations and docblocks
57+
58+
```neon
59+
ignoreLineTypes:
60+
namespaceDeclaration: true
61+
docBlocks: true
62+
```
63+
64+
### Ignore all supported line types
65+
66+
```neon
67+
ignoreLineTypes:
68+
useStatements: true
69+
namespaceDeclaration: true
70+
docBlocks: true
71+
```

src/CleanCode/MaxLineLengthRule.php

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace Phauthentic\PHPStanRules\CleanCode;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Namespace_;
89
use PhpParser\Node\Stmt\Use_;
910
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\FileNode;
1012
use PHPStan\Rules\Rule;
1113
use PHPStan\Rules\RuleError;
1214
use PHPStan\Rules\RuleErrorBuilder;
@@ -17,7 +19,9 @@
1719
*
1820
* - Checks if a line exceeds the configured maximum line length.
1921
* - Optionally excludes files matching specific patterns.
20-
* - Optionally ignores use statements.
22+
* - Optionally ignores use statements (BC: via ignoreUseStatements parameter or 'useStatements' in ignoreLineTypes).
23+
* - Optionally ignores namespace declaration (via 'namespaceDeclaration' in ignoreLineTypes).
24+
* - Optionally ignores docblock comments (via 'docBlocks' in ignoreLineTypes).
2125
*
2226
* @implements Rule<Node>
2327
*/
@@ -36,6 +40,10 @@ class MaxLineLengthRule implements Rule
3640

3741
private bool $ignoreUseStatements;
3842

43+
private bool $ignoreNamespaceDeclaration;
44+
45+
private bool $ignoreDocBlocks;
46+
3947
/**
4048
* @var array<string, array<int, int>>
4149
*/
@@ -52,14 +60,35 @@ class MaxLineLengthRule implements Rule
5260
*/
5361
private array $useStatementLines = [];
5462

63+
/**
64+
* @var array<string, array<int, bool>>
65+
* Cache of which lines contain namespace statements per file
66+
*/
67+
private array $namespaceLines = [];
68+
69+
/**
70+
* @var array<string, array<int, bool>>
71+
* Cache of which lines contain docblock comments per file
72+
*/
73+
private array $docBlockLines = [];
74+
5575
/**
5676
* @param string[] $excludePatterns
77+
* @param array<string, bool> $ignoreLineTypes Array of line types to ignore (e.g., ['useStatements' => true, 'namespaceDeclaration' => true, 'docBlocks' => true])
5778
*/
58-
public function __construct(int $maxLineLength, array $excludePatterns = [], bool $ignoreUseStatements = false)
59-
{
79+
public function __construct(
80+
int $maxLineLength,
81+
array $excludePatterns = [],
82+
bool $ignoreUseStatements = false,
83+
array $ignoreLineTypes = []
84+
) {
6085
$this->maxLineLength = $maxLineLength;
6186
$this->excludePatterns = $excludePatterns;
62-
$this->ignoreUseStatements = $ignoreUseStatements;
87+
88+
// BC: ignoreUseStatements parameter takes precedence over array when both are set
89+
$this->ignoreUseStatements = $ignoreUseStatements ?: ($ignoreLineTypes['useStatements'] ?? false);
90+
$this->ignoreNamespaceDeclaration = $ignoreLineTypes['namespaceDeclaration'] ?? false;
91+
$this->ignoreDocBlocks = $ignoreLineTypes['docBlocks'] ?? false;
6392
}
6493

6594
public function getNodeType(): string
@@ -77,13 +106,18 @@ public function getNodeType(): string
77106
*/
78107
public function processNode(Node $node, Scope $scope): array
79108
{
109+
// Skip PHPStan-specific nodes that don't represent actual PHP code
110+
if ($node instanceof FileNode) {
111+
return [];
112+
}
113+
80114
// Skip if file should be excluded
81115
if ($this->shouldExcludeFile($scope)) {
82116
return [];
83117
}
84118

85119
$filePath = $scope->getFile();
86-
$lineNumber = $node->getLine();
120+
$lineNumber = $node->getStartLine();
87121

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

132+
// Track namespace statement lines for this file
133+
if ($node instanceof Namespace_) {
134+
// Only mark the start line where the namespace declaration appears
135+
$namespaceLine = $node->getStartLine();
136+
$this->markLineAsNamespace($filePath, $namespaceLine);
137+
138+
// If ignoring namespaces and this is the namespace declaration line, skip it
139+
if ($this->ignoreNamespaceDeclaration && $lineNumber === $namespaceLine) {
140+
return [];
141+
}
142+
}
143+
144+
// Handle docblock lines for this node
145+
$errors = [];
146+
$docComment = $node->getDocComment();
147+
if ($docComment !== null) {
148+
$startLine = $docComment->getStartLine();
149+
$endLine = $docComment->getEndLine();
150+
151+
// Mark all docblock lines
152+
for ($line = $startLine; $line <= $endLine; $line++) {
153+
$this->markLineAsDocBlock($filePath, $line);
154+
}
155+
156+
// If not ignoring docblocks, check each line in the docblock
157+
if (!$this->ignoreDocBlocks) {
158+
for ($line = $startLine; $line <= $endLine; $line++) {
159+
// Skip if we've already processed this line
160+
if ($this->isLineProcessed($filePath, $line)) {
161+
continue;
162+
}
163+
164+
$lineLength = $this->getLineLength($filePath, $line);
165+
if ($lineLength > $this->maxLineLength) {
166+
$this->markLineAsProcessed($filePath, $line);
167+
$errors[] = RuleErrorBuilder::message($this->buildErrorMessage($line, $lineLength))
168+
->identifier(self::IDENTIFIER)
169+
->line($line)
170+
->build();
171+
}
172+
}
173+
}
174+
}
175+
98176
// Skip if this line is a use statement and we're ignoring them
99177
if ($this->ignoreUseStatements && $this->isUseStatementLine($filePath, $lineNumber)) {
100178
return [];
101179
}
102180

181+
// Skip if this line is a namespace and we're ignoring them
182+
if ($this->ignoreNamespaceDeclaration && $this->isNamespaceLine($filePath, $lineNumber)) {
183+
return [];
184+
}
185+
186+
// Skip if this line is a docblock and we're ignoring them
187+
if ($this->ignoreDocBlocks && $this->isDocBlockLine($filePath, $lineNumber)) {
188+
return [];
189+
}
190+
103191
// Skip if we've already processed this line
104192
if ($this->isLineProcessed($filePath, $lineNumber)) {
105193
return [];
@@ -111,15 +199,13 @@ public function processNode(Node $node, Scope $scope): array
111199
if ($lineLength > $this->maxLineLength) {
112200
$this->markLineAsProcessed($filePath, $lineNumber);
113201

114-
return [
115-
RuleErrorBuilder::message($this->buildErrorMessage($lineNumber, $lineLength))
116-
->identifier(self::IDENTIFIER)
117-
->line($lineNumber)
118-
->build()
119-
];
202+
$errors[] = RuleErrorBuilder::message($this->buildErrorMessage($lineNumber, $lineLength))
203+
->identifier(self::IDENTIFIER)
204+
->line($lineNumber)
205+
->build();
120206
}
121207

122-
return [];
208+
return $errors;
123209
}
124210

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

256+
private function isNamespaceLine(string $filePath, int $lineNumber): bool
257+
{
258+
return isset($this->namespaceLines[$filePath][$lineNumber]);
259+
}
260+
261+
private function markLineAsNamespace(string $filePath, int $lineNumber): void
262+
{
263+
if (!isset($this->namespaceLines[$filePath])) {
264+
$this->namespaceLines[$filePath] = [];
265+
}
266+
267+
$this->namespaceLines[$filePath][$lineNumber] = true;
268+
}
269+
270+
private function isDocBlockLine(string $filePath, int $lineNumber): bool
271+
{
272+
return isset($this->docBlockLines[$filePath][$lineNumber]);
273+
}
274+
275+
private function markLineAsDocBlock(string $filePath, int $lineNumber): void
276+
{
277+
if (!isset($this->docBlockLines[$filePath])) {
278+
$this->docBlockLines[$filePath] = [];
279+
}
280+
281+
$this->docBlockLines[$filePath][$lineNumber] = true;
282+
}
283+
170284
private function getLineLength(string $filePath, int $lineNumber): int
171285
{
172286
// Cache file line lengths to avoid reading the same file multiple times
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\PHPStanRules\Tests\TestCases\CleanCode;
6+
7+
use Phauthentic\PHPStanRules\CleanCode\MaxLineLengthRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* Test the new array API for ignore options
13+
*
14+
* @extends RuleTestCase<MaxLineLengthRule>
15+
*/
16+
class MaxLineLengthRuleArrayApiTest extends RuleTestCase
17+
{
18+
protected function getRule(): Rule
19+
{
20+
// Use the new array API to ignore use statements via the array
21+
return new MaxLineLengthRule(80, [], false, ['useStatements' => true]);
22+
}
23+
24+
/**
25+
* Test that the new array API works for useStatements
26+
*/
27+
public function testArrayApiForUseStatements(): void
28+
{
29+
// Lines 5, 6, 7 have use statements that exceed 80 characters - should be ignored
30+
// Line 16 has a method signature that exceeds 80 characters - should be detected
31+
// Line 18 has a variable assignment that exceeds 80 characters - should be detected
32+
$this->analyse([__DIR__ . '/../../../data/MaxLineLengthLongUseStatementsClass.php'], [
33+
[
34+
'Line 16 exceeds the maximum length of 80 characters (found 117 characters).',
35+
16,
36+
],
37+
[
38+
'Line 18 exceeds the maximum length of 80 characters (found 114 characters).',
39+
18,
40+
],
41+
]);
42+
}
43+
}
44+

0 commit comments

Comments
 (0)