diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..344fda2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,96 @@ +version: "2" +run: + concurrency: 4 + # also lint _test.go files + tests: true + timeout: 5m +linters: + enable: + - govet + - staticcheck + - unused + - errcheck + - gocritic + - gocyclo + - revive + - ineffassign + - unconvert + - goconst + # - depguard + - prealloc + # - dupl + - misspell + - bodyclose + - sqlclosecheck + - nilerr + - nestif + - forcetypeassert + - exhaustive + - funlen + # - wsl_v5 + - testifylint + + # disable noisy/controversial ones which you might enable later + disable: + - lll # line length — handled by gfmt/gofumpt + + settings: + dupl: + threshold: 20 # lower => stricter (tokens) + errcheck: + check-type-assertions: true + funlen: + lines: 120 + statements: 80 + goconst: + min-len: 2 + min-occurrences: 3 + gocyclo: + min-complexity: 15 # strict; lower => stricter + wsl_v5: + allow-first-in-block: true + allow-whole-block: false + branch-max-lines: 2 + testifylint: + disable: + - require-error + - float-compare + + exclusions: + generated: lax + paths: + - examples$ + rules: + - linters: + - revive + text: "^unused-parameter:" + - linters: + - revive + text: "^package-comments:" + - linters: + - errcheck + text: "Error return value of `.*\\.Body\\.Close` is not checked" + # linters disabled in tests + - linters: + - dupl + - goconst + - funlen + path: "_test\\.go$" + + + +issues: + max-issues-per-linter: 0 # 0 = unlimited (we want ALL issues) + max-same-issues: 100 + + +formatters: + enable: + - gofmt + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/channel.go b/channel.go index a1e2cdd..d0cf6e9 100644 --- a/channel.go +++ b/channel.go @@ -8,6 +8,7 @@ import ( "github.com/samber/lo/internal/rand" ) +// DispatchingStrategy is a function that distributes messages to channels. type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int // ChannelDispatcher distributes messages from input channels into N child channels. @@ -22,7 +23,7 @@ func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, // propagate channel closing to children defer closeChannels(children) - var i uint64 = 0 + var i uint64 for { msg, ok := <-stream diff --git a/concurrency.go b/concurrency.go index a2ebbce..7c823b0 100644 --- a/concurrency.go +++ b/concurrency.go @@ -17,7 +17,7 @@ func (s *synchronize) Do(cb func()) { } // Synchronize wraps the underlying callback in a mutex. It receives an optional mutex. -func Synchronize(opt ...sync.Locker) *synchronize { +func Synchronize(opt ...sync.Locker) *synchronize { //nolint:revive if len(opt) > 1 { panic("unexpected arguments") } else if len(opt) == 0 { diff --git a/condition.go b/condition.go index eaeaa2b..ef873ef 100644 --- a/condition.go +++ b/condition.go @@ -26,9 +26,9 @@ type ifElse[T any] struct { done bool } -// If. +// If is a 1 line if/else statement. // Play: https://go.dev/play/p/WSw3ApMxhyW -func If[T any](condition bool, result T) *ifElse[T] { +func If[T any](condition bool, result T) *ifElse[T] { //nolint:revive if condition { return &ifElse[T]{result, true} } @@ -37,9 +37,9 @@ func If[T any](condition bool, result T) *ifElse[T] { return &ifElse[T]{t, false} } -// IfF. +// IfF is a 1 line if/else statement whose options are functions // Play: https://go.dev/play/p/WSw3ApMxhyW -func IfF[T any](condition bool, resultF func() T) *ifElse[T] { +func IfF[T any](condition bool, resultF func() T) *ifElse[T] { //nolint:revive if condition { return &ifElse[T]{resultF(), true} } @@ -98,7 +98,7 @@ type switchCase[T comparable, R any] struct { // Switch is a pure functional switch/case/default statement. // Play: https://go.dev/play/p/TGbKUMAeRUd -func Switch[T comparable, R any](predicate T) *switchCase[T, R] { +func Switch[T comparable, R any](predicate T) *switchCase[T, R] { //nolint:revive var result R return &switchCase[T, R]{ diff --git a/errors.go b/errors.go index a234721..3deb265 100644 --- a/errors.go +++ b/errors.go @@ -25,7 +25,7 @@ func messageFromMsgAndArgs(msgAndArgs ...any) string { return fmt.Sprintf("%+v", msgAndArgs[0]) } if len(msgAndArgs) > 1 { - return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) //nolint:errcheck,forcetypeassert } return "" } @@ -51,9 +51,8 @@ func must(err any, messageArgs ...any) { message := messageFromMsgAndArgs(messageArgs...) if message != "" { panic(message + ": " + e.Error()) - } else { - panic(e.Error()) } + panic(e.Error()) default: panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") diff --git a/errors_test.go b/errors_test.go index 879fad3..07cfb06 100644 --- a/errors_test.go +++ b/errors_test.go @@ -518,7 +518,9 @@ func TestTryWithErrorValue(t *testing.T) { return errors.New("foo") }) is.False(ok) - is.EqualError(err.(error), "foo") + e, isError := err.(error) + is.True(isError) + is.EqualError(e, "foo") err, ok = TryWithErrorValue(func() error { return nil @@ -618,7 +620,7 @@ func TestAssert(t *testing.T) { Assert(false, "user defined message") }) - //checks that the examples in `README.md` compile + // checks that the examples in `README.md` compile { age := 20 is.NotPanics(func() { @@ -650,7 +652,7 @@ func TestAssertf(t *testing.T) { Assertf(false, "user defined message %d %d", 1, 2) }) - //checks that the example in `README.md` compiles + // checks that the example in `README.md` compiles { age := 7 is.PanicsWithValue("assertion failed: user age must be >= 15, got 7", func() { diff --git a/find.go b/find.go index f04dc08..48affa7 100644 --- a/find.go +++ b/find.go @@ -631,7 +631,7 @@ func Samples[T any, Slice ~[]T](collection Slice, count int) Slice { func SamplesBy[T any, Slice ~[]T](collection Slice, count int, randomIntGenerator randomIntGenerator) Slice { size := len(collection) - copy := append(Slice{}, collection...) + cOpy := append(Slice{}, collection...) results := Slice{} @@ -639,12 +639,12 @@ func SamplesBy[T any, Slice ~[]T](collection Slice, count int, randomIntGenerato copyLength := size - i index := randomIntGenerator(size - i) - results = append(results, copy[index]) + results = append(results, cOpy[index]) // Removes element. // It is faster to swap with last element and remove it. - copy[index] = copy[copyLength-1] - copy = copy[:copyLength-1] + cOpy[index] = cOpy[copyLength-1] + cOpy = cOpy[:copyLength-1] } return results diff --git a/internal/constraints/ordered_go121.go b/internal/constraints/ordered_go121.go index c02de93..085a743 100644 --- a/internal/constraints/ordered_go121.go +++ b/internal/constraints/ordered_go121.go @@ -6,4 +6,8 @@ import ( "cmp" ) +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. type Ordered = cmp.Ordered diff --git a/internal/rand/ordered_go118.go b/internal/rand/ordered_go118.go index 9fbc538..d86e0a1 100644 --- a/internal/rand/ordered_go118.go +++ b/internal/rand/ordered_go118.go @@ -1,22 +1,30 @@ //go:build !go1.22 +//nolint:revive + package rand import "math/rand" +// Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. func Shuffle(n int, swap func(i, j int)) { rand.Shuffle(n, swap) } +// IntN returns, as an int, a pseudo-random number in the half-open interval [0,n) +// from the default Source. +// It panics if n <= 0. func IntN(n int) int { // bearer:disable go_gosec_crypto_weak_random return rand.Intn(n) } +// Int64 returns a non-negative pseudo-random 63-bit integer as an int64 +// from the default Source. func Int64() int64 { // bearer:disable go_gosec_crypto_weak_random n := rand.Int63() - + // bearer:disable go_gosec_crypto_weak_random if rand.Intn(2) == 0 { return -n diff --git a/internal/rand/ordered_go122.go b/internal/rand/ordered_go122.go index 65abf51..a9c3e2f 100644 --- a/internal/rand/ordered_go122.go +++ b/internal/rand/ordered_go122.go @@ -4,14 +4,20 @@ package rand import "math/rand/v2" +// Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. func Shuffle(n int, swap func(i, j int)) { rand.Shuffle(n, swap) } +// IntN returns, as an int, a pseudo-random number in the half-open interval [0,n) +// from the default Source. +// It panics if n <= 0. func IntN(n int) int { return rand.IntN(n) } +// Int64 returns a non-negative pseudo-random 63-bit integer as an int64 +// from the default Source. func Int64() int64 { return rand.Int64() } diff --git a/map.go b/map.go index a52dcd4..3097735 100644 --- a/map.go +++ b/map.go @@ -262,7 +262,7 @@ func ChunkEntries[K comparable, V any](m map[K]V, size int) []map[K]V { chunksNum := count / size if count%size != 0 { - chunksNum += 1 + chunksNum++ } result := make([]map[K]V, 0, chunksNum) diff --git a/map_test.go b/map_test.go index 7b74ca5..f6dbec1 100644 --- a/map_test.go +++ b/map_test.go @@ -477,7 +477,7 @@ func TestMapEntries(t *testing.T) { // return v, k // }, map[string]string{"1": "foo", "2": "bar", "true": "ccc"}) //} - //NormalMappers + // NormalMappers { mapEntriesTest(t, map[string]string{"foo": "1", "foo2": "2", "Foo": "2", "Foo2": "2", "bar": "2", "ccc": "true"}, func(k string, v string) (string, string) { return k, k + v diff --git a/math.go b/math.go index 16d3396..7c6ade3 100644 --- a/math.go +++ b/math.go @@ -68,7 +68,7 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T { // Sum sums the values in a collection. If collection is empty 0 is returned. // Play: https://go.dev/play/p/upfeJVqs4Bt func Sum[T constraints.Float | constraints.Integer | constraints.Complex](collection []T) T { - var sum T = 0 + var sum T for i := range collection { sum += collection[i] } @@ -78,9 +78,9 @@ func Sum[T constraints.Float | constraints.Integer | constraints.Complex](collec // SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. // Play: https://go.dev/play/p/Dz_a_7jN_ca func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) R) R { - var sum R = 0 + var sum R for i := range collection { - sum = sum + iteratee(collection[i]) + sum += iteratee(collection[i]) } return sum } @@ -116,28 +116,28 @@ func ProductBy[T any, R constraints.Float | constraints.Integer | constraints.Co var product R = 1 for i := range collection { - product = product * iteratee(collection[i]) + product *= iteratee(collection[i]) } return product } // Mean calculates the mean of a collection of numbers. func Mean[T constraints.Float | constraints.Integer](collection []T) T { - var length = T(len(collection)) + length := T(len(collection)) if length == 0 { return 0 } - var sum = Sum(collection) + sum := Sum(collection) return sum / length } // MeanBy calculates the mean of a collection of numbers using the given return value from the iteration function. func MeanBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(item T) R) R { - var length = R(len(collection)) + length := R(len(collection)) if length == 0 { return 0 } - var sum = SumBy(collection, iteratee) + sum := SumBy(collection, iteratee) return sum / length } @@ -145,12 +145,12 @@ func MeanBy[T any, R constraints.Float | constraints.Integer](collection []T, it // If multiple values ​​have the same highest frequency, then multiple values ​​are returned. // If the collection is empty, then the zero value of T is returned. func Mode[T constraints.Integer | constraints.Float](collection []T) []T { - var length = T(len(collection)) + length := T(len(collection)) if length == 0 { return []T{} } - var mode = make([]T, 0) + mode := make([]T, 0) maxFreq := 0 frequency := make(map[T]int) diff --git a/mutable/slice.go b/mutable/slice.go index 969f399..510e975 100644 --- a/mutable/slice.go +++ b/mutable/slice.go @@ -64,7 +64,7 @@ func Reverse[T any, Slice ~[]T](collection Slice) { length := len(collection) half := length / 2 - for i := 0; i < half; i = i + 1 { + for i := 0; i < half; i++ { j := length - 1 - i collection[i], collection[j] = collection[j], collection[i] } diff --git a/retry_test.go b/retry_test.go index 6a99b25..1a61c89 100644 --- a/retry_test.go +++ b/retry_test.go @@ -526,7 +526,6 @@ func TestNewThrottle(t *testing.T) { reset() th() is.Equal(3, callCount) - } func TestNewThrottleWithCount(t *testing.T) { @@ -598,7 +597,6 @@ func TestNewThrottleBy(t *testing.T) { th("a") is.Equal(3, callCountA) is.Equal(2, callCountB) - } func TestNewThrottleByWithCount(t *testing.T) { diff --git a/slice.go b/slice.go index 0ee817f..ee63e42 100644 --- a/slice.go +++ b/slice.go @@ -211,7 +211,7 @@ func Chunk[T any, Slice ~[]T](collection Slice, size int) []Slice { chunksNum := len(collection) / size if len(collection)%size != 0 { - chunksNum += 1 + chunksNum++ } result := make([]Slice, 0, chunksNum) @@ -662,13 +662,13 @@ func Slice[T any, Slice ~[]T](collection Slice, start int, end int) Slice { // Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. // Play: https://go.dev/play/p/XfPzmf9gql6 -func Replace[T comparable, Slice ~[]T](collection Slice, old T, new T, n int) Slice { +func Replace[T comparable, Slice ~[]T](collection Slice, old T, nEw T, n int) Slice { result := make(Slice, len(collection)) copy(result, collection) for i := range result { if result[i] == old && n != 0 { - result[i] = new + result[i] = nEw n-- } } @@ -678,8 +678,8 @@ func Replace[T comparable, Slice ~[]T](collection Slice, old T, new T, n int) Sl // ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. // Play: https://go.dev/play/p/a9xZFUHfYcV -func ReplaceAll[T comparable, Slice ~[]T](collection Slice, old T, new T) Slice { - return Replace(collection, old, new, -1) +func ReplaceAll[T comparable, Slice ~[]T](collection Slice, old T, nEw T) Slice { + return Replace(collection, old, nEw, -1) } // Compact returns a slice of all non-zero elements. @@ -732,15 +732,16 @@ func Splice[T any, Slice ~[]T](collection Slice, i int, elements ...T) Slice { sizeElements := len(elements) output := make(Slice, 0, sizeCollection+sizeElements) // preallocate memory for the output slice - if sizeElements == 0 { + switch { + case sizeElements == 0: return append(output, collection...) // simple copy - } else if i > sizeCollection { + case i > sizeCollection: // positive overflow return append(append(output, collection...), elements...) - } else if i < -sizeCollection { + case i < -sizeCollection: // negative overflow return append(append(output, elements...), collection...) - } else if i < 0 { + case i < 0: // backward i = sizeCollection + i } diff --git a/string.go b/string.go index 923faa3..5581b61 100644 --- a/string.go +++ b/string.go @@ -14,6 +14,7 @@ import ( ) var ( + //nolint:revive LowerCaseLettersCharset = []rune("abcdefghijklmnopqrstuvwxyz") UpperCaseLettersCharset = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") LettersCharset = append(LowerCaseLettersCharset, UpperCaseLettersCharset...) @@ -35,7 +36,7 @@ func RandomString(size int, charset []rune) string { if size <= 0 { panic("lo.RandomString: Size parameter must be greater than 0") } - if len(charset) <= 0 { + if len(charset) == 0 { panic("lo.RandomString: Charset parameter must not be empty") } @@ -44,27 +45,27 @@ func RandomString(size int, charset []rune) string { sb.Grow(size) // Calculate the number of bits required to represent the charset, // e.g., for 62 characters, it would need 6 bits (since 62 -> 64 = 2^6) - letterIdBits := int(math.Log2(float64(nearestPowerOfTwo(len(charset))))) + letterIDBits := int(math.Log2(float64(nearestPowerOfTwo(len(charset))))) // Determine the corresponding bitmask, // e.g., for 62 characters, the bitmask would be 111111. - var letterIdMask int64 = 1<= 0; { + for i, cache, remain := size-1, rand.Int64(), letterIDMax; i >= 0; { // Regenerate the random number if all available bits have been used if remain == 0 { - cache, remain = rand.Int64(), letterIdMax + cache, remain = rand.Int64(), letterIDMax } // Select a character from the charset - if idx := int(cache & letterIdMask); idx < len(charset) { + if idx := int(cache & letterIDMask); idx < len(charset) { sb.WriteRune(charset[idx]) i-- } // Shift the bits to the right to prepare for the next character selection, // e.g., for 62 characters, shift by 6 bits. - cache >>= letterIdBits + cache >>= letterIDBits // Decrease the remaining number of uses for the current random number. remain-- } @@ -72,8 +73,8 @@ func RandomString(size int, charset []rune) string { } // nearestPowerOfTwo returns the nearest power of two. -func nearestPowerOfTwo(cap int) int { - n := cap - 1 +func nearestPowerOfTwo(capacity int) int { + n := capacity - 1 n |= n >> 1 n |= n >> 2 n |= n >> 4 @@ -128,7 +129,7 @@ func ChunkString[T ~string](str T, size int) []T { return []T{str} } - var chunks = make([]T, 0, ((len(str)-1)/size)+1) + chunks := make([]T, 0, ((len(str)-1)/size)+1) currentLen := 0 currentStart := 0 for i := range str { diff --git a/tuples_test.go b/tuples_test.go index 3b10ba7..2933c1b 100644 --- a/tuples_test.go +++ b/tuples_test.go @@ -350,18 +350,14 @@ func TestZipBy(t *testing.T) { r1 := ZipBy2( []string{"a", "b"}, []int{1, 2}, - func(a string, b int) Tuple2[string, int] { - return T2(a, b) - }, + T2[string, int], ) r2 := ZipBy3( []string{"a", "b", "c"}, []int{1, 2, 3}, []int{4, 5, 6}, - func(a string, b int, c int) Tuple3[string, int, int] { - return T3(a, b, c) - }, + T3[string, int, int], ) r3 := ZipBy4( @@ -369,9 +365,7 @@ func TestZipBy(t *testing.T) { []int{1, 2, 3, 4}, []int{5, 6, 7, 8}, []bool{true, true, true, true}, - func(a string, b int, c int, d bool) Tuple4[string, int, int, bool] { - return T4(a, b, c, d) - }, + T4[string, int, int, bool], ) r4 := ZipBy5( @@ -380,9 +374,7 @@ func TestZipBy(t *testing.T) { []int{6, 7, 8, 9, 10}, []bool{true, true, true, true, true}, []float32{0.1, 0.2, 0.3, 0.4, 0.5}, - func(a string, b int, c int, d bool, e float32) Tuple5[string, int, int, bool, float32] { - return T5(a, b, c, d, e) - }, + T5[string, int, int, bool, float32], ) r5 := ZipBy6( @@ -392,9 +384,7 @@ func TestZipBy(t *testing.T) { []bool{true, true, true, true, true, true}, []float32{0.1, 0.2, 0.3, 0.4, 0.5, 0.6}, []float64{0.01, 0.02, 0.03, 0.04, 0.05, 0.06}, - func(a string, b int, c int, d bool, e float32, f float64) Tuple6[string, int, int, bool, float32, float64] { - return T6(a, b, c, d, e, f) - }, + T6[string, int, int, bool, float32, float64], ) r6 := ZipBy7( @@ -405,9 +395,7 @@ func TestZipBy(t *testing.T) { []float32{0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7}, []float64{0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07}, []int8{1, 2, 3, 4, 5, 6, 7}, - func(a string, b int, c int, d bool, e float32, f float64, g int8) Tuple7[string, int, int, bool, float32, float64, int8] { - return T7(a, b, c, d, e, f, g) - }, + T7[string, int, int, bool, float32, float64, int8], ) r7 := ZipBy8( @@ -419,9 +407,7 @@ func TestZipBy(t *testing.T) { []float64{0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08}, []int8{1, 2, 3, 4, 5, 6, 7, 8}, []int16{1, 2, 3, 4, 5, 6, 7, 8}, - func(a string, b int, c int, d bool, e float32, f float64, g int8, h int16) Tuple8[string, int, int, bool, float32, float64, int8, int16] { - return T8(a, b, c, d, e, f, g, h) - }, + T8[string, int, int, bool, float32, float64, int8, int16], ) r8 := ZipBy9( @@ -434,9 +420,7 @@ func TestZipBy(t *testing.T) { []int8{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int16{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int32{1, 2, 3, 4, 5, 6, 7, 8, 9}, - func(a string, b int, c int, d bool, e float32, f float64, g int8, h int16, i int32) Tuple9[string, int, int, bool, float32, float64, int8, int16, int32] { - return T9(a, b, c, d, e, f, g, h, i) - }, + T9[string, int, int, bool, float32, float64, int8, int16, int32], ) is.Equal([]Tuple2[string, int]{ diff --git a/type_manipulation.go b/type_manipulation.go index bcf990c..106ee94 100644 --- a/type_manipulation.go +++ b/type_manipulation.go @@ -106,7 +106,7 @@ func FromAnySlice[T any](in []any) (out []T, ok bool) { result := make([]T, len(in)) for i := range in { - result[i] = in[i].(T) + result[i] = in[i].(T) //nolint:errcheck,forcetypeassert } return result, true } diff --git a/type_manipulation_test.go b/type_manipulation_test.go index a8a05fe..5978c9b 100644 --- a/type_manipulation_test.go +++ b/type_manipulation_test.go @@ -27,7 +27,7 @@ func TestIsNil(t *testing.T) { var ifaceWithNilValue any = (*string)(nil) //nolint:staticcheck is.True(IsNil(ifaceWithNilValue)) - is.False(ifaceWithNilValue == nil) //nolint:staticcheck + is.False(ifaceWithNilValue == nil) //nolint:staticcheck,testifylint } func TestIsNotNil(t *testing.T) { @@ -51,7 +51,7 @@ func TestIsNotNil(t *testing.T) { var ifaceWithNilValue any = (*string)(nil) //nolint:staticcheck is.False(IsNotNil(ifaceWithNilValue)) - is.True(ifaceWithNilValue != nil) //nolint:staticcheck + is.True(ifaceWithNilValue != nil) //nolint:staticcheck,testifylint } func TestToPtr(t *testing.T) { @@ -75,11 +75,11 @@ func TestNil(t *testing.T) { is.Equal(expNilFloat64, nilFloat64) is.Nil(nilFloat64) - is.NotEqual(nil, nilFloat64) + is.NotEqual(nil, nilFloat64) //nolint:testifylint is.Equal(expNilString, nilString) is.Nil(nilString) - is.NotEqual(nil, nilString) + is.NotEqual(nil, nilString) //nolint:testifylint is.NotEqual(nilString, nilFloat64) }