Skip to content

Commit

Permalink
Implement TodoByVersionRule
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Dec 18, 2023
1 parent b0f48a2 commit b79691f
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"keywords": ["dev", "phpstan", "phpstan-extension", "static analysis"],

"require": {
"php": "^7.4 || ^8.0"
"php": "^7.4 || ^8.0",
"composer/semver": "^3.4"
},

"require-dev": {
Expand Down
145 changes: 145 additions & 0 deletions src/TodoByVersionRule.php
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;
}
}
90 changes: 90 additions & 0 deletions tests/TodoByVersionRuleTest.php
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,
]
]
];
}

}
10 changes: 10 additions & 0 deletions tests/data/version.php
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

0 comments on commit b79691f

Please sign in to comment.