diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c58ec38 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: Checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + + check-code: + name: Check code + runs-on: ubuntu-latest + env: + GO_VERSION: stable + GOLANGCI_LINT_VERSION: v2.0 + CGO_ENABLED: 0 + + steps: + + # https://github.com/marketplace/actions/checkout + - name: Check out code + uses: actions/checkout@v4 + + # https://github.com/marketplace/actions/setup-go-environment + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Check and get dependencies + run: | + go mod tidy + git diff --exit-code go.mod + git diff --exit-code go.sum + + - name: golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + + - name: Tests + run: make test + + - name: Build + run: make build + + check-local-install-script: + name: Installation script (local) + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Check installation script + run: cat ./install-misspell.sh | sh -s -- -d -b "./install-misspell" diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml new file mode 100644 index 0000000..259770a --- /dev/null +++ b/.github/workflows/go-cross.yml @@ -0,0 +1,34 @@ +name: Compilation + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + + cross: + name: Go + runs-on: ${{ matrix.os }} + env: + CGO_ENABLED: 0 + + strategy: + matrix: + go-version: [ stable, oldstable ] + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Test + run: go test -v -cover ./... + + - name: Build + run: go build -v -ldflags "-s -w" -trimpath diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 0000000..a03236d --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,17 @@ +name: "Post release" + +on: + release: + types: + - published + +jobs: + check-install-script: + name: Installation script (remote) + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - run: curl -sSfL https://raw.githubusercontent.com/golangci/misspell/HEAD/install-misspell.sh | sh -s -- -b "./install-misspell" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a1069df --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: "Release a tag" + +on: + push: + tags: + - v* + +jobs: + release: + name: Release Process + runs-on: ubuntu-latest + env: + GO_VERSION: stable + CGO_ENABLED: 0 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + # https://goreleaser.com/ci/actions/ + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }} diff --git a/.gitignore b/.gitignore index b1b707e..5e5c368 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ dist/ bin/ vendor/ +.idea/ +/misspell + # editor turds *~ *.gz diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1811db3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,102 @@ +version: "2" + +formatters: + enable: + - gci + - gofumpt + settings: + gofumpt: + extra-rules: true + +linters: + default: all + disable: + - cyclop # duplicate of gocyclo + - dupl + - err113 + - errcheck # FIXME(ldez) must be fixed + - exhaustive + - exhaustruct + - forbidigo + - gochecknoglobals + - gochecknoinits + - gosmopolitan + - gosec # FIXME(ldez) must be fixed + - lll + - misspell + - mnd + - nakedret # FIXME(ldez) must be fixed + - nilnil + - nlreturn + - nonamedreturns # FIXME(ldez) must be fixed + - paralleltest + - prealloc + - rowserrcheck # not relevant (SQL) + - sqlclosecheck # not relevant (SQL) + - testpackage + - tparallel + - varnamelen + - wrapcheck + - wsl # FIXME(ldez) must be fixed + + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/instana/testify + desc: not allowed + - pkg: github.com/pkg/errors + desc: Should be replaced by standard lib errors package + forbidigo: + forbid: + - pattern: ^print(ln)?$ + - pattern: ^panic$ + - pattern: ^spew\.Print(f|ln)?$ + - pattern: ^spew\.Dump$ + funlen: + lines: -1 + statements: 40 + goconst: + min-len: 3 + min-occurrences: 3 + gocritic: + disabled-checks: + - sloppyReassign + - rangeValCopy + - octalLiteral + - paramTypeCombine # already handle by gofumpt.extra-rules + - exitAfterDefer # FIXME(ldez) must be fixed + - ifElseChain # FIXME(ldez) must be fixed + enabled-tags: + - diagnostic + - style + - performance + settings: + hugeParam: + sizeThreshold: 100 + gocyclo: + min-complexity: 16 + godox: + keywords: + - FIXME + govet: + disable: + - fieldalignment + enable-all: true + misspell: + locale: US + + exclusions: + warn-unused: true + presets: + - comments + rules: + - linters: + - funlen + - goconst + path: .*_test.go + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..5319a75 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +- id: misspell + name: misspell + description: Correct commonly misspelled English words... quickly + language: golang + entry: misspell + args: + - -w + - -error diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e63e6c2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -sudo: required -dist: trusty -group: edge -language: go -go: - - "1.10" -git: - depth: 1 - -script: - - ./scripts/travis.sh - -# calls goreleaser when a new tag is pushed -deploy: -- provider: script - skip_cleanup: true - script: curl -sL http://git.io/goreleaser | bash - on: - tags: true - condition: $TRAVIS_OS_NAME = linux diff --git a/Dockerfile b/Dockerfile index b8ea37b..c85cd68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,13 @@ -FROM golang:1.10.0-alpine +FROM golang:1.22-alpine # cache buster -RUN echo 4 +RUN echo 4 # git is needed for "go get" below RUN apk add --no-cache git make # these are my standard testing / linting tools RUN /bin/true \ - && go get -u github.com/golang/dep/cmd/dep \ - && go get -u github.com/alecthomas/gometalinter \ - && gometalinter --install \ && rm -rf /go/src /go/pkg # # * SCOWL word list @@ -35,3 +32,4 @@ RUN /bin/true \ && wget -O /scowl-wl/words-US-60.txt ${SOURCE_US} \ && wget -O /scowl-wl/words-GB-ise-60.txt ${SOURCE_GB_ISE} +RUN git config --global --add safe.directory "/go/src/github.com/golangci/misspell" diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 90ed451..0000000 --- a/Gopkg.lock +++ /dev/null @@ -1,24 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/gobwas/glob" - packages = [ - ".", - "compiler", - "match", - "syntax", - "syntax/ast", - "syntax/lexer", - "util/runes", - "util/strings" - ] - revision = "5ccd90ef52e1e632236f7326478d4faa74f99438" - version = "v0.2.3" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "087ea4c49358ea8258ad9edfe514cd5ce9975c889c258e5ec7b5d2b720aae113" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index e9b8e6a..0000000 --- a/Gopkg.toml +++ /dev/null @@ -1,34 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - -[[constraint]] - name = "github.com/gobwas/glob" - version = "0.2.3" - -[prune] - go-tests = true - unused-packages = true diff --git a/LICENSE b/LICENSE index 423e1f9..bfcfcd3 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Makefile b/Makefile index 862ab77..fcda870 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,18 @@ -CONTAINER=nickg/misspell +CONTAINER=golangci/misspell + +default: lint test build install: ## install misspell into GOPATH/bin go install ./cmd/misspell -build: hooks ## build and lint misspell - ./scripts/build.sh +build: ## build misspell + go build ./cmd/misspell test: ## run all tests - go test . + CGO_ENABLED=1 go test -v -race . -# real publishing is done only by travis -publish: ## test goreleaser - ./scripts/goreleaser-dryrun.sh +lint: ## run linter + golangci-lint run # the grep in line 2 is to remove misspellings in the spelling dictionary # that trigger false positives!! @@ -37,31 +38,22 @@ clean: ## clean up time go clean ./... git gc --aggressive -ci: ## run test like travis-ci does, requires docker +ci: docker-build ## run test like travis-ci does, requires docker docker run --rm \ - -v $(PWD):/go/src/github.com/client9/misspell \ - -w /go/src/github.com/client9/misspell \ + -v $(PWD):/go/src/github.com/golangci/misspell \ + -w /go/src/github.com/golangci/misspell \ ${CONTAINER} \ - make build falsepositives + make install falsepositives docker-build: ## build a docker test image docker build -t ${CONTAINER} . -docker-pull: ## pull latest test image - docker pull ${CONTAINER} - docker-console: ## log into the test image docker run --rm -it \ - -v $(PWD):/go/src/github.com/client9/misspell \ - -w /go/src/github.com/client9/misspell \ + -v $(PWD):/go/src/github.com/golangci/misspell \ + -w /go/src/github.com/golangci/misspell \ ${CONTAINER} sh -.git/hooks/pre-commit: scripts/pre-commit.sh - cp -f scripts/pre-commit.sh .git/hooks/pre-commit -.git/hooks/commit-msg: scripts/commit-msg.sh - cp -f scripts/commit-msg.sh .git/hooks/commit-msg -hooks: .git/hooks/pre-commit .git/hooks/commit-msg ## install git precommit hooks - .PHONY: help ci console docker-build bench # https://www.client9.com/self-documenting-makefiles/ @@ -69,6 +61,6 @@ help: @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) -.DEFAULT_GOAL=help +.DEFAULT_GOAL=default .PHONY=help diff --git a/README.md b/README.md index 5b68af0..237a00b 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,37 @@ -[![Build Status](https://travis-ci.org/client9/misspell.svg?branch=master)](https://travis-ci.org/client9/misspell) [![Go Report Card](https://goreportcard.com/badge/github.com/client9/misspell)](https://goreportcard.com/report/github.com/client9/misspell) [![GoDoc](https://godoc.org/github.com/client9/misspell?status.svg)](https://godoc.org/github.com/client9/misspell) [![Coverage](http://gocover.io/_badge/github.com/client9/misspell)](http://gocover.io/github.com/client9/misspell) [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/client9/misspell/master/LICENSE) +[![Main](https://github.com/golangci/misspell/actions/workflows/ci.yml/badge.svg)](https://github.com/golangci/misspell/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/golangci/misspell)](https://goreportcard.com/report/github.com/golangci/misspell) +[![Go Reference](https://pkg.go.dev/badge/github.com/golangci/misspell.svg)](https://pkg.go.dev/github.com/golangci/misspell) +[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.golangci.com/golangci/misspell/master/LICENSE) Correct commonly misspelled English words... quickly. ### Install - If you just want a binary and to start using `misspell`: +```bash +curl -sfL https://raw.githubusercontent.com/golangci/misspell/master/install-misspell.sh | sh -s -- -b ./bin ${MISSPELL_VERSION} ``` -curl -L -o ./install-misspell.sh https://git.io/misspell -sh ./install-misspell.sh -``` - - -Both will install as `./bin/misspell`. You can adjust the download location using the `-b` flag. File a ticket if you want another platform supported. +Both will install as `./bin/misspell`. +You can adjust the download location using the `-b` flag. +File a ticket if you want another platform supported. -If you use [Go](https://golang.org/), the best way to run `misspell` is by using [gometalinter](#gometalinter). Otherwise, install `misspell` the old-fashioned way: +If you use [Go](https://golang.org/), the best way to run `misspell` is by using [golangci-lint](https://github.com/golangci/golangci-lint). +Otherwise, install `misspell` the old-fashioned way: +```bash +go install github.com/golangci/misspell/cmd/misspell@latest ``` -go get -u github.com/client9/misspell/cmd/misspell -``` - -and misspell will be in your `GOPATH` - -Also if you like to live dangerously, one could do +Also, if you like to live dangerously, one could do ```bash -curl -L https://git.io/misspell | bash +curl -sfL https://raw.githubusercontent.com/golangci/misspell/master/install-misspell.sh | sh -s -- -b $(go env GOPATH)/bin ${MISSPELL_VERSION} ``` ### Usage - ```bash $ misspell all.html your.txt important.md files.go your.txt:42:10 found "langauge" a misspelling of "language" @@ -41,29 +39,49 @@ your.txt:42:10 found "langauge" a misspelling of "language" # ^ file, line, column ``` -``` +```console $ misspell -help Usage of misspell: -debug - Debug matching, very slow + Debug matching, very slow + -dict string + User defined corrections file path (.csv). CSV format: typo,fix -error - Exit with 2 if misspelling found + Exit with 2 if misspelling found -f string - 'csv', 'sqlite3' or custom Golang template for output + 'csv', 'sqlite3' or custom Golang template for output -i string - ignore the following corrections, comma separated + ignore the following corrections, comma-separated -j int - Number of workers, 0 = number of CPUs + Number of workers, 0 = number of CPUs -legal - Show legal information and exit + Show legal information and exit -locale string - Correct spellings using locale perferances for US or UK. Default is to use a neutral variety of English. Setting locale to US will correct the British spelling of 'colour' to 'color' + Correct spellings using locale preferences for US or UK. Default is to use a neutral variety of English. Setting locale to US will correct the British spelling of 'colour' to 'color' -o string - output file or [stderr|stdout|] (default "stdout") - -q Do not emit misspelling output + output file or [stderr|stdout|] (default "stdout") + -q Do not emit misspelling output -source string - Source mode: auto=guess, go=golang source, text=plain or markdown-like text (default "auto") - -w Overwrite file with corrections (default is just to display) + Source mode: text (default), go (comments only) (default "text") + -v Show version and exit + -w Overwrite file with corrections (default is just to display) +``` + +### Pre-commit hook + +To use misspell with [pre-commit](https://pre-commit.com/), add the following to your `.pre-commit-config.yaml`: + + +```yaml +- repo: https://github.com/golangci/misspell + rev: v0.6.0 + hooks: + - id: misspell + # The hook will run on all files by default. + # To limit to some files only, use pre-commit patterns/types + # files: + # exclude: + # types: ``` ## FAQ @@ -72,7 +90,6 @@ Usage of misspell: * [Converting UK spellings to US](#locale) * [Using pipes and stdin](#stdin) * [Golang special support](#golang) -* [gometalinter support](#gometalinter) * [CSV Output](#csv) * [Using SQLite3](#sqlite) * [Changing output format](#output) @@ -92,7 +109,7 @@ Usage of misspell: Just add the `-w` flag! -``` +```console $ misspell -w all.html your.txt important.md files.go your.txt:9:21:corrected "langauge" to "language" @@ -104,20 +121,19 @@ your.txt:9:21:corrected "langauge" to "language" Add the `-locale US` flag! -```bash +```console $ misspell -locale US important.txt important.txt:10:20 found "colour" a misspelling of "color" ``` Add the `-locale UK` flag! -```bash +```console $ echo "My favorite color is blue" | misspell -locale UK stdin:1:3:found "favorite color" a misspelling of "favourite colour" ``` -Help is appreciated as I'm neither British nor an -expert in the English language. +Help is appreciated as I'm neither British nor an expert in the English language. ### How do you check an entire folder recursively? @@ -141,7 +157,8 @@ or find . -type f | xargs misspell ``` -You can select a type of file as well. The following examples selects all `.txt` files that are *not* in the `vendor` directory: +You can select a type of file as well. +The following examples selects all `.txt` files that are *not* in the `vendor` directory: ```bash find . -type f -name '*.txt' | grep -v vendor/ | xargs misspell -error @@ -154,14 +171,14 @@ Yes! Print messages to `stderr` only: -```bash +```console $ echo "zeebra" | misspell stdin:1:0:found "zeebra" a misspelling of "zebra" ``` Print messages to `stderr`, and corrected text to `stdout`: -```bash +```console $ echo "zeebra" | misspell -w stdin:1:0:corrected "zeebra" to "zebra" zebra @@ -169,7 +186,7 @@ zebra Only print the corrected text to `stdout`: -```bash +```console $ echo "zeebra" | misspell -w -q zebra ``` @@ -177,55 +194,23 @@ zebra ### Are there special rules for golang source files? -Yes! If the file ends in `.go`, then misspell will only check spelling in -comments. - -If you want to force a file to be checked as a golang source, use `-source=go` -on the command line. Conversely, you can check a golang source as if it were -pure text by using `-source=text`. You might want to do this since many -variable names have misspellings in them! - -### Can I check only-comments in other other programming languages? - -I'm told the using `-source=go` works well for ruby, javascript, java, c and -c++. - -It doesn't work well for python and bash. - - -### Does this work with gometalinter? - -[gometalinter](https://github.com/alecthomas/gometalinter) runs -multiple golang linters. Starting on [2016-06-12](https://github.com/alecthomas/gometalinter/pull/134) -gometalinter supports `misspell` natively but it is disabled by default. - -```bash -# update your copy of gometalinter -go get -u github.com/alecthomas/gometalinter - -# install updates and misspell -gometalinter --install --update -``` - -To use, just enable `misspell` - -``` -gometalinter --enable misspell ./... -``` +yes, if you want to force a file to be checked as a golang source, use `-source=go` on the command line. +Conversely, you can check a golang source as if it were pure text by using `-source=text`. +You might want to do this since many variable names have misspellings in them! -Note that gometalinter only checks golang files, and uses the default options -of `misspell` +### Can I check only-comments in other programming languages? -You may wish to run this on your plaintext (.txt) and/or markdown files too. +I'm told the using `-source=go` works well for Ruby, Javascript, Java, C and C++. +It doesn't work well for Python and Bash. ### How Can I Get CSV Output? -Using `-f csv`, the output is standard comma-seprated values with headers in the first row. +Using `-f csv`, the output is standard comma-separated values with headers in the first row. -``` -misspell -f csv * +```console +$ misspell -f csv * file,line,column,typo,corrected "README.md",9,22,langauge,language "README.md",47,25,langauge,language @@ -236,7 +221,7 @@ file,line,column,typo,corrected Using `-f sqlite`, the output is a [sqlite3](https://www.sqlite.org/index.html) dump-file. -```bash +```console $ misspell -f sqlite * > /tmp/misspell.sql $ cat /tmp/misspell.sql @@ -254,12 +239,12 @@ INSERT INTO misspell VALUES("install.txt",202,31,"immediatly","immediately"); COMMIT; ``` -```bash +```console $ sqlite3 -init /tmp/misspell.sql :memory: 'select count(*) from misspell' 1 ``` -With some tricks you can directly pipe output to sqlite3 by using `-init /dev/stdin`: +With some tricks you can directly pipe output to `sqlite3` by using `-init /dev/stdin`: ``` misspell -f sqlite * | sqlite3 -init /dev/stdin -column -cmd '.width 60 15' ':memory' \ @@ -271,20 +256,22 @@ misspell -f sqlite * | sqlite3 -init /dev/stdin -column -cmd '.width 60 15' ':me Using the `-i "comma,separated,rules"` flag you can specify corrections to ignore. -For example, if you were to run `misspell -w -error -source=text` against document that contains the string `Guy Finkelshteyn Braswell`, misspell would change the text to `Guy Finkelstheyn Bras well`. You can then -determine the rules to ignore by reverting the change and running the with the `-debug` flag. You can then see -that the corrections were `htey -> they` and `aswell -> as well`. To ignore these two rules, you add `-i "htey,aswell"` to -your command. With debug mode on, you can see it print the corrections, but it will no longer make them. +For example, if you were to run `misspell -w -error -source=text` against document that contains the string `Guy Finkelshteyn Braswell`, +misspell would change the text to `Guy Finkelstheyn Bras well`. +You can then determine the rules to ignore by reverting the change and running the with the `-debug` flag. +You can then see that the corrections were `htey -> they` and `aswell -> as well`. +To ignore these two rules, you add `-i "htey,aswell"` to your command. +With debug mode on, you can see it print the corrections, but it will no longer make them. ### How can I change the output format? -Using the `-f template` flag you can pass in a -[golang text template](https://golang.org/pkg/text/template/) to format the output. +Using the `-f template` flag you can pass in a [golang text template](https://golang.org/pkg/text/template/) to format the output. One can use `printf "%q" VALUE` to safely quote a value. -The default template is compatible with [gometalinter](https://github.com/alecthomas/gometalinter) +The default template: + ``` {{ .Filename }}:{{ .Line }}:{{ .Column }}:corrected {{ printf "%q" .Original }} to "{{ printf "%q" .Corrected }}" ``` @@ -298,14 +285,12 @@ To just print probable misspellings: ### What problem does this solve? -This corrects commonly misspelled English words in computer source -code, and other text-based formats (`.txt`, `.md`, etc). +This corrects commonly misspelled English words in computer source code, and other text-based formats (`.txt`, `.md`, etc.). -It is designed to run quickly so it can be -used as a [pre-commit hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) -with minimal burden on the developer. +It is designed to run quickly, +so it can be used as a [pre-commit hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) with minimal burden on the developer. -It does not work with binary formats (e.g. Word, etc). +It does not work with binary formats (e.g. Word, etc.). It is not a complete spell-checking program nor a grammar checker. @@ -322,78 +307,71 @@ They all work but had problems that prevented me from using them at scale: * slow, all of the above check one misspelling at a time (i.e. linear) using regexps * not MIT/Apache2 licensed (or equivalent) -* have dependencies that don't work for me (python3, bash, linux sed, etc) +* have dependencies that don't work for me (Python3, Bash, GNU sed, etc.) * don't understand American vs. British English and sometimes makes unwelcome "corrections" -That said, they might be perfect for you and many have more features -than this project! +That said, they might be perfect for you and many have more features than this project! ### How fast is it? -Misspell is easily 100x to 1000x faster than other spelling correctors. You -should be able to check and correct 1000 files in under 250ms. +Misspell is easily 100x to 1000x faster than other spelling correctors. +You should be able to check and correct 1000 files in under 250ms. -This uses the mighty power of golang's -[strings.Replacer](https://golang.org/pkg/strings/#Replacer) which is -a implementation or variation of the -[Aho–Corasick algorithm](https://en.wikipedia.org/wiki/Aho–Corasick_algorithm). +This uses the mighty power of golang's [strings.Replacer](https://golang.org/pkg/strings/#Replacer) +which is an implementation or variation of the [Aho–Corasick algorithm](https://en.wikipedia.org/wiki/Aho–Corasick_algorithm). This makes multiple substring matches *simultaneously*. -In addition this uses multiple CPU cores to work on multiple files. +It also uses multiple CPU cores to work on multiple files concurrently. ### What problems does it have? -Unlike the other projects, this doesn't know what a "word" is. There may be -more false positives and false negatives due to this. On the other hand, it -sometimes catches things others don't. +Unlike the other projects, this doesn't know what a "word" is. +There may be more false positives and false negatives due to this. +On the other hand, it sometimes catches things others don't. Either way, please file bugs and we'll fix them! -Since it operates in parallel to make corrections, it can be non-obvious to -determine exactly what word was corrected. +Since it operates in parallel to make corrections, +it can be non-obvious to determine exactly what word was corrected. ### It's making mistakes. How can I debug? -Run using `-debug` flag on the file you want. It should then print what word -it is trying to correct. Then [file a -bug](https://github.com/client9/misspell/issues) describing the problem. +Run using `-debug` flag on the file you want. +It should then print what word it is trying to correct. +Then [file a bug](https://github.com/golangci/misspell/issues) describing the problem. Thanks! ### Why is it making mistakes or missing items in golang files? -The matching function is *case-sensitive*, so variable names that are multiple -worlds either in all-upper or all-lower case sometimes can cause false -positives. For instance a variable named `bodyreader` could trigger a false -positive since `yrea` is in the middle that could be corrected to `year`. -Other problems happen if the variable name uses a English contraction that -should use an apostrophe. The best way of fixing this is to use the -[Effective Go naming -conventions](https://golang.org/doc/effective_go.html#mixed-caps) and use -[camelCase](https://en.wikipedia.org/wiki/CamelCase) for variable names. You -can check your code using [golint](https://github.com/golang/lint) +The matching function is *case-sensitive*, +so variable names that are multiple worlds either in all-uppercase or all-lowercase case sometimes can cause false positives. +For instance a variable named `bodyreader` could trigger a false positive since `yrea` is in the middle that could be corrected to `year`. +Other problems happen if the variable name uses an English contraction that should use an apostrophe. +The best way of fixing this is to use the [Effective Go naming conventions](https://golang.org/doc/effective_go.html#mixed-caps) +and use [camelCase](https://en.wikipedia.org/wiki/CamelCase) for variable names. +You can check your code using [golint](https://github.com/golang/lint) ### What license is this? -The main code is [MIT](https://github.com/client9/misspell/blob/master/LICENSE). +The main code is [MIT](https://github.com/golangci/misspell/blob/master/LICENSE). Misspell also makes uses of the Golang standard library and contains a modified version of Golang's [strings.Replacer](https://golang.org/pkg/strings/#Replacer) -which are covered under a [BSD License](https://github.com/golang/go/blob/master/LICENSE). Type `misspell -legal` for more details or see [legal.go](https://github.com/client9/misspell/blob/master/legal.go) +which is covered under a [BSD License](https://github.com/golang/go/blob/master/LICENSE). +Type `misspell -legal` for more details or see [legal.go](https://github.com/golangci/misspell/blob/master/legal.go) ### Where do the word lists come from? It started with a word list from [Wikipedia](https://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings/For_machines). -Unfortunately, this list had to be highly edited as many of the words are -obsolete or based from mistakes on mechanical typewriters (I'm guessing). +Unfortunately, this list had to be highly edited as many of the words are obsolete or based on mistakes on mechanical typewriters (I'm guessing). -Additional words were added based on actually mistakes seen in -the wild (meaning self-generated). +Additional words were added based on actually mistakes seen in the wild (meaning self-generated). Variations of UK and US spellings are based on many sources including: @@ -401,24 +379,23 @@ Variations of UK and US spellings are based on many sources including: * http://www.oxforddictionaries.com/us/words/american-and-british-spelling-american (excellent site but incomplete) * Diffing US and UK [scowl dictionaries](http://wordlist.aspell.net) -American English is more accepting of spelling variations than is British -English, so "what is American or not" is subject to opinion. Corrections and help welcome. +American English is more accepting of spelling variations than is British English, +so "what is American or not" is subject to opinion. +Corrections and help welcome. ### What are some other enhancements that could be done? -Here's some ideas for enhancements: +Here are some ideas for enhancements: *Capitalization of proper nouns* could be done (e.g. weekday and month names, country names, language names) -*Opinionated US spellings* US English has a number of words with alternate -spellings. Think [adviser vs. -advisor](http://grammarist.com/spelling/adviser-advisor/). While "advisor" is not wrong, the opinionated US -locale would correct "advisor" to "adviser". +*Opinionated US spellings* US English has a number of words with alternate spellings. +Think [adviser vs. advisor](http://grammarist.com/spelling/adviser-advisor/). +While "advisor" is not wrong, the opinionated US locale would correct "advisor" to "adviser". *Versioning* Some type of versioning is needed so reporting mistakes and errors is easier. -*Feedback* Mistakes would be sent to some server for agregation and feedback review. +*Feedback* Mistakes would be sent to some server for aggregation and feedback review. -*Contractions and Apostrophes* This would optionally correct "isnt" to -"isn't", etc. +*Contractions and Apostrophes* This would optionally correct "isnt" to "isn't", etc. diff --git a/ascii.go b/ascii.go index 1430718..74abe51 100644 --- a/ascii.go +++ b/ascii.go @@ -1,7 +1,7 @@ package misspell -// ByteToUpper converts an ascii byte to upper cases -// Uses a branchless algorithm +// ByteToUpper converts an ascii byte to upper cases. +// Uses a branch-less algorithm. func ByteToUpper(x byte) byte { b := byte(0x80) | x c := b - byte(0x61) @@ -10,8 +10,8 @@ func ByteToUpper(x byte) byte { return x - (e >> 2) } -// ByteToLower converts an ascii byte to lower case -// uses a branchless algorithm +// ByteToLower converts an ascii byte to lower case. +// Uses a branch-less algorithm. func ByteToLower(eax byte) byte { ebx := eax&byte(0x7f) + byte(0x25) ebx = ebx&byte(0x7f) + byte(0x1a) @@ -19,7 +19,7 @@ func ByteToLower(eax byte) byte { return eax + ebx } -// ByteEqualFold does ascii compare, case insensitive +// ByteEqualFold does ascii compare, case insensitive. func ByteEqualFold(a, b byte) bool { return a == b || ByteToLower(a) == ByteToLower(b) } @@ -27,12 +27,12 @@ func ByteEqualFold(a, b byte) bool { // StringEqualFold ASCII case-insensitive comparison // golang toUpper/toLower for both bytes and strings // appears to be Unicode based which is super slow -// based from https://codereview.appspot.com/5180044/patch/14007/21002 +// based from https://codereview.appspot.com/5180044/patch/14007/21002. func StringEqualFold(s1, s2 string) bool { if len(s1) != len(s2) { return false } - for i := 0; i < len(s1); i++ { + for i := range len(s1) { c1 := s1[i] c2 := s2[i] // c1 & c2 @@ -47,9 +47,7 @@ func StringEqualFold(s1, s2 string) bool { return true } -// StringHasPrefixFold is similar to strings.HasPrefix but comparison -// is done ignoring ASCII case. -// / +// StringHasPrefixFold is similar to strings.HasPrefix but comparison is done ignoring ASCII case. func StringHasPrefixFold(s1, s2 string) bool { // prefix is bigger than input --> false if len(s1) < len(s2) { diff --git a/benchmark_test.go b/benchmark_test.go index d8126db..f47c146 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -2,7 +2,7 @@ package misspell import ( "bytes" - "io/ioutil" + "io" "testing" ) @@ -15,7 +15,6 @@ var ( ) func init() { - buf := bytes.Buffer{} for i := 0; i < len(DictMain); i += 2 { buf.WriteString(DictMain[i+1] + " ") @@ -28,19 +27,19 @@ func init() { rep = New() } -// BenchmarkCleanString takes a clean string (one with no errors) +// BenchmarkCleanString takes a clean string (one with no errors). func BenchmarkCleanString(b *testing.B) { b.ResetTimer() b.ReportAllocs() var updated string var diffs []Diff var count int - for n := 0; n < b.N; n++ { + for n := 0; n < b.N; n++ { //nolint:intrange // use Loop -> go1.24 updated, diffs = rep.Replace(sampleClean) count += len(diffs) } - // prevent compilier optimizations + // prevent compiler optimizations tmpCount = count tmp = updated } @@ -49,14 +48,14 @@ func discardDiff(_ Diff) { tmpCount++ } -// BenchmarkCleanStream takes a clean reader (no misspells) and outputs to a buffer +// BenchmarkCleanStream takes a clean reader (no misspells) and outputs to a buffer. func BenchmarkCleanStream(b *testing.B) { b.ResetTimer() b.ReportAllocs() tmpCount = 0 buf := bytes.NewBufferString(sampleClean) out := bytes.NewBuffer(make([]byte, 0, len(sampleClean)+100)) - for n := 0; n < b.N; n++ { + for n := 0; n < b.N; n++ { //nolint:intrange // use Loop -> go1.24 buf.Reset() buf.WriteString(sampleClean) out.Reset() @@ -64,33 +63,33 @@ func BenchmarkCleanStream(b *testing.B) { } } -// BenchmarkCleanStreamDiscard takes a clean reader and discards output +// BenchmarkCleanStreamDiscard takes a clean reader and discards output. func BenchmarkCleanStreamDiscard(b *testing.B) { b.ResetTimer() b.ReportAllocs() buf := bytes.NewBufferString(sampleClean) tmpCount = 0 - for n := 0; n < b.N; n++ { + for n := 0; n < b.N; n++ { //nolint:intrange // use Loop -> go1.24 buf.Reset() buf.WriteString(sampleClean) - rep.ReplaceReader(buf, ioutil.Discard, discardDiff) + rep.ReplaceReader(buf, io.Discard, discardDiff) } } -// BenchmarkCleanString takes a clean string (one with no errors) +// BenchmarkCleanString takes a clean string (one with no errors). func BenchmarkDirtyString(b *testing.B) { b.ResetTimer() b.ReportAllocs() var updated string var diffs []Diff var count int - for n := 0; n < b.N; n++ { + for n := 0; n < b.N; n++ { //nolint:intrange // use Loop -> go1.24 updated, diffs = rep.Replace(sampleDirty) count += len(diffs) } - // prevent compilier optimizations + // prevent compiler optimizations tmpCount = count tmp = updated } @@ -99,7 +98,7 @@ func BenchmarkCompile(b *testing.B) { r := New() b.ReportAllocs() b.ResetTimer() - for n := 0; n < b.N; n++ { + for n := 0; n < b.N; n++ { //nolint:intrange // use Loop -> go1.24 r.Compile() } } diff --git a/case.go b/case.go index 2ea3850..533ce4d 100644 --- a/case.go +++ b/case.go @@ -4,10 +4,10 @@ import ( "strings" ) -// WordCase is an enum of various word casing styles +// WordCase is an enum of various word casing styles. type WordCase int -// Various WordCase types.. likely to be not correct +// Various WordCase types... likely to be not correct. const ( CaseUnknown WordCase = iota CaseLower @@ -15,13 +15,13 @@ const ( CaseTitle ) -// CaseStyle returns what case style a word is in +// CaseStyle returns what case style a word is in. func CaseStyle(word string) WordCase { upperCount := 0 lowerCount := 0 // this iterates over RUNES not BYTES - for i := 0; i < len(word); i++ { + for i := range len(word) { ch := word[i] switch { case ch >= 'a' && ch <= 'z': @@ -42,11 +42,10 @@ func CaseStyle(word string) WordCase { return CaseUnknown } -// CaseVariations returns -// If AllUpper or First-Letter-Only is upcased: add the all upper case version -// If AllLower, add the original, the title and upcase forms -// If Mixed, return the original, and the all upcase form -// +// CaseVariations returns: +// If AllUpper or First-Letter-Only is upper-cased: add the all upper case version. +// If AllLower, add the original, the title and upper-case forms. +// If Mixed, return the original, and the all upper-case form. func CaseVariations(word string, style WordCase) []string { switch style { case CaseLower: diff --git a/case_test.go b/case_test.go index 1705cf0..cfb1765 100644 --- a/case_test.go +++ b/case_test.go @@ -6,37 +6,46 @@ import ( ) func TestCaseStyle(t *testing.T) { - cases := []struct { + testCases := []struct { word string want WordCase }{ - {"lower", CaseLower}, - {"what's", CaseLower}, - {"UPPER", CaseUpper}, - {"Title", CaseTitle}, - {"CamelCase", CaseUnknown}, - {"camelCase", CaseUnknown}, + {word: "lower", want: CaseLower}, + {word: "what's", want: CaseLower}, + {word: "UPPER", want: CaseUpper}, + {word: "Title", want: CaseTitle}, + {word: "CamelCase", want: CaseUnknown}, + {word: "camelCase", want: CaseUnknown}, } - for pos, tt := range cases { - got := CaseStyle(tt.word) - if tt.want != got { - t.Errorf("Case %d %q: want %v got %v", pos, tt.word, tt.want, got) - } + for _, test := range testCases { + t.Run(test.word, func(t *testing.T) { + t.Parallel() + + got := CaseStyle(test.word) + if test.want != got { + t.Errorf("want %v got %v", test.want, got) + } + }) } } func TestCaseVariations(t *testing.T) { - cases := []struct { + testCases := []struct { word string want []string }{ - {"that's", []string{"that's", "That's", "THAT'S"}}, + {word: "that's", want: []string{"that's", "That's", "THAT'S"}}, } - for pos, tt := range cases { - got := CaseVariations(tt.word, CaseStyle(tt.word)) - if !reflect.DeepEqual(tt.want, got) { - t.Errorf("Case %d %q: want %v got %v", pos, tt.word, tt.want, got) - } + + for _, test := range testCases { + t.Run(test.word, func(t *testing.T) { + t.Parallel() + + got := CaseVariations(test.word, CaseStyle(test.word)) + if !reflect.DeepEqual(test.want, got) { + t.Errorf("want %v got %v", test.want, got) + } + }) } } diff --git a/cmd/misspell/main.go b/cmd/misspell/main.go index 174d79d..8724e55 100644 --- a/cmd/misspell/main.go +++ b/cmd/misspell/main.go @@ -3,10 +3,10 @@ package main import ( "bytes" + "encoding/csv" "flag" "fmt" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -15,22 +15,18 @@ import ( "text/template" "time" - "github.com/client9/misspell" + "github.com/golangci/misspell" ) -var ( - defaultWrite *template.Template - defaultRead *template.Template - - stdout *log.Logger - debug *log.Logger - - version = "dev" +const ( + outputFormatCSV = "csv" + outputFormatSQLite = "sqlite" + outputFormatSQLite3 = "sqlite3" ) const ( // Note for gometalinter it must be "File:Line:Column: Msg" - // note space beteen ": Msg" + // note space between ": Msg" defaultWriteTmpl = `{{ .Filename }}:{{ .Line }}:{{ .Column }}: corrected "{{ .Original }}" to "{{ .Corrected }}"` defaultReadTmpl = `{{ .Filename }}:{{ .Line }}:{{ .Column }}: "{{ .Original }}" is a misspelling of "{{ .Corrected }}"` csvTmpl = `{{ printf "%q" .Filename }},{{ .Line }},{{ .Column }},{{ .Original }},{{ .Corrected }}` @@ -44,73 +40,35 @@ CREATE TABLE misspell( sqliteFooter = "COMMIT;" ) -func worker(writeit bool, r *misspell.Replacer, mode string, files <-chan string, results chan<- int) { - count := 0 - for filename := range files { - orig, err := misspell.ReadTextFile(filename) - if err != nil { - log.Println(err) - continue - } - if len(orig) == 0 { - continue - } - - debug.Printf("Processing %s", filename) +var version = "dev" - var updated string - var changes []misspell.Diff - - if mode == "go" { - updated, changes = r.ReplaceGo(orig) - } else { - updated, changes = r.Replace(orig) - } - - if len(changes) == 0 { - continue - } - count += len(changes) - for _, diff := range changes { - // add in filename - diff.Filename = filename - - // output can be done by doing multiple goroutines - // and can clobber os.Stdout. - // - // the log package can be used simultaneously from multiple goroutines - var output bytes.Buffer - if writeit { - defaultWrite.Execute(&output, diff) - } else { - defaultRead.Execute(&output, diff) - } - - // goroutine-safe print to os.Stdout - stdout.Println(output.String()) - } +var ( + output *log.Logger + debug *log.Logger +) - if writeit { - ioutil.WriteFile(filename, []byte(updated), 0) - } - } - results <- count -} +var ( + defaultWrite *template.Template + defaultRead *template.Template +) +//nolint:funlen,nestif,gocognit,gocyclo,maintidx // TODO(ldez) must be fixed. func main() { t := time.Now() + var ( - workers = flag.Int("j", 0, "Number of workers, 0 = number of CPUs") - writeit = flag.Bool("w", false, "Overwrite file with corrections (default is just to display)") - quietFlag = flag.Bool("q", false, "Do not emit misspelling output") - outFlag = flag.String("o", "stdout", "output file or [stderr|stdout|]") - format = flag.String("f", "", "'csv', 'sqlite3' or custom Golang template for output") - ignores = flag.String("i", "", "ignore the following corrections, comma separated") - locale = flag.String("locale", "", "Correct spellings using locale perferances for US or UK. Default is to use a neutral variety of English. Setting locale to US will correct the British spelling of 'colour' to 'color'") - mode = flag.String("source", "auto", "Source mode: auto=guess, go=golang source, text=plain or markdown-like text") - debugFlag = flag.Bool("debug", false, "Debug matching, very slow") - exitError = flag.Bool("error", false, "Exit with 2 if misspelling found") - showVersion = flag.Bool("v", false, "Show version and exit") + workers = flag.Int("j", 0, "Number of workers, 0 = number of CPUs") + writeit = flag.Bool("w", false, "Overwrite file with corrections (default is just to display)") + quietFlag = flag.Bool("q", false, "Do not emit misspelling output") + outFlag = flag.String("o", "stdout", "output file or [stderr|stdout|]") + format = flag.String("f", "", "'csv', 'sqlite3' or custom Golang template for output") + ignores = flag.String("i", "", "ignore the following corrections, comma-separated") + userDictPath = flag.String("dict", "", "User defined corrections file path (.csv). CSV format: typo,fix") + locale = flag.String("locale", "", "Correct spellings using locale preferences for US or UK. Default is to use a neutral variety of English. Setting locale to US will correct the British spelling of 'colour' to 'color'") + mode = flag.String("source", "text", "Source mode: text (default), go (comments only)") + debugFlag = flag.Bool("debug", false, "Debug matching, very slow") + exitError = flag.Bool("error", false, "Exit with 2 if misspelling found") + showVersion = flag.Bool("v", false, "Show version and exit") showLegal = flag.Bool("legal", false, "Show legal information and exit") ) @@ -120,20 +78,41 @@ func main() { fmt.Println(version) return } + if *showLegal { fmt.Println(misspell.Legal) return } + + // + // Number of Workers / CPU to use + // + if *workers < 0 { + log.Fatalf("-j must >= 0") + } + if *workers == 0 { + *workers = runtime.NumCPU() + } if *debugFlag { - debug = log.New(os.Stderr, "DEBUG ", 0) - } else { - debug = log.New(ioutil.Discard, "", 0) + *workers = 1 + } + + // + // Source input mode + // + switch *mode { + case "auto", "go", "text": + default: + log.Fatalf("Mode must be one of auto=guess, go=golang source, text=plain or markdown-like text") } + debug = newDebugLogger(*debugFlag) + r := misspell.Replacer{ Replacements: misspell.DictMain, Debug: *debugFlag, } + // // Figure out regional variations // @@ -151,87 +130,49 @@ func main() { } // - // Stuff to ignore + // Load user defined words // - if len(*ignores) > 0 { - r.RemoveRule(strings.Split(*ignores, ",")) + if *userDictPath != "" { + userDict, err := readUserDict(*userDictPath) + if err != nil { + log.Fatalf("reading user defined corrections: %v", err) + } + + r.AddRuleList(userDict) } // - // Source input mode + // Stuff to ignore // - switch *mode { - case "auto": - case "go": - case "text": - default: - log.Fatalf("Mode must be one of auto=guess, go=golang source, text=plain or markdown-like text") + if *ignores != "" { + r.RemoveRule(strings.Split(*ignores, ",")) } // - // Custom output + // Output logger // - switch { - case *format == "csv": - tmpl := template.Must(template.New("csv").Parse(csvTmpl)) - defaultWrite = tmpl - defaultRead = tmpl - stdout.Println(csvHeader) - case *format == "sqlite" || *format == "sqlite3": - tmpl := template.Must(template.New("sqlite3").Parse(sqliteTmpl)) - defaultWrite = tmpl - defaultRead = tmpl - stdout.Println(sqliteHeader) - case len(*format) > 0: - t, err := template.New("custom").Parse(*format) - if err != nil { - log.Fatalf("Unable to compile log format: %s", err) - } - defaultWrite = t - defaultRead = t - default: // format == "" - defaultWrite = template.Must(template.New("defaultWrite").Parse(defaultWriteTmpl)) - defaultRead = template.Must(template.New("defaultRead").Parse(defaultReadTmpl)) - } - - // we cant't just write to os.Stdout directly since we have multiple goroutine - // all writing at the same time causing broken output. Log is routine safe. - // we see it so it doesn't use a prefix or include a time stamp. - switch { - case *quietFlag || *outFlag == "/dev/null": - stdout = log.New(ioutil.Discard, "", 0) - case *outFlag == "/dev/stderr" || *outFlag == "stderr": - stdout = log.New(os.Stderr, "", 0) - case *outFlag == "/dev/stdout" || *outFlag == "stdout": - stdout = log.New(os.Stdout, "", 0) - case *outFlag == "" || *outFlag == "-": - stdout = log.New(os.Stdout, "", 0) - default: - fo, err := os.Create(*outFlag) - if err != nil { - log.Fatalf("unable to create outfile %q: %s", *outFlag, err) - } - defer fo.Close() - stdout = log.New(fo, "", 0) - } + var cleanup func() error + output, cleanup = newLogger(*quietFlag, *outFlag) + defer func() { _ = cleanup() }() // - // Number of Workers / CPU to use + // Custom output format // - if *workers < 0 { - log.Fatalf("-j must >= 0") - } - if *workers == 0 { - *workers = runtime.NumCPU() + var err error + defaultWrite, defaultRead, err = createTemplates(*format) + if err != nil { + log.Fatal(err) } - if *debugFlag { - *workers = 1 + + switch *format { + case outputFormatCSV: + output.Println(csvHeader) + case outputFormatSQLite, outputFormatSQLite3: + output.Println(sqliteHeader) } - // // Done with Flags. - // Compile the Replacer and process files - // + // Compile the Replacer and process files r.Compile() args := flag.Args() @@ -239,27 +180,26 @@ func main() { // stdin/stdout if len(args) == 0 { - // if we are working with pipes/stdin/stdout - // there is no concurrency, so we can directly - // send data to the writers - var fileout io.Writer - var errout io.Writer + // If we are working with pipes/stdin/stdout there is no concurrency, + // so we can directly send data to the writers. + var fileOut io.Writer + var errOut io.Writer switch *writeit { case true: - // if we ARE writing the corrected stream - // the corrected stream goes to stdout - // and the misspelling errors goes to stderr + // If we are writing the corrected stream, + // the corrected stream goes to stdout, + // and the misspelling errors goes to stderr, // so we can do something like this: - // curl something | misspell -w | gzip > afile.gz - fileout = os.Stdout - errout = os.Stderr + // curl something | misspell -w | gzip > afile.gz + fileOut = os.Stdout + errOut = os.Stderr case false: - // if we are not writing out the corrected stream - // then work just like files. Misspelling errors - // are sent to stdout - fileout = ioutil.Discard - errout = os.Stdout + // If we are not writing out the corrected stream then work just like files. + // Misspelling errors are sent to stdout. + fileOut = io.Discard + errOut = os.Stdout } + count := 0 next := func(diff misspell.Diff) { count++ @@ -268,34 +208,40 @@ func main() { if *quietFlag { return } + diff.Filename = "stdin" + if *writeit { - defaultWrite.Execute(errout, diff) + defaultWrite.Execute(errOut, diff) } else { - defaultRead.Execute(errout, diff) + defaultRead.Execute(errOut, diff) } - errout.Write([]byte{'\n'}) + errOut.Write([]byte{'\n'}) } - err := r.ReplaceReader(os.Stdin, fileout, next) + + err := r.ReplaceReader(os.Stdin, fileOut, next) if err != nil { - os.Exit(1) + log.Fatal(err) } + switch *format { - case "sqlite", "sqlite3": - fileout.Write([]byte(sqliteFooter)) + case outputFormatSQLite, outputFormatSQLite3: + fileOut.Write([]byte(sqliteFooter)) } + if count != 0 && *exitError { // error os.Exit(2) } + return } c := make(chan string, 64) results := make(chan int, *workers) - for i := 0; i < *workers; i++ { + for range *workers { go worker(*writeit, &r, *mode, c, results) } @@ -310,17 +256,153 @@ func main() { close(c) count := 0 - for i := 0; i < *workers; i++ { + for range *workers { changed := <-results count += changed } switch *format { - case "sqlite", "sqlite3": - stdout.Println(sqliteFooter) + case outputFormatSQLite, outputFormatSQLite3: + output.Println(sqliteFooter) } if count != 0 && *exitError { os.Exit(2) } } + +func worker(writeit bool, r *misspell.Replacer, mode string, files <-chan string, results chan<- int) { + count := 0 + for filename := range files { + orig, err := misspell.ReadTextFile(filename) + if err != nil { + log.Println(err) + continue + } + + if orig == "" { + continue + } + + debug.Printf("Processing %s", filename) + + var updated string + var changes []misspell.Diff + + if mode == "go" { + updated, changes = r.ReplaceGo(orig) + } else { + updated, changes = r.Replace(orig) + } + + if len(changes) == 0 { + continue + } + + count += len(changes) + + for _, diff := range changes { + // add in filename + diff.Filename = filename + + // Output can be done by doing multiple goroutines + // and can clobber os.Stdout. + // + // the log package can be used simultaneously from multiple goroutines + var buffer bytes.Buffer + if writeit { + defaultWrite.Execute(&buffer, diff) + } else { + defaultRead.Execute(&buffer, diff) + } + + // goroutine-safe print to os.Stdout + output.Println(buffer.String()) + } + + if writeit { + os.WriteFile(filename, []byte(updated), 0) + } + } + results <- count +} + +func readUserDict(userDictPath string) ([]string, error) { + file, err := os.Open(userDictPath) + if err != nil { + return nil, fmt.Errorf("failed to load user defined corrections %q: %w", userDictPath, err) + } + defer func() { _ = file.Close() }() + + reader := csv.NewReader(file) + reader.FieldsPerRecord = 2 + + data, err := reader.ReadAll() + if err != nil { + return nil, fmt.Errorf("reading user defined corrections: %w", err) + } + + var userDict []string + for _, row := range data { + userDict = append(userDict, row...) + } + + return userDict, nil +} + +func createTemplates(format string) (writeTmpl, readTmpl *template.Template, err error) { + switch { + case format == outputFormatCSV: + tmpl := template.Must(template.New(outputFormatCSV).Parse(csvTmpl)) + return tmpl, tmpl, nil + + case format == outputFormatSQLite || format == outputFormatSQLite3: + tmpl := template.Must(template.New(outputFormatSQLite3).Parse(sqliteTmpl)) + return tmpl, tmpl, nil + + case format != "": + tmpl, err := template.New("custom").Parse(format) + if err != nil { + return nil, nil, fmt.Errorf("unable to compile log format: %w", err) + } + return tmpl, tmpl, nil + + default: // format == "" + writeTmpl = template.Must(template.New("defaultWrite").Parse(defaultWriteTmpl)) + readTmpl = template.Must(template.New("defaultRead").Parse(defaultReadTmpl)) + return + } +} + +func newLogger(quiet bool, outputPath string) (logger *log.Logger, cleanup func() error) { + // We can't just write to os.Stdout directly + // since we have multiple goroutine all writing at the same time causing broken output. + // Log is routine safe. + // We see it, so it doesn't use a prefix or include a time stamp. + switch { + case quiet || outputPath == os.DevNull: + logger = log.New(io.Discard, "", 0) + case outputPath == "/dev/stderr" || outputPath == "stderr": + logger = log.New(os.Stderr, "", 0) + case outputPath == "/dev/stdout" || outputPath == "stdout": + logger = log.New(os.Stdout, "", 0) + case outputPath == "" || outputPath == "-": + logger = log.New(os.Stdout, "", 0) + default: + fo, err := os.Create(outputPath) + if err != nil { + log.Fatalf("unable to create outfile %q: %s", outputPath, err) + } + return log.New(fo, "", 0), fo.Close + } + + return logger, func() error { return nil } +} + +func newDebugLogger(enable bool) *log.Logger { + if enable { + return log.New(os.Stderr, "DEBUG ", 0) + } + + return log.New(io.Discard, "", 0) +} diff --git a/falsepositives_test.go b/falsepositives_test.go index 445cb2d..6b2df46 100644 --- a/falsepositives_test.go +++ b/falsepositives_test.go @@ -5,7 +5,7 @@ import ( ) func TestFalsePositives(t *testing.T) { - cases := []string{ + testCases := []string{ "importEnd", "drinkeries", "subscripting", @@ -125,12 +125,18 @@ func TestFalsePositives(t *testing.T) { "\\nto", // https://github.com/client9/misspell/issues/93 "4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358", // https://github.com/client9/misspell/issues/97 } + r := New() r.Debug = true - for casenum, tt := range cases { - got, _ := r.Replace(tt) - if got != tt { - t.Errorf("%d: %q got converted to %q", casenum, tt, got) - } + + for _, test := range testCases { + t.Run(test, func(t *testing.T) { + t.Parallel() + + got, _ := r.Replace(test) + if got != test { + t.Errorf("%q got converted to %q", test, got) + } + }) } } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aaab3a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/golangci/misspell + +go 1.23.0 + +require github.com/gobwas/glob v0.2.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..39fa9fa --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= diff --git a/goreleaser.yml b/goreleaser.yml index 560cb38..2d2be1a 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -1,11 +1,7 @@ -# goreleaser.yml -# https://github.com/goreleaser/goreleaser - project_name: misspell builds: - - - main: cmd/misspell/main.go + - main: cmd/misspell/main.go binary: misspell ldflags: -s -w -X main.version={{.Version}} goos: @@ -14,22 +10,19 @@ builds: - windows goarch: - amd64 + - arm64 env: - CGO_ENABLED=0 - ignore: - - goos: darwin - goarch: 386 - - goos: windows - goarch: 386 -archive: - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" - replacements: - amd64: 64bit - 386: 32bit - darwin: mac - files: - - none* +archives: + - format: tar.gz + wrap_in_directory: true + format_overrides: + - goos: windows + format: zip + name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' + files: + - LICENSE checksum: name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" diff --git a/ignore/glob.go b/ignore/glob.go index 13281d1..c95d5f2 100644 --- a/ignore/glob.go +++ b/ignore/glob.go @@ -7,25 +7,24 @@ import ( "github.com/gobwas/glob" ) -// Matcher defines an interface for filematchers -// +// Matcher defines an interface for filematchers. type Matcher interface { - Match(string) bool + Match(arg string) bool True() bool MarshalText() ([]byte, error) } -// MultiMatch has matching on a list of matchers +// MultiMatch has matching on a list of matchers. type MultiMatch struct { matchers []Matcher } -// NewMultiMatch creates a new MultiMatch instance +// NewMultiMatch creates a new MultiMatch instance. func NewMultiMatch(matchers []Matcher) *MultiMatch { return &MultiMatch{matchers: matchers} } -// Match satifies the Matcher iterface +// Match satisfies the Matcher interface. func (mm *MultiMatch) Match(arg string) bool { // Normal: OR // false, false -> false @@ -44,26 +43,26 @@ func (mm *MultiMatch) Match(arg string) bool { use = m.True() } } - return use + return use } -// True returns true +// True returns true. func (mm *MultiMatch) True() bool { return true } -// MarshalText satifies the ?? interface +// MarshalText satisfies the ?? interface. func (mm *MultiMatch) MarshalText() ([]byte, error) { return []byte("multi"), nil } -// GlobMatch handle glob matching +// GlobMatch handle glob matching. type GlobMatch struct { orig string matcher glob.Glob normal bool } -// NewGlobMatch creates a new GlobMatch instance or error +// NewGlobMatch creates a new GlobMatch instance or error. func NewGlobMatch(arg []byte) (*GlobMatch, error) { truth := true if len(arg) > 0 && arg[0] == '!' { @@ -90,7 +89,7 @@ func NewBaseGlobMatch(arg string, truth bool) (*GlobMatch, error) { // Arg true should be set to false if the output is inverted. func NewPathGlobMatch(arg string, truth bool) (*GlobMatch, error) { // if starts with "/" then glob only applies to top level - if len(arg) > 0 && arg[0] == '/' { + if arg != "" && arg[0] == '/' { arg = arg[1:] } @@ -103,16 +102,15 @@ func NewPathGlobMatch(arg string, truth bool) (*GlobMatch, error) { } // True returns true if this should be evaluated normally ("true is true") -// and false if the result should be inverted ("false is true") -// +// and false if the result should be inverted ("false is true"). func (g *GlobMatch) True() bool { return g.normal } -// MarshalText is really a debug function +// MarshalText is really a debug function. func (g *GlobMatch) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("\"%s: %v %s\"", "GlobMatch", g.normal, g.orig)), nil } -// Match satisfies the Matcher interface +// Match satisfies the Matcher interface. func (g *GlobMatch) Match(file string) bool { return g.matcher.Match(file) } diff --git a/ignore/parse.go b/ignore/parse.go index cb63b25..3752dc2 100644 --- a/ignore/parse.go +++ b/ignore/parse.go @@ -4,10 +4,11 @@ import ( "bytes" ) -// Parse reads in a gitignore file and returns a Matcher -func Parse(src []byte) (Matcher, error) { - matchers := []Matcher{} +// Parse reads in a gitignore file and returns a Matcher. +func Parse(src []byte) (*MultiMatch, error) { + var matchers []Matcher lines := bytes.Split(src, []byte{'\n'}) + for _, line := range lines { if len(line) == 0 || len(bytes.TrimSpace(line)) == 0 { continue @@ -29,7 +30,9 @@ func Parse(src []byte) (Matcher, error) { if err != nil { return nil, err } + matchers = append(matchers, m) } + return NewMultiMatch(matchers), nil } diff --git a/ignore/parse_test.go b/ignore/parse_test.go index c3c7b69..ca647f5 100644 --- a/ignore/parse_test.go +++ b/ignore/parse_test.go @@ -5,51 +5,56 @@ import ( ) func TestParseMatchSingle(t *testing.T) { - cases := []struct { + testCases := []struct { pattern string filename string want bool }{ - {"*.c", "foo.c", true}, - {"*.c", "foo/bar.c", true}, - {"Documentation/*.html", "Documentation/git.html", true}, - {"Documentation/*.html", "Documentation/ppc/ppc.html", false}, - {"/*.c", "cat-file.c", true}, - {"/*.c", "mozilla-sha1/sha1.c", false}, - {"foo", "foo", true}, - {"**/foo", "./foo", true}, // <--- leading './' required - {"**/foo", "junk/foo", true}, - {"**/foo/bar", "./foo/bar", true}, // <--- leading './' required - {"**/foo/bar", "junk/foo/bar", true}, - {"abc/**", "abc/foo", true}, - {"abc/**", "abc/foo/bar", true}, - {"a/**/b", "a/b", true}, - {"a/**/b", "a/x/b", true}, - {"a/**/b", "a/x/y/b", true}, - - {"*_test*", "foo_test.go", true}, - {"*_test*", "junk/foo_test.go", true}, - {"junk\n!junk", "foo", false}, - {"junk\n!junk", "junk", false}, - - {"*.html\n!foo.html", "junk.html", true}, - {"*.html\n!foo.html", "foo.html", false}, - - {"/*\n!/foo\n/foo/*\n!/foo/bar", "crap", true}, - {"/*\n!/foo\n/foo/*\n!/foo/bar", "foo/crap", true}, - {"/*\n!/foo\n/foo/*\n!/foo/bar", "foo/bar", false}, - {"/*\n!/foo\n/foo/*\n!/foo/bar", "foo/bar/other", false}, - {"/*\n!/foo\n/foo/*\n!/foo/bar", "foo", false}, + {pattern: "*.c", filename: "foo.c", want: true}, + {pattern: "*.c", filename: "foo/bar.c", want: true}, + {pattern: "Documentation/*.html", filename: "Documentation/git.html", want: true}, + {pattern: "Documentation/*.html", filename: "Documentation/ppc/ppc.html"}, + {pattern: "/*.c", filename: "cat-file.c", want: true}, + {pattern: "/*.c", filename: "mozilla-sha1/sha1.c"}, + {pattern: "foo", filename: "foo", want: true}, + {pattern: "**/foo", filename: "./foo", want: true}, // <--- leading './' required + {pattern: "**/foo", filename: "junk/foo", want: true}, + {pattern: "**/foo/bar", filename: "./foo/bar", want: true}, // <--- leading './' required + {pattern: "**/foo/bar", filename: "junk/foo/bar", want: true}, + {pattern: "abc/**", filename: "abc/foo", want: true}, + {pattern: "abc/**", filename: "abc/foo/bar", want: true}, + {pattern: "a/**/b", filename: "a/b", want: true}, + {pattern: "a/**/b", filename: "a/x/b", want: true}, + {pattern: "a/**/b", filename: "a/x/y/b", want: true}, + + {pattern: "*_test*", filename: "foo_test.go", want: true}, + {pattern: "*_test*", filename: "junk/foo_test.go", want: true}, + {pattern: "junk\n!junk", filename: "foo"}, + {pattern: "junk\n!junk", filename: "junk"}, + + {pattern: "*.html\n!foo.html", filename: "junk.html", want: true}, + {pattern: "*.html\n!foo.html", filename: "foo.html"}, + + {pattern: "/*\n!/foo\n/foo/*\n!/foo/bar", filename: "crap", want: true}, + {pattern: "/*\n!/foo\n/foo/*\n!/foo/bar", filename: "foo/crap", want: true}, + {pattern: "/*\n!/foo\n/foo/*\n!/foo/bar", filename: "foo/bar"}, + {pattern: "/*\n!/foo\n/foo/*\n!/foo/bar", filename: "foo/bar/other"}, + {pattern: "/*\n!/foo\n/foo/*\n!/foo/bar", filename: "foo"}, } - for i, testcase := range cases { - matcher, err := Parse([]byte(testcase.pattern)) - if err != nil { - t.Errorf("%d) error: %s", i, err) - } - got := matcher.Match(testcase.filename) - if testcase.want != got { - t.Errorf("%d) %q.Match(%q) = %v, got %v", i, testcase.pattern, testcase.filename, testcase.want, got) - } + for _, test := range testCases { + t.Run(test.pattern, func(t *testing.T) { + t.Parallel() + + matcher, err := Parse([]byte(test.pattern)) + if err != nil { + t.Errorf("error: %s", err) + } + + got := matcher.Match(test.filename) + if test.want != got { + t.Errorf("%q.Match(%q) = %v, got %v", test.pattern, test.filename, test.want, got) + } + }) } } diff --git a/install-misspell.sh b/install-misspell.sh index e24a84a..8d30022 100755 --- a/install-misspell.sh +++ b/install-misspell.sh @@ -1,39 +1,33 @@ #!/bin/sh set -e -# Code generated by godownloader. DO NOT EDIT. -# usage() { this=$1 cat <] [-d] [] -b sets bindir or installation directory, Defaults to ./bin - [tag] is a tag from - https://github.com/client9/misspell/releases - If tag is missing, then an attempt to find the latest will be found. - - Consider setting GITHUB_TOKEN to avoid triggering GitHub rate limits. - See the following for more details: - https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ - - Generated by godownloader - https://github.com/goreleaser/godownloader + -d turns on debug logging + is a tag from + https://github.com/golangci/misspell/releases + If tag is missing, then the latest will be used. EOF exit 2 } parse_args() { - #BINDIR is ./bin unless set be ENV - # over-ridden by flag below + # BINDIR is ./bin unless set be ENV + # overridden by flag below BINDIR=${BINDIR:-./bin} - while getopts "b:h?" arg; do + while getopts "b:dh?x" arg; do case "$arg" in b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; h | \?) usage "$0" ;; + x) set -x ;; esac done shift $((OPTIND - 1)) @@ -44,69 +38,66 @@ parse_args() { # network, either nothing will happen or will syntax error # out preventing half-done work execute() { - TMPDIR=$(mktmpdir) - log_debug "downloading tarball ${TARBALL_URL}" - http_download "${TMPDIR}/${TARBALL}" "${TARBALL_URL}" - log_debug "downloading checksum ${CHECKSUM_URL}" - http_download "${TMPDIR}/${CHECKSUM}" "${CHECKSUM_URL}" - hash_sha256_verify "${TMPDIR}/${TARBALL}" "${TMPDIR}/${CHECKSUM}" - - (cd "${TMPDIR}" && untar "${TARBALL}") - install -d "${BINDIR}" - install "${TMPDIR}/${BINARY}" "${BINDIR}/" - log_info "installed as ${BINDIR}/${BINARY}" + tmpdir=$(mktemp -d) + log_debug "downloading files into ${tmpdir}" + http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}" + http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}" + hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}" + srcdir="${tmpdir}/${NAME}" + rm -rf "${srcdir}" + (cd "${tmpdir}" && untar "${TARBALL}") + test ! -d "${BINDIR}" && install -d "${BINDIR}" + for binexe in $BINARIES; do + if [ "$OS" = "windows" ]; then + binexe="${binexe}.exe" + fi + install "${srcdir}/${binexe}" "${BINDIR}/" + log_info "installed ${BINDIR}/${binexe}" + done + rm -rf "${tmpdir}" } -is_supported_platform() { - platform=$1 - found=1 - case "$platform" in - darwin/amd64) found=0 ;; - linux/amd64) found=0 ;; - windows/amd64) found=0 ;; - esac - case "$platform" in - darwin/386) found=1 ;; - windows/386) found=1 ;; +get_binaries() { + case "$PLATFORM" in + darwin/amd64) BINARIES="misspell" ;; + darwin/arm64) BINARIES="misspell" ;; + linux/amd64) BINARIES="misspell" ;; + linux/arm64) BINARIES="misspell" ;; + windows/amd64) BINARIES="misspell" ;; + windows/arm64) BINARIES="misspell" ;; + *) + log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new" + exit 1 + ;; esac - return $found -} -check_platform() { - if is_supported_platform "$PLATFORM"; then - # optional logging goes here - true - else - log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new" - exit 1 - fi } tag_to_version() { if [ -z "${TAG}" ]; then log_info "checking GitHub for latest tag" - TAG=$(github_last_release "$OWNER/$REPO") + else + log_info "checking GitHub for tag '${TAG}'" + fi + REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details" + exit 1 fi # if version starts with 'v', remove it + TAG="$REALTAG" VERSION=${TAG#v} } adjust_format() { - # change format (tar.gz or zip) based on ARCH + # change format (tar.gz or zip) based on OS + case ${OS} in + windows) FORMAT=zip ;; + esac true } adjust_os() { # adjust archive name based on OS - case ${OS} in - 386) OS=32bit ;; - amd64) OS=64bit ;; - darwin) OS=mac ;; - esac true } adjust_arch() { # adjust archive name based on ARCH - case ${ARCH} in - 386) ARCH=32bit ;; - amd64) ARCH=64bit ;; - darwin) ARCH=mac ;; - esac true } @@ -124,9 +115,6 @@ is_command() { echoerr() { echo "$@" 1>&2 } -log_prefix() { - echo "$0" -} _logp=6 log_set_priority() { _logp="$1" @@ -136,24 +124,45 @@ log_priority() { echo "$_logp" return fi - [ "$1" -ge "$_logp" ] + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac } log_debug() { - log_priority 7 && echoerr "$(log_prefix)" "DEBUG" "$@" + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" } log_info() { - log_priority 6 && echoerr "$(log_prefix)" "INFO" "$@" + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" } log_err() { - log_priority 3 && echoerr "$(log_prefix)" "ERR" "$@" + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" } log_crit() { - log_priority 2 && echoerr "$(log_prefix)" "CRIT" "$@" + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" } uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in - msys_nt) os="windows" ;; + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; + win*) os="windows" ;; + sunos) [ "$(uname -o)" = "illumos" ] && os=illumos ;; esac echo "$os" } @@ -164,12 +173,14 @@ uname_arch() { x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; + i86pc) arch="amd64" ;; aarch64) arch="arm64" ;; - armv5*) arch="arm5" ;; - armv6*) arch="arm6" ;; - armv7*) arch="arm7" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + loongarch64) arch="loong64" ;; esac - echo ${arch} + echo "${arch}" } uname_os_check() { os=$(uname_os) @@ -177,6 +188,7 @@ uname_os_check() { darwin) return 0 ;; dragonfly) return 0 ;; freebsd) return 0 ;; + illumos) return 0;; linux) return 0 ;; android) return 0 ;; nacl) return 0 ;; @@ -186,7 +198,7 @@ uname_os_check() { solaris) return 0 ;; windows) return 0 ;; esac - log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value." return 1 } uname_arch_check() { @@ -205,16 +217,18 @@ uname_arch_check() { mips64) return 0 ;; mips64le) return 0 ;; s390x) return 0 ;; + riscv64) return 0 ;; amd64p32) return 0 ;; + loong64) return 0 ;; esac - log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value." return 1 } untar() { tarball=$1 case "${tarball}" in - *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; - *.tar) tar -xf "${tarball}" ;; + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; *.zip) unzip "${tarball}" ;; *) log_err "untar unknown archive format for ${tarball}" @@ -222,52 +236,77 @@ untar() { ;; esac } -mktmpdir() { - test -z "$TMPDIR" && TMPDIR="$(mktemp -d)" - mkdir -p "${TMPDIR}" - echo "${TMPDIR}" -} -http_download() { + +http_download_curl() { local_file=$1 source_url=$2 header=$3 - headerflag='' - destflag='' - if is_command curl; then - cmd='curl --fail -sSL' - destflag='-o' - headerflag='-H' - elif is_command wget; then - cmd='wget -q' - destflag='-O' - headerflag='--header' - else - log_crit "http_download unable to find wget or curl" - return 1 + + # workaround https://github.com/curl/curl/issues/13845 + curl_version=$(curl --version | head -n 1 | awk '{ print $2 }') + if [ "$curl_version" = "8.8.0" ]; then + log_debug "http_download_curl curl $curl_version detected" + if [ -z "$header" ]; then + curl -sL -o "$local_file" "$source_url" + else + curl -sL -H "$header" -o "$local_file" "$source_url" + nf=$(jq -r '.error // ""' "$local_file") + if [ -n "$nf" ]; then + log_debug "http_download_curl received an error: $nf" + return 1 + fi + fi + return 0 fi + if [ -z "$header" ]; then - $cmd $destflag "$local_file" "$source_url" + code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") else - $cmd $headerflag "$header" $destflag "$local_file" "$source_url" + code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 } -github_api() { + +http_download_wget() { local_file=$1 source_url=$2 - header="" - case "$source_url" in - https://api.github.com*) - test -z "$GITHUB_TOKEN" || header="Authorization: token $GITHUB_TOKEN" - ;; - esac - http_download "$local_file" "$source_url" "$header" + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" } -github_last_release() { +github_release() { owner_repo=$1 version=$2 test -z "$version" && version="latest" giturl="https://github.com/${owner_repo}/releases/${version}" - json=$(http_download "-" "$giturl" "Accept:application/json") + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') test -z "$version" && return 1 echo "$version" @@ -316,9 +355,10 @@ End of functions from https://github.com/client9/shlib ------------------------------------------------------------------------ EOF -OWNER=client9 -REPO="misspell" -BINARY=misspell +PROJECT_NAME="misspell" +OWNER=golangci +REPO=$PROJECT_NAME +BINARY=$PROJECT_NAME FORMAT=tar.gz OS=$(uname_os) ARCH=$(uname_arch) @@ -336,7 +376,7 @@ uname_arch_check "$ARCH" parse_args "$@" -check_platform +get_binaries tag_to_version @@ -354,9 +394,4 @@ TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL} CHECKSUM=${BINARY}_${VERSION}_checksums.txt CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM} -# Adjust binary name if windows -if [ "$OS" = "windows" ]; then - BINARY="${BINARY}.exe" -fi - execute diff --git a/legal.go b/legal.go index 2007697..4f9bcfc 100644 --- a/legal.go +++ b/legal.go @@ -3,7 +3,7 @@ package misspell // Legal provides licensing info. const Legal = ` -Execept where noted below, the source code for misspell is +Except where noted below, the source code for misspell is copyright Nick Galbreath and distribution is allowed under a MIT license. See the following for details: @@ -44,5 +44,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -` +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.` diff --git a/mime.go b/mime.go index 9db4902..19d49e0 100644 --- a/mime.go +++ b/mime.go @@ -4,21 +4,19 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" + "slices" "strings" ) -// The number of possible binary formats is very large -// items that might be checked into a repo or be an -// artifact of a build. Additions welcome. +// The number of possible binary formats is very large items that might be checked into a repo or be an artifact of a build. +// Additions welcome. // -// Golang's internal table is very small and can't be -// relied on. Even then things like ".js" have a mime -// type of "application/javascipt" which isn't very helpful. -// "[x]" means we have sniff test and suffix test should be eliminated +// Golang's internal table is very small and can't be relied on. +// Even then things like ".js" have a mime type of "application/javascript" which isn't very helpful. +// "[x]" means we have sniff test and suffix test should be eliminated. var binary = map[string]bool{ ".a": true, // [ ] archive ".bin": true, // [ ] binary @@ -52,12 +50,10 @@ var binary = map[string]bool{ ".zip": true, // [x] archive } -// isBinaryFilename returns true if the file is likely to be binary +// isBinaryFilename returns true if the file is likely to be binary. // -// Better heuristics could be done here, in particular a binary -// file is unlikely to be UTF-8 encoded. However this is cheap -// and will solve the immediate need of making sure common -// binary formats are not corrupted by mistake. +// Better heuristics could be done here, in particular a binary file is unlikely to be UTF-8 encoded. +// However, this is cheap and will solve the immediate need of making sure common binary formats are not corrupted by mistake. func isBinaryFilename(s string) bool { return binary[strings.ToLower(filepath.Ext(s))] } @@ -70,8 +66,8 @@ var scm = map[string]bool{ "CVS": true, } -// isSCMPath returns true if the path is likely part of a (private) SCM -// directory. E.g. ./git/something = true +// isSCMPath returns true if the path is likely part of a (private) SCM directory. +// E.g. ./git/something = true. func isSCMPath(s string) bool { // hack for .git/COMMIT_EDITMSG and .git/TAG_EDITMSG // normally we don't look at anything in .git @@ -82,13 +78,12 @@ func isSCMPath(s string) bool { if strings.Contains(filepath.Base(s), "EDITMSG") { return false } + parts := strings.Split(filepath.Clean(s), string(filepath.Separator)) - for _, dir := range parts { - if scm[dir] { - return true - } - } - return false + + return slices.ContainsFunc(parts, func(dir string) bool { + return scm[dir] + }) } var magicHeaders = [][]byte{ @@ -128,29 +123,30 @@ func isTextFile(raw []byte) bool { } } - // allow any text/ type with utf-8 encoding - // DetectContentType sometimes returns charset=utf-16 for XML stuff - // in which case ignore. + // allow any text/ type with utf-8 encoding. + // DetectContentType sometimes returns charset=utf-16 for XML stuff in which case ignore. mime := http.DetectContentType(raw) return strings.HasPrefix(mime, "text/") && strings.HasSuffix(mime, "charset=utf-8") } -// ReadTextFile returns the contents of a file, first testing if it is a text file -// returns ("", nil) if not a text file -// returns ("", error) if error -// returns (string, nil) if text +// ReadTextFile returns the contents of a file, first testing if it is a text file: // -// unfortunately, in worse case, this does -// 1 stat -// 1 open,read,close of 512 bytes -// 1 more stat,open, read everything, close (via ioutil.ReadAll) -// This could be kinder to the filesystem. +// returns ("", nil) if not a text file +// returns ("", error) if error +// returns (string, nil) if text +// +// unfortunately, in worse case, this does: +// +// 1 stat +// 1 open,read,close of 512 bytes +// 1 more stat,open, read everything, close (via io.ReadAll) +// This could be kinder to the filesystem. // // This uses some heuristics of the file's extension (e.g. .zip, .txt) and // uses a sniffer to determine if the file is text or not. // Using file extensions isn't great, but probably // good enough for real-world use. -// Golang's built in sniffer is problematic for differnet reasons. It's +// Golang's built-in sniffer is problematic for different reasons. It's // optimized for HTML, and is very limited in detection. It would be good // to explicitly add some tests for ELF/DWARF formats to make sure we never // corrupt binary files. @@ -164,9 +160,8 @@ func ReadTextFile(filename string) (string, error) { } fstat, err := os.Stat(filename) - if err != nil { - return "", fmt.Errorf("Unable to stat %q: %s", filename, err) + return "", fmt.Errorf("unable to stat %q: %w", filename, err) } // directory: nothing to do. @@ -179,28 +174,29 @@ func ReadTextFile(filename string) (string, error) { // if not-text, then exit isText := false if fstat.Size() > 50000 { - fin, err := os.Open(filename) + var fin *os.File + fin, err = os.Open(filename) if err != nil { - return "", fmt.Errorf("Unable to open large file %q: %s", filename, err) + return "", fmt.Errorf("unable to open large file %q: %w", filename, err) } defer fin.Close() buf := make([]byte, 512) _, err = io.ReadFull(fin, buf) if err != nil { - return "", fmt.Errorf("Unable to read 512 bytes from %q: %s", filename, err) + return "", fmt.Errorf("unable to read 512 bytes from %q: %w", filename, err) } if !isTextFile(buf) { return "", nil } - // set so we don't double check this file + // set so we don't double-check this file isText = true } // read in whole file - raw, err := ioutil.ReadFile(filename) + raw, err := os.ReadFile(filename) if err != nil { - return "", fmt.Errorf("Unable to read all %q: %s", filename, err) + return "", fmt.Errorf("unable to read all %q: %w", filename, err) } if !isText && !isTextFile(raw) { diff --git a/notwords.go b/notwords.go index 06d0d5a..f694f46 100644 --- a/notwords.go +++ b/notwords.go @@ -4,23 +4,28 @@ import ( "bytes" "regexp" "strings" + "unicode" ) var ( - reEmail = regexp.MustCompile(`[a-zA-Z0-9_.%+-]+@[a-zA-Z0-9-.]+\.[a-zA-Z]{2,6}[^a-zA-Z]`) - reHost = regexp.MustCompile(`[a-zA-Z0-9-.]+\.[a-zA-Z]+`) - reBackslash = regexp.MustCompile(`\\[a-z]`) + reEmail = regexp.MustCompile(`[[:alnum:]_.%+-]+@[[:alnum:]-.]+\.[[:alpha:]]{2,6}[^[:alpha:]]`) + reBackslash = regexp.MustCompile(`\\[[:lower:]]`) + + // reHost Host name regular expression. + // The length of any one label is limited between 1 and 63 octets. (https://www.ietf.org/rfc/rfc2181.txt) + // A TLD has at least 2 letters. + reHost = regexp.MustCompile(`([[:alnum:]-]+\.)+[[:alpha:]]{2,63}`) ) // RemovePath attempts to strip away embedded file system paths, e.g. -// /foo/bar or /static/myimg.png // -// TODO: windows style +// /foo/bar or /static/myimg.png // +// TODO: windows style. func RemovePath(s string) string { out := bytes.Buffer{} var idx int - for len(s) > 0 { + for s != "" { if idx = strings.IndexByte(s, '/'); idx == -1 { out.WriteString(s) break @@ -57,28 +62,40 @@ func RemovePath(s string) string { return out.String() } -// replaceWithBlanks returns a string with the same number of spaces as the input +// replaceWithBlanks returns a string with the same number of spaces as the input. func replaceWithBlanks(s string) string { return strings.Repeat(" ", len(s)) } -// RemoveEmail remove email-like strings, e.g. "nickg+junk@xfoobar.com", "nickg@xyz.abc123.biz" +// replaceHost same as replaceWithBlanks but if the string contains at least one uppercase letter returns the string. +// Domain names are case-insensitive but browsers and DNS convert uppercase to lower case. (https://www.ietf.org/rfc/rfc4343.txt) +func replaceHost(s string) string { + for _, r := range s { + if unicode.IsUpper(r) { + return s + } + } + + return replaceWithBlanks(s) +} + +// RemoveEmail remove email-like strings, e.g. "nickg+junk@xfoobar.com", "nickg@xyz.abc123.biz". func RemoveEmail(s string) string { return reEmail.ReplaceAllStringFunc(s, replaceWithBlanks) } -// RemoveHost removes host-like strings "foobar.com" "abc123.fo1231.biz" +// RemoveHost removes host-like strings "foobar.com" "abc123.fo1231.biz". func RemoveHost(s string) string { - return reHost.ReplaceAllStringFunc(s, replaceWithBlanks) + return reHost.ReplaceAllStringFunc(s, replaceHost) } -// RemoveBackslashEscapes removes characters that are preceeded by a backslash -// commonly found in printf format stringd "\nto" +// RemoveBackslashEscapes removes characters that are preceded by a backslash. +// commonly found in printf format string "\nto". func removeBackslashEscapes(s string) string { return reBackslash.ReplaceAllStringFunc(s, replaceWithBlanks) } -// RemoveNotWords blanks out all the not words +// RemoveNotWords blanks out all the not words. func RemoveNotWords(s string) string { // do most selective/specific first return removeBackslashEscapes(RemoveHost(RemoveEmail(RemovePath(StripURL(s))))) diff --git a/notwords_test.go b/notwords_test.go index e52e1aa..84df8e3 100644 --- a/notwords_test.go +++ b/notwords_test.go @@ -5,23 +5,40 @@ import ( ) func TestNotWords(t *testing.T) { - cases := []struct { + testCases := []struct { word string want string }{ - {" /foo/bar abc", " abc"}, - {"X/foo/bar abc", "X/foo/bar abc"}, - {"[/foo/bar] abc", "[ ] abc"}, - {"/", "/"}, - {"x nickg@client9.xxx y", "x y"}, - {"x infinitie.net y", "x y"}, - {"(s.svc.GetObject(", "( ("}, - {"\\nto", " to"}, + {word: " /foo/bar abc", want: " abc"}, + {word: "X/foo/bar abc", want: "X/foo/bar abc"}, + {word: "[/foo/bar] abc", want: "[ ] abc"}, + {word: "/", want: "/"}, + {word: "x nickg@client9.xxx y", want: "x y"}, + {word: "x fqdn.example.org. y", want: "x . y"}, + {word: "x infinitie.net y", want: "x y"}, + {word: "x infinitie.net ", want: "x "}, + {word: "x infinitie.net", want: "x "}, + {word: "x foo.example.com y", want: "x y"}, + {word: "x foo.example.com ", want: "x "}, + {word: "x foo.example.com", want: "x "}, + {word: "foo.example.com y", want: " y"}, + {word: "foo.example.com", want: " "}, + {word: "(s.svc.GetObject(", want: "(s.svc.GetObject("}, + {word: "defer file.Close()", want: "defer file.Close()"}, + {word: "defer file.c()", want: "defer file.c()"}, + {word: "defer file.cl()", want: "defer ()"}, // false negative + {word: "defer file.close()", want: "defer ()"}, // false negative + {word: "\\nto", want: " to"}, } - for pos, tt := range cases { - got := RemoveNotWords(tt.word) - if got != tt.want { - t.Errorf("%d want %q got %q", pos, tt.want, got) - } + + for _, test := range testCases { + t.Run(test.word, func(t *testing.T) { + t.Parallel() + + got := RemoveNotWords(test.word) + if got != test.want { + t.Errorf("want %q got %q", test.want, got) + } + }) } } diff --git a/replace.go b/replace.go index a99bbcc..7465cdf 100644 --- a/replace.go +++ b/replace.go @@ -5,29 +5,20 @@ import ( "bytes" "io" "regexp" + "slices" "strings" "text/scanner" ) -func max(x, y int) int { - if x > y { - return x - } - return y -} - func inArray(haystack []string, needle string) bool { - for _, word := range haystack { - if needle == word { - return true - } - } - return false + return slices.ContainsFunc(haystack, func(word string) bool { + return strings.EqualFold(needle, word) + }) } var wordRegexp = regexp.MustCompile(`[a-zA-Z0-9']+`) -// Diff is datastructure showing what changed in a single line +// Diff is datastructures showing what changed in a single line. type Diff struct { Filename string FullLine string @@ -37,7 +28,7 @@ type Diff struct { Corrected string } -// Replacer is the main struct for spelling correction +// Replacer is the main struct for spelling correction. type Replacer struct { Replacements []string Debug bool @@ -45,7 +36,7 @@ type Replacer struct { corrected map[string]string } -// New creates a new default Replacer using the main rule list +// New creates a new default Replacer using the main rule list. func New() *Replacer { r := Replacer{ Replacements: DictMain, @@ -54,31 +45,32 @@ func New() *Replacer { return &r } -// RemoveRule deletes existings rules. -// TODO: make inplace to save memory +// RemoveRule deletes existing rules. +// The content of `ignore` is case-insensitive. +// TODO: make in place to save memory. func (r *Replacer) RemoveRule(ignore []string) { - newwords := make([]string, 0, len(r.Replacements)) + newWords := make([]string, 0, len(r.Replacements)) for i := 0; i < len(r.Replacements); i += 2 { if inArray(ignore, r.Replacements[i]) { continue } - newwords = append(newwords, r.Replacements[i:i+2]...) + newWords = append(newWords, r.Replacements[i:i+2]...) } r.engine = nil - r.Replacements = newwords + r.Replacements = newWords } // AddRuleList appends new rules. // Input is in the same form as Strings.Replacer: [ old1, new1, old2, new2, ....] -// Note: does not check for duplictes +// Note: does not check for duplicates. func (r *Replacer) AddRuleList(additions []string) { r.engine = nil r.Replacements = append(r.Replacements, additions...) } -// Compile compiles the rules. Required before using the Replace functions +// Compile compiles the rules. +// Required before using the Replace functions. func (r *Replacer) Compile() { - r.corrected = make(map[string]string, len(r.Replacements)/2) for i := 0; i < len(r.Replacements); i += 2 { r.corrected[r.Replacements[i]] = r.Replacements[i+1] @@ -92,11 +84,14 @@ extract words from each line1 replace word -> newword if word == new-word - continue + + continue + if new-word in list of replacements - continue -new word not original, and not in list of replacements - some substring got mixed up. UNdo + + continue + +new word not original, and not in list of replacements some substring got mixed up. UNdo. */ func (r *Replacer) recheckLine(s string, lineNum int, buf io.Writer, next func(Diff)) { first := 0 @@ -136,9 +131,8 @@ func (r *Replacer) recheckLine(s string, lineNum int, buf io.Writer, next func(D io.WriteString(buf, s[first:]) } -// ReplaceGo is a specialized routine for correcting Golang source -// files. Currently only checks comments, not identifiers for -// spelling. +// ReplaceGo is a specialized routine for correcting Golang source files. +// Currently only checks comments, not identifiers for spelling. func (r *Replacer) ReplaceGo(input string) (string, []Diff) { var s scanner.Scanner s.Init(strings.NewReader(input)) @@ -169,14 +163,14 @@ Loop: return input, nil } if lastPos < len(input) { - output = output + input[lastPos:] + output += input[lastPos:] } diffs := make([]Diff, 0, 8) buf := bytes.NewBuffer(make([]byte, 0, max(len(input), len(output))+100)) // faster that making a bytes.Buffer and bufio.ReadString outlines := strings.SplitAfter(output, "\n") inlines := strings.SplitAfter(input, "\n") - for i := 0; i < len(inlines); i++ { + for i := range inlines { if inlines[i] == outlines[i] { buf.WriteString(outlines[i]) continue @@ -187,11 +181,9 @@ Loop: } return buf.String(), diffs - } -// Replace is corrects misspellings in input, returning corrected version -// along with a list of diffs. +// Replace is correcting misspellings in input, returning corrected version along with a list of diffs. func (r *Replacer) Replace(input string) (string, []Diff) { output := r.engine.Replace(input) if input == output { @@ -202,7 +194,7 @@ func (r *Replacer) Replace(input string) (string, []Diff) { // faster that making a bytes.Buffer and bufio.ReadString outlines := strings.SplitAfter(output, "\n") inlines := strings.SplitAfter(input, "\n") - for i := 0; i < len(inlines); i++ { + for i := range inlines { if inlines[i] == outlines[i] { buf.WriteString(outlines[i]) continue @@ -215,8 +207,8 @@ func (r *Replacer) Replace(input string) (string, []Diff) { return buf.String(), diffs } -// ReplaceReader applies spelling corrections to a reader stream. Diffs are -// emitted through a callback. +// ReplaceReader applies spelling corrections to a reader stream. +// Diffs are emitted through a callback. func (r *Replacer) ReplaceReader(raw io.Reader, w io.Writer, next func(Diff)) error { var ( err error @@ -239,7 +231,7 @@ func (r *Replacer) ReplaceReader(raw io.Reader, w io.Writer, next func(Diff)) er io.WriteString(w, line) continue } - // but it can be inaccurate, so we need to double check + // but it can be inaccurate, so we need to double-check r.recheckLine(line, lineNum, w, next) } return nil diff --git a/replace_test.go b/replace_test.go index 538f5ba..177c138 100644 --- a/replace_test.go +++ b/replace_test.go @@ -28,18 +28,23 @@ func TestReplaceLocale(t *testing.T) { orig string want string }{ - {"The colours are pretty", "The colors are pretty"}, - {"summaries", "summaries"}, + {orig: "The colours are pretty", want: "The colors are pretty"}, + {orig: "summaries", want: "summaries"}, } r := New() r.AddRuleList(DictAmerican) r.Compile() - for line, tt := range cases { - got, _ := r.Replace(tt.orig) - if got != tt.want { - t.Errorf("%d: ReplaceLocale want %q got %q", line, tt.orig, got) - } + + for _, test := range cases { + t.Run(test.orig, func(t *testing.T) { + t.Parallel() + + got, _ := r.Replace(test.orig) + if got != test.want { + t.Errorf("ReplaceLocale want %q got %q", test.orig, got) + } + }) } } @@ -48,72 +53,105 @@ func TestReplace(t *testing.T) { orig string want string }{ - {"I live in Amercia", "I live in America"}, - {"grill brocoli now", "grill broccoli now"}, - {"There is a zeebra", "There is a zebra"}, - {"foo other bar", "foo other bar"}, - {"ten fiels", "ten fields"}, - {"Closeing Time", "Closing Time"}, - {"closeing Time", "closing Time"}, - {" TOOD: foobar", " TODO: foobar"}, - {" preceed ", " precede "}, - {"preceeding", "preceding"}, - {"functionallity", "functionality"}, + {orig: "I live in Amercia", want: "I live in America"}, + {orig: "grill brocoli now", want: "grill broccoli now"}, + {orig: "There is a zeebra", want: "There is a zebra"}, + {orig: "foo other bar", want: "foo other bar"}, + {orig: "ten fiels", want: "ten fields"}, + {orig: "Closeing Time", want: "Closing Time"}, + {orig: "closeing Time", want: "closing Time"}, + {orig: " TOOD: foobar", want: " TODO: foobar"}, + {orig: " preceed ", want: " precede "}, + {orig: "preceeding", want: "preceding"}, + {orig: "functionallity", want: "functionality"}, } + r := New() - for line, tt := range cases { - got, _ := r.Replace(tt.orig) - if got != tt.want { - t.Errorf("%d: Replace files want %q got %q", line, tt.orig, got) - } + + for _, test := range cases { + t.Run(test.orig, func(t *testing.T) { + t.Parallel() + + got, _ := r.Replace(test.orig) + if got != test.want { + t.Errorf("Replace files want %q got %q", test.orig, got) + } + }) } } func TestCheckReplace(t *testing.T) { - r := Replacer{ - engine: NewStringReplacer("foo", "foobar", "runing", "running"), - corrected: map[string]string{ - "foo": "foobar", - "runing": "running", - }, - } + t.Run("Nothing at all", func(t *testing.T) { + t.Parallel() - s := "nothing at all" - news, diffs := r.Replace(s) - if s != news || len(diffs) != 0 { - t.Errorf("Basic recheck failed: %q vs %q", s, news) - } + r := Replacer{ + engine: NewStringReplacer("foo", "foobar"), + corrected: map[string]string{"foo": "foobar"}, + } - // - // Test single, correct,.Correctedacements - // - s = "foo" - news, diffs = r.Replace(s) - if news != "foobar" || len(diffs) != 1 || diffs[0].Original != "foo" && diffs[0].Corrected != "foobar" && diffs[0].Column != 0 { - t.Errorf("basic recheck1 failed %q vs %q", s, news) - } - s = "foo junk" - news, diffs = r.Replace(s) - if news != "foobar junk" || len(diffs) != 1 || diffs[0].Original != "foo" && diffs[0].Corrected != "foobar" && diffs[0].Column != 0 { - t.Errorf("basic recheck2 failed %q vs %q, %v", s, news, diffs[0]) - } + s := "nothing at all" + news, diffs := r.Replace(s) + if s != news || len(diffs) != 0 { + t.Errorf("Basic recheck failed: %q vs %q", s, news) + } + }) - s = "junk foo" - news, diffs = r.Replace(s) - if news != "junk foobar" || len(diffs) != 1 || diffs[0].Original != "foo" && diffs[0].Corrected != "foobar" && diffs[0].Column != 5 { - t.Errorf("basic recheck3 failed: %q vs %q", s, news) - } + t.Run("Single, correct,.Correctedacements", func(t *testing.T) { + t.Parallel() - s = "junk foo junk" - news, diffs = r.Replace(s) - if news != "junk foobar junk" || len(diffs) != 1 || diffs[0].Original != "foo" && diffs[0].Corrected != "foobar" && diffs[0].Column != 5 { - t.Errorf("basic recheck4 failed: %q vs %q", s, news) - } + testCases := []struct { + orig string + expected string + }{ + { + orig: "foo", + expected: "foobar", + }, + { + orig: "foo junk", + expected: "foobar junk", + }, + { + orig: "junk foo", + expected: "junk foobar", + }, + { + orig: "junk foo junk", + expected: "junk foobar junk", + }, + } - // Incorrect.Correctedacements - s = "food pruning" - news, _ = r.Replace(s) - if news != s { - t.Errorf("incorrect.Correctedacement failed: %q vs %q", s, news) - } + for _, test := range testCases { + orig := test.orig + expected := test.expected + t.Run(orig, func(t *testing.T) { + t.Parallel() + + r := Replacer{ + engine: NewStringReplacer("foo", "foobar", "runing", "running"), + corrected: map[string]string{"foo": "foobar", "runing": "running"}, + } + + news, diffs := r.Replace(orig) + if news != expected || len(diffs) != 1 || diffs[0].Original != "foo" && diffs[0].Corrected != expected && diffs[0].Column != 0 { + t.Errorf("basic recheck failed %q vs %q", expected, news) + } + }) + } + }) + + t.Run("Incorrect.Correctedacements", func(t *testing.T) { + t.Parallel() + + r := Replacer{ + engine: NewStringReplacer("foo", "foobar"), + corrected: map[string]string{"foo": "foobar"}, + } + + s := "food pruning" + news, _ := r.Replace(s) + if news != s { + t.Errorf("incorrect.Correctedacement failed: %q vs %q", s, news) + } + }) } diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index 737796c..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -ex -dep ensure -go install ./cmd/misspell -gometalinter \ - --vendor \ - --deadline=60s \ - --disable-all \ - --enable=vet \ - --enable=golint \ - --enable=gofmt \ - --enable=goimports \ - --enable=gosimple \ - --enable=staticcheck \ - --enable=ineffassign \ - --exclude=/usr/local/go/src/net/lookup_unix.go \ - ./... -go test . diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh deleted file mode 100755 index 3655bd0..0000000 --- a/scripts/commit-msg.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -ex -misspell -error "$1" diff --git a/scripts/goreleaser-dryrun.sh b/scripts/goreleaser-dryrun.sh deleted file mode 100755 index e786602..0000000 --- a/scripts/goreleaser-dryrun.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -echo "Real publishing is done by travis-ci" - -set -ex -echo "TAG:= $(git tag | tail -1)" -rm -rf ./dist -goreleaser --skip-publish --skip-validate diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh deleted file mode 100755 index 7730059..0000000 --- a/scripts/pre-commit.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -ex -./scripts/build.sh diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index 10e7062..0000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -ex - -# DEP -curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - -# GOMETALINTER -go get -u github.com/alecthomas/gometalinter && gometalinter --install - -# remove the default misspell to make sure -rm -f `which misspell` diff --git a/scripts/travis.sh b/scripts/travis.sh deleted file mode 100755 index 8eeabfd..0000000 --- a/scripts/travis.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -ex -./scripts/setup.sh -./scripts/build.sh diff --git a/stringreplacer.go b/stringreplacer.go index 3151ece..83fb0b1 100644 --- a/stringreplacer.go +++ b/stringreplacer.go @@ -6,7 +6,6 @@ package misspell import ( "io" - // "log" "strings" ) @@ -38,7 +37,7 @@ func (r *StringReplacer) Replace(s string) string { } // WriteString writes s to w with all replacements performed. -func (r *StringReplacer) WriteString(w io.Writer, s string) (n int, err error) { +func (r *StringReplacer) WriteString(w io.Writer, s string) (int, error) { return r.r.WriteString(w, s) } @@ -46,14 +45,14 @@ func (r *StringReplacer) WriteString(w io.Writer, s string) (n int, err error) { // and values may be empty. For example, the trie containing keys "ax", "ay", // "bcbc", "x" and "xy" could have eight nodes: // -// n0 - -// n1 a- -// n2 .x+ -// n3 .y+ -// n4 b- -// n5 .cbc+ -// n6 x+ -// n7 .y+ +// n0 - +// n1 a- +// n2 .x+ +// n3 .y+ +// n4 b- +// n5 .cbc+ +// n6 x+ +// n7 .y+ // // n0 is the root node, and its children are n1, n4 and n6; n1's children are // n2 and n3; n4's child is n5; n6's child is n7. Nodes n0, n1 and n4 (marked @@ -111,9 +110,10 @@ func (t *trieNode) add(key, val string, priority int, r *genericReplacer) { break } } - if n == len(t.prefix) { + switch n { + case len(t.prefix): t.next.add(key[n:], val, priority, r) - } else if n == 0 { + case 0: // First byte differs, start a new lookup table here. Looking up // what is currently t.prefix[0] will lead to prefixNode, and // looking up key[0] will lead to keyNode. @@ -133,7 +133,7 @@ func (t *trieNode) add(key, val string, priority int, r *genericReplacer) { t.prefix = "" t.next = nil keyNode.add(key[1:], val, priority, r) - } else { + default: // Insert new node after the common section of the prefix. next := &trieNode{ prefix: t.prefix[n:], @@ -143,54 +143,22 @@ func (t *trieNode) add(key, val string, priority int, r *genericReplacer) { t.next = next next.add(key[n:], val, priority, r) } - } else if t.table != nil { + return + } + + if t.table != nil { // Insert into existing table. m := r.mapping[key[0]] if t.table[m] == nil { t.table[m] = new(trieNode) } t.table[m].add(key[1:], val, priority, r) - } else { - t.prefix = key - t.next = new(trieNode) - t.next.add("", val, priority, r) + return } -} -func (r *genericReplacer) lookup(s string, ignoreRoot bool) (val string, keylen int, found bool) { - // Iterate down the trie to the end, and grab the value and keylen with - // the highest priority. - bestPriority := 0 - node := &r.root - n := 0 - for node != nil { - if node.priority > bestPriority && !(ignoreRoot && node == &r.root) { - bestPriority = node.priority - val = node.value - keylen = n - found = true - } - - if s == "" { - break - } - if node.table != nil { - index := r.mapping[ByteToLower(s[0])] - if int(index) == r.tableSize { - break - } - node = node.table[index] - s = s[1:] - n++ - } else if node.prefix != "" && StringHasPrefixFold(s, node.prefix) { - n += len(node.prefix) - s = s[len(node.prefix):] - node = node.next - } else { - break - } - } - return + t.prefix = key + t.next = new(trieNode) + t.next.add("", val, priority, r) } // genericReplacer is the fully generic algorithm. @@ -209,7 +177,7 @@ func makeGenericReplacer(oldnew []string) *genericReplacer { // Find each byte used, then assign them each an index. for i := 0; i < len(oldnew); i += 2 { key := strings.ToLower(oldnew[i]) - for j := 0; j < len(key); j++ { + for j := range len(key) { r.mapping[key[j]] = 1 } } @@ -236,38 +204,40 @@ func makeGenericReplacer(oldnew []string) *genericReplacer { return r } -type appendSliceWriter []byte - -// Write writes to the buffer to satisfy io.Writer. -func (w *appendSliceWriter) Write(p []byte) (int, error) { - *w = append(*w, p...) - return len(p), nil -} - -// WriteString writes to the buffer without string->[]byte->string allocations. -func (w *appendSliceWriter) WriteString(s string) (int, error) { - *w = append(*w, s...) - return len(s), nil -} - -type stringWriterIface interface { - WriteString(string) (int, error) -} - -type stringWriter struct { - w io.Writer -} - -func (w stringWriter) WriteString(s string) (int, error) { - return w.w.Write([]byte(s)) -} +func (r *genericReplacer) lookup(s string, ignoreRoot bool) (val string, keylen int, found bool) { + // Iterate down the trie to the end, and grab the value and keylen with + // the highest priority. + bestPriority := 0 + node := &r.root + n := 0 + for node != nil { + if node.priority > bestPriority && (!ignoreRoot || node != &r.root) { + bestPriority = node.priority + val = node.value + keylen = n + found = true + } -func getStringWriter(w io.Writer) stringWriterIface { - sw, ok := w.(stringWriterIface) - if !ok { - sw = stringWriter{w} + if s == "" { + break + } + if node.table != nil { + index := r.mapping[ByteToLower(s[0])] + if int(index) == r.tableSize { + break + } + node = node.table[index] + s = s[1:] + n++ + } else if node.prefix != "" && StringHasPrefixFold(s, node.prefix) { + n += len(node.prefix) + s = s[len(node.prefix):] + node = node.next + } else { + break + } } - return sw + return } func (r *genericReplacer) Replace(s string) string { @@ -316,7 +286,7 @@ func (r *genericReplacer) WriteString(w io.Writer, s string) (n int, err error) if err != nil { return } - //log.Printf("%d: Going to correct %q with %q", i, s[i:i+keylen], val) + // debug helper: log.Printf("%d: Going to correct %q with %q", i, s[i:i+keylen], val) wn, err = sw.WriteString(val) n += wn if err != nil { @@ -334,3 +304,33 @@ func (r *genericReplacer) WriteString(w io.Writer, s string) (n int, err error) } return } + +type appendSliceWriter []byte + +// Write writes to the buffer to satisfy io.Writer. +func (w *appendSliceWriter) Write(p []byte) (int, error) { + *w = append(*w, p...) + return len(p), nil +} + +// WriteString writes to the buffer without string->[]byte->string allocations. +func (w *appendSliceWriter) WriteString(s string) (int, error) { + *w = append(*w, s...) + return len(s), nil +} + +type stringWriter struct { + w io.Writer +} + +func (w stringWriter) WriteString(s string) (int, error) { + return w.w.Write([]byte(s)) +} + +func getStringWriter(w io.Writer) io.StringWriter { + sw, ok := w.(io.StringWriter) + if !ok { + sw = stringWriter{w} + } + return sw +} diff --git a/url.go b/url.go index 1a259f5..a91d1d9 100644 --- a/url.go +++ b/url.go @@ -7,11 +7,12 @@ import ( // Regexp for URL https://mathiasbynens.be/demo/url-regex // // original @imme_emosol (54 chars) has trouble with dashes in hostname -// @(https?|ftp)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s]*)?$@iS -var reURL = regexp.MustCompile(`(?i)(https?|ftp)://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?`) +// @(https?|ftp)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s]*)?$@iS. +var reURL = regexp.MustCompile(`(?i)(https?|ftp)://(-\.)?([^\s/?.#]+\.?)+(/\S*)?`) -// StripURL attemps to replace URLs with blank spaces, e.g. -// "xxx http://foo.com/ yyy -> "xxx yyyy" +// StripURL attempts to replace URLs with blank spaces, e.g. +// +// "xxx http://foo.com/ yyy -> "xxx yyyy". func StripURL(s string) string { return reURL.ReplaceAllStringFunc(s, replaceWithBlanks) } diff --git a/url_test.go b/url_test.go index 0cf9ce2..2d830a5 100644 --- a/url_test.go +++ b/url_test.go @@ -6,7 +6,6 @@ import ( ) // Test suite partiall from https://mathiasbynens.be/demo/url-regex -// func TestStripURL(t *testing.T) { cases := []string{ "HTTP://FOO.COM/BLAH_BLAH", @@ -50,7 +49,7 @@ func TestStripURL(t *testing.T) { for num, tt := range cases { got := strings.TrimSpace(StripURL(tt)) - if len(got) != 0 { + if got != "" { t.Errorf("case %d: unable to match %q", num, tt) } } @@ -79,26 +78,26 @@ func TestStripURL(t *testing.T) { ":// should fail", "http://foo.bar/foo(bar)baz quux", "ftps://foo.bar/", - //"http://-error-.invalid/", - //"http://a.b--c.de/", - //"http://-a.b.co", - //"http://a.b-.co", - //"http://0.0.0.0", - //"http://10.1.1.0", - //"http://10.1.1.255", - //"http://224.1.1.1", - //"http://1.1.1.1.1", - //"http://123.123.123", - //"http://3628126748", + // "http://-error-.invalid/", + // "http://a.b--c.de/", + // "http://-a.b.co", + // "http://a.b-.co", + // "http://0.0.0.0", + // "http://10.1.1.0", + // "http://10.1.1.255", + // "http://224.1.1.1", + // "http://1.1.1.1.1", + // "http://123.123.123", + // "http://3628126748", "http://.www.foo.bar/", - //"http://www.foo.bar./", + // "http://www.foo.bar./", "http://.www.foo.bar./", - //"http://10.1.1.1", + // "http://10.1.1.1", } for num, tt := range cases { got := strings.TrimSpace(StripURL(tt)) - if len(got) == 0 { + if got == "" { t.Errorf("case %d: incorrect match %q", num, tt) } } diff --git a/words.go b/words.go index c92dd19..5e9b5a8 100644 --- a/words.go +++ b/words.go @@ -1,6 +1,6 @@ -package misspell +// Code generated by xxx. DO NOT EDIT. -// Code generated automatically. DO NOT EDIT. +package misspell // DictMain is the main rule set, not including locale-specific spellings var DictMain = []string{ @@ -591,6 +591,7 @@ var DictMain = []string{ "competitioners", "competitions", "comphrehensive", "comprehensive", "computationnal", "computational", + "concatentation", "concatenation", "conciderations", "considerations", "condescenscion", "condescension", "condradictions", "contradictions", @@ -1321,6 +1322,7 @@ var DictMain = []string{ "conecntration", "concentrations", "conenctration", "concentrations", "confidentally", "confidentially", + "configrations", "configurations", "configruation", "configurations", "configuartion", "configuration", "configuracion", "configuration", @@ -3612,6 +3614,7 @@ var DictMain = []string{ "environmentl", "environmentally", "environmetal", "environmental", "envrionments", "environments", + "errorneously", "erroneously", "establishmet", "establishments", "evelutionary", "evolutionary", "exagerrating", "exaggerating", @@ -5913,6 +5916,7 @@ var DictMain = []string{ "attributred", "attributed", "attributted", "attribute", "attrocities", "atrocities", + "atttributes", "attributes", "audiobookas", "audiobooks", "audioboooks", "audiobook", "auotcorrect", "autocorrect", @@ -8053,6 +8057,7 @@ var DictMain = []string{ "invincinble", "invincible", "invisibiity", "invisibility", "invisibiliy", "invisibility", + "invokations", "invocations", "involantary", "involuntary", "involentary", "involuntary", "involintary", "involuntary", @@ -8537,6 +8542,7 @@ var DictMain = []string{ "opprotunity", "opportunity", "optimisitic", "optimistic", "optimizaton", "optimization", + "optmization", "optimization", "orchestraed", "orchestrated", "orchestrial", "orchestra", "oreintation", "orientation", @@ -10745,6 +10751,7 @@ var DictMain = []string{ "assersions", "assertions", "assesement", "assessment", "assestment", "assessment", + "assigments", "assignments", "assignemnt", "assignment", "assimalate", "assimilate", "assimilant", "assimilate", @@ -14366,6 +14373,7 @@ var DictMain = []string{ "omnisicent", "omniscient", "omniverous", "omnivorous", "omnsicient", "omniscient", + "on-premise", "on-premises", "onmipotent", "omnipotent", "onmiscient", "omniscient", "operatings", "operations", @@ -17362,6 +17370,7 @@ var DictMain = []string{ "commiteed", "commited", "commiting", "committing", "commitmet", "commitments", + "commments", "comments", "commongly", "commonly", "communiss", "communists", "communite", "communities", @@ -17456,6 +17465,7 @@ var DictMain = []string{ "construcs", "constructs", "construde", "construed", "construst", "constructs", + "constucts", "constructs", "constured", "construed", "consulant", "consultant", "consultat", "consultant", @@ -17643,6 +17653,7 @@ var DictMain = []string{ "delcining", "declining", "delegatie", "delegate", "delerious", "delirious", + "deleteing", "deleting", "delfation", "deflation", "deliveres", "delivers", "deliverys", "delivers", @@ -17746,6 +17757,7 @@ var DictMain = []string{ "direcotry", "directory", "directoty", "directory", "directroy", "directory", + "disapears", "disappears", "disaprity", "disparity", "disastros", "disastrous", "disatrous", "disastrous", @@ -17898,6 +17910,7 @@ var DictMain = []string{ "enclsoure", "enclosure", "encolsure", "enclosure", "encompase", "encompass", + "enconding", "encoding", "encounted", "encountered", "encrpyted", "encrypted", "encrytped", "encrypted", @@ -18359,6 +18372,7 @@ var DictMain = []string{ "icongnito", "incognito", "idealisim", "idealism", "idealistc", "idealistic", + "identifer", "identifier", "identifiy", "identify", "ideologis", "ideologies", "ignornace", "ignorance", @@ -18530,6 +18544,7 @@ var DictMain = []string{ "intesnely", "intensely", "intesnity", "intensity", "intestins", "intestines", + "intialize", "initialize", "inticrate", "intricate", "intimidad", "intimidated", "intircate", "intricate", @@ -18851,6 +18866,7 @@ var DictMain = []string{ "monstorus", "monstrous", "monstruos", "monstrous", "montanous", "mountainous", + "montoring", "monitoring", "monumnets", "monuments", "moratlity", "mortality", "morbidley", "morbidly", @@ -19658,6 +19674,7 @@ var DictMain = []string{ "reteriver", "retriever", "retirever", "retriever", "retrevier", "retriever", + "retriving", "retrieving", "reuptable", "reputable", "reveiwers", "reviewers", "revelaing", "revealing", @@ -21346,6 +21363,7 @@ var DictMain = []string{ "contracr", "contractor", "contracs", "contracts", "controll", "control", + "contruct", "construct", "convenit", "convenient", "convento", "convention", "converst", "converts", @@ -21455,6 +21473,7 @@ var DictMain = []string{ "daugther", "daughter", "deadlfit", "deadlift", "deadlifs", "deadlifts", + "deafauts", "defaults", "deafeted", "defeated", "deafults", "defaults", "dealying", "delaying", @@ -22654,6 +22673,7 @@ var DictMain = []string{ "lithuana", "lithuania", "litigato", "litigation", "liverpol", "liverpool", + "locagion", "location", "logtiech", "logitech", "longitme", "longtime", "longtiem", "longtime", @@ -23875,6 +23895,7 @@ var DictMain = []string{ "serentiy", "serenity", "sergaent", "sergeant", "settigns", "settings", + "settting", "setting", "seventen", "seventeen", "severeal", "several", "severeid", "severed", @@ -24209,6 +24230,7 @@ var DictMain = []string{ "supirsed", "suprised", "suposing", "supposing", "supporre", "supporters", + "suppoted", "supported", "suprised", "surprised", "suprized", "surprised", "suprsied", "suprised", @@ -25189,6 +25211,7 @@ var DictMain = []string{ "chispet", "chipset", "chivaly", "chivalry", "chlesea", "chelsea", + "chnages", "changes", "choatic", "chaotic", "chocies", "choices", "choosen", "chosen", @@ -25649,6 +25672,7 @@ var DictMain = []string{ "expolit", "exploit", "exposse", "exposes", "expries", "expires", + "exracts", "extracts", "exsited", "existed", "extered", "exerted", "exterme", "extreme", @@ -25948,6 +25972,7 @@ var DictMain = []string{ "incldue", "include", "incluse", "includes", "indains", "indians", + "indeces", "indices", "indiaan", "indiana", "indluge", "indulge", "indugle", "indulge", @@ -26490,6 +26515,7 @@ var DictMain = []string{ "posions", "poisons", "positon", "position", "positve", "positive", + "possibe", "possible", "possiby", "possibly", "postdam", "potsdam", "postion", "position", @@ -26697,6 +26723,7 @@ var DictMain = []string{ "repalys", "replays", "repblic", "republic", "repeast", "repeats", + "repects", "respects", "repitle", "reptile", "replase", "replaces", "replayd", "replayed", @@ -26720,6 +26747,7 @@ var DictMain = []string{ "reslove", "resolve", "resolvs", "resolves", "resonet", "resonate", + "resouce", "resource", "resovle", "resolve", "respest", "respects", "respone", "response", @@ -26728,6 +26756,7 @@ var DictMain = []string{ "restord", "restored", "resuced", "rescued", "resuces", "rescues", + "retrive", "retrieve", "returnd", "returned", "reuinon", "reunion", "reveald", "revealed", @@ -27467,6 +27496,7 @@ var DictMain = []string{ "coform", "conform", "comany", "company", "coucil", "council", + "curent", "current", "densly", "densely", "deside", "decide", "devels", "delves", @@ -27506,6 +27536,7 @@ var DictMain = []string{ "fufill", "fulfill", "futher", "further", "gardai", "gardaí", + "geting", "getting", "ghandi", "gandhi", "glight", "flight", "gloabl", "global", @@ -27534,6 +27565,7 @@ var DictMain = []string{ "inital", "initial", "interm", "interim", "intial", "initial", + "invlid", "invalid", "iunior", "junior", "jaques", "jacques", "jospeh", "joseph", @@ -27643,8 +27675,10 @@ var DictMain = []string{ "shreak", "shriek", "siezed", "seized", "sixtin", "sistine", + "skiped", "skipped", "sneeks", "sneaks", "somene", "someone", + "soruce", "source", "soudns", "sounds", "sourth", "south", "speach", "speech", @@ -27688,6 +27722,8 @@ var DictMain = []string{ "tyrany", "tyranny", "unabel", "unable", "unkown", "unknown", + "unmont", "unmount", + "unmout", "unmount", "untill", "until", "usally", "usually", "useage", "usage", diff --git a/words_test.go b/words_test.go index 31fcf28..7f7cde1 100644 --- a/words_test.go +++ b/words_test.go @@ -19,14 +19,16 @@ func (a sortByLen) Less(i, j int) bool { return len(a[i]) > len(a[j]) } -func TestWordSort(t *testing.T) { +func Test_wordSort(t *testing.T) { if len(DictMain)%2 == 1 { t.Errorf("Dictionary is a not a multiple of 2") } + words := make([]string, 0, len(DictMain)/2) for i := 0; i < len(DictMain); i += 2 { words = append(words, DictMain[i]) } + if !sort.IsSorted(sortByLen(words)) { t.Errorf("Words not sorted by len, by alpha!") t.Errorf("Words.go is autogenerated -- do not edit.")