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

Expire comment by composer dependency package version #32

Merged
merged 30 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function doBar() {

}

// TODO: phpunit/phpunit:5.3.* This has to be fixed when updating to phpunit 5.3.*
function doFooBar() {

}

```

## Supported todo formats
Expand All @@ -34,8 +39,8 @@ When a text is given after the date, this text will be picked up for the PHPStan

The comment can expire by different constraints, examples are:
- by date with format of `YYYY-MM-DD`
- by semantic version matched against the project itself

- by a semantic version constraint matched against the project itself
- by a semantic version constraint matched against a Composer dependency

see examples of different comment variants which are supported:

Expand All @@ -46,8 +51,8 @@ see examples of different comment variants which are supported:
// todo - 2023-12-14 fix it
// todo 2023-12-14 - fix it

// TODO@lars 2023-12-14 - fix it
// TODO@lars: 2023-12-14 - fix it
// TODO@staabm 2023-12-14 - fix it
// TODO@markus: 2023-12-14 - fix it

/*
* other text
Expand All @@ -58,6 +63,9 @@ see examples of different comment variants which are supported:

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

// TODO: phpunit/phpunit:<5 This has to be fixed when updating to phpunit 5.x
// TODO@markus: phpunit/phpunit:5.3.* This has to be fixed when updating to phpunit 5.3.*
```

## Configuration
Expand Down Expand Up @@ -103,6 +111,9 @@ parameters:
### Reference version

By default version-todo-comments are checked against `"nextMajor"` version.

_Note: The reference version is not applied to package-version-todo-comments._

This is determined by fetching the latest local available git tag and incrementing the major version number.

This behaviour can be configured with the `referenceVersion` option.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
],
"require": {
"php": "^7.4 || ^8.0",
"composer-runtime-api": "^2",
"composer/semver": "^3.4",
"nikolaposa/version": "^4.1"
},
Expand Down
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ services:
arguments:
- %todo_by.singleGitRepo%

-
class: staabm\PHPStanTodoBy\TodoByPackageVersionRule
tags: [phpstan.rules.rule]

-
class: staabm\PHPStanTodoBy\utils\GitTagFetcher

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

namespace staabm\PHPStanTodoBy;

use Composer\InstalledVersions;
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 staabm\PHPStanTodoBy\utils\CommentMatcher;
use staabm\PHPStanTodoBy\utils\ExpiredCommentErrorBuilder;
use staabm\PHPStanTodoBy\utils\ReferenceVersionFinder;
use staabm\PHPStanTodoBy\utils\VersionNormalizer;
use function preg_match_all;
use function substr_count;
use function trim;
use const PREG_OFFSET_CAPTURE;
use const PREG_SET_ORDER;

/**
* @implements Rule<Node>
*/
final class TodoByPackageVersionRule implements Rule
{
// composer package-name pattern from https://getcomposer.org/doc/04-schema.md#name
private const PATTERN = <<<'REGEXP'
{
@?TODO # possible @ prefix
@?[a-zA-Z0-9_-]*\s* # optional username
\s*[:-]?\s* # optional colon or hyphen
(?:(?P<package>[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*):) # composer package name, followed by ":"
(?P<version>[^\s:\-]+) # version constraint
\s*[:-]?\s* # optional colon or hyphen
(?P<comment>.*) # rest of line as comment text
}ix
REGEXP;

private ExpiredCommentErrorBuilder $errorBuilder;

public function __construct(
ExpiredCommentErrorBuilder $errorBuilder
) {
$this->errorBuilder = $errorBuilder;
}

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

public function processNode(Node $node, Scope $scope): array
{
$it = CommentMatcher::matchComments($node, self::PATTERN);

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

$package = $match['package'][0];

// see https://getcomposer.org/doc/07-runtime.md#installed-versions
if (!InstalledVersions::isInstalled($package)) {
$errors[] = $this->errorBuilder->buildError(
$comment,
'Package "' . $package . '" is not installed via Composer.',
null,
$match[0][1]
);

continue;
}

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

if (InstalledVersions::satisfies(new VersionParser(), $package, $version)) {
staabm marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

// Have always present date at the start of the message.
// If there is further text, append it.
if ($todoText !== '') {
$errorMessage = "{$package} version requirement {$version} not satisfied: ". rtrim($todoText, '.') .".";
} else {
$errorMessage = "{$package} version requirement {$version} not satisfied.";
}

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

return $errors;
}

}
4 changes: 4 additions & 0 deletions src/utils/CommentMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public static function matchComments(Node $node, string $pattern): iterable
preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER) === false
|| count($matches) === 0
) {
if (preg_last_error() !== PREG_NO_ERROR) {
throw new \RuntimeException('Error in PCRE: '. preg_last_error_msg());
}

continue;
}

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

namespace staabm\PHPStanTodoBy\Tests;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use staabm\PHPStanTodoBy\TodoByPackageVersionRule;
use staabm\PHPStanTodoBy\TodoByVersionRule;
use staabm\PHPStanTodoBy\utils\ExpiredCommentErrorBuilder;
use staabm\PHPStanTodoBy\utils\GitTagFetcher;
use staabm\PHPStanTodoBy\utils\ReferenceVersionFinder;
use staabm\PHPStanTodoBy\utils\VersionNormalizer;

/**
* @extends RuleTestCase<TodoByPackageVersionRule>
*/
final class TodoByPackageVersionRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new TodoByPackageVersionRule(
new ExpiredCommentErrorBuilder(true),
);
}

/**
* @param list<array{0: string, 1: int, 2?: string|null}> $errors
* @dataProvider provideErrors
*/
public function testRule(array $errors): void
{
$this->analyse([__DIR__ . '/data/packageVersion.php'], $errors);
}

/**
* @return iterable<array{list<array{0: string, 1: int, 2?: string|null}>}>
*/
public static function provideErrors(): iterable
{
yield [
[
[
'phpunit/phpunit version requirement <5 not satisfied: This has to be fixed when updating to phpunit 5.x.',
7
],
[
'phpunit/phpunit version requirement 5.3.* not satisfied: This has to be fixed when updating to phpunit 5.3.*.',
8
],
[
'Package "not-installed/package" is not installed via Composer.',
11
],
[
'phpunit/phpunit version requirement <9 not satisfied.',
13
]
]
];
}

}
15 changes: 15 additions & 0 deletions tests/data/packageVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace ExamplePackageVersion;

// TODO: phpunit/phpunit:<50 This has to be fixed when updating to phpunit 50.x

// TODO: phpunit/phpunit:<5 This has to be fixed when updating to phpunit 5.x
// TODO: phpunit/phpunit:5.3.* This has to be fixed when updating to phpunit 5.3.*
Copy link

@nikophil nikophil Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually I've done my check the other way around, and the rule fails when constraint is satisfied

phpunit/phpunit:>=10

I think it reads better: "please phpstan, fail when phpunit meets this dependency"

About this specific todo: phpunit/phpunit:5.3.*, I think it would not fail when phpunit is at 5.3.*
but it will if we have phpunit>=5.4.0 and phpunit<5.3, which seems pretty strange

My take was to always add >= if the constraint does not specifies it



// TODO: not-installed/package:<5 this should error because package is not in composer.json

// TODO: phpunit/phpunit:<9
// TODO: phpunit/phpunit:<10

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this test fail when using phpunit 10? 🤔

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, I had had the logic reversed. I think all feedback is addressed now

Copy link

@nikophil nikophil Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and now your rule looks even closer from mine 😅

// TODO: phpunit/phpunit:<11