Skip to content

Commit fc8019c

Browse files
committed
MAJOR: add aspell as additional check for spelling errors
1 parent f74106a commit fc8019c

17 files changed

+762
-96
lines changed

.aspell.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
mode: all
2+
minlength: 3
3+
ignore:
4+
- '*_test.go'
5+
allowed:
6+
- aspell
7+
- repo
8+
- yaml
9+
- config
10+
- Github
11+
- Gitlab
12+
- env
13+
- failsafe
14+
- golang
15+
- mkdir
16+
- WORKDIR
17+
- apk
18+
- ENTRYPOINT
19+
- ubuntu
20+
- golangci
21+
- splitted
22+
- sudo
23+
- mri
24+
- IID
25+
- repo
26+
- OPTIM
27+
- Submatch
28+
- Lshortfile
29+
- MAXSUBJECTLEN
30+
- MAXSUBJECTPARTS
31+
- MINSUBJECTLEN
32+
- MINSUBJECTPARTS
33+
- malformatted
34+
- cmdline

.github/workflows/actions.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ jobs:
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v4
9+
- name: Set up Go
10+
uses: actions/setup-go@v4
11+
with:
12+
go-version-file: 'go.mod'
13+
check-latest: true
914
- name: golangci-lint
10-
uses: golangci/golangci-lint-action@v3
15+
uses: golangci/golangci-lint-action@v6
1116
go_build:
1217
name: Go build
1318
runs-on: ubuntu-latest
@@ -32,6 +37,8 @@ jobs:
3237
runs-on: ubuntu-latest
3338
needs: ["go_build"]
3439
steps:
40+
- name: Install Aspell
41+
run: sudo apt-get update && sudo apt-get install -y aspell aspell-en
3542
- uses: actions/checkout@v4
3643
- name: Set up Go
3744
uses: actions/setup-go@v4
@@ -51,6 +58,8 @@ jobs:
5158
runs-on: ubuntu-latest
5259
needs: ["go_build"]
5360
steps:
61+
- name: Install Aspell
62+
run: sudo apt-get update && sudo apt-get install -y aspell aspell-en
5463
- uses: actions/checkout@v4
5564
with:
5665
fetch-depth: 0

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
FROM golang:alpine as builder
1+
FROM golang:alpine AS builder
22
RUN mkdir /build
33
ADD . /build/
44
WORKDIR /build
55
RUN go build -o check
66

77
FROM alpine:latest
8+
RUN apk --no-cache add aspell aspell-en
89
COPY --from=builder /build/check /check
910
WORKDIR /
1011
ENTRYPOINT ["/check"]

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ bug: fix set-var parsing bug in config-parser
3232
```
3333
BUG/MEDIUM: fix set-var
3434
```
35-
- Unkown severity
35+
- Unknown severity
3636
```
3737
BUG/MODERATE: fix set-var parsing bug in config-parser
3838
```
@@ -95,3 +95,35 @@ TagOrder:
9595
### Optional parameters
9696

9797
The program accepts an optional parameter to specify the location (path) of the base of the git repository. This can be useful in certain cases where the checked-out repo is in a non-standard location within the CI environment, compared to the running path from which the check-commit binary is being invoked.
98+
99+
### aspell
100+
101+
to check also spellcheck errors aspell was added. it can be configured with `.aspell.yml`
102+
103+
example
104+
```yaml
105+
mode: subject
106+
min_length: 3
107+
ignore:
108+
- go.mod
109+
- go.sum
110+
- '*test.go'
111+
- 'gen/*'
112+
allowed:
113+
- aspell
114+
- config
115+
```
116+
117+
`min_length` is minimal word size that is checked (default: 3)
118+
119+
`mode` can be set as
120+
121+
- `subject`
122+
- `default` option
123+
- only subject of commit message will be checked
124+
- `commit`
125+
- whole commit message will be checked
126+
- `all`
127+
- both commit message and all code committed
128+
- `disabled`
129+
- check is disabled

aspell/aspell.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package aspell
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"log"
7+
"os/exec"
8+
"slices"
9+
"sort"
10+
"strings"
11+
12+
"check-commit/match"
13+
14+
"github.com/fatih/camelcase"
15+
)
16+
17+
type Aspell struct {
18+
Mode mode `yaml:"mode"`
19+
MinLength int `yaml:"min_length"`
20+
Ignore []string `yaml:"ignore"`
21+
AllowedWords []string `yaml:"allowed"`
22+
HelpText string `yaml:"-"`
23+
}
24+
25+
var (
26+
camelCaseOK = map[string]struct{}{
27+
"HAProxy": {},
28+
"golang": {},
29+
"ascii": {},
30+
"api": {},
31+
}
32+
camelCaseNotOK = map[string]struct{}{}
33+
)
34+
35+
func (a Aspell) checkSingle(data string, allowedWords []string) error {
36+
var words []string
37+
var badWords []string
38+
39+
checkRes, err := checkWithAspellExec(data)
40+
if checkRes != "" {
41+
words = strings.Split(checkRes, "\n")
42+
}
43+
if err != nil {
44+
return err
45+
}
46+
47+
for _, word := range words {
48+
wordLower := strings.ToLower(word)
49+
if len(word) < a.MinLength {
50+
continue
51+
}
52+
if _, ok := camelCaseNotOK[wordLower]; ok {
53+
badWords = append(badWords, wordLower)
54+
continue
55+
}
56+
if _, ok := camelCaseOK[wordLower]; ok {
57+
continue
58+
}
59+
if slices.Contains(a.AllowedWords, wordLower) || slices.Contains(allowedWords, wordLower) {
60+
continue
61+
}
62+
splitted := camelcase.Split(word)
63+
if len(splitted) < 2 {
64+
splitted = strings.FieldsFunc(word, func(r rune) bool {
65+
return r == '_' || r == '-'
66+
})
67+
}
68+
if len(splitted) > 1 {
69+
for _, s := range splitted {
70+
er := a.checkSingle(s, allowedWords)
71+
if er != nil {
72+
camelCaseNotOK[wordLower] = struct{}{}
73+
badWords = append(badWords, word+":"+s)
74+
break
75+
}
76+
}
77+
} else {
78+
camelCaseNotOK[wordLower] = struct{}{}
79+
badWords = append(badWords, word)
80+
}
81+
}
82+
83+
if len(badWords) > 0 {
84+
m := map[string]struct{}{}
85+
for _, w := range badWords {
86+
m[w] = struct{}{}
87+
}
88+
badWords = []string{}
89+
for k := range m {
90+
badWords = append(badWords, k)
91+
}
92+
sort.Strings(badWords)
93+
return fmt.Errorf("aspell: %s", badWords)
94+
}
95+
return nil
96+
}
97+
98+
func (a Aspell) Check(subjects []string, commitsFull []string, content []map[string]string) error {
99+
var response string
100+
var checks []string
101+
switch a.Mode {
102+
case modeDisabled:
103+
return nil
104+
case modeSubject:
105+
checks = subjects
106+
case modeCommit:
107+
checks = commitsFull
108+
case modeAll:
109+
for _, file := range content {
110+
for name, v := range file {
111+
nextFile := false
112+
for _, filter := range a.Ignore {
113+
if match.MatchFilter(name, filter) {
114+
// log.Println("File", name, "in ignore list")
115+
nextFile = true
116+
continue
117+
}
118+
}
119+
if nextFile {
120+
continue
121+
}
122+
var imports []string
123+
if strings.HasSuffix(name, ".go") {
124+
imports = match.GetImportWordsFromGoFile(name)
125+
}
126+
if err := a.checkSingle(v, imports); err != nil {
127+
log.Println(name, err.Error())
128+
response += fmt.Sprintf("%s\n", err)
129+
}
130+
}
131+
}
132+
checks = []string{}
133+
default:
134+
checks = subjects
135+
}
136+
137+
for _, subject := range checks {
138+
if err := a.checkSingle(subject, []string{}); err != nil {
139+
response += fmt.Sprintf("%s\n", err)
140+
}
141+
}
142+
143+
if len(response) > 0 {
144+
return fmt.Errorf("%s", response)
145+
}
146+
return nil
147+
}
148+
149+
func checkWithAspellExec(subject string) (string, error) {
150+
cmd := exec.Command("aspell", "--list")
151+
cmd.Stdin = strings.NewReader(subject)
152+
153+
var stdout, stderr bytes.Buffer
154+
cmd.Stdout = &stdout
155+
cmd.Stderr = &stderr
156+
err := cmd.Run()
157+
if err != nil {
158+
log.Printf("aspell error: %s, stderr: %s", err, stderr.String())
159+
return "", err
160+
}
161+
162+
return stdout.String() + stderr.String(), nil
163+
}

aspell/aspell_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package aspell
2+
3+
import "testing"
4+
5+
func Test_checkWithAspell(t *testing.T) {
6+
aspell := Aspell{
7+
Mode: modeSubject,
8+
MinLength: 3,
9+
AllowedWords: []string{"config"},
10+
}
11+
tests := []struct {
12+
name string
13+
subject string
14+
wantErr bool
15+
}{
16+
{"OK 1", "BUG/MEDIUM: config: add default location of path to the configuration file", false},
17+
{"OK 2", "BUG/MEDIUM: config: add default location of path to the configuration file xtra", false},
18+
{"error - flie", "BUG/MEDIUM: config: add default location of path to the configuration flie", true},
19+
{"error - locatoin", "CLEANUP/MEDIUM: config: add default locatoin of path to the configuration file", true},
20+
{"error - locatoin+flie", "CLEANUP/MEDIUM: config: add default locatoin of path to the configuration flie", true},
21+
}
22+
for _, tt := range tests {
23+
t.Run(tt.name, func(t *testing.T) {
24+
err := aspell.checkSingle(tt.subject, []string{"xtra"})
25+
if tt.wantErr && err == nil {
26+
t.Errorf("checkWithAspell() error = %v, wantErr %v", err, tt.wantErr)
27+
}
28+
if !tt.wantErr && err != nil {
29+
t.Errorf("checkWithAspell() error = %v, wantErr %v", err, tt.wantErr)
30+
}
31+
})
32+
}
33+
}

aspell/mode.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package aspell
2+
3+
type mode string
4+
5+
const (
6+
modeDisabled mode = "disabled"
7+
modeSubject mode = "subject"
8+
modeCommit mode = "commit"
9+
modeAll mode = "all"
10+
)

0 commit comments

Comments
 (0)