diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b737661..93ea036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,12 @@ jobs: - name: Run tests run: go test ./... -coverprofile=coverage.out -covermode=atomic + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.out + fail_ci_if_error: false + - name: Validate coverage threshold shell: bash run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..38a086f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... -covermode=atomic + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/README.md b/README.md index 935abe2..c951a90 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/gocanto/collection.svg)](https://pkg.go.dev/github.com/gocanto/collection) [![Go Version](https://img.shields.io/badge/go-1.25-blue.svg)](https://golang.org/doc/devel/release.html#go1.25) +[![CI](https://github.com/gocanto/collection/actions/workflows/ci.yml/badge.svg)](https://github.com/gocanto/collection/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/gocanto/collection/branch/main/graph/badge.svg)](https://codecov.io/gh/gocanto/collection) +[![Go Report Card](https://goreportcard.com/badge/github.com/gocanto/collection)](https://goreportcard.com/report/github.com/gocanto/collection) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) A powerful Go port of [Laravel Collections](https://laravel.com/docs/collections) — fluent, type-safe, and powered by Go generics and `iter.Seq` lazy evaluation. diff --git a/arr/sorting.go b/arr/sorting.go index 778044f..0532dd6 100644 --- a/arr/sorting.go +++ b/arr/sorting.go @@ -63,5 +63,8 @@ func Random[T any](items []T, counts ...int) []T { return shuffled } - return shuffled[:count] + result := make([]T, count) + copy(result, shuffled[:count]) + + return result } diff --git a/collection/mutation.go b/collection/mutation.go index 5c742a4..4672141 100644 --- a/collection/mutation.go +++ b/collection/mutation.go @@ -67,6 +67,13 @@ func (c *Collection[T]) Pop(counts ...int) (T, bool) { _ = count // For single pop item := c.items[len(c.items)-1] + + // Zero the removed element so the GC can reclaim it — without this, + // the backing array keeps a stale reference and the value is never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + c.items[len(c.items)-1] = zero c.items = c.items[:len(c.items)-1] return item, true @@ -82,7 +89,20 @@ func (c *Collection[T]) PopMany(count int) *Collection[T] { } idx := len(c.items) - count - popped := Collect(c.items[idx:]) + + items := make([]T, count) + copy(items, c.items[idx:]) + popped := Collect(items) + + // Zero the removed tail so the GC can reclaim those elements — without this, + // the backing array keeps stale references that are never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + for i := idx; i < len(c.items); i++ { + c.items[i] = zero + } + c.items = c.items[:idx] return popped @@ -98,6 +118,13 @@ func (c *Collection[T]) Shift() (T, bool) { } item := c.items[0] + + // Zero the removed element so the GC can reclaim it — without this, + // the backing array keeps a stale reference and the value is never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + c.items[0] = zero c.items = c.items[1:] return item, true @@ -112,7 +139,19 @@ func (c *Collection[T]) ShiftMany(count int) *Collection[T] { return shifted } - shifted := Collect(c.items[:count]) + items := make([]T, count) + copy(items, c.items[:count]) + shifted := Collect(items) + + // Zero the removed head so the GC can reclaim those elements — without this, + // the backing array keeps stale references that are never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + for i := 0; i < count; i++ { + c.items[i] = zero + } + c.items = c.items[count:] return shifted diff --git a/collection/sorting.go b/collection/sorting.go index 54c0a6d..44486e6 100644 --- a/collection/sorting.go +++ b/collection/sorting.go @@ -75,5 +75,8 @@ func (c *Collection[T]) Random(counts ...int) *Collection[T] { return shuffled } - return Collect(shuffled.items[:count]) + result := make([]T, count) + copy(result, shuffled.items[:count]) + + return Collect(result) }