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
8 changes: 1 addition & 7 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,8 @@ jobs:
uses: sympress/workflows/.github/workflows/lint-workflows.yml@v1

qa:
strategy:
fail-fast: false
matrix:
php-version:
- '8.4'
- '8.5'
uses: sympress/workflows/.github/workflows/sympress-qa.yml@v1
with:
php_version: ${{ matrix.php-version }}
php_version: '8.5'
secrets:
COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }}
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The package is designed for projects that keep domain and application code frame

The SymPress Coding Standards package requires:

- PHP 8.4 or newer to run the standard
- PHP 8.5 or newer to run the standard
- Composer 2
- PHP_CodeSniffer 3.13.5 or newer on the 3.x line
- PHPCompatibility 9.3 or 10
Expand Down Expand Up @@ -256,6 +256,22 @@ Example: change the maximum function length:
</rule>
```

Example: change class and file length guardrails:

```xml
<rule ref="SymPress.Classes.ClassLength">
<properties>
<property name="maxLength" type="integer" value="700" />
</properties>
</rule>

<rule ref="SymPress.Files.FileLength">
<properties>
<property name="maxLength" type="integer" value="1200" />
</properties>
</rule>
```

Example: configure PSR-4 checks:

```xml
Expand Down
6 changes: 6 additions & 0 deletions SymPress-Enterprise-LTS/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<rule ref="SymPress.Classes.AccessorNaming">
<type>warning</type>
</rule>
<rule ref="SymPress.Classes.ClassLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Classes.PropertyLimit">
<type>warning</type>
</rule>
Expand All @@ -24,6 +27,9 @@
<rule ref="SymPress.Functions.FunctionLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Files.FileLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Variables.RedundantAssignment">
<type>warning</type>
</rule>
Expand Down
6 changes: 6 additions & 0 deletions SymPress-Enterprise-Modern/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<rule ref="SymPress.Classes.AccessorNaming">
<type>warning</type>
</rule>
<rule ref="SymPress.Classes.ClassLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Classes.PropertyLimit">
<type>warning</type>
</rule>
Expand All @@ -24,6 +27,9 @@
<rule ref="SymPress.Functions.FunctionLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Files.FileLength">
<type>warning</type>
</rule>
<rule ref="SymPress.Variables.RedundantAssignment">
<type>warning</type>
</rule>
Expand Down
2 changes: 2 additions & 0 deletions SymPress-Pure/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<rule ref="SymPress.Classes.DeprecatedSerializableInterface" />
<rule ref="SymPress.Classes.DeprecatedSerializeMagicMethod" />
<rule ref="SymPress.Classes.AccessorNaming" />
<rule ref="SymPress.Classes.ClassLength" />
<rule ref="SymPress.Classes.PropertyLimit" />
<rule ref="SymPress.Complexity.NestingLevel">
<properties>
Expand All @@ -30,6 +31,7 @@
<property name="lineLimit" type="integer" value="120" />
</properties>
</rule>
<rule ref="SymPress.Files.FileLength" />
<rule ref="SymPress.Formatting.AlphabeticalUseStatements" />
<rule ref="SymPress.Formatting.UnnecessaryNamespaceUsage" />
<rule ref="SymPress.Functions.ArgumentTypeDeclaration" />
Expand Down
177 changes: 177 additions & 0 deletions SymPress/Sniffs/Classes/ClassLengthSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php

declare(strict_types=1);

namespace SymPressCS\SymPress\Sniffs\Classes;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SymPressCS\SymPress\Helpers\Names;

final class ClassLengthSniff implements Sniff
{
public bool $ignoreBlankLines = true;
public bool $ignoreComments = true;
public bool $ignoreDocBlocks = true;
public int $maxLength = 500;

/** @return list<int|string> */
public function register(): array
{
return [
T_CLASS,
T_ENUM,
T_INTERFACE,
T_TRAIT,
];
}

/**
* @param File $phpcsFile
* @param int $stackPtr
*/
public function process(File $phpcsFile, $stackPtr): void
{
$length = $this->structureLinesCount($phpcsFile, $stackPtr);
if ($length <= $this->maxLength) {
return;
}

$ignored = $this->ignoredLines();
$ignoring = $ignored ? sprintf(' (ignoring: %s)', implode(', ', $ignored)) : '';
$tokenTypeName = Names::tokenTypeName($phpcsFile, $stackPtr);

$phpcsFile->addWarning(
'%s length (%d) exceeds allowed maximum of %d lines%s',
$stackPtr,
'TooLong',
[ucfirst($tokenTypeName), $length, $this->maxLength, $ignoring],
);
}

private function structureLinesCount(File $file, int $position): int
{
/** @var array<int, array<string, mixed>> $tokens */
$tokens = $file->getTokens();
$token = $tokens[$position] ?? [];

if (
!array_key_exists('scope_opener', $token)
|| !array_key_exists('scope_closer', $token)
) {
return 0;
}

$start = (int) $token['scope_opener'];
$end = (int) $token['scope_closer'];
$length = (int) $tokens[$end]['line'] - (int) $tokens[$start]['line'];

if ($length <= $this->maxLength) {
return $length;
}

return $length - $this->collectLinesToExclude($start, $end, $tokens);
}

/**
* @param int $start
* @param int $end
* @param array<int, array<string, mixed>> $tokens
*/
private function collectLinesToExclude(int $start, int $end, array $tokens): int
{
$docBlocks = [];
$linesData = [];
$skipLines = [$tokens[$start + 1]['line'], $tokens[$end]['line']];

for ($i = $start + 1; $i < $end - 1; $i++) {
if (in_array($tokens[$i]['line'], $skipLines, true)) {
continue;
}

$docBlocks = $this->docBlocksData($tokens, $i, $docBlocks);
$linesData = $this->ignoredLinesData($tokens[$i], $linesData);
}

$empty = array_filter(array_column($linesData, 'empty'));
$onlyComment = array_filter(array_column($linesData, 'only-comment'));
$toExcludeCount = (int) array_sum($docBlocks);

if ($this->ignoreBlankLines) {
$toExcludeCount += count($empty);
}
if ($this->ignoreComments) {
$toExcludeCount += count($onlyComment) - count($empty);
}

return $toExcludeCount;
}

/**
* @param array<string, mixed> $token
* @param array<int, array{empty:bool, only-comment:bool}> $lines
* @return array<int, array{empty:bool, only-comment:bool}>
*/
private function ignoredLinesData(array $token, array $lines): array
{
$line = (int) $token['line'];
if (!array_key_exists($line, $lines)) {
$lines[$line] = ['empty' => true, 'only-comment' => true];
}

if (!in_array($token['code'], [T_COMMENT, T_WHITESPACE], true)) {
$lines[$line]['only-comment'] = false;
}

if ($token['code'] !== T_WHITESPACE) {
$lines[$line]['empty'] = false;
}

return $lines;
}

/**
* @param array<int, array<string, mixed>> $tokens
* @param int $position
* @param list<int> $docBlocks
* @return list<int>
*/
private function docBlocksData(array $tokens, int $position, array $docBlocks): array
{
if (
!$this->ignoreDocBlocks
|| $tokens[$position]['code'] !== T_DOC_COMMENT_OPEN_TAG
) {
return $docBlocks;
}

$closer = $tokens[$position]['comment_closer'] ?? null;

$docBlocks[] = is_numeric($closer)
? 1 + (int) $tokens[(int) $closer]['line'] - (int) $tokens[$position]['line']
: 1;

return $docBlocks;
}

/** @return list<string> */
private function ignoredLines(): array
{
$ignored = [];
$flags = [
'ignoreBlankLines' => 'blank lines',
'ignoreComments' => 'inline comments',
'ignoreDocBlocks' => 'doc blocks',
];

foreach ($flags as $flag => $type) {
if (!filter_var($this->{$flag}, FILTER_VALIDATE_BOOLEAN)) {
continue;
}

$ignored[] = $type;
}

return $ignored;
}
}
51 changes: 51 additions & 0 deletions SymPress/Sniffs/Files/FileLengthSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace SymPressCS\SymPress\Sniffs\Files;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

final class FileLengthSniff implements Sniff
{
public int $maxLength = 1000;

/** @return list<int|string> */
public function register(): array
{
return [
T_OPEN_TAG,
];
}

/**
* @param File $phpcsFile
* @param int $stackPtr
*/
public function process(File $phpcsFile, $stackPtr): int
{
$length = $this->fileLength($phpcsFile);
if ($length > $this->maxLength) {
$phpcsFile->addWarning(
'File length (%d) exceeds allowed maximum of %d lines',
$stackPtr,
'TooLong',
[$length, $this->maxLength],
);
}

return $phpcsFile->numTokens + 1;
}

private function fileLength(File $file): int
{
$contents = file_get_contents($file->getFilename());

if (!is_string($contents) || $contents === '') {
return 0;
}

return substr_count($contents, "\n") + (str_ends_with($contents, "\n") ? 0 : 1);
}
}
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
}
],
"require": {
"php": ">=8.4 <9.0",
"php": "^8.5",
"automattic/vipwpcs": "^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"phpcompatibility/php-compatibility": "^9.3 || ^10.0@alpha",
Expand Down Expand Up @@ -81,14 +81,14 @@
"Composer\\Config::disableProcessTimeout",
"phpunit"
],
"tests:coverage": [
"Composer\\Config::disableProcessTimeout",
"phpunit --coverage-text --coverage-clover tmp/coverage.xml --coverage-html tmp/coverage"
],
"qa": [
"@cs",
"@static-analysis",
"@tests"
],
"tests:coverage": [
"Composer\\Config::disableProcessTimeout",
"phpunit --coverage-text --coverage-clover tmp/coverage.xml --coverage-html tmp/coverage"
]
}
}
2 changes: 1 addition & 1 deletion docs/Compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SymPress Coding Standards separates the PHP version required to run the standard

## Runtime Requirement

The package can be installed on PHP 8.4 or newer. Projects that use syntax introduced after the PHP version running PHP_CodeSniffer should run PHPCS on the same PHP minor version as the project target, because tokenization of newer language syntax depends on the executing PHP runtime.
The package can be installed on PHP 8.5 or newer. Projects that use syntax introduced after the PHP version running PHP_CodeSniffer should run PHPCS on the same PHP minor version as the project target, because tokenization of newer language syntax depends on the executing PHP runtime.

## Enterprise Profiles

Expand Down
2 changes: 2 additions & 0 deletions docs/Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ profile, not in the shared enterprise default.
| `SymPress.Arrays.ArrayDoubleArrowAlignment` | Formatting | Error, fixable where safe | Normalizes multiline array alignment. |
| `SymPress.Arrays.MultiLineArray` | Formatting | Error, fixable where safe | Keeps multiline arrays stable in diffs. |
| `SymPress.Classes.AccessorNaming` | Architecture | Warning in adoption, strict in Next | Opinionated. Tune or demote when a domain model intentionally avoids getter/setter naming. |
| `SymPress.Classes.ClassLength` | Maintainability | Warning | Signals classes that likely need responsibility review. Default threshold: 500 effective lines. |
| `SymPress.Classes.DeprecatedSerializableInterface` | Risk | Error | Blocks deprecated serialization APIs. |
| `SymPress.Classes.DeprecatedSerializeMagicMethod` | Risk | Error | Blocks deprecated magic serialization APIs. |
| `SymPress.Classes.PropertyLimit` | Architecture | Warning in enterprise profiles | Signals large objects. Treat as design pressure, not proof of a bug. |
| `SymPress.Complexity.NestingLevel` | Maintainability | Warning, then error at higher nesting | Thresholds are configurable. |
| `SymPress.ControlStructures.AlternativeSyntax` | Template formatting | Warning | Applies to template syntax rules. |
| `SymPress.ControlStructures.DisallowElse` | Architecture | Warning in enterprise profiles | Encourages early returns. Exclude when legacy diff churn is too high. |
| `SymPress.Encoding.Utf8EncodingComment` | Formatting | Warning | Keeps source encoding comments consistent. |
| `SymPress.Files.FileLength` | Maintainability | Warning | Signals oversized files. Default threshold: 1000 physical lines. |
| `SymPress.Files.LineLength` | Maintainability | Warning | WordPress i18n functions are allowed in the WordPress layer. |
| `SymPress.Formatting.AlphabeticalUseStatements` | Formatting | Error, fixable | Mechanical import ordering. |
| `SymPress.Formatting.TrailingSemicolon` | Template formatting | Error, fixable | Template shorthand output cleanup. |
Expand Down
Loading