-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
247 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
<?php | ||
|
||
namespace staabm\PHPStanTodoBy; | ||
|
||
use Composer\Semver\Comparator; | ||
use Composer\Semver\VersionParser; | ||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Node\VirtualNode; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\ShouldNotHappenException; | ||
use function preg_match_all; | ||
use function strtotime; | ||
use function substr_count; | ||
use function time; | ||
use function trim; | ||
use const PREG_OFFSET_CAPTURE; | ||
use const PREG_SET_ORDER; | ||
|
||
/** | ||
* @implements Rule<Node> | ||
*/ | ||
final class TodoByVersionRule implements Rule | ||
{ | ||
private const COMPARATORS = ['<', '>', '=']; | ||
|
||
private const PATTERN = <<<'REGEXP' | ||
/ | ||
@?TODO # possible @ prefix | ||
@?[a-zA-Z0-9_-]*\s* # optional username | ||
\s*[:-]?\s* # optional colon or hyphen | ||
(?P<version>[<>=]+[^\s:\-]+) # version | ||
\s*[:-]?\s* # optional colon or hyphen | ||
(?P<comment>.*) # rest of line as comment text | ||
/ix | ||
REGEXP; | ||
|
||
private string $referenceVersion; | ||
private bool $nonIgnorable; | ||
|
||
private VersionParser $versionParser; | ||
|
||
public function __construct(bool $nonIgnorable, string $referenceVersion) | ||
{ | ||
$this->versionParser = new VersionParser(); | ||
|
||
$this->referenceVersion = $this->versionParser->normalize($referenceVersion); | ||
$this->nonIgnorable = $nonIgnorable; | ||
} | ||
|
||
private function getVersionComparator(string $version): ?string { | ||
$comparator = null; | ||
for($i = 0; $i < strlen($version); $i++) { | ||
if (!in_array($version[$i], self::COMPARATORS)) { | ||
break; | ||
} | ||
$comparator .= $version[$i]; | ||
} | ||
|
||
return $comparator; | ||
|
||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if ( | ||
$node instanceof VirtualNode | ||
|| $node instanceof Node\Expr | ||
) { | ||
// prevent duplicate errors | ||
return []; | ||
} | ||
|
||
$errors = []; | ||
|
||
foreach ($node->getComments() as $comment) { | ||
|
||
$text = $comment->getText(); | ||
|
||
/** | ||
* PHP doc comments have the entire multi-line comment as the text. | ||
* Since this could potentially contain multiple "todo" comments, we need to check all lines. | ||
* This works for single line comments as well. | ||
* | ||
* PREG_OFFSET_CAPTURE: Track where each "todo" comment starts within the whole comment text. | ||
* PREG_SET_ORDER: Make each value of $matches be structured the same as if from preg_match(). | ||
*/ | ||
if (preg_match_all(self::PATTERN, $text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER) === FALSE) { | ||
continue; | ||
} | ||
|
||
/** @var array<int, array<array{0: string, 1: int}>> $matches */ | ||
foreach ($matches as $match) { | ||
|
||
$version = $match['version'][0]; | ||
$todoText = trim($match['comment'][0]); | ||
|
||
$versionComparator = $this->getVersionComparator($version); | ||
$plainVersion = ltrim($version, implode("", self::COMPARATORS)); | ||
$normalized = $this->versionParser->normalize($plainVersion); | ||
|
||
$expired = false; | ||
if ($versionComparator === '<') { | ||
$expired = Comparator::greaterThanOrEqualTo($this->referenceVersion, $normalized); | ||
} elseif ($versionComparator === '>') { | ||
$expired = Comparator::greaterThan($this->referenceVersion, $normalized); | ||
} | ||
|
||
if (!$expired) { | ||
continue; | ||
} | ||
|
||
// Have always present date at the start of the message. | ||
// If there is further text, append it. | ||
if ($todoText !== '') { | ||
$errorMessage = "Version requirement {$version} not satisfied: {$todoText}"; | ||
} else { | ||
$errorMessage = "Version requirement {$version} not satisfied"; | ||
} | ||
|
||
$wholeMatchStartOffset = $match[0][1]; | ||
|
||
// Count the number of newlines between the start of the whole comment, and the start of the match. | ||
$newLines = substr_count($text, "\n", 0, $wholeMatchStartOffset); | ||
|
||
// Set the message line to match the line the comment actually starts on. | ||
$messageLine = $comment->getStartLine() + $newLines; | ||
|
||
$errBuilder = RuleErrorBuilder::message($errorMessage)->line($messageLine); | ||
if ($this->nonIgnorable) { | ||
$errBuilder->nonIgnorable(); | ||
} | ||
$errors[] = $errBuilder->build(); | ||
} | ||
} | ||
|
||
return $errors; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<?php | ||
|
||
namespace staabm\PHPStanTodoBy\Tests; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
use staabm\PHPStanTodoBy\TodoByDateRule; | ||
use staabm\PHPStanTodoBy\TodoByVersionRule; | ||
|
||
/** | ||
* @extends RuleTestCase<TodoByVersionRule> | ||
*/ | ||
final class TodoByVersionRuleTest extends RuleTestCase | ||
{ | ||
private string $referenceVersion; | ||
protected function getRule(): Rule | ||
{ | ||
return new TodoByVersionRule(true, $this->referenceVersion); | ||
} | ||
|
||
/** | ||
* @param list<array{0: string, 1: int, 2?: string|null}> $errors | ||
* @dataProvider provideErrors | ||
*/ | ||
public function testRule(string $referenceVersion, array $errors): void | ||
{ | ||
$this->referenceVersion = $referenceVersion; | ||
|
||
$this->analyse([__DIR__ . '/data/version.php'], $errors); | ||
} | ||
|
||
/** | ||
* @return iterable<array{string, list<array{0: string, 1: int, 2?: string|null}>}> | ||
*/ | ||
static public function provideErrors(): iterable | ||
{ | ||
yield [ | ||
"0.1", | ||
[ | ||
] | ||
]; | ||
|
||
yield [ | ||
"1.0", | ||
[ | ||
[ | ||
'Version requirement <1.0.0 not satisfied: This has to be in the first major release', | ||
5, | ||
], | ||
[ | ||
'Version requirement <1.0.0 not satisfied', | ||
10, | ||
] | ||
] | ||
]; | ||
|
||
yield [ | ||
"123.4", | ||
[ | ||
[ | ||
'Version requirement <1.0.0 not satisfied: This has to be in the first major release', | ||
5, | ||
], | ||
[ | ||
'Version requirement <1.0.0 not satisfied', | ||
10, | ||
] | ||
] | ||
]; | ||
|
||
yield [ | ||
"123.5", | ||
[ | ||
[ | ||
'Version requirement <1.0.0 not satisfied: This has to be in the first major release', | ||
5, | ||
], | ||
[ | ||
'Version requirement >123.4 not satisfied: Must fix this or bump the version', | ||
7, | ||
], | ||
[ | ||
'Version requirement <1.0.0 not satisfied', | ||
10, | ||
] | ||
] | ||
]; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace ExampleVersion; | ||
|
||
// TODO: <1.0.0 This has to be in the first major release | ||
function doFoo():void { | ||
// TODO >123.4: Must fix this or bump the version | ||
} | ||
|
||
// TODO: <1.0.0 |