Skip to content

Commit

Permalink
Adjusted parsing to handle php doc style comments.
Browse files Browse the repository at this point in the history
Which then required parsing multi-line comments.
  • Loading branch information
m29corey committed Dec 16, 2023
1 parent 979a915 commit 1f7c156
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/
/composer.lock
/test.php
/.idea/
87 changes: 63 additions & 24 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 leading @
\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;

Expand All @@ -26,7 +42,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 @@ -36,35 +52,58 @@ 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) {

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

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) {

$errors[] = RuleErrorBuilder::message($errorMessage)->line($comment->getStartLine())->build();
$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.
$errorMessage = "Expired on {$date}";

// If there is further text, append it.
if ($todoText !== '') {

$errorMessage .= ": {$todoText}";
}

$whole_match_start_offset = $match[0][1];

// Count the number of newlines between the start of the whole comment, and the start of the match.
$new_lines = substr_count($text, "\n", 0, $whole_match_start_offset);

// Set the message line to match the line the comment actually starts on.
$message_line = $comment->getStartLine() + $new_lines;

$errors[] = RuleErrorBuilder::message($errorMessage)->line($message_line)->build();
}
}

return $errors;
Expand Down
34 changes: 21 additions & 13 deletions tests/TodoByRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,52 @@ 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.",
'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,
],
[
"comment 'in method comment2' expired on 2023-12-14.",
31,
'Expired on 2023-12-14: in method comment2',
38,
],
]);
}
Expand Down
7 changes: 7 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 Down

0 comments on commit 1f7c156

Please sign in to comment.