Skip to content

Commit

Permalink
MAJOR: add aspell as additional check for spelling errors
Browse files Browse the repository at this point in the history
  • Loading branch information
oktalz committed Jul 30, 2024
1 parent f74106a commit 754bea8
Show file tree
Hide file tree
Showing 17 changed files with 618 additions and 91 deletions.
10 changes: 10 additions & 0 deletions .aspell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mode: all
ignore:
- go.mod
- go.sum
- .aspell.yml
allowed:
- aspell
- repo
- yaml
- config
11 changes: 10 additions & 1 deletion .github/workflows/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: 'go.mod'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
go_build:
name: Go build
runs-on: ubuntu-latest
Expand All @@ -32,6 +37,8 @@ jobs:
runs-on: ubuntu-latest
needs: ["go_build"]
steps:
- name: Install Aspell
run: sudo apt-get update && sudo apt-get install -y aspell aspell-en
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
Expand All @@ -51,6 +58,8 @@ jobs:
runs-on: ubuntu-latest
needs: ["go_build"]
steps:
- name: Install Aspell
run: sudo apt-get update && sudo apt-get install -y aspell aspell-en
- uses: actions/checkout@v4
with:
fetch-depth: 0
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
FROM golang:alpine as builder
FROM golang:alpine AS builder
RUN mkdir /build
ADD . /build/
WORKDIR /build
RUN go build -o check

FROM alpine:latest
RUN apk --no-cache add aspell aspell-en
COPY --from=builder /build/check /check
WORKDIR /
ENTRYPOINT ["/check"]
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,31 @@ TagOrder:
### Optional parameters

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.

### aspell

to check also spellcheck errors aspell was added. it can be configured with `.aspell.yml`

example
```yaml
mode: subject
ignore:
- go.mod
- go.sum
- *test.go
allowed:
- aspell
- config
```

mode can be set as

- `subject`
- `default` option
- only subject of commit message will be checked
- `commit`
- whole commit message will be checked
- `all`
- both commit message and all code commited
- `disabled`
- check is disabled
141 changes: 141 additions & 0 deletions aspell/aspell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package aspell

import (
"bytes"
"fmt"
"log"
"os/exec"
"slices"
"strings"

"check-commit/match"

"github.com/fatih/camelcase"
)

type Aspell struct {
Mode mode `yaml:"mode"`
Ignore []string `yaml:"ignore"`
AllowedWords []string `yaml:"allowed"`
HelpText string `yaml:"-"`
}

var (
camelCaseOK = map[string]struct{}{}
camelCaseNotOK = map[string]struct{}{}
)

func (a Aspell) checkSingle(data string, allowedWords []string) error {
var words []string
var badWords []string

checkRes, err := checkWithAspellExec(data)
if checkRes != "" {
words = strings.Split(checkRes, "\n")
}
if err != nil {
return err
}

for _, word := range words {
if len(word) < 1 {
continue
}
if _, ok := camelCaseNotOK[word]; ok {
badWords = append(badWords, word)
continue
}
if _, ok := camelCaseOK[word]; ok {
continue
}
if slices.Contains(a.AllowedWords, word) || slices.Contains(allowedWords, word) {
continue
}
splitted := camelcase.Split(word)
if len(splitted) > 1 {
for _, s := range splitted {
er := a.checkSingle(s, allowedWords)
if er != nil {
camelCaseNotOK[word] = struct{}{}
badWords = append(badWords, word+":"+s)
break
}
}
} else {
camelCaseNotOK[word] = struct{}{}
badWords = append(badWords, word)
}
}

if len(badWords) > 0 {
return fmt.Errorf("aspell: %s", badWords)
}
return nil
}

func (a Aspell) Check(subjects []string, commitsFull []string, content []map[string]string) error {
var response string
var checks []string
switch a.Mode {
case modeDisabled:
return nil
case modeSubject:
checks = subjects
case modeCommit:
checks = commitsFull
case modeAll:
for _, file := range content {
for name, v := range file {
nextFile := false
for _, filter := range a.Ignore {
if match.MatchFilter(name, filter) {
// log.Println("File", name, "in ignore list")
nextFile = true
continue
}
}
if nextFile {
continue
}
var imports []string
if strings.HasSuffix(name, ".go") {
imports = match.GetImportWordsFromGoFile(name)
}
if err := a.checkSingle(v, imports); err != nil {
log.Println("File", name, err.Error())
response += fmt.Sprintf("%s\n", err)
}
}
}
checks = []string{}
default:
checks = subjects
}

for _, subject := range checks {
if err := a.checkSingle(subject, []string{}); err != nil {
response += fmt.Sprintf("%s\n", err)
}
}

if len(response) > 0 {
return fmt.Errorf("%s", response)
}
return nil
}

func checkWithAspellExec(subject string) (string, error) {
cmd := exec.Command("aspell", "--list")
cmd.Stdin = strings.NewReader(subject)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
log.Printf("aspell error: %s, stderr: %s", err, stderr.String())
return "", err
}

return stdout.String() + stderr.String(), nil
}
32 changes: 32 additions & 0 deletions aspell/aspell_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package aspell

import "testing"

func Test_checkWithAspell(t *testing.T) {
aspell := Aspell{
Mode: modeSubject,
AllowedWords: []string{"config"},
}
tests := []struct {
name string
subject string
wantErr bool
}{
{"OK 1", "BUG/MEDIUM: config: add default location of path to the configuration file", false},
{"OK 2", "BUG/MEDIUM: config: add default location of path to the configuration file xtra", false},
{"error - flie", "BUG/MEDIUM: config: add default location of path to the configuration flie", true},
{"error - locatoin", "CLEANUP/MEDIUM: config: add default locatoin of path to the configuration file", true},
{"error - locatoin+flie", "CLEANUP/MEDIUM: config: add default locatoin of path to the configuration flie", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := aspell.checkSingle(tt.subject, []string{"xtra"})
if tt.wantErr && err == nil {
t.Errorf("checkWithAspell() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && err != nil {
t.Errorf("checkWithAspell() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
10 changes: 10 additions & 0 deletions aspell/mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package aspell

type mode string

const (
modeDisabled mode = "disabled"
modeSubject mode = "subject"
modeCommit mode = "commit"
modeAll mode = "all"
)
48 changes: 48 additions & 0 deletions aspell/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package aspell

import (
"fmt"
"log"
"os"

"gopkg.in/yaml.v3"
)

func New(filename string) (Aspell, error) {
var data []byte
var err error
if data, err = os.ReadFile(filename); err != nil {
log.Printf("warning: aspell exceptions file not found (%s)", err)
}

var aspell Aspell
err = yaml.Unmarshal(data, &aspell)
if err != nil {
return Aspell{}, err
}

switch aspell.Mode {
case modeDisabled:
case modeSubject:
case modeCommit:
case modeAll:
case "":
aspell.Mode = modeSubject
default:
return Aspell{}, fmt.Errorf("invalid mode: %s", aspell.Mode)
}

log.Printf("aspell mode set to %s", aspell.Mode)
aspell.HelpText = `aspell can be configured with .aspell.yml file.
content example:
mode: subject
ignore:
- go.mod
- go.sum
- .aspell.yml
allowed:
- aspell
- config
`
return aspell, nil
}
34 changes: 34 additions & 0 deletions aspell_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"bufio"
"os"
"testing"

"check-commit/aspell"
)

func Test_checkWithAspell(t *testing.T) {
aspell, err := aspell.New(".aspell.yml")
if err != nil {
t.Errorf("checkWithAspell() error = %v", err)
}

readmeFile, err := os.Open("README.md")
if err != nil {
t.Errorf("could not open README.md file: %v", err)
}
defer readmeFile.Close()

scanner := bufio.NewScanner(readmeFile)
readme := ""
for scanner.Scan() {
readme += scanner.Text() + "\n"
}
if err := scanner.Err(); err != nil {
t.Errorf("could not read README.md file: %v", err)
}
aspell.Check([]string{"subject"}, []string{"body"}, []map[string]string{

Check failure on line 31 in aspell_test.go

View workflow job for this annotation

GitHub Actions / Go lint

Error return value of `aspell.Check` is not checked (errcheck)
{"README.md": readme},
})
}
Loading

0 comments on commit 754bea8

Please sign in to comment.