Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ Inspired by [parker-codes/todo-by](https://github.com/parker-codes/todo_by).

## Examples

The main idea is, that comments within the source code will be turned into PHPStan errors when a condition is satisfied, e.g. a date reached, a version met, a issue tracker ticket is closed.
The main idea is, that comments within the source code will be turned into PHPStan errors when a condition is satisfied, e.g. a date reached, a keyword, a version met, a issue tracker ticket is closed.

```php
<?php

// TODO: 2023-12-14 This comment turns into a PHPStan error as of 14th december 2023
function doFoo() { /* ... */ }

// TODO: asap Don’t let these these line in next commit
function legacyFunc() { /* ... */ }

// TODO https://github.com/staabm/phpstan-todo-by/issues/91 fix me when this GitHub issue is closed
class FooClass {}

Expand Down Expand Up @@ -44,6 +47,7 @@ When a text is given after the date, this text will be picked up for the PHPStan

**Out of the box** comments can expire by different constraints:
- by date with format of `YYYY-MM-DD` matched against the [reference-time](https://github.com/staabm/phpstan-todo-by#reference-time)
- by a keyword matched against a [list](https://github.com/staabm/phpstan-todo-by#keywords)
- by a full github issue url
- by a semantic version constraint matched against a Composer dependency (via `composer.lock`)

Expand Down Expand Up @@ -73,6 +77,7 @@ see examples of different comment variants which are supported:
* more comment data
*/

// TODO: keyword fix it before commit
// TODO: <1.0.0 This has to be in the first major release
// TODO >123.4: Must fix this or bump the version

Expand Down Expand Up @@ -124,6 +129,15 @@ parameters:

`TODOBY_REF_TIME="now+7days" vendor/bin/phpstan analyze`

### Keywords

A keyword list can be defined to create a PHPStan error each time one of these keywords is met after todo.

```neon
parameters:
todo_by:
keywords: ["asap", "now"]
```

### Reference version

Expand Down
10 changes: 10 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ parametersSchema:
todo_by: structure([
nonIgnorable: bool()
referenceTime: string()
keywords: listOf(string())
referenceVersion: string()
singleGitRepo: bool()
virtualPackages: arrayOf(string(), string())
Expand Down Expand Up @@ -37,6 +38,9 @@ parameters:
# any strtotime() compatible string
referenceTime: "now"

# list of keywords to create an error if one is met after todo
keywords: []

# "nextMajor", "nextMinor", "nextPatch" or a version string like "1.2.3"
referenceVersion: "nextMajor"

Expand Down Expand Up @@ -120,6 +124,12 @@ services:
arguments:
- %todo_by.referenceTime%

-
class: staabm\PHPStanTodoBy\TodoByKeywordRule
tags: [phpstan.rules.rule]
arguments:
- %todo_by.keywords%

-
class: staabm\PHPStanTodoBy\TodoByTicketRule

Expand Down
88 changes: 88 additions & 0 deletions src/TodoByKeywordRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace staabm\PHPStanTodoBy;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use staabm\PHPStanTodoBy\utils\CommentMatcher;
use staabm\PHPStanTodoBy\utils\ExpiredCommentErrorBuilder;

use function trim;

/**
* @implements Rule<Node>
*/
final class TodoByKeywordRule implements Rule
{
private const ERROR_IDENTIFIER = 'keyword';

private array $keywords;

private const PATTERN = <<<'REGEXP'
{
@?(?:TODO|FIXME|XXX) # possible @ prefix
@?[a-zA-Z0-9_-]* # optional username
\s*[:-]?\s* # optional colon or hyphen
\s+ # separator
(?P<keyword>(?:%s)) # keyword
\s*[:-]?\s* # optional colon or hyphen
(?P<comment>(?:(?!\*+/).)*) # rest of line as comment text, excluding block end
}ix
REGEXP;

private ExpiredCommentErrorBuilder $errorBuilder;

/**
* @param list<non-empty-string> $keywords
*/
public function __construct(
array $keywords,
ExpiredCommentErrorBuilder $errorBuilder
) {
$this->keywords = $keywords;
$this->errorBuilder = $errorBuilder;
}

public function getNodeType(): string
{
return Node::class;
}

public function processNode(Node $node, Scope $scope): array
{
if ([] === $this->keywords) {
return [];
}

$it = CommentMatcher::matchComments($node, sprintf(self::PATTERN, implode('|', $this->keywords)));

$errors = [];
foreach ($it as $comment => $matches) {
/** @var array<int, array<array{0: string, 1: int}>> $matches */
foreach ($matches as $match) {
$keyword = $match['keyword'][0];
$todoText = trim($match['comment'][0]);

// Have a keyword at the start of the message.
// If there is further text, append it.
if ('' !== $todoText) {
$errorMessage = "Should be processed ({$keyword}): ". rtrim($todoText, '.') .'.';
} else {
$errorMessage = "Comment should be processed ({$keyword}).";
}

$errors[] = $this->errorBuilder->buildError(
$comment->getText(),
$comment->getStartLine(),
$errorMessage,
self::ERROR_IDENTIFIER,
null,
$match[0][1]
);
}
}

return $errors;
}
}
135 changes: 135 additions & 0 deletions tests/TodoByKeywordRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace staabm\PHPStanTodoBy\Tests;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use staabm\PHPStanTodoBy\TodoByKeywordRule;
use staabm\PHPStanTodoBy\utils\ExpiredCommentErrorBuilder;

/**
* @extends RuleTestCase<TodoByKeywordRule>
* @internal
*/
final class TodoByKeywordRuleTest extends RuleTestCase
{
private array $keywords;

protected function getRule(): Rule
{
return new TodoByKeywordRule(
$this->keywords,
new ExpiredCommentErrorBuilder(true),
);
}

public function testRule(): void
{
$this->keywords = [
'keyword1',
'keyword2',
];

$this->analyse([__DIR__ . '/data/keyword.php'], [
[
'Should be processed (keyword1): comment1.',
9,
],
[
'Should be processed (keyword2): comment2.',
10,
],
[
'Should be processed (kEywORd1): comment3.',
11,
],
[
'Should be processed (keyword1): comment4.',
12,
],
[
'Should be processed (keyword1): comment5.',
13,
],
[
'Should be processed (keyword1): comment6.',
14,
],
[
'Should be processed (keyword1): comment7.',
15,
],
[
'Should be processed (keyword1): commentX.',
20,
],
[
'Comment should be processed (keyword1).',
22,
],
[
'Should be processed (keyword1): class comment.',
30,
],
[
'Should be processed (keyword1): class comment.',
31,
],
[
'Should be processed (keyword1): method comment.',
35,
],
[
'Should be processed (keyword1): in method comment1.',
37,
],
[
'Should be processed (keyword1): in method comment2.',
39,
],
[
'Should be processed (keyword1): Convert to standard Drupal $content code.',
44,
],
[
'Should be processed (keyword1): Decide to fix all the broken instances of class as a string.',
47,
],
[
'Should be processed (keyword1): fix it.',
49,
],
[
'Should be processed (keyword1): fix it.',
50,
],
[
'Should be processed (keyword1): fix it.',
51,
],
[
'Should be processed (keyword1): fix it.',
52,
],
[
'Should be processed (keyword1): fix it.',
54,
],
[
'Should be processed (keyword1): fix it.',
55,
],
[
'Should be processed (keyword1): classic multi line comment.',
60,
],
]);
}

public function testNoKeywordRule(): void
{
$this->keywords = [];

$this->analyse([__DIR__ . '/data/keyword.php'], []);
}
}
62 changes: 62 additions & 0 deletions tests/data/keyword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Keyword;

function doFoo():void {

}

// TODO: keyword1 comment1
// TODO: keyword2 comment2
// TODO: kEywORd1 comment3
// TODO keyword1 comment4
//TODO: keyword1 comment5
//TODO keyword1 comment6
//TODO keyword1 comment7

// just a comment
class X {}

//TODO: keyword1 commentX

// TODO: keyword1
function doFooBar():void {

}

/**
* other text
*
* @todo keyword1 class comment
* @TODO keyword1 - class comment
* more comment data
*/
class Z {
// TODO: keyword1 method comment
public function XY():void {
// TODO: keyword1 in method comment1
$x = 1;
// TODO keyword1: in method comment2
}
}

/**
* @todo keyword1 - Convert to standard Drupal $content code.
*/

// @todo keyword1 Decide to fix all the broken instances of class as a string

// @todo: keyword1 fix it
// @todo keyword1: fix it
// todo - keyword1 fix it
// todo keyword1 - fix it

// TODO@lars keyword1 - fix it
// TODO@lars: keyword1 - fix it

/*
* other text
*
* @todo keyword1 classic multi line comment
* more comment data
*/