Skip to content

Commit

Permalink
SlevomatCodingStandard.Functions.FunctionLengthSniff: New options "in…
Browse files Browse the repository at this point in the history
…cludeComments" and "includeWhitespace"
  • Loading branch information
bkdotcom authored and kukulich committed Mar 29, 2022
1 parent a0b5814 commit 9da58ae
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 10 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ Reports closures not using `$this` that are not declared `static`.

Disallows long functions. This sniff provides the following setting:

* `includeComments`: should comments be included in the count (default value is false).
* `includeWhitespace`: shoud empty lines be included in the count (default value is false).
* `maxLinesLength`: specifies max allowed function lines length (default value is 20).

#### SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall 🔧
Expand Down
91 changes: 83 additions & 8 deletions SlevomatCodingStandard/Helpers/FunctionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Generator;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use SlevomatCodingStandard\Helpers\Annotation\ParameterAnnotation;
use SlevomatCodingStandard\Helpers\Annotation\ReturnAnnotation;
use function array_filter;
Expand All @@ -17,6 +18,7 @@
use function preg_match;
use function preg_replace;
use function sprintf;
use function substr_count;
use const T_ANON_CLASS;
use const T_BITWISE_AND;
use const T_CLASS;
Expand All @@ -32,6 +34,7 @@
use const T_TRAIT;
use const T_USE;
use const T_VARIABLE;
use const T_WHITESPACE;
use const T_YIELD;
use const T_YIELD_FROM;

Expand All @@ -41,6 +44,9 @@
class FunctionHelper
{

public const LINE_INCLUDE_COMMENT = 1;
public const LINE_INCLUDE_WHITESPACE = 2;

public const SPECIAL_FUNCTIONS = [
'array_key_exists',
'array_slice',
Expand Down Expand Up @@ -428,19 +434,88 @@ static function (int $functionOrMethodPointer) use ($phpcsFile): bool {
);
}

public static function getFunctionLengthInLines(File $file, int $position): int
/**
* @param File $file
* @param int $functionPosition
* @param int $flags optional bitmask of self::LINE_INCLUDE_* constants
*/
public static function getFunctionLengthInLines(File $file, int $functionPosition, int $flags = 0): int
{
$tokens = $file->getTokens();
$token = $tokens[$position];

if (self::isAbstract($file, $position)) {
if (self::isAbstract($file, $functionPosition)) {
return 0;
}

$firstToken = $tokens[$token['scope_opener']];
$lastToken = $tokens[$token['scope_closer']];
$includeWhitespace = ($flags & self::LINE_INCLUDE_WHITESPACE) === self::LINE_INCLUDE_WHITESPACE;
$includeComments = ($flags & self::LINE_INCLUDE_COMMENT) === self::LINE_INCLUDE_COMMENT;

return $lastToken['line'] - $firstToken['line'] - 1;
$tokens = $file->getTokens();
$token = $tokens[$functionPosition];

$tokenOpenerPosition = $token['scope_opener'];
$tokenCloserPosition = $token['scope_closer'];
$tokenOpenerLine = $tokens[$tokenOpenerPosition]['line'];
$tokenCloserLine = $tokens[$tokenCloserPosition]['line'];

$lineCount = 0;
$lastCommentLine = null;
$previousIncludedPosition = null;

for ($position = $tokenOpenerPosition; $position <= $tokenCloserPosition - 1; $position++) {
$token = $tokens[$position];
if ($includeComments === false) {
if (in_array($token['code'], Tokens::$commentTokens, true)) {
if (
$previousIncludedPosition !== null &&
substr_count($token['content'], $file->eolChar) > 0 &&
$token['line'] === $tokens[$previousIncludedPosition]['line']
) {
// Comment with linebreak starting on same line as included Token
$lineCount++;
}
// Don't include comment
$lastCommentLine = $token['line'];
continue;
}
if (
$previousIncludedPosition !== null &&
$token['code'] === T_WHITESPACE &&
$token['line'] === $lastCommentLine &&
$token['line'] !== $tokens[$previousIncludedPosition]['line']
) {
// Whitespace after block comment... still on comment line...
// Ignore along with the comment
continue;
}
}
if ($token['code'] === T_WHITESPACE) {
$nextNonWhitespacePosition = $file->findNext(T_WHITESPACE, $position + 1, $tokenCloserPosition + 1, true);
if (
$includeWhitespace === false &&
$token['column'] === 1 &&
$nextNonWhitespacePosition !== false &&
$tokens[$nextNonWhitespacePosition]['line'] !== $token['line']
) {
// This line is nothing but whitepace
$position = $nextNonWhitespacePosition - 1;
continue;
}
if ($previousIncludedPosition === $tokenOpenerPosition && $token['line'] === $tokenOpenerLine) {
// Don't linclude line break after opening "{"
// Unless there was code or an (included) comment following the "{"
continue;
}
}
if ($token['code'] !== T_WHITESPACE) {
$previousIncludedPosition = $position;
}
$newLineFoundCount = substr_count($token['content'], $file->eolChar);
$lineCount += $newLineFoundCount;
}
if ($tokens[$previousIncludedPosition]['line'] === $tokenCloserLine) {
// There is code or comment on the closing "}" line...
$lineCount++;
}
return $lineCount;
}

/**
Expand Down
19 changes: 18 additions & 1 deletion SlevomatCodingStandard/Sniffs/Functions/FunctionLengthSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\FunctionHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use function array_filter;
use function array_keys;
use function array_reduce;
use function sprintf;
use const T_FUNCTION;

Expand All @@ -17,6 +20,12 @@ class FunctionLengthSniff implements Sniff
/** @var int */
public $maxLinesLength = 20;

/** @var bool */
public $includeComments = false;

/** @var bool */
public $includeWhitespace = false;

/**
* @return array<int, (int|string)>
*/
Expand All @@ -32,7 +41,15 @@ public function register(): array
*/
public function process(File $file, $functionPointer): void
{
$length = FunctionHelper::getFunctionLengthInLines($file, $functionPointer);
$flags = array_keys(array_filter([
FunctionHelper::LINE_INCLUDE_COMMENT => $this->includeComments,
FunctionHelper::LINE_INCLUDE_WHITESPACE => $this->includeWhitespace,
]));
$flags = array_reduce($flags, static function ($carry, $flag): int {
return $carry | $flag;
}, 0);

$length = FunctionHelper::getFunctionLengthInLines($file, $functionPointer, $flags);

if ($length <= SniffSettingsHelper::normalizeInteger($this->maxLinesLength)) {
return;
Expand Down
23 changes: 23 additions & 0 deletions tests/Helpers/FunctionHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,29 @@ public function testGetAllFunctionNames(): void
self::assertSame(['foo', 'boo'], FunctionHelper::getAllFunctionNames($phpcsFile));
}

public function testGetFunctionLengthInLines(): void
{
$phpcsFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionLength.php');
$functionPointer = $this->findFunctionPointerByName($phpcsFile, 'countMe');

self::assertSame(2, FunctionHelper::getFunctionLengthInLines($phpcsFile, $functionPointer));
self::assertSame(7, FunctionHelper::getFunctionLengthInLines(
$phpcsFile,
$functionPointer,
FunctionHelper::LINE_INCLUDE_COMMENT
));
self::assertSame(3, FunctionHelper::getFunctionLengthInLines(
$phpcsFile,
$functionPointer,
FunctionHelper::LINE_INCLUDE_WHITESPACE
));
self::assertSame(8, FunctionHelper::getFunctionLengthInLines(
$phpcsFile,
$functionPointer,
FunctionHelper::LINE_INCLUDE_COMMENT | FunctionHelper::LINE_INCLUDE_WHITESPACE
));
}

public function testFindClassPointer(): void
{
$phpcsFile = $this->getCodeSnifferFile(__DIR__ . '/data/functionNames.php');
Expand Down
16 changes: 16 additions & 0 deletions tests/Helpers/data/functionLength.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

class FooClass
{

public function countMe(): string
{
/*
block comment
*/
1 + 1; // slash comment
# hash comment

return 'This is the only line that matters (by default)'; /* inline comment */
/* inline comment */ }
}
30 changes: 29 additions & 1 deletion tests/Sniffs/Functions/FunctionLengthSniffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,35 @@ public function testErrors(): void

self::assertSame(1, $report->getErrorCount());

self::assertSniffError($report, 3, FunctionLengthSniff::CODE_FUNCTION_LENGTH);
self::assertSniffError($report, 3, FunctionLengthSniff::CODE_FUNCTION_LENGTH, 'Currently using 21 lines');
}

public function testWithComments(): void
{
$report = self::checkFile(
__DIR__ . '/data/functionLengthErrors.php',
[
'includeComments' => true,
]
);

self::assertSame(1, $report->getErrorCount());

self::assertSniffError($report, 3, FunctionLengthSniff::CODE_FUNCTION_LENGTH, 'Currently using 24 lines');
}

public function testWithWhitespace(): void
{
$report = self::checkFile(
__DIR__ . '/data/functionLengthErrors.php',
[
'includeWhitespace' => true,
]
);

self::assertSame(1, $report->getErrorCount());

self::assertSniffError($report, 3, FunctionLengthSniff::CODE_FUNCTION_LENGTH, 'Currently using 22 lines');
}

}
4 changes: 4 additions & 0 deletions tests/Sniffs/Functions/data/functionLengthErrors.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

function dummyFunctionWithTooManyLines()
{
/*
Block comment
*/

echo 'line 1';
echo 'line 2';
echo 'line 3';
Expand Down

0 comments on commit 9da58ae

Please sign in to comment.