Skip to content

Commit 5c218b0

Browse files
feat: add footer type rule
1 parent 0772506 commit 5c218b0

File tree

6 files changed

+208
-23
lines changed

6 files changed

+208
-23
lines changed

README.md

+22-21
Original file line numberDiff line numberDiff line change
@@ -195,27 +195,28 @@ Commonly used commit types from [Conventional Commit Types](https://github.com/c
195195
196196
The list of available lint rules
197197
198-
| name | argument | flags | description |
199-
| ---------------------- | -------- | ----------------- | --------------------------------------------- |
200-
| header-min-length | int | n/a | checks the min length of header (first line) |
201-
| header-max-length | int | n/a | checks the max length of header (first line) |
202-
| body-max-line-length | int | n/a | checks the max length of each line in body |
203-
| footer-max-line-length | int | n/a | checks the max length of each line in footer |
204-
| type-enum | []string | n/a | restrict type to given list of string |
205-
| scope-enum | []string | allow-empty: bool | restrict scope to given list of string |
206-
| footer-enum | []string | n/a | restrict footer token to given list of string |
207-
| type-min-length | int | n/a | checks the min length of type |
208-
| type-max-length | int | n/a | checks the max length of type |
209-
| scope-min-length | int | n/a | checks the min length of scope |
210-
| scope-max-length | int | n/a | checks the max length of scope |
211-
| description-min-length | int | n/a | checks the min length of description |
212-
| description-max-length | int | n/a | checks the max length of description |
213-
| body-min-length | int | n/a | checks the min length of body |
214-
| body-max-length | int | n/a | checks the max length of body |
215-
| footer-min-length | int | n/a | checks the min length of footer |
216-
| footer-max-length | int | n/a | checks the max length of footer |
217-
| type-charset | string | n/a | restricts type to given charset |
218-
| scope-charset | string | n/a | restricts scope to given charset |
198+
| name | argument | flags | description |
199+
| ---------------------- | ------------------------ | ----------------- | --------------------------------------------- |
200+
| header-min-length | int | n/a | checks the min length of header (first line) |
201+
| header-max-length | int | n/a | checks the max length of header (first line) |
202+
| body-max-line-length | int | n/a | checks the max length of each line in body |
203+
| footer-max-line-length | int | n/a | checks the max length of each line in footer |
204+
| type-enum | []string | n/a | restrict type to given list of string |
205+
| scope-enum | []string | allow-empty: bool | restrict scope to given list of string |
206+
| footer-enum | []string | n/a | restrict footer token to given list of string |
207+
| type-min-length | int | n/a | checks the min length of type |
208+
| type-max-length | int | n/a | checks the max length of type |
209+
| scope-min-length | int | n/a | checks the min length of scope |
210+
| scope-max-length | int | n/a | checks the max length of scope |
211+
| description-min-length | int | n/a | checks the min length of description |
212+
| description-max-length | int | n/a | checks the max length of description |
213+
| body-min-length | int | n/a | checks the min length of body |
214+
| body-max-length | int | n/a | checks the max length of body |
215+
| footer-min-length | int | n/a | checks the min length of footer |
216+
| footer-max-length | int | n/a | checks the max length of footer |
217+
| type-charset | string | n/a | restricts type to given charset |
218+
| scope-charset | string | n/a | restricts scope to given charset |
219+
| footer-type-enum | []{token, types, values} | n/a | enforces footer notes for given type |
219220
220221
## Available Formatters
221222

config/default.go

+5
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ func NewDefault() *lint.Config {
125125
(&rule.FooterEnumRule{}).Name(): {
126126
Argument: []interface{}{},
127127
},
128+
129+
// Footer Type Enum Rule
130+
(&rule.FooterTypeEnumRule{}).Name(): {
131+
Argument: []map[interface{}]interface{}{},
132+
},
128133
}
129134

130135
def := &lint.Config{

internal/registry/registry.go

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ func newRegistry() *registry {
5858

5959
&rule.TypeMaxLenRule{}, &rule.ScopeMaxLenRule{}, &rule.DescriptionMaxLenRule{},
6060
&rule.TypeMinLenRule{}, &rule.ScopeMinLenRule{}, &rule.DescriptionMinLenRule{},
61+
62+
&rule.FooterTypeEnumRule{},
6163
}
6264

6365
defaultFormatters := []lint.Formatter{

internal/registry/registry_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package registry
33
import "testing"
44

55
func TestDefaultRules(t *testing.T) {
6-
var m = make(map[string]struct{})
6+
m := make(map[string]struct{})
77
for _, r := range globalRegistry.Rules() {
88
_, ok := m[r.Name()]
99
if ok {
@@ -14,7 +14,7 @@ func TestDefaultRules(t *testing.T) {
1414
}
1515

1616
func TestDefaultFormatters(t *testing.T) {
17-
var m = make(map[string]struct{})
17+
m := make(map[string]struct{})
1818
for _, r := range globalRegistry.Formatters() {
1919
_, ok := m[r.Name()]
2020
if ok {

rule/footer_type_enum.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package rule
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"sort"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/conventionalcommit/commitlint/lint"
11+
)
12+
13+
var _ lint.Rule = (*FooterTypeEnumRule)(nil)
14+
15+
// FooterTypeEnumRule to validate footer tokens
16+
type FooterTypeEnumRule struct {
17+
Params []*FooterTypeEnumParam
18+
}
19+
20+
// FooterTypeEnumParam represent a single footer type param
21+
type FooterTypeEnumParam struct {
22+
Token string
23+
Types []string
24+
Values []string
25+
}
26+
27+
// Name return name of the rule
28+
func (r *FooterTypeEnumRule) Name() string { return "footer-type-enum" }
29+
30+
// Apply sets the needed argument for the rule
31+
func (r *FooterTypeEnumRule) Apply(setting lint.RuleSetting) error {
32+
confParams, ok := setting.Argument.([]interface{})
33+
if !ok {
34+
return errInvalidArg(r.Name(), fmt.Errorf("expects array of params, but got %#v", setting.Argument))
35+
}
36+
37+
params := make([]*FooterTypeEnumParam, 0, len(confParams))
38+
39+
for index, p := range confParams {
40+
v, ok := p.(map[interface{}]interface{})
41+
if !ok {
42+
return errInvalidArg(r.Name()+": params", fmt.Errorf("expects key-value object, but got %#v", p))
43+
}
44+
45+
param, err := processParam(r, v, index)
46+
if err != nil {
47+
return err
48+
}
49+
params = append(params, param)
50+
}
51+
52+
for _, p := range params {
53+
// sorting the string elements for binary search
54+
sort.Strings(p.Types)
55+
sort.Strings(p.Values)
56+
}
57+
58+
r.Params = params
59+
return nil
60+
}
61+
62+
func processParam(r lint.Rule, val map[interface{}]interface{}, index int) (*FooterTypeEnumParam, error) {
63+
tok, ok := val["token"]
64+
if !ok {
65+
return nil, errMissingArg(r.Name(), "token in param "+strconv.Itoa(index+1))
66+
}
67+
68+
types, ok := val["types"]
69+
if !ok {
70+
return nil, errMissingArg(r.Name(), "types in param "+strconv.Itoa(index+1))
71+
}
72+
73+
values, ok := val["values"]
74+
if !ok {
75+
return nil, errMissingArg(r.Name(), "values in param "+strconv.Itoa(index+1))
76+
}
77+
78+
param := &FooterTypeEnumParam{}
79+
80+
err := setStringArg(&param.Token, tok)
81+
if err != nil {
82+
return nil, errInvalidArg(r.Name()+": token", err)
83+
}
84+
85+
err = setStringArrArg(&param.Types, types)
86+
if err != nil {
87+
return nil, errInvalidArg(r.Name()+": types", err)
88+
}
89+
90+
err = setStringArrArg(&param.Values, values)
91+
if err != nil {
92+
return nil, errInvalidArg(r.Name()+": values", err)
93+
}
94+
95+
// validate the arguments
96+
if param.Token == "" {
97+
return nil, errInvalidArg(r.Name(), errors.New("token cannot be empty in param "+strconv.Itoa(index+1)))
98+
}
99+
100+
if len(param.Types) < 1 {
101+
return nil, errNeedAtleastOneArg(r.Name(), "types in param "+strconv.Itoa(index+1))
102+
}
103+
104+
if len(param.Values) < 1 {
105+
return nil, errNeedAtleastOneArg(r.Name(), "values in param "+strconv.Itoa(index+1))
106+
}
107+
108+
return param, nil
109+
}
110+
111+
// Validate validates FooterTypeEnumRule
112+
func (r *FooterTypeEnumRule) Validate(msg lint.Commit) (*lint.Issue, bool) {
113+
var invalids []string
114+
115+
// find missing footer notes
116+
for _, param := range r.Params {
117+
isType := search(param.Types, msg.Type())
118+
if !isType {
119+
continue
120+
}
121+
122+
isNote := searchNote(msg.Notes(), param.Token)
123+
if !isNote {
124+
a := fmt.Sprintf("'%s' should exist for type '%s'", param.Token, msg.Type())
125+
invalids = append(invalids, a)
126+
}
127+
}
128+
129+
outer:
130+
for _, note := range msg.Notes() {
131+
for _, param := range r.Params {
132+
isType := search(param.Types, msg.Type())
133+
if !isType {
134+
// not applicable for current type
135+
continue
136+
}
137+
138+
if note.Token() != param.Token {
139+
// not applicable for current token
140+
continue
141+
}
142+
143+
for _, val := range param.Values {
144+
if strings.HasPrefix(note.Value(), val) {
145+
// has valid prefix, check next footer note
146+
continue outer
147+
}
148+
}
149+
150+
// invalid - matches non of the mentioned prefix
151+
a := fmt.Sprintf("'%s' should have one of prefix [%s]", note.Token(), strings.Join(param.Values, ", "))
152+
invalids = append(invalids, a)
153+
}
154+
}
155+
156+
if len(invalids) == 0 {
157+
return nil, true
158+
}
159+
160+
desc := "footer token is invalid"
161+
return lint.NewIssue(desc, invalids...), false
162+
}

rule/rule.go

+15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ func errInvalidArg(ruleName string, err error) error {
1313
return fmt.Errorf("%s: invalid argument: %v", ruleName, err)
1414
}
1515

16+
func errNeedAtleastOneArg(ruleName, msg string) error {
17+
return fmt.Errorf("%s: need atleast one argument for %s", ruleName, msg)
18+
}
19+
20+
func errMissingArg(ruleName, argName string) error {
21+
return fmt.Errorf("%s: missing argument: %v", ruleName, argName)
22+
}
23+
1624
func errInvalidFlag(ruleName, flagName string, err error) error {
1725
return fmt.Errorf("%s: invalid flag '%s': %v", ruleName, flagName, err)
1826
}
@@ -36,6 +44,13 @@ func search(arr []string, toFind string) bool {
3644
return ind < len(arr) && arr[ind] == toFind
3745
}
3846

47+
func searchNote(arr []lint.Note, toFind string) bool {
48+
ind := sort.Search(len(arr), func(i int) bool {
49+
return arr[i].Token() >= toFind
50+
})
51+
return ind < len(arr) && arr[ind].Token() == toFind
52+
}
53+
3954
func validateCharset(allowedCharset, toCheck string) (string, bool) {
4055
invalidRunes := ""
4156
for _, ch := range toCheck {

0 commit comments

Comments
 (0)