Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend tag matching #4

Merged
merged 10 commits into from
Jun 18, 2024
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,31 @@ It has setters:
It detects TODO-tags in single-line comments started with both `//` and `#` symbols
and multiple-line comments `/* ... */` and phpDoc `/** ... **/`.

1. Tag and comment separated by semicolon
1. Tag and comment separated by colon
```php
// TODO: comment summary
```
2. Tag and comment does not separated by semicolon
2. Tag and comment does not separated by colon
```php
// TODO comment summary
```
3. Tag with assignee and comment separated by semicolon
3. Tag with assignee and comment separated by colon
```php
// TODO@assigne: comment summary
```
4. Tag with assignee and comment does not separated by semicolon
4. Tag with assignee and comment does not separated by colon
```php
// TODO@assigne comment summary
```
5. Multiline comment with complex description. All lines after the first one with tag MUST have indentation
same to the text of the first line. So, all af them will be detected af part of description of TODO.
Multiple line comments may have assignee and semicolon same as single-line comments/.
Multiple line comments may have assignee and colon same as single-line comments/.
```php
/**
* TODO: comment summary
* and some complex description
* which must have indentation same as end of one presented:
* - semicolon
* - colon
* - assignee
* - tag
* So, all this text will be passed to registrar as description
Expand All @@ -71,26 +71,26 @@ and multiple-line comments `/* ... */` and phpDoc `/** ... **/`.
```

As a result of processing of such comments, ID of ISSUE will be injected before comment summary
and after semicolon and assignee when they are presented. For example:
1. Tag and comment separated by semicolon
and after colon and assignee when they are presented. For example:
1. Tag and comment separated by colon
```php
// TODO: XX-001 comment summary
```
2. Tag and comment does not separated by semicolon
2. Tag and comment does not separated by colon
```php
// TODO XX-001 comment summary
```
3. Tag with assignee and comment separated by semicolon
3. Tag with assignee and comment separated by colon
```php
// TODO@assigne: XX-001 comment summary
```
4. Tag with assignee and comment does not separated by semicolon
4. Tag with assignee and comment does not separated by colon
```php
// TODO@assigne XX-001 comment summary
```
5. Multiline comment with complex description. All lines after the first one with tag MUST have indentation
same to the text of the first line. So, all af them will be detected af part of description of TODO.
Multiple line comments may have assignee and semicolon same as single-line comments/.
Multiple line comments may have assignee and colon same as single-line comments/.
```php
/**
* TODO: XX-001 comment summary
Expand Down
2 changes: 2 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</include>
</source>

<!--
<coverage includeUncoveredFiles="true"
pathCoverage="true"
ignoreDeprecatedCodeUnits="true"
Expand All @@ -35,4 +36,5 @@
<xml outputDirectory=".phpunit/report/xml-coverage"/>
</report>
</coverage>
-->
</phpunit>
6 changes: 6 additions & 0 deletions src/Dto/Tag/TagMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public function __construct(
private ?string $tag = null,
private ?int $prefixLength = null,
private ?string $assignee = null,
private ?string $ticketKey = null,
) {
}

Expand All @@ -27,4 +28,9 @@ public function getTag(): ?string
{
return $this->tag;
}

public function getTicketKey(): ?string
{
return $this->ticketKey;
}
}
3 changes: 3 additions & 0 deletions src/Service/CommentRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ private function registerTodos(array $tokens): bool
foreach ($tokens as $token) {
$commentParts = $this->commentExtractor->extract($token->text);
foreach ($commentParts->getTodos() as $commentPart) {
if ($commentPart->getTagMetadata()?->getTicketKey()) {
continue;
}
$todo = $this->todoFactory->create($commentPart);
if ($this->registrar->isRegistered($todo)) {
continue;
Expand Down
3 changes: 3 additions & 0 deletions src/Service/Registrar/RegistrarInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

interface RegistrarInterface
{
/**
* @deprecated skip registered by metadata {@see \Aeliot\TodoRegistrar\Dto\Tag\TagMetadata::getTicketKey() }
*/
public function isRegistered(Todo $todo): bool;

public function register(Todo $todo): string;
Expand Down
45 changes: 42 additions & 3 deletions src/Service/Tag/Detector.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,54 @@ class Detector
*/
public function __construct(array $tags = ['todo', 'fixme'])
{
$this->pattern = sprintf('~^([\s\#*/]*(%s)(?:@([a-z0-9._-]+))?\b\s*:?)~i', implode('|', $tags));
$tagsPart = implode('|', $tags);
$keyRegex = implode('|', $this->getTicketKeyRegExpressions());
$this->pattern = <<<REGEXP
~
^(
[\s\#*/]*@?(?P<tag>$tagsPart) # tags
(?:@(?P<assignee>[a-z0-9._-]+))? # assignee
(\s*[:-]?\s+(?P<ticketKey>$keyRegex))? # keyword/ticket separator & ticket key
\s*[:-]?\s* # optional spaces and colon or hyphen
)
~ix
REGEXP;
}

public function getTagMetadata(string $line): ?TagMetadata
{
if (!preg_match($this->pattern, $line, $matches)) {
if (!preg_match($this->pattern, $line, $matches, PREG_UNMATCHED_AS_NULL)) {
return null;
}

return new TagMetadata(strtoupper($matches[2]), strlen(rtrim($matches[1])), $matches[3] ?? null);
return new TagMetadata(
strtoupper($matches['tag']),
strlen(rtrim($matches[1])),
$matches['assignee'],
$matches['ticketKey'],
);
}

/**
* @return non-empty-array<string>
*/
private function getTicketKeyRegExpressions(): array
{
return [
// date consisting of YYYY-MM-DD format
'(?:\d{4}-\d\d?-\d\d?)',
// Github issue URL
'(?:https://github\.com/\S{2,}/\S+/issues/\d++)',
// Github pull request number of exact repo
'(?:\S{2,}/\S+\#\d++)',
// Github issue number
'(?:\#\d++)',
// JIRA & YouTack issue
'(?:[A-Z0-9]++-\d++)',
// "php" or a composer package name, followed by ":" and version
'(?:php|[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+):(?:[<>=]?[^\s:\-]+)',
// version
'(?:[<>=]?v?[0-9]++(\.[0-9]++){0,2})',
];
}
}
33 changes: 28 additions & 5 deletions tests/Unit/Service/CommentRegistrarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function testDontRegisterTwice(): void
{
$tokens = $this->getTokens();
$commentDetector = $this->mockCommentDetector($tokens);
$commentParts = $this->createCommentParts();
$commentParts = $this->createCommentParts($tokens[2]->text);
$commentExtractor = $this->mockCommentExtractor($commentParts);

$todoFactory = new TodoFactory();
Expand All @@ -48,11 +48,34 @@ public function testDontRegisterTwice(): void
self::assertSame('// TODO single line comment', $tokens[2]->text);
}

public function testDontRegisterTwiceWithIssueKey(): void
{
$tokens = $this->getTokens();
$tokens[2]->text = '// TODO X-001 single line comment';

$commentDetector = $this->mockCommentDetector($tokens);
$commentParts = $this->createCommentParts($tokens[2]->text, 'X-001');
$commentExtractor = $this->mockCommentExtractor($commentParts);

$todoFactory = new TodoFactory();

$registrar = $this->createMock(RegistrarInterface::class);
$registrar
->expects($this->never())
->method('isRegistered');
$registrar
->expects($this->never())
->method('register');

$commentRegistrar = new CommentRegistrar($commentDetector, $commentExtractor, $registrar, $todoFactory);
$commentRegistrar->register($tokens);
}

public function testRegisterNewTodos(): void
{
$tokens = $this->getTokens();
$commentDetector = $this->mockCommentDetector($tokens);
$commentParts = $this->createCommentParts();
$commentParts = $this->createCommentParts($tokens[2]->text);
$commentExtractor = $this->mockCommentExtractor($commentParts);

$token = $commentParts->getTodos()[0];
Expand All @@ -72,10 +95,10 @@ public function testRegisterNewTodos(): void
self::assertSame('// TODO X-001 single line comment', $tokens[2]->text);
}

private function createCommentParts(): CommentParts
private function createCommentParts(string $line, ?string $ticketKey = null): CommentParts
{
$commentPart = new CommentPart(new TagMetadata('TODO', 7));
$commentPart->addLine('// TODO single line comment');
$commentPart = new CommentPart(new TagMetadata('TODO', 7, null, $ticketKey));
$commentPart->addLine($line);
$commentParts = new CommentParts();
$commentParts->addPart($commentPart);

Expand Down
70 changes: 70 additions & 0 deletions tests/Unit/Service/Tag/DetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,69 @@ public static function getDataForTestTagUppercased(): iterable
yield ['TAG', '// TAG', ['tag']];
}

public static function getDataForTestTicketKeyMatch(): iterable
{
yield [
'2023-12-14',
'// TODO: 2023-12-14 This comment turns into a PHPStan error as of 14th december 2023',
];

yield [
'https://github.com/staabm/phpstan-td-by/issues/91',
'// TODO https://github.com/staabm/phpstan-td-by/issues/91 fix me when this GitHub issue is closed',
];

// url with expected tag as part of URL
yield [
'https://github.com/staabm/phpstan-todo-by/issues/91',
'// TODO https://github.com/staabm/phpstan-todo-by/issues/91 fix me when this GitHub issue is closed',
];

yield [
'<1.0.0',
'// TODO: <1.0.0 This has to be in the first major release of this repo',
];

yield [
'phpunit/phpunit:5.3',
'// TODO: phpunit/phpunit:5.3 This has to be fixed when updating phpunit to 5.3.x or higher',
];

yield [
'php:8',
'// TODO: php:8 drop this polyfill when php 8.x is required',
];

yield [
'APP-2137',
'// TODO: APP-2137 A comment which errors when the issue tracker ticket gets resolved',
];

yield ['2023-12-14', '// todo 2023-12-14'];
yield ['2023-12-14', '// @todo: 2023-12-14 fix it'];
yield ['2023-12-14', '// @todo 2023-12-14: fix it'];
yield ['2023-12-14', '// TODO - 2023-12-14 fix it'];
yield ['2023-12-14', '// FIXME 2023-12-14 - fix it'];
yield ['2023-12-14', '// TODO@staabm 2023-12-14 - fix it'];
yield ['2023-12-14', '// TODO@markus: 2023-12-14 - fix it'];
yield ['>123.4', '// TODO >123.4: Must fix this or bump the version'];
yield ['>v123.4', '// TODO >v123.4: Must fix this or bump the version'];
yield ['phpunit/phpunit:<5', '// TODO: phpunit/phpunit:<5 This has to be fixed before updating to phpunit 5.x'];

yield [
'phpunit/phpunit:5.3',
'// TODO@markus: phpunit/phpunit:5.3 This has to be fixed when updating phpunit to 5.3.x or higher',
];

yield ['APP-123', '// TODO: APP-123 fix it when this Jira ticket is closed'];
yield ['#123', '// TODO: #123 fix it when this GitHub issue is closed'];
yield ['GH-123', '// TODO: GH-123 fix it when this GitHub issue is closed'];
yield [
'some-organization/some-repo#123',
'// TODO: some-organization/some-repo#123 change me if this GitHub pull request is closed'
];
}

#[DataProvider('getDataForTestAssigneeDetection')]
public function testAssigneeDetection(string $expectedAssignee, string $line): void
{
Expand Down Expand Up @@ -121,6 +184,13 @@ public function testTagUppercased(string $expectedTag, string $line, array $tags
self::assertSame($expectedTag, $this->getTagMetadata($line, $tags)->getTag());
}

// vendor/bin/phpunit --filter='DetectorTest::testTicketKeyMatch'
#[DataProvider('getDataForTestTicketKeyMatch')]
public function testTicketKeyMatch(string $expectedTicketKey, string $line): void
{
self::assertSame($expectedTicketKey, $this->getTagMetadata($line)->getTicketKey());
}

private function getTagMetadata(string $line, array $tags = []): TagMetadata
{
$detector = $tags ? new Detector($tags) : new Detector();
Expand Down