Skip to content

Commit

Permalink
Support more variations of todo comments
Browse files Browse the repository at this point in the history
Co-authored-by: Markus Staab <[email protected]>
  • Loading branch information
m29corey and staabm authored Dec 17, 2023
1 parent c175585 commit 9ab6e8a
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function doFoo() {

Every comment which matches the [supported pattern](https://github.com/staabm/phpstan-todo-by/blob/main/src/TodoByRule.php#L15) will be checked.

A todo comment can also consist of just a date without any text, like `// TODO 2023-12-14`.
A todo comment can also consist of just a date without any text, like `// @todo 2023-12-14`.
When a text is given after the date, this text will be picked up for the PHPStan error message.

The supported dateformat is `YYYY-MM-DD`. See [all supported examples](https://github.com/staabm/phpstan-todo-by/blob/main/tests/data/example.php) in the Testsuite.
Expand Down
93 changes: 66 additions & 27 deletions src/TodoByRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@
namespace staabm\PHPStanTodoBy;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\VirtualNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
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 TodoByRule implements Rule
{
private const PATTERN = '/^TODO:?\s*([0-9]{4}-[0-9]{2}-[0-9]{2}):?(.*)$/';
private const PATTERN = <<<'REGEXP'
/
@?TODO # possible @ prefix
\s*[:-]?\s* # optional colon or hyphen
(?P<date>\d{4}-\d{2}-\d{2}) # date consisting of YYYY-MM-DD format
\s*[:-]?\s* # optional colon or hyphen
(?P<comment>.*) # rest of line as comment text
/ix
REGEXP;

private int $now;
private bool $nonIgnorable;
Expand All @@ -28,7 +44,7 @@ public function getNodeType(): string
return Node::class;
}

public function processNode(\PhpParser\Node $node, \PHPStan\Analyser\Scope $scope): array
public function processNode(Node $node, Scope $scope): array
{
if (
$node instanceof VirtualNode
Expand All @@ -38,39 +54,62 @@ public function processNode(\PhpParser\Node $node, \PHPStan\Analyser\Scope $scop
return [];
}

$comments = $node->getComments();
if (count($comments) === 0) {
return [];
}

$errors = [];
foreach($comments as $comment) {
$text = ltrim($comment->getText(), "\t /");
if (!str_starts_with($text, 'TODO')) {
continue;
}

if (preg_match(self::PATTERN, $text, $matches) !== 1) {
continue;
}
foreach ($node->getComments() as $comment) {

$text = $comment->getText();

$date = $matches[1];
$todoText = trim($matches[2]);
if (strtotime($date) > $this->now) {
/**
* 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;
}

if ($todoText === '') {
$errorMessage = 'comment expired on '. $date .'.';
} else {
$errorMessage = "comment '$todoText' expired on ". $date .'.';
}
/** @var array<int, array<array{0: string, 1: int}>> $matches */
foreach ($matches as $match) {

$date = $match['date'][0];
$todoText = trim($match['comment'][0]);

/**
* strtotime() will parse date-only values with time set to 00:00:00.
* This is fine, because this will count any expiration matching
* the current date as expired, except when ran exactly at 00:00:00.
*/
if (strtotime($date) > $this->now) {
continue;
}


// Have always present date at the start of the message.
// If there is further text, append it.
if ($todoText !== '') {
$errorMessage = "Expired on {$date}: {$todoText}";
} else {
$errorMessage = "Comment expired on {$date}";
}

$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($comment->getStartLine());
if ($this->nonIgnorable) {
$errBuilder->nonIgnorable();
$errBuilder = RuleErrorBuilder::message($errorMessage)->line($messageLine);
if ($this->nonIgnorable) {
$errBuilder->nonIgnorable();
}
$errors[] = $errBuilder->build();
}
$errors[] = $errBuilder->build();
}

return $errors;
Expand Down
58 changes: 45 additions & 13 deletions tests/TodoByRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,76 @@ public function testRule(): void
{
$this->analyse([__DIR__ . '/data/example.php'], [
[
"comment 'Expired comment1' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired comment1',
9,
],
[
"comment 'Expired comment2' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired comment2',
10,
],
[
"comment 'Expired comment3' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired comment3',
11,
],
[
"comment 'Expired comment4' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired comment4',
12,
],
[
"comment 'Expired comment5' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired comment5',
13,
],
[
"comment 'Expired commentX' expired on 2023-12-14.",
'Expired on 2023-12-14: Expired commentX',
19,
],
[
"comment expired on 2023-12-14.",
'Comment expired on 2023-12-14',
21,
],
[
"comment 'method comment' expired on 2023-12-14.",
27,
'Expired on 2023-12-14: class comment',
29,
],
[
"comment 'in method comment1' expired on 2023-12-14.",
29,
'Expired on 2023-12-13: class comment',
30,
],
[
"Expired on 2023-12-14: method comment",
34,
],
[
'Expired on 2023-12-14: in method comment1',
36,
],
[
'Expired on 2023-12-14: in method comment2',
38,
],
[
'Expired on 2023-12-14: Convert to standard Drupal $content code.',
43,
],
[
'Expired on 2023-12-14: Decide to fix all the broken instances of class as a string',
46,
],
[
'Expired on 2023-12-14: fix it',
48,
],
[
'Expired on 2023-12-14: fix it',
49,
],
[
'Expired on 2023-12-14: fix it',
50,
],
[
"comment 'in method comment2' expired on 2023-12-14.",
31,
'Expired on 2023-12-14: fix it',
51,
],
]);
}
Expand Down
18 changes: 18 additions & 0 deletions tests/data/example.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ function doFooBar():void {

}

/**
* other text
*
* @todo 2023-12-14 class comment
* @TODO 2023-12-13 - class comment
* more comment data
*/
class Z {
// TODO: 2023-12-14 method comment
public function XY():void {
Expand All @@ -31,3 +38,14 @@ public function XY():void {
// TODO 2023-12-14: in method comment2
}
}

/**
* @todo 2023-12-14 - Convert to standard Drupal $content code.
*/

// @todo 2023-12-14 Decide to fix all the broken instances of class as a string

// @todo: 2023-12-14 fix it
// @todo 2023-12-14: fix it
// todo - 2023-12-14 fix it
// todo 2023-12-14 - fix it

0 comments on commit 9ab6e8a

Please sign in to comment.