Skip to content

Commit b9e5ca2

Browse files
feat: support new rules
1 parent fd2f76c commit b9e5ca2

File tree

14 files changed

+1730
-3
lines changed

14 files changed

+1730
-3
lines changed

config/default.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,59 @@ func NewDefault() *lint.Config {
172172
(&rule.FooterTypeEnumRule{}).Name(): {
173173
Argument: []interface{}{},
174174
},
175+
176+
// Case Rules
177+
(&rule.TypeCaseRule{}).Name(): {
178+
Argument: "lower-case",
179+
},
180+
(&rule.ScopeCaseRule{}).Name(): {
181+
Argument: "lower-case",
182+
},
183+
(&rule.DescriptionCaseRule{}).Name(): {
184+
Argument: "lower-case",
185+
},
186+
(&rule.BodyCaseRule{}).Name(): {
187+
Argument: "lower-case",
188+
},
189+
(&rule.HeaderCaseRule{}).Name(): {
190+
Argument: "lower-case",
191+
},
192+
193+
// Full-stop Rules
194+
(&rule.HeaderFullStopRule{}).Name(): {
195+
Argument: ".",
196+
},
197+
(&rule.BodyFullStopRule{}).Name(): {
198+
Argument: ".",
199+
},
200+
(&rule.DescriptionFullStopRule{}).Name(): {
201+
Argument: ".",
202+
},
203+
204+
// Trailer / Signed-off-by
205+
(&rule.SignedOffByRule{}).Name(): {
206+
Argument: "Signed-off-by",
207+
},
208+
(&rule.TrailerExistsRule{}).Name(): {
209+
Argument: "Signed-off-by",
210+
},
211+
212+
// Empty rules (no argument needed)
213+
(&rule.TypeEmptyRule{}).Name(): {},
214+
(&rule.ScopeEmptyRule{}).Name(): {},
215+
(&rule.BodyEmptyRule{}).Name(): {},
216+
(&rule.FooterEmptyRule{}).Name(): {},
217+
(&rule.DescriptionEmptyRule{}).Name(): {},
218+
219+
// Leading-blank rules (no argument needed)
220+
(&rule.BodyLeadingBlankRule{}).Name(): {},
221+
(&rule.FooterLeadingBlankRule{}).Name(): {},
222+
223+
// Header trim (no argument needed)
224+
(&rule.HeaderTrimRule{}).Name(): {},
225+
226+
// Breaking change (no argument needed)
227+
(&rule.BreakingChangeExclamationMarkRule{}).Name(): {},
175228
}
176229

177230
def := &lint.Config{

internal/casing/casing.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Package casing provides case-format constants and validators used by commit
2+
// lint rules. Each format has a corresponding exported predicate and a shared
3+
// [Check] dispatcher.
4+
package casing
5+
6+
import (
7+
"strings"
8+
"unicode"
9+
)
10+
11+
// Case-format constants. These are the only values accepted by rules that take
12+
// a case argument (e.g. type-case, scope-case, …).
13+
const (
14+
Lower = "lower-case" // all characters lower-cased, e.g. "feat"
15+
Upper = "upper-case" // all characters upper-cased, e.g. "FEAT"
16+
Camel = "camel-case" // starts lowercase, no separators, e.g. "myFeat"
17+
Kebab = "kebab-case" // lowercase words joined by hyphens, e.g. "my-feat"
18+
Pascal = "pascal-case" // starts uppercase, no separators, e.g. "MyFeat"
19+
Sentence = "sentence-case" // first letter uppercase, rest lowercase, e.g. "My feat"
20+
Snake = "snake-case" // lowercase words joined by underscores, e.g. "my_feat"
21+
Start = "start-case" // every word starts uppercase, e.g. "My Feat"
22+
)
23+
24+
// All is the ordered list of all valid case formats.
25+
var All = []string{Lower, Upper, Camel, Kebab, Pascal, Sentence, Snake, Start}
26+
27+
// Check returns true when s conforms to the given caseFormat constant.
28+
// An empty string always returns true (treated as "not present").
29+
// Returns false for any unrecognised caseFormat value.
30+
func Check(s, caseFormat string) bool {
31+
if s == "" {
32+
return true
33+
}
34+
switch caseFormat {
35+
case Lower:
36+
return s == strings.ToLower(s)
37+
case Upper:
38+
return s == strings.ToUpper(s)
39+
case Camel:
40+
return IsCamelCase(s)
41+
case Kebab:
42+
return IsKebabCase(s)
43+
case Pascal:
44+
return IsPascalCase(s)
45+
case Sentence:
46+
return IsSentenceCase(s)
47+
case Snake:
48+
return IsSnakeCase(s)
49+
case Start:
50+
return IsStartCase(s)
51+
default:
52+
return false
53+
}
54+
}
55+
56+
// IsCamelCase reports whether s is in camelCase format.
57+
// camelCase: starts with a lowercase letter and contains only letters and
58+
// digits (no separators). Examples: "feat", "myFeature", "parseHTML".
59+
func IsCamelCase(s string) bool {
60+
if s == "" {
61+
return true
62+
}
63+
runes := []rune(s)
64+
if unicode.IsUpper(runes[0]) {
65+
return false
66+
}
67+
for _, r := range runes {
68+
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
69+
return false
70+
}
71+
}
72+
return true
73+
}
74+
75+
// IsKebabCase reports whether s is in kebab-case format.
76+
// kebab-case: only lowercase letters, digits, and hyphens.
77+
// Examples: "my-feature", "kebab-case", "v2-api".
78+
func IsKebabCase(s string) bool {
79+
for _, r := range s {
80+
if !unicode.IsLower(r) && !unicode.IsDigit(r) && r != '-' {
81+
return false
82+
}
83+
}
84+
return true
85+
}
86+
87+
// IsPascalCase reports whether s is in PascalCase format.
88+
// PascalCase: starts with an uppercase letter and contains only letters and
89+
// digits (no separators). Examples: "MyFeature", "ParseHTML", "Feat".
90+
func IsPascalCase(s string) bool {
91+
if s == "" {
92+
return true
93+
}
94+
runes := []rune(s)
95+
if !unicode.IsUpper(runes[0]) {
96+
return false
97+
}
98+
for _, r := range runes {
99+
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
100+
return false
101+
}
102+
}
103+
return true
104+
}
105+
106+
// IsSentenceCase reports whether s is in Sentence case format.
107+
// Sentence case: first rune is uppercase; every subsequent letter is lowercase.
108+
// Digits and punctuation are allowed anywhere.
109+
// Examples: "My feature", "Add endpoint", "Fix #123".
110+
func IsSentenceCase(s string) bool {
111+
if s == "" {
112+
return true
113+
}
114+
runes := []rune(s)
115+
if !unicode.IsUpper(runes[0]) {
116+
return false
117+
}
118+
for _, r := range runes[1:] {
119+
if unicode.IsLetter(r) && unicode.IsUpper(r) {
120+
return false
121+
}
122+
}
123+
return true
124+
}
125+
126+
// IsSnakeCase reports whether s is in snake_case format.
127+
// snake_case: only lowercase letters, digits, and underscores.
128+
// Examples: "my_feature", "snake_case", "v2_api".
129+
func IsSnakeCase(s string) bool {
130+
for _, r := range s {
131+
if !unicode.IsLower(r) && !unicode.IsDigit(r) && r != '_' {
132+
return false
133+
}
134+
}
135+
return true
136+
}
137+
138+
// IsStartCase reports whether s is in Start Case format.
139+
// Start Case: every whitespace-separated word starts with an uppercase letter.
140+
// Examples: "My Feature", "Add New Endpoint".
141+
func IsStartCase(s string) bool {
142+
for _, w := range strings.Fields(s) {
143+
runes := []rune(w)
144+
if !unicode.IsUpper(runes[0]) {
145+
return false
146+
}
147+
}
148+
return true
149+
}

0 commit comments

Comments
 (0)