Skip to content

Commit 63e1a9a

Browse files
authored
[Feature] Add title conventional rule (#58)
* Add rule * fix test * update docs * up version / changelog * fix ArrayResolver on nullable type
1 parent 6f08207 commit 63e1a9a

File tree

9 files changed

+258
-6
lines changed

9 files changed

+258
-6
lines changed

CHANGELOG.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ This file contains changelogs.
66

77
-----------------------------------------------------------------
88

9+
## [v0.16.3 (2023-08-29)](https://github.com/ArtARTs36/php-merge-request-linter/compare/0.16.2..0.16.3)
10+
11+
## Added
12+
* Added rule `title_conventional`
13+
14+
[💾 Assets](https://github.com/ArtARTs36/php-merge-request-linter/releases/tag/0.16.3)
15+
16+
-----------------------------------------------------------------
17+
918
## [v0.16.2 (2023-08-29)](https://github.com/ArtARTs36/php-merge-request-linter/compare/0.16.1..0.16.2)
1019

1120
## Optimized
1221
* Throw http exceptions and catch their in CI requests
1322

14-
[💾 Assets](https://github.com/ArtARTs36/php-merge-request-linter/releases/tag/0.16.1)
23+
[💾 Assets](https://github.com/ArtARTs36/php-merge-request-linter/releases/tag/0.16.2)
1524

1625
-----------------------------------------------------------------
1726

docs/rules.md

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Currently is available that rules:
2222
| @mr-linter/diff_limit | The request must contain no more than {linesMax} changes. |
2323
| @mr-linter/no_ssh_keys | Prevent ssh keys from being included in the merge request. |
2424
| @mr-linter/disable_file_extensions | Disable adding files of certain extensions. |
25+
| @mr-linter/title_conventional | The title must match conventional commit pattern https://www.conventionalcommits.org/en/v1.0.0. |
2526

2627

2728
## @mr-linter/changed_files_limit
@@ -163,3 +164,11 @@ Disable adding files of certain extensions.
163164
| Name | Description | Type | Examples |
164165
| ------------ | ------------ |------ | ------|
165166
| extensions | Array of file extensions | array of strings | "pem", "pub", "php" |
167+
168+
## @mr-linter/title_conventional
169+
170+
The title must match conventional commit pattern https://www.conventionalcommits.org/en/v1.0.0.
171+
172+
| Name | Description | Type | Examples |
173+
| ------------ | ------------ |------ | ------|
174+
| types | Commit types | array of strings | "build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test" |

mr-linter-config-schema.json

+52
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,21 @@
289289
"minItems": 1
290290
}
291291
]
292+
},
293+
"@mr-linter/title_conventional": {
294+
"description": "The title must match conventional commit pattern https://www.conventionalcommits.org/en/v1.0.0.",
295+
"oneOf": [
296+
{
297+
"$ref": "#/definitions/rules_properties_@mr-linter_title_conventional"
298+
},
299+
{
300+
"type": "array",
301+
"items": {
302+
"$ref": "#/definitions/rules_properties_@mr-linter_title_conventional"
303+
},
304+
"minItems": 1
305+
}
306+
]
292307
}
293308
},
294309
"additionalProperties": false
@@ -6575,6 +6590,43 @@
65756590
"extensions"
65766591
]
65776592
},
6593+
"rules_properties_@mr-linter_title_conventional": {
6594+
"type": "object",
6595+
"properties": {
6596+
"critical": {
6597+
"type": "boolean",
6598+
"default": true
6599+
},
6600+
"when": {
6601+
"description": "Conditions that determine whether the rule should run.",
6602+
"$ref": "#/definitions/rule_conditions"
6603+
},
6604+
"types": {
6605+
"type": "array",
6606+
"description": "Commit types",
6607+
"items": [
6608+
{
6609+
"type": "string",
6610+
"description": "Commit types",
6611+
"examples": [
6612+
"build",
6613+
"chore",
6614+
"ci",
6615+
"docs",
6616+
"feat",
6617+
"fix",
6618+
"perf",
6619+
"refactor",
6620+
"revert",
6621+
"style",
6622+
"test"
6623+
]
6624+
}
6625+
]
6626+
}
6627+
},
6628+
"additionalProperties": false
6629+
},
65786630
"notifications_channel_telegram_bot": {
65796631
"properties": {
65806632
"type": {

src/Application/Rule/Rules/DefaultRules.php

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ final class DefaultRules
3131
DiffLimitRule::NAME => DiffLimitRule::class,
3232
NoSshKeysRule::NAME => NoSshKeysRule::class,
3333
DisableFileExtensionsRule::NAME => DisableFileExtensionsRule::class,
34+
TitleConventionalRule::NAME => TitleConventionalRule::class,
3435
];
3536

3637
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace ArtARTs36\MergeRequestLinter\Application\Rule\Rules;
4+
5+
use ArtARTs36\MergeRequestLinter\Application\Rule\Definition\Definition;
6+
use ArtARTs36\MergeRequestLinter\Domain\Note\LintNote;
7+
use ArtARTs36\MergeRequestLinter\Domain\Request\MergeRequest;
8+
use ArtARTs36\MergeRequestLinter\Domain\Rule\RuleDefinition;
9+
use ArtARTs36\MergeRequestLinter\Shared\Attributes\Description;
10+
use ArtARTs36\MergeRequestLinter\Shared\Attributes\Example;
11+
use ArtARTs36\MergeRequestLinter\Shared\Attributes\Generic;
12+
use ArtARTs36\MergeRequestLinter\Shared\DataStructure\Arrayee;
13+
14+
/**
15+
* The title must match conventional commit pattern https://www.conventionalcommits.org/en/v1.0.0.
16+
* @link https://www.conventionalcommits.org/en/v1.0.0
17+
*/
18+
final class TitleConventionalRule extends NamedRule
19+
{
20+
public const NAME = '@mr-linter/title_conventional';
21+
22+
private const REGEX = '/^([a-z]+){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)/mis';
23+
24+
private const DEFAULT_TYPES = [
25+
'build',
26+
'chore',
27+
'ci',
28+
'docs',
29+
'feat',
30+
'fix',
31+
'perf',
32+
'refactor',
33+
'revert',
34+
'style',
35+
'test',
36+
];
37+
38+
/**
39+
* @param Arrayee<int, string> $types
40+
*/
41+
public function __construct(
42+
#[Generic(Generic::OF_STRING)]
43+
private readonly Arrayee $types,
44+
) {
45+
}
46+
47+
/**
48+
* @param Arrayee<int, string>|null $types
49+
*/
50+
public static function make(
51+
#[Generic(Generic::OF_STRING)]
52+
#[Description('Commit types')]
53+
#[Example('build')]
54+
#[Example('chore')]
55+
#[Example('ci')]
56+
#[Example('docs')]
57+
#[Example('feat')]
58+
#[Example('fix')]
59+
#[Example('perf')]
60+
#[Example('refactor')]
61+
#[Example('revert')]
62+
#[Example('style')]
63+
#[Example('test')]
64+
?Arrayee $types = null,
65+
): self {
66+
$types ??= new Arrayee(self::DEFAULT_TYPES);
67+
68+
return new self($types);
69+
}
70+
71+
public function lint(MergeRequest $request): array
72+
{
73+
$matches = [];
74+
75+
preg_match(self::REGEX, $request->title, $matches);
76+
77+
if (! array_key_exists(1, $matches) || ! is_string($matches[1])) {
78+
return [new LintNote('The title must matches with conventional commit')];
79+
}
80+
81+
$type = $matches[1];
82+
83+
if (! $this->types->contains($type)) {
84+
return [new LintNote(sprintf('Title conventional: type "%s" is unknown', $type))];
85+
}
86+
87+
return [];
88+
}
89+
90+
/**
91+
* @codeCoverageIgnore
92+
*/
93+
public function getDefinition(): RuleDefinition
94+
{
95+
return new Definition(sprintf(
96+
'The title must matches with conventional commit with types: [%s]',
97+
$this->types->implode(', '),
98+
));
99+
}
100+
}

src/Shared/Reflection/TypeResolver/ArrayeeResolver.php

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public function canResolve(Type $type, mixed $value): bool
1717
public function resolve(Type $type, mixed $value): mixed
1818
{
1919
if (! is_array($value)) {
20+
if ($type->nullable) {
21+
return null;
22+
}
23+
2024
throw new ValueInvalidException(sprintf(
2125
'Arg with type %s not supported. Expected type: array',
2226
gettype($value),

src/Version.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99
final class Version
1010
{
11-
public const VERSION = '0.16.2';
11+
public const VERSION = '0.16.3';
1212

1313
private function __construct()
1414
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace ArtARTs36\MergeRequestLinter\Tests\Unit\Application\Rule\Rules;
4+
5+
use ArtARTs36\MergeRequestLinter\Application\Rule\Rules\TitleConventionalRule;
6+
use ArtARTs36\MergeRequestLinter\Domain\Request\MergeRequest;
7+
use ArtARTs36\MergeRequestLinter\Shared\DataStructure\Arrayee;
8+
use ArtARTs36\MergeRequestLinter\Tests\TestCase;
9+
10+
final class TitleMatchesConventionalRuleTest extends TestCase
11+
{
12+
public function providerForTestLint(): array
13+
{
14+
return [
15+
'lint failed: invalid title' => [
16+
$this->makeMergeRequest([
17+
'title' => 'Test',
18+
]),
19+
[],
20+
true,
21+
],
22+
'lint ok: title with no body' => [
23+
$this->makeMergeRequest([
24+
'title' => 'docs: correct spelling of CHANGELOG',
25+
]),
26+
[],
27+
false,
28+
],
29+
'link ok: commit message with scope' => [
30+
$this->makeMergeRequest([
31+
'title' => 'feat(lang): add Polish language',
32+
]),
33+
[],
34+
false,
35+
],
36+
'lint failed: commit message with scope and unknown type' => [
37+
$this->makeMergeRequest([
38+
'title' => 'unknown(lang): add Polish language',
39+
]),
40+
[],
41+
true,
42+
],
43+
'lint ok: commit message with scope and custom type' => [
44+
$this->makeMergeRequest([
45+
'title' => 'custom(lang): add Polish language',
46+
]),
47+
[
48+
'types' => new Arrayee([
49+
'custom',
50+
]),
51+
],
52+
false,
53+
],
54+
'lint failed: commit message with scope, custom types, unknown type' => [
55+
$this->makeMergeRequest([
56+
'title' => 'unknown(lang): add Polish language',
57+
]),
58+
[
59+
'types' => new Arrayee([
60+
'custom',
61+
]),
62+
],
63+
true,
64+
],
65+
];
66+
}
67+
68+
/**
69+
* @covers \ArtARTs36\MergeRequestLinter\Application\Rule\Rules\TitleConventionalRule::lint
70+
*
71+
* @dataProvider providerForTestLint
72+
*/
73+
public function testLint(MergeRequest $request, array $ruleParams, bool $hasNotes): void
74+
{
75+
self::assertHasNotes($request, TitleConventionalRule::make(...$ruleParams), $hasNotes);
76+
}
77+
}

tests/Unit/Shared/Reflection/TypeResolver/ArrayeeResolverTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,22 @@ public function testCanResolve(Type $type, mixed $value, bool $expected): void
4444
public function providerForTestResolve(): array
4545
{
4646
return [
47-
[new Type(TypeName::Array), ['val1']],
47+
[new Type(TypeName::Array), ['val1'], new Arrayee(['val1'])],
48+
[new Type(TypeName::Array, nullable: true), null, null],
4849
];
4950
}
5051

5152
/**
5253
* @covers \ArtARTs36\MergeRequestLinter\Shared\Reflection\TypeResolver\ArrayeeResolver::resolve
5354
* @dataProvider providerForTestResolve
5455
*/
55-
public function testResolve(Type $type, mixed $value): void
56+
public function testResolve(Type $type, mixed $value, ?Arrayee $expectedVal): void
5657
{
5758
$resolver = new ArrayeeResolver();
5859

5960
$result = $resolver->resolve($type, $value);
6061

61-
self::assertInstanceOf(Arrayee::class, $result);
62-
self::assertEquals($value, $result->mapToArray(fn ($val) => $val));
62+
self::assertEquals($expectedVal, $result);
6363
}
6464

6565
/**

0 commit comments

Comments
 (0)