Skip to content

Conversation

@deltics
Copy link
Member

@deltics deltics commented Sep 12, 2025

Summary by CodeRabbit

  • New Features

    • New parsing API with default-value support, parse-into helpers, read-only vars map, presence check, transactional file-based env loading, and environment state restore. New value converters (boolean, filename, duration, port) and improved URL handling.
  • Refactor

    • Reworked legacy env APIs and load behavior for safer, reader-aware loading and clearer error semantics.
  • Documentation

    • README rewritten with updated examples and guidance.
  • Chores

    • CI, Dependabot, linter, workflow updates and Go toolchain bump.
  • Tests

    • Large test-suite refactor to DSL-style tests with expanded coverage.

@github-advanced-security
Copy link

This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation.

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Walkthrough

Refactors env parsing/loading and testing seams, adds new as converters and error types, replaces several public APIs (Override→Parse/ParseInto, Vars type→Vars func, State.Reset→Restore), introduces internal IO abstractions and reader-based Load/LoadFile/LoadFromReader, and updates CI/linting/docs and many tests.

Changes

Cohort / File(s) Summary
CI & Tooling
/.coderabbit.yaml, /.github/dependabot.yml, /.github/workflows/release.yml, /.golangci.yml, /go.mod
Add CodeRabbit reviews config, Dependabot Go gomod config, bump reusable workflow ref, add golangci lint config, and update Go toolchain and test dependency versions.
Documentation & pkgdocs
/README.md, /pkgdoc.go, /as/pkgdoc.go, /internal/pkgdoc.go
Revise README content and examples; add package-level documentation files for env, as, and internal packages.
Core API & errors
/env.go, /parse.go, /vars.go, /state.go, /errors.go
Replace Override with Parse/ParseInto and ConversionFunc type; remove Vars type in favor of Vars(...string) map[string]string; add IsSet; rename state.Reset→Restore; introduce/rename Err* values, FileError, and broaden Is/Unwrap matching.
Loading pipeline
/load.go
Rework Load to accept multiple filenames, dedupe/ignore missing, prefer .env first, provide transactional restore on failure, and expose LoadFile and LoadFromReader.
Internal test seams & consts
/internal/consts.go, /internal/funcs.go, /as/internal/funcs.go
Add internal EnvFile constant; expose OS/IO seams (Open, Stat, LookupEnv, Setenv, Unsetenv, NewFileReader, FileExists) and provide internal.ParseURL alias to enable test substitution.
as converters & errors
/as/bool.go, /as/duration.go, /as/filename.go, /as/int.go, /as/portNo.go, /as/url.go, /as/errors.go, /as/pkgdoc.go
Add Bool and Filename converters; split Duration API into Duration and DurationIn; standardize error wrapping in Int/PortNo; route URL parsing via internal.ParseURL; add new as error variables and wrapper types; add package doc.
Tests (migrated & new)
/env_test.go, /parse_test.go, /vars_test.go, /state_test.go, /errors_test.go, /load_test.go, /internal/funcs_test.go, /as/*_test.go
Migrate many tests to external _test packages, adopt DSL test harness, add/adjust tests for Parse/ParseInto, Load/LoadFromReader/LoadFile, Vars, State().Restore, FileError, internal helpers, and new/changed as converters.
Removals / legacy helpers & tests
/as/as_test.go, /as/funcs.go, /funcs.go, /funcs_test.go
Remove legacy in-package test seams and associated tests; replace them with internal-package indirection and new tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Env as env.Parse / env.ParseInto
  participant Internal as internal.LookupEnv
  participant Cnv as Converter
  participant Err as ParseError

  App->>Env: Parse(name, cnv, [def])
  Env->>Internal: LookupEnv(name)
  alt not set and default provided
    Env-->>App: return default, nil
  else not set and no default
    Env-->>App: error ErrNotSet wrapped in ParseError
  else set
    Env->>Cnv: cnv(value)
    alt convert ok
      Env-->>App: value, nil
    else convert err
      Env-->>App: ParseError(wrapped InvalidValueError)
    end
  end
Loading
sequenceDiagram
  autonumber
  actor App
  participant Env as env.Load
  participant Int as internal (FileExists/Open/Stat)
  participant Reader as LoadFromReader
  participant State as env.State()

  App->>Env: Load(filenames...)
  Env->>Int: FileExists(".env")?
  alt .env exists
    Env->>Env: prepend ".env"
  end
  Env->>Env: sanitize, dedupe, filter missing
  Env->>State: state := State()
  loop per file
    Env->>Int: Open(filename)
    alt open ok
      Env->>Reader: LoadFromReader(reader)
      alt parse ok
        Reader-->>Env: nil
      else parse error
        Env->>State: state.Restore()
        Env-->>App: FileError / ErrInvalidEntry
      end
    else open error
      Env-->>App: FileError(filename, err)
    end
  end
  Env-->>App: nil
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Initial Version #2 — Prior refactor touching the same core areas (Parse/Override → Parse/ParseInto, Load/loadFile → Load/LoadFile/LoadFromReader, State.Reset→Restore, Vars changes, and as converters); strong code-level overlap.

Poem

In my burrow I parse the env with care,
I hop through files and stitch states with flair.
Defaults snug, errors wrapped in a row,
I restore the world when the test winds blow.
Thump — docs and tests, tidied in a row. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.51% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly lists the key changes—adding lint and CI configurations, updating the release workflow, refactoring the env API, and enhancing the as package—which accurately reflect the main areas touched by this PR.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e981db and cf8b292.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/pkgdoc.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (4 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • internal/funcs_test.go (1 hunks)
  • internal/pkgdoc.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • pkgdoc.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • as/funcs.go
  • as/as_test.go
  • funcs.go
  • funcs_test.go
🧰 Additional context used
🧠 Learnings (16)
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • state.go
  • vars.go
  • internal/funcs.go
📚 Learning: 2025-09-24T23:04:07.744Z
Learnt from: deltics
PR: blugnu/env#10
File: as/duration_test.go:111-118
Timestamp: 2025-09-24T23:04:07.744Z
Learning: The ErrNotAnIntegerOrDuration error in the as package is implemented as a combinedError that allows testing for any constituent error (ErrNotAnInteger, ErrNotADuration, or strconv.ErrSyntax) using errors.Is(), providing flexible error handling for callers.

Applied to files:

  • as/duration_test.go
  • as/duration.go
  • as/int_test.go
  • as/int.go
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • as/duration_test.go
  • errors_test.go
  • as/portNo_test.go
📚 Learning: 2025-09-25T22:32:23.679Z
Learnt from: deltics
PR: blugnu/env#10
File: go.mod:3-5
Timestamp: 2025-09-25T22:32:23.679Z
Learning: In the blugnu/env project, the CI/CD pipeline is configured to specifically detect go.mod/go.sum drift issues and does not report problems when Go version and dependencies are updated as seen in this PR. The pipeline handles module dependency changes appropriately without requiring manual go mod tidy interventions.

Applied to files:

  • go.mod
  • .golangci.yml
📚 Learning: 2025-09-24T22:24:17.657Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:1-3
Timestamp: 2025-09-24T22:24:17.657Z
Learning: In the blugnu/env project, deltics prefers to rely on golangci-lint's automatic Go version detection from go.mod rather than explicit run.go configuration, as it's more robust and avoids version synchronization issues. The project size doesn't warrant explicit timeout configuration.

Applied to files:

  • go.mod
  • .golangci.yml
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load_test.go
  • load.go
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T22:33:15.369Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:21-24
Timestamp: 2025-09-24T22:33:15.369Z
Learning: In the blugnu/env project, deltics prefers to keep the intrange linter disabled, favoring explicit loop constructs over int ranges for clarity in individual circumstances.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/duration.go
  • as/int_test.go
  • as/int.go
📚 Learning: 2025-09-24T23:20:43.209Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/func_test.go:16-18
Timestamp: 2025-09-24T23:20:43.209Z
Learning: In tests, prefer using actual values that consumers would use (like ".env") rather than internal constants, even when those constants exist. Tests should reflect real-world usage patterns, not implementation details.

Applied to files:

  • internal/funcs_test.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/funcs_test.go
  • load.go
  • internal/funcs.go
  • as/filename_test.go
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In as/filename.go, differentiate between empty filename conditions and file-not-existing conditions using distinct sentinel errors for better semantic clarity.

Applied to files:

  • errors_test.go
  • load.go
  • internal/funcs.go
  • errors.go
  • as/filename_test.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
📚 Learning: 2025-09-25T03:13:16.923Z
Learnt from: deltics
PR: blugnu/env#10
File: as/pkgdoc.go:1-6
Timestamp: 2025-09-25T03:13:16.923Z
Learning: In Go doc comments, when referencing packages within the same module, use the short form [pkg.Symbol] rather than fully qualified links [module.com/pkg.Symbol]. The short form is the preferred and idiomatic approach for intra-module references in Go documentation, while fully qualified paths are typically reserved for external packages or cases where ambiguity might exist.

Applied to files:

  • as/pkgdoc.go
🧬 Code graph analysis (25)
state.go (1)
internal/funcs.go (1)
  • Setenv (22-22)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (3)
  • ErrNotADuration (30-30)
  • ErrNotAnInteger (29-29)
  • ErrNotAnIntegerOrDuration (31-31)
parse.go (2)
errors.go (4)
  • ParseError (86-89)
  • InvalidValueError (150-153)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (16-16)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • Setenv (22-22)
  • LookupEnv (16-16)
  • ScanLines (19-19)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (33-74)
  • LoadFile (86-102)
  • LoadFromReader (112-171)
internal/consts.go (1)
  • EnvFile (5-5)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (10-10)
state_test.go (2)
state.go (1)
  • State (38-40)
env.go (1)
  • IsSet (35-38)
as/portNo.go (1)
errors.go (1)
  • RangeError (215-218)
as/internal/funcs.go (1)
parse.go (1)
  • Parse (55-73)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (16-16)
  • Setenv (22-22)
  • Unsetenv (28-28)
vars_test.go (2)
internal/funcs.go (1)
  • Setenv (22-22)
vars.go (1)
  • Vars (29-60)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (30-30)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (16-16)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
internal/funcs_test.go (1)
internal/funcs.go (4)
  • FileExists (38-48)
  • Stat (25-25)
  • Open (13-13)
  • NewFileReader (61-63)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (35-40)
  • ParseError (86-89)
  • InvalidValueError (150-153)
  • RangeError (215-218)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (5-5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • ScanLines (19-19)
  • LookupEnv (16-16)
  • Setenv (22-22)
errors.go (3)
  • NewFileError (35-40)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (2)
  • Setenv (22-22)
  • Unsetenv (28-28)
env.go (6)
  • Clear (11-13)
  • IsSet (35-38)
  • Get (28-30)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (32-32)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (16-16)
parse.go (2)
  • Parse (55-73)
  • ParseInto (97-109)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (86-89)
  • InvalidValueError (150-153)
  • ErrTargetIsNil (22-22)
errors.go (1)
as/filename.go (1)
  • Filename (9-28)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (32-47)
errors.go (1)
  • RangeError (215-218)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (9-28)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: release / release / release: preflight checks
🔇 Additional comments (32)
parse.go (1)

98-104: Nice nil-target guard in ParseInto.

This early return with a ParseError wrapping ErrTargetIsNil keeps the API consistent with Parse’s error semantics and eliminates the panic risk we spotted earlier.

go.mod (1)

3-5: Go 1.23 alignment and test harness bump look good.

The module version tracks the language upgrade, and pulling in github.com/blugnu/test v0.12.0 lines up with the new helpers used in the updated suites.

internal/pkgdoc.go (1)

1-11: Clear guidance on internal package scope.

The package comment now makes it explicit that these helpers are for intra-module use only, which should prevent accidental external reliance.

as/bool_test.go (1)

28-66: Comprehensive Bool coverage.

Great job expanding the cases to include mixed case, whitespace, and genuinely invalid tokens—the assertions now mirror as.Bool’s trimming and normalization behaviour perfectly.

as/pkgdoc.go (1)

1-10: Doc comment succinctly advertises the as helpers.

Highlighting that any single-string converter can plug into env.Parse while pointing to the bundled helpers keeps pkgsite guidance sharp.

as/filename_test.go (1)

29-69: Filename tests cover the edge matrix well.

Thanks for nailing the empty string, directory, invalid byte sequence, and os.ErrNotExist cases—the platform-agnostic assertions keep this solid.

pkgdoc.go (1)

1-21: Top-level package overview reads well.

This captures the new Parse/Load surface, the helper packages, and the test-state utilities in one place—perfect for pkgsite discovery.

as/string_test.go (1)

11-22: String happy-path smoke test is spot on.

Nice, tight Arrange–Act–Assert that verifies the helper stays a no-op and surfaces errors properly.

as/portNo.go (1)

33-44: Consistent port parsing improvements look great

I like the shared handleError wrapper—keeps the prefixing uniform—and the TrimSpace plus math.MaxUint16 range check covers the edge cases cleanly.

as/portNo_test.go (1)

23-54: Nice boundary coverage

The table-driven cases nail both the range limits and the wrapped error expectations, including errors.As on the RangeError; great safety net for the refactor.

.golangci.yml (1)

1-52: Configuration matches project lint posture

The mix of default-on, selectively disabled linters plus staticcheck’s all minus QF1008 aligns with the documented preferences and keeps coverage broad without fighting local style.

vars.go (1)

10-57: API docs and implementation stay in sync

The new Vars helper makes the trimming semantics explicit in both comment and code, and the split between all-vars and named lookups keeps the behavior straightforward.

env.go (1)

32-95: Good use of the internal seams

Routing IsSet, Set, and Unset through the internal shims while keeping error prefixes consistent with the package gives solid testability without over-seaming.

errors.go (1)

9-257: FileError and friends now feel polished

The updated docs, pointer-aware Is implementations, and consistent env.* prefixes make the error types much easier to match and diagnose—nicely done.

as/int.go (1)

20-24: Helpful sentinel wrapping

Keeping ErrNotAnInteger in the chain while preserving the original Atoi error gives callers the best of both worlds.

internal/consts.go (1)

3-6: Doc comment hits the mark

The short description keeps EnvFile lint- and doc-friendly without overexplaining.

as/url.go (1)

32-35: Good call on the ParseURL seam

Forwarding through internal.ParseURL keeps the production code thin while still letting tests stub the parser when needed. Clean seam, nice and simple.

errors_test.go (1)

235-263: Great addition of pointer-target assertions.

Covering the pointer forms in the Is matrix closes the remaining gap and keeps behavior symmetrical with the struct targets. Nicely done.

as/duration_test.go (1)

128-137: Solid guard for the combined sentinel handling.

Exercising ErrNotAnInteger, ErrNotADuration, and ErrNotAnIntegerOrDuration on the same failure path proves the combinedError behaves exactly as intended. Great edge-case coverage.

internal/funcs_test.go (1)

61-69: Nice catch on the directory branch.

The explicit directory fixture makes the !fi.IsDir() guard future-proof and ensures we won’t regress on that path.

vars_test.go (1)

47-65: Whitespace behavior is nailed.

The paired cases for trimmed keys vs. preserved values mirror the contract in Vars perfectly.

internal/funcs.go (1)

31-48: FileExists logic looks sharp.

The explicit os.ErrNotExist short-circuit plus the directory guard line up with the documented behavior and keep callers’ expectations clear.

load_test.go (1)

123-158: Awesome transactional and dedup coverage.

The “load all or none” rollback check together with the dedup test validates the trickiest branches in Load. Great use of the seams.

state_test.go (1)

29-39: Appreciate the OS-level safety net here.

Capturing the raw environment and deferring an os.* restore means these tests no longer depend on the state/restore API they’re validating, so a panic won’t leak a cleared env into the suite. Based on learnings

as/duration.go (1)

15-48: Whitespace trim + ParseInt upgrade look great.

Normalizing the input before parsing and switching to ParseInt avoids 32-bit truncation while keeping the duration fallback intact. Reads clean and fixes the edge cases.

README.md (1)

47-115: Examples now mirror the public APIs nicely.

The refreshed Parse/ParseInto walkthroughs line up with the new signatures and make the default/error behavior obvious at a glance.

load.go (1)

41-170: Transactional loader flow looks solid.

Deduping/cleaning filenames up front and routing every failure through the captured state keeps .env precedence predictable and guarantees no partial env writes escape the rollback path.

as/int_test.go (1)

37-41: Nice job locking both error contracts.

Asserting both strconv.ErrSyntax and as.ErrNotAnInteger guarantees the combined error wrapping keeps working as intended. Based on learnings.

as/internal/funcs.go (1)

8-10: Alias looks clean and purposeful.

The exported ParseURL seam is clear and keeps tests free to stub url.Parse without leaking that detail elsewhere.

env_test.go (1)

91-98: Great catch covering the empty-string case.

Verifying that Lookup treats "" as present protects an easy-to-miss regression.

as/url_test.go (1)

25-31: Targeted assertions keep the URL test resilient.

Checking scheme and host fields instead of the whole struct avoids brittleness if net/url grows new fields.

parse_test.go (1)

109-113: Error-chain assertions are spot on.

Confirming the returned error matches the sentinel, ParseError, and InvalidValueError all at once nails the contract for consumers.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot changed the title @coderabbitai Add lint/CI configs, update release, refactor env API, enhance as Sep 12, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 37

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
state.go (2)

16-17: Docs still reference Reset(); update to Restore()

Public docs/examples under State() still say Reset, which no longer exists. This is user-facing; fix to avoid confusion.

Apply:

-// the test has run (by deferring a call to the Reset method on the
-// returned state value).
+// the test has run (by deferring a call to the Restore method on the
+// returned state value).
@@
-//		defer env.State().Reset()
+//		defer env.State().Restore()

Also applies to: 29-29


3-6: Use internal.Setenv alias for testability; keep behavior identical

Using os.Setenv here bypasses the internal alias that’s provided specifically for monkey-patching in tests. Switch to internal.Setenv.

Apply:

 import (
 	"os"
 	"strings"
+	"github.com/blugnu/env/internal"
 )
@@
-		_ = os.Setenv(k, v)
+		_ = internal.Setenv(k, v)

Also applies to: 65-66

.golangci.yml (1)

31-53: Misplaced linter settings block; use top-level linters-settings

golangci-lint expects linter configuration under the top-level key linters-settings, not linters.settings. As-is, these settings will be ignored.

Apply:

-  settings:
-    depguard:
-      rules:
-        tests:
-          files:
-            - $test
-          list-mode: lax
-          allow: ["*"]
-
-    # disabled until I figure out a way to FAIL on a PR branch and only WARN on feature branches
-    # godox:
-    #   keywords:
-    #     - TODO
-
-    revive:
-      rules:
-        - name: package-comments
-          disabled: true
-
-    staticcheck:
-      checks:
-        - -QF1008    # "could remove embedded field <name> from selector"; sometimes it is useful (if unnecessary) to disambiguate
+linters-settings:
+  depguard:
+    rules:
+      tests:
+        files:
+          - $test
+        list-mode: lax
+        allow: ["*"]
+
+  # disabled until I figure out a way to FAIL on a PR branch and only WARN on feature branches
+  # godox:
+  #   keywords:
+  #     - TODO
+
+  revive:
+    rules:
+      - name: package-comments
+        disabled: true
+
+  staticcheck:
+    checks:
+      - -QF1008    # "could remove embedded field <name> from selector"; sometimes it is useful (if unnecessary) to disambiguate
parse.go (1)

52-54: Docs contradict implementation for ErrNotSet.

Code wraps ErrNotSet in ParseError; comment claims nil error. Fix the doc.

-//  // any error resulting from parsing the environment variable;
-//  // if the error is ErrNotSet a nil error is returned
+//  // any error resulting from parsing the environment variable;
+//  // if the variable is not set and no default is provided, a [ParseError]
+//  // wrapping [ErrNotSet] is returned
README.md (2)

16-16: Go version badge alt text is inaccurate (uses generics and errors.Join).

The code uses generics (Go ≥1.18) and errors.Join (Go ≥1.20). Update the badge alt text accordingly.

-      <img alt="go version >= 1.14" src="https://img.shields.io/github/go-mod/go-version/blugnu/env?style=flat-square"/>
+      <img alt="go version >= 1.20" src="https://img.shields.io/github/go-mod/go-version/blugnu/env?style=flat-square"/>

39-45: Document minimum supported Go version.

Make the requirement explicit near Installation to prevent confusion.

 ## Installation
 
 ```bash
 go get github.com/blugnu/env

+> Requires Go 1.20+.


</blockquote></details>
<details>
<summary>load.go (2)</summary><blockquote>

`30-31`: **Parameter name mismatch in docs.**

Signature uses “filenames”; update the parameter name in the docs for consistency.

```diff
-//  files: ...string    // 0..n file path(s)
+//  filenames: ...string    // 0..n file path(s)

37-41: Over-specific error formatting in docs.

Docs prescribe “path/to/file: error”, but errors are wrapped in env.FileError and formatting depends on FileError.Error(). Rephrase to avoid promising a specific string shape.

-// with the file path that caused the error:
-//
-//  "path/to/file: error"
+// and annotated with the filename via env.FileError in the error chain.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4c7bc and fb1cb1e.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (41)
  • .coderabbit-golangci.yml (1 hunks)
  • .coderabbit.yaml (1 hunks)
  • .github/dependabot.yml (1 hunks)
  • .github/workflows/release.yml (1 hunks)
  • .golangci.yml (1 hunks)
  • README.md (3 hunks)
  • as/as_test.go (0 hunks)
  • as/bool.go (1 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/errors.go (1 hunks)
  • as/filename.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (1 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/func_test.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • load.go (2 hunks)
  • load_test.go (1 hunks)
  • parse.go (4 hunks)
  • parse_test.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • funcs_test.go
  • as/funcs.go
  • funcs.go
  • as/as_test.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/func_test.go
  • load_test.go
🧬 Code graph analysis (24)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (10-24)
as/errors.go (1)
  • ErrFileDoesNotExist (6-6)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseUrl (7-7)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/filename.go (1)
as/errors.go (1)
  • ErrFileDoesNotExist (6-6)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (25-37)
as/int_test.go (1)
as/int.go (1)
  • Int (19-25)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (31-45)
errors.go (1)
  • RangeError (181-184)
internal/func_test.go (1)
internal/funcs.go (3)
  • FileExists (27-30)
  • Open (14-14)
  • NewFileReader (43-45)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (21-27)
  • DurationIn (49-66)
as/errors.go (1)
  • ErrNotADuration (8-8)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (10-10)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (27-30)
  • NewFileReader (43-45)
  • Environ (11-11)
  • LookupEnv (17-17)
  • Setenv (20-20)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (69-101)
  • LoadFile (113-129)
  • LoadFromReader (147-187)
internal/consts.go (1)
  • EnvFile (4-4)
errors.go (2)
  • FileError (25-28)
  • ErrInvalidEntry (11-11)
as/portNo.go (1)
errors.go (1)
  • RangeError (181-184)
vars.go (1)
internal/funcs.go (2)
  • Environ (11-11)
  • LookupEnv (17-17)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (17-17)
parse.go (2)
  • Parse (61-79)
  • ParseInto (101-109)
errors.go (3)
  • ErrNotSet (19-19)
  • InvalidValueError (125-128)
  • ParseError (70-73)
env.go (1)
internal/funcs.go (2)
  • LookupEnv (17-17)
  • Unsetenv (23-23)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (7-7)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (8-8)
load.go (4)
internal/consts.go (1)
  • EnvFile (4-4)
internal/funcs.go (4)
  • FileExists (27-30)
  • NewFileReader (43-45)
  • LookupEnv (17-17)
  • Setenv (20-20)
errors.go (3)
  • NewFileError (30-35)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (15-15)
state.go (1)
  • State (38-40)
vars_test.go (2)
state.go (1)
  • State (38-40)
vars.go (1)
  • Vars (28-59)
state.go (1)
internal/funcs.go (1)
  • Setenv (20-20)
errors_test.go (1)
errors.go (5)
  • FileError (25-28)
  • NewFileError (30-35)
  • ParseError (70-73)
  • InvalidValueError (125-128)
  • RangeError (181-184)
parse.go (2)
errors.go (3)
  • ParseError (70-73)
  • InvalidValueError (125-128)
  • ErrNotSet (19-19)
internal/funcs.go (1)
  • LookupEnv (17-17)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (3)
  • Setenv (20-20)
  • LookupEnv (17-17)
  • Unsetenv (23-23)
env.go (6)
  • Clear (11-13)
  • Get (28-30)
  • IsSet (35-38)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
state_test.go (2)
internal/funcs.go (2)
  • Environ (11-11)
  • Setenv (20-20)
state.go (1)
  • State (38-40)
🪛 GitHub Actions: release
go.mod

[error] 1-1: Run # a script to extract build information from ./go.mod failed: file does not exist (./go.mod)

🔇 Additional comments (27)
go.mod (1)

3-5: Go 1.23 bump and test-lib upgrade — ensure CI/toolchain alignment and sums updated.

  • Confirm the reusable workflow sets up Go 1.23; otherwise builds will fail.
  • Ensure go.sum was regenerated for github.com/blugnu/test v0.12.0.

Run:

#!/bin/bash
set -euo pipefail

# Confirm files present
test -f go.mod && echo "go.mod: OK" || { echo "go.mod missing"; exit 1; }
if [[ -f go.sum ]]; then echo "go.sum: OK"; else echo "go.sum missing"; fi

# Show go directive and test dep
awk '/^go [0-9]+\.[0-9]+/ {print "go directive:", $0}' go.mod
grep -nE 'github.com/blugnu/test' go.mod || true
.github/workflows/release.yml (1)

6-7: ```shell
#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo
echo "---- Search for 'blugnu/.reusable' ----"
if command -v rg >/dev/null 2>&1; then
rg -n --hidden "blugnu/.reusable" || true
else
grep -RIn --hidden "blugnu/.reusable" || true
fi

echo
echo "---- Matches for 'uses:.*blugnu/.reusable' ----"
if command -v rg >/dev/null 2>&1; then
rg -n --hidden "uses:.*blugnu/.reusable" -S || true
else
grep -RIn --hidden "uses:.*blugnu/.reusable" || true
fi

echo
echo "---- Show .github/workflows/release.yml (lines 1-120) ----"
if [ -f .github/workflows/release.yml ]; then
nl -ba -w3 -s' ' .github/workflows/release.yml | sed -n '1,120p'
else
echo "file not found: .github/workflows/release.yml"
fi


</blockquote></details>
<details>
<summary>.github/dependabot.yml (1)</summary><blockquote>

`1-11`: **LGTM.**

Sensible defaults (weekly cadence, PR cap, reviewer/label set).

</blockquote></details>
<details>
<summary>as/string_test.go (1)</summary><blockquote>

`11-23`: **LGTM.**

Straightforward positive-case test; consistent with the test harness used elsewhere.

</blockquote></details>
<details>
<summary>state.go (1)</summary><blockquote>

`61-67`: **Confirm intent to drop Setenv errors**

Errors from Setenv are intentionally discarded via blank identifier. If this is to appease errcheck only, consider at least a debug log in tests, or document the rationale.

</blockquote></details>
<details>
<summary>as/bool_test.go (1)</summary><blockquote>

`28-51`: **LGTM: comprehensive truthy/falsey coverage and error assertions**

Cases align with as.Bool behavior (extended yes/no forms, wrapped strconv.ErrSyntax). Table harness usage is clean.

</blockquote></details>
<details>
<summary>.golangci.yml (1)</summary><blockquote>

`50-53`: **Verify that staticcheck code QF1008 is valid in your toolchain**

Ensure your golangci-lint/staticcheck version recognizes QF1008; otherwise this line will be ignored.

Run locally:

```shell
#!/bin/bash
# Show supported staticcheck checks (requires golangci-lint installed)
golangci-lint linters | sed -n '/staticcheck/,/^[[:alnum:]]/p'
as/filename_test.go (1)

28-51: LGTM: clear coverage of exists/missing/invalid filename paths

Assertions correctly use sentinel (ErrFileDoesNotExist) and syscall.EINVAL for invalid names; tidy table-driven style.

as/url.go (1)

10-13: ```shell
#!/bin/bash
set -euo pipefail

echo "Repo root: $(pwd)"
echo "Listing as/ directory (if present):"
ls -la as || true

echo
echo "Show as/url.go (if exists):"
if [ -f as/url.go ]; then
nl -ba as/url.go | sed -n '1,240p'
else
echo "as/url.go not found; searching repository for AbsoluteURL, ErrNotAnAbsoluteURL, IsAbs, ParseUrl, ParseURL..."
rg -n --hidden -S "func\s+AbsoluteURL" || true
rg -n --hidden -S "ErrNotAnAbsoluteURL|not an absolute URI" || true
rg -n --hidden -S "IsAbs(" || true
rg -n --hidden -S "ParseUrl|ParseURL" || true
fi


</blockquote></details>
<details>
<summary>internal/func_test.go (1)</summary><blockquote>

`37-50`: **LGTM: negative path correctly verifies error passthrough from Open**

The failure stub and assertions are straightforward and robust.

</blockquote></details>
<details>
<summary>as/portNo.go (1)</summary><blockquote>

`32-41`: **LGTM: correct bounds and consistent error wrapping.**

Uses math.MaxUint16 and wraps errors with context. Behavior matches tests.

</blockquote></details>
<details>
<summary>as/url_test.go (1)</summary><blockquote>

`25-47`: **LGTM: covers absolute, relative, and invalid cases.**

Good use of type assertion for *url.Error and ErrNotAnAbsoluteURL via errors.Is semantics.

</blockquote></details>
<details>
<summary>as/duration_test.go (2)</summary><blockquote>

`16-36`: **LGTM: Duration happy-path and error-path are covered.**

Assertions align with as.Duration behavior.

---

`47-81`: **LGTM: DurationIn covers unit application, precedence of explicit duration, and syntax errors.**

Clear separation of scenarios; matches implementation comments.

</blockquote></details>
<details>
<summary>vars_test.go (1)</summary><blockquote>

`26-46`: **LGTM: verifies both “all vars” and “selected vars” flows.**

Good isolation via State().Restore() and Clearenv; assertions match new Vars API.

</blockquote></details>
<details>
<summary>env_test.go (2)</summary><blockquote>

`15-27`: **Tests: structure and isolation LGTM.**

Good use of State().Restore(), t.Setenv, and assertions across Clear/Get/IsSet/Lookup/Set.



Also applies to: 29-48, 50-63, 65-86, 88-101

---

`137-149`: ```shell
#!/bin/bash
set -euo pipefail

echo "===== Searching for 'func Unset(' ====="
rg -nP --hidden --follow --no-ignore 'func\s+Unset\(' || true

echo
echo "===== Searching for 'Unsetenv' occurrences/definitions ====="
rg -nP --hidden --follow --no-ignore '\bUnsetenv\b' || true

echo
if [ -f go.mod ]; then
  echo "===== go.mod go version ====="
  rg -n '^go\s' go.mod || true
  echo "===== go.mod (head) ====="
  sed -n '1,80p' go.mod
else
  echo "===== no go.mod found in repo root ====="
fi

echo
echo "===== Showing files with 'func Unset(' (first 240 lines) ====="
for f in $(rg -lP --hidden --follow --no-ignore 'func\s+Unset\(' || true); do
  echo
  echo "---- $f ----"
  sed -n '1,240p' "$f"
done

echo
echo "===== Showing files that reference or define 'Unsetenv' (first 240 lines) ====="
for f in $(rg -lP --hidden --follow --no-ignore '\bUnsetenv\b' || true); do
  echo
  echo "---- $f ----"
  sed -n '1,240p' "$f"
done

echo
echo "===== Searching for call sites of 'Unset(' ====="
rg -nP --hidden --follow --no-ignore '\bUnset\(' || true

echo
echo "===== Showing env_test.go files (lines 1-300) ====="
find . -type f -name 'env_test.go' -print -exec sed -n '1,300p' {} \; || true
errors.go (1)

17-20: Message change for ErrNotSet — verify external callers.

The message changed to “variable not set.” If any consumers compare error strings (ill-advised but common), this is breaking. Confirm dependents use errors.Is(err, env.ErrNotSet) instead.

state_test.go (1)

12-23: LGTM: State snapshot reflects os.Environ().

parse_test.go (7)

14-28: LGTM: Happy path parse via LookupEnv stub.


30-44: LGTM: Not-set path asserts ErrNotSet and zero value.


46-61: LGTM: Conversion error wraps with ParseError→InvalidValueError.


63-77: LGTM: Default used when not set.


79-93: LGTM: Default ignored when value set.


111-126: LGTM: ParseInto updates target on success.


128-145: Avoid future flakiness: don’t parallelize tests that stub globals.

Because tests replace internal.LookupEnv (a global), adding t.Parallel would race. Keep these serial or gate the stub with a mutex if parallelizing later.

load_test.go (1)

16-107: Good DI of file I/O; cases read well.

Stubbing internal.FileExists/NewFileReader and restoring state per test is clean and reliable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
README.md (1)

32-41: Resolved: unchecked boxes replaced.

This addresses the prior feedback about unchecked task boxes.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb1cb1e and 28421c4.

📒 Files selected for processing (1)
  • README.md (3 hunks)
🔇 Additional comments (5)
README.md (5)

5-5: Tagline reads well.

Concise and on-message.


76-86: Required value example looks correct.

API usage and error handling are clear.


91-101: ParseInto example looks correct.

Good illustration of pointer destination and converter usage.


118-126: Vars example is clear.

Nicely communicates “only if set” behavior.


103-116: Docs: rename cfg.Url → cfg.URL; ParseInto already accepts an optional default

  • ParseInto signature is ParseInto[T any](target *T, name string, cnv ConversionFunc[T], def ...T) — optional default supported.
  • Update README example (README.md, around lines 109–111).
-    errs = append(errs, env.ParseInto(&cfg.Url, "AUTH_SERVICE_URL", as.AbsoluteURL))
+    errs = append(errs, env.ParseInto(&cfg.URL, "AUTH_SERVICE_URL", as.AbsoluteURL))

Likely an incorrect or invalid review comment.

@deltics deltics force-pushed the develop branch 2 times, most recently from 7571c76 to dca2cf3 Compare September 12, 2025 18:56
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
.golangci.yml (1)

31-53: Misplaced linter settings; should be top-level linters-settings, not linters.settings.

As written, depguard/revive/staticcheck configs won’t apply.

-  settings:
-    depguard:
-      rules:
-        tests:
-          files:
-            - $test
-          list-mode: lax
-          allow: ["*"]
-
-    # disabled until I figure out a way to FAIL on a PR branch and only WARN on feature branches
-    # godox:
-    #   keywords:
-    #     - TODO
-
-    revive:
-      rules:
-        - name: package-comments
-          disabled: true
-
-    staticcheck:
-      checks:
-        - -QF1008
+linters-settings:
+  depguard:
+    rules:
+      tests:
+        files:
+          - $test
+        list-mode: lax
+        allow: ["*"]
+
+  # disabled until I figure out a way to FAIL on a PR branch and only WARN on feature branches
+  # godox:
+  #   keywords:
+  #     - TODO
+
+  revive:
+    rules:
+      - name: package-comments
+        disabled: true
+
+  staticcheck:
+    checks:
+      - -QF1008
state.go (1)

13-30: Docs still reference Reset; rename to Restore in comments and example.

Keep API docs consistent with the method name.

Apply:

-// the test has run (by deferring a call to the Reset method on the
+// the test has run (by deferring a call to the Restore method on the
 // returned state value).
@@
-//		defer env.State().Reset()
+//		defer env.State().Restore()
env.go (1)

56-58: Use internal.LookupEnv in Lookup as well.

Keeps lookup behavior mockable and consistent with IsSet/Unset.

 func Lookup(name string) (string, bool) {
-	return os.LookupEnv(name)
+	return internal.LookupEnv(name)
 }
♻️ Duplicate comments (39)
.coderabbit-golangci.yml (1)

1-1: Remove stale v1 stub; consolidate on .golangci.yml.

This file isn’t a valid golangci-lint config and is no longer needed now that CodeRabbit supports v2. Keeping it is confusing.

.coderabbit.yaml (1)

1-6: Point CodeRabbit’s golangci-lint at the project config for consistency.

Autodetection should find .golangci.yml, but making it explicit avoids drift.

 reviews:
   profile: assertive
   request_changes_workflow: true
   tools:
+    golangci-lint:
+      config_file: .golangci.yml
     yamllint:
       enabled: false
as/internal/funcs.go (1)

5-8: Rename ParseUrl → ParseURL (initialism).

Go style prefers URL over Url. Update call sites accordingly.

 var (
-	// alias for [url.Parse]
-	ParseUrl = url.Parse
+	// ParseURL is an alias for [url.Parse].
+	ParseURL = url.Parse
 )
as/bool.go (1)

9-14: Fix GoDoc grammar and tighten phrasing.

“an Boolean” → “a Boolean”; keep comments idiomatic and concise.

-// Bool converts a string to an Boolean.  Conversion is initially attempted
-//
-// using [strconv.ParseBool].  If that fails, the value is checked for
-//
-// "y"/"yes" or "n"/"no" (case-insensitive).
-//
-//
-// If the value is not supported by [strconv.ParseBool] and is not one of
-//
-// the additional values above, an error is returned.
+// Bool converts a string to a Boolean.
+// It first tries strconv.ParseBool. If that fails, it accepts "y"/"yes" and "n"/"no" (case-insensitive).
+// Returns the parsed value or an error if unsupported.
as/errors.go (1)

10-10: Fix wording to match identifier: URL vs URI.

Align the public error message with the identifier and related docs/usages.

-   ErrNotAnAbsoluteURL       = errors.New("not an absolute URI")
+   ErrNotAnAbsoluteURL       = errors.New("not an absolute URL")
errors.go (1)

37-46: Use consistent "env.FileError" prefix and include filename.

Match formatting used by other error types; improves diagnostics.

 // Error implements the error interface.
 func (err FileError) Error() string {
-	switch {
-	case err.err != nil:
-		return fmt.Sprintf("file error: %v", err.err)
-
-	default:
-		return "file error"
-	}
+	switch {
+	case err.err != nil && err.Filename != "":
+		return fmt.Sprintf("env.FileError: %s: %v", err.Filename, err.err)
+	case err.err != nil:
+		return fmt.Sprintf("env.FileError: %v", err.err)
+	case err.Filename != "":
+		return fmt.Sprintf("env.FileError: %s", err.Filename)
+	default:
+		return "env.FileError"
+	}
 }
as/int.go (2)

3-6: Import errors to correctly join sentinel and cause.

 import (
-	"fmt"
+	"errors"
+	"fmt"
 	"strconv"
 )

22-22: Fix invalid error wrapping: only one %w allowed.

Preserve both ErrNotAnInteger and the original error via errors.Join.

-		return 0, fmt.Errorf("as.Int: %w: %w", ErrNotAnInteger, err)
+		return 0, fmt.Errorf("as.Int: %w", errors.Join(ErrNotAnInteger, err))
internal/consts.go (1)

1-6: Prefer filepath.Join for OS-agnostic paths.

-package internal
-
-const (
-	EnvFile     = ".env"
-	EnvFilePath = "./" + EnvFile
-)
+package internal
+
+import "path/filepath"
+
+const (
+	EnvFile     = ".env"
+	EnvFilePath = filepath.Join(".", EnvFile)
+)
as/url.go (1)

32-35: Use initialism: ParseURL (not ParseUrl). Also align docs to “URL”.

Apply in this file and rename the alias in as/internal accordingly.

-	u, err := internal.ParseUrl(s)
+	u, err := internal.ParseURL(s)

Additional doc tweak (outside diff hunk, for consistency):

-// AbsoluteURL converts a string to an absolute URI. An absolute URI is a URI
-// that includes a scheme, host, and path. The function returns an error if the
-// string is not an absolute URI.
+// AbsoluteURL converts a string to an absolute URL. An absolute URL includes a
+// scheme (per url.URL.IsAbs). The function returns an error if the string is
+// not an absolute URL.
internal/func_test.go (1)

23-35: Return a real, closeable file from the Open stub and close it to avoid leaks.

Returning &os.File{} constructs an invalid handle and the test never closes the reader.

Apply:

 Run(Test("when Open is successful", func() {
   // arrange
-  defer Restore(Original(&internal.Open).ReplacedBy(func(string) (*os.File, error) {
-    return &os.File{}, nil
-  }))
+  defer Restore(Original(&internal.Open).ReplacedBy(func(string) (*os.File, error) {
+    f, err := os.CreateTemp(t.TempDir(), "open-ok-*")
+    if err != nil {
+      t.Fatalf("setup temp file: %v", err)
+    }
+    return f, nil
+  }))
@@
   // assert
   Expect(err).IsNil()
   Expect(result).IsNotNil()
+  _ = result.Close()
 }))
as/filename.go (1)

15-23: Validate empty path and reject directories (Filename should only accept files).

Guard empty string and ensure the path is not a directory.

Apply:

- if _, err := os.Stat(v); err != nil {
+ if v == "" {
+   return handleError(ErrFileDoesNotExist)
+ }
+ fi, err := os.Stat(v)
+ if err != nil {
   if errors.Is(err, os.ErrNotExist) {
     return handleError(ErrFileDoesNotExist)
   }
 
   return handleError(err)
- }
+ }
+ if fi.IsDir() {
+   return handleError(ErrFileDoesNotExist)
+ }
 
 return v, nil
as/portNo_test.go (3)

3-11: Strengthen tests: assert RangeError bounds.

Also import errors and math for the checks.

Apply:

 import (
+  "errors"
+  "math"
   "strconv"
   "testing"
@@
   "github.com/blugnu/env"
   "github.com/blugnu/env/as"
 )

22-25: Add Min/Max assertions for -1 case.

Apply:

   Case("-1", func(result int, err error) {
     Expect(err).Is(env.RangeError[int]{})
+    var re env.RangeError[int]
+    if errors.As(err, &re) {
+      Expect(re.Min).To(Equal(0))
+      Expect(re.Max).To(Equal(int(math.MaxUint16)))
+    }
     Expect(result).To(Equal(0))
   }),

42-45: Add Min/Max assertions for 65536 case.

Apply:

   Case("65536", func(result int, err error) {
     Expect(err).Is(env.RangeError[int]{})
+    var re env.RangeError[int]
+    if errors.As(err, &re) {
+      Expect(re.Min).To(Equal(0))
+      Expect(re.Max).To(Equal(int(math.MaxUint16)))
+    }
     Expect(result).To(Equal(0))
   }),
as/int_test.go (1)

13-20: Fix double %w in as.Int (as/int.go:22).
fmt.Errorf currently uses two %w verbs; replace with a single %w wrapping errors.Join so errors.Is and wrapping behave correctly.

-import "fmt"
+import (
+  "errors"
+  "fmt"
+)

 func Int(s string) (int, error) {
   i, err := strconv.Atoi(s)
   if err != nil {
-    return 0, fmt.Errorf("as.Int: %w: %w", ErrNotAnInteger, err)
+    return 0, fmt.Errorf("as.Int: %w", errors.Join(ErrNotAnInteger, err))
   }
   return i, nil
 }
env.go (2)

32-37: Route IsSet through internal.LookupEnv for consistency/testability.

Use the internal seam like other helpers.

Apply this diff:

 func IsSet(name string) bool {
-	_, ok := os.LookupEnv(name)
+	_, ok := internal.LookupEnv(name)
 	return ok
 }

92-95: Prefix error with env.Unset (not os.Unsetenv).

Aligns with exported API naming used elsewhere.

-		if err := internal.Unsetenv(k); err != nil {
-			return fmt.Errorf("os.Unsetenv: %s: %w", k, err)
+		if err := internal.Unsetenv(k); err != nil {
+			return fmt.Errorf("env.Unset: %s: %w", k, err)
 		}
vars.go (5)

3-8: Import cleanup and internal seam for Environ.

Use internal.Environ and drop the direct os import.

-import (
-	"os"
-	"strings"
-
-	"github.com/blugnu/env/internal"
-)
+import (
+	"strings"
+
+	"github.com/blugnu/env/internal"
+)

35-41: Source env via internal.Environ().

Keeps all OS hooks behind internal.

-	case 0: // all environment variables
-		src = os.Environ()
+	case 0: // all environment variables
+		src = internal.Environ()

42-47: Trim names before lookup to avoid misses with whitespace.

Return the trimmed key from the handler.

 	default: // only the named variables (if set)
 		src = names
 		fn = func(s string) (string, string, bool) {
-			v, isSet := internal.LookupEnv(s)
-			return s, v, isSet
+			k := strings.TrimSpace(s)
+			v, isSet := internal.LookupEnv(k)
+			return k, v, isSet
 		}

50-56: Avoid double-trimming; rely on handlers to return cleaned keys.

Removes redundant TrimSpace in the loop.

 	result := make(map[string]string, len(src))
 	for _, s := range src {
 		if k, v, isSet := fn(s); isSet {
-			k = strings.TrimSpace(k)
 			result[k] = v
 		}
 	}

10-28: Document trimming semantics in the return description.

Prevents surprises for callers.

-//	map[string]string   // a map of environment variables where the key is the
-//	                    // name of the environment variable
+//	map[string]string   // a map of environment variables where the key is the
+//	                    // name of the environment variable; keys are trimmed of
+//	                    // surrounding whitespace
as/duration.go (2)

3-7: Fix invalid multi-%w wrapping; also trim input before parsing.

fmt.Errorf supports only one %w. Current code won’t compile. Also, accept surrounding whitespace. Join errors once and wrap.

 import (
+	"errors"
 	"fmt"
 	"strconv"
+	"strings"
 	"time"
 )
 
 // Duration parses a string into a time.Duration. The string must contain
 // a valid duration expression (e.g., "1h30m").
 
 func Duration(s string) (time.Duration, error) {
-	dur, err := time.ParseDuration(s)
+	s = strings.TrimSpace(s)
+	dur, err := time.ParseDuration(s)
 	if err != nil {
-		return 0, fmt.Errorf("as.Duration: %w: %w", ErrNotADuration, err)
+		return 0, fmt.Errorf("as.Duration: %w", errors.Join(ErrNotADuration, err))
 	}
 	return dur, nil
 }

Also applies to: 21-27


49-65: Preserve both root causes on integer-parse fallback; trim input.

Currently the fallback discards the duration-parse error and doesn’t trim. Join both errors and keep context.

 func DurationIn(u time.Duration) func(string) (time.Duration, error) {
 	return func(s string) (time.Duration, error) {
-		i, err := strconv.Atoi(s)
-		if err != nil {
-			// attempting to parse as a duration expression is a fallback,
-			// but if that fails then the issue is that the value is not
-			// an integer, so any duration parsing error is discarded in
-			// favor of the original integer parsing error
-			if dur, err := time.ParseDuration(s); err == nil {
-				return dur, nil
-			}
-
-			return 0, fmt.Errorf("as.DurationIn(%s): %w", u, err)
-		}
-
-		return time.Duration(i) * u, nil
+		s = strings.TrimSpace(s)
+
+		if i, err := strconv.Atoi(s); err == nil {
+			return time.Duration(i) * u, nil
+		} else {
+			if dur, durErr := time.ParseDuration(s); durErr == nil {
+				return dur, nil
+			} else {
+				return 0, fmt.Errorf("as.DurationIn(%s): %w", u, errors.Join(err, durErr))
+			}
+		}
 	}
 }
parse.go (1)

101-109: Guard against nil target in ParseInto to avoid panic.

Dereferencing a nil target panics.

-import "github.com/blugnu/env/internal"
+import (
+	"fmt"
+	"github.com/blugnu/env/internal"
+)
@@
 func ParseInto[T any](target *T, name string, cnv ConversionFunc[T], def ...T) error {
+	if target == nil {
+		return fmt.Errorf("env.ParseInto(%q): target is nil", name)
+	}
 	v, err := Parse(name, cnv, def...)
 	if err != nil {
 		return err
 	}
 
 	*target = v
 	return nil
 }
state_test.go (1)

25-27: Add a safety-net restore to prevent leaking a cleared env on panic.

Capture original env and defer restore before clearing.

 func TestState_Restore(t *testing.T) {
 	With(t)
+	orig := env.State()
+	defer orig.Restore()
@@
 	// assert
 	Expect(os.Getenv("VAR1")).To(Equal("value1"))
 	Expect(os.Getenv("VAR2")).To(Equal("value2"))

Also applies to: 36-41

internal/funcs.go (3)

9-24: Mark globals as intentional test seams (silence gochecknoglobals).

These are purposeful seams for substitution in tests.

-var (
+//nolint:gochecknoglobals // intentional test seams for OS primitives
+var (

26-31: Clarify and correct FileExists semantics.

Current impl returns true on permission or other errors. If “exists” should mean “Stat succeeded,” use err == nil and update the comment.

-// FileExists returns true if a file with the given filename exists.
+// FileExists reports whether the path exists (returns true only if os.Stat succeeds).
 var FileExists = func(filename string) bool {
 	_, err := os.Stat(filename)
-	return !errors.Is(err, os.ErrNotExist)
+	return err == nil
 }

43-45: Mark NewFileReader seam and intent.

Add nolint to avoid churn.

-var NewFileReader = func(path string) (io.ReadCloser, error) {
+//nolint:gochecknoglobals // test seam
+var NewFileReader = func(path string) (io.ReadCloser, error) {
 	return Open(path)
 }
errors_test.go (2)

100-107: Remove duplicated test case.

Same scenario listed twice.

-			{scenario: "FileError/different Filename/matching error",
-				target:   env.NewFileError("other.txt", sut.Unwrap()),
-				expected: false,
-			},

55-58: Add coverage for FileError.Unwrap for symmetry.

Mirror ParseError/InvalidValueError Unwrap tests.

 func TestFileError_Is(t *testing.T) {
 	With(t)
 
 	sut := env.NewFileError("file.txt", errors.New("some error"))
+
+	// also verify Unwrap symmetry
+	t.Run("Unwrap returns inner error", func(t *testing.T) {
+		With(t)
+		err := errors.New("inner")
+		got := env.NewFileError("any.txt", err).Unwrap()
+		Expect(got).To(Equal(err))
+	})
parse_test.go (1)

17-21: Deduplicate LookupEnv stubbing with a tiny helper.

Reduces repetition and centralizes restore handling.

 package env_test
@@
 import (
   "errors"
   "strconv"
   "testing"
 
   . "github.com/blugnu/test"
 
   "github.com/blugnu/env"
   "github.com/blugnu/env/internal"
 )
 
+// withLookupEnv replaces internal.LookupEnv for the test and returns a restore func.
+func withLookupEnv(val string, ok bool) func() {
+	return Restore(Original(&internal.LookupEnv).ReplacedBy(
+		func(string) (string, bool) { return val, ok },
+	))
+}
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "", false
-	}))
+	defer withLookupEnv("", false)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "not-a-number", true
-	}))
+	defer withLookupEnv("not-a-number", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "", false
-	}))
+	defer withLookupEnv("", false)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "abc", true
-	}))
+	defer withLookupEnv("abc", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "not a number", true
-	}))
+	defer withLookupEnv("not a number", true)()

Also applies to: 33-37, 51-54, 67-70, 83-86, 99-102, 116-119, 133-136

load_test.go (2)

198-206: Add test: comment lines are ignored.

Covers “# …” with/without leading spaces.

@@
 	Case("contains empty lines", testcase{
 		content: "\nVAR1=value1\n\nVAR2=value2\n",
 		assert: func(err error) {
 			Expect(err).IsNil()
 			Expect(os.Environ()).To(ContainItems([]string{
 				"VAR1=value1",
 				"VAR2=value2",
 			}))
 		},
 	}),
+
+	Case("comment lines (with and without leading spaces) are ignored", testcase{
+		content: "# c1\n  # c2\nVAR=ok\n",
+		assert: func(err error) {
+			Expect(err).IsNil()
+			Expect(os.Getenv("VAR")).To(Equal("ok"))
+		},
+	}),

220-227: Add test: scanner/read error restores env and returns error.

Simulate reader failure to cover scanner.Err path once implementation is fixed.

@@
 	Case("setting variable fails", testcase{
 		content: "VAR1=value1\nVAR2=value2\n",
 		setErr:  os.ErrInvalid,
 		assert: func(err error) {
 			Expect(err).Is(os.ErrInvalid)
 			Expect(os.Environ()).Should(BeEmpty())
 		},
 	}),
+	Case("scanner/read error rolls back changes", testcase{
+		content: "", // unused
+		assert: func(err error) {
+			Expect(err).Is(os.ErrInvalid)
+			Expect(os.Environ()).Should(BeEmpty())
+		},
+	}),

And in the arrange section, inject a failing reader for that case:

@@
- src := io.NopCloser(bytes.NewReader([]byte(tc.content)))
+ var src io.ReadCloser
+ if tc.content == "" && t.Name() == "scanner/read error rolls back changes" {
+   type failingReader struct{ n int }
+   func (r *failingReader) Read(p []byte) (int, error) {
+     if r.n > 0 { return 0, os.ErrInvalid }
+     r.n++
+     copy(p, []byte("VAR=ok\nTOO-LONG"))
+     return len("VAR=ok\nTOO-LONG"), nil
+   }
+   src = io.NopCloser(&failingReader{})
+ } else {
+   src = io.NopCloser(bytes.NewReader([]byte(tc.content)))
+ }
load.go (4)

44-55: Docs contradict behavior for missing .env and explicit filenames. Align with implementation and tests.

Missing files are ignored; no error is returned when nothing is loadable.

-// The function will ALWAYS attempt to load variables from a ".env" file, as follows:
+// The function will always attempt to load variables from a ".env" file, as follows:
@@
-// If ".env" (or "./.env") is included in the files parameter it will be loaded
-// in the order specified relative to other files; if the ".env" file does not exist
-// an error will be included in the returned error.
+// If ".env" (or "./.env") is included in the files parameter it will be loaded
+// in the order specified relative to other files; if the ".env" file does not exist
+// it is ignored without error (consistent with other filenames).
@@
-// If ".env" is NOT explicitly specified it will be loaded before any other files; if it does
-// not exist it is ignored without error unless no other files are specified.
+// If ".env" is NOT explicitly specified it will be loaded before any other files; if it does
+// not exist it is ignored without error.
@@
-// If no files are specified the function will attempt to load variables from ".env"
-// and will return an error if the file does not exist.
+// If no files are specified the function will attempt to load variables from ".env".
+// If it does not exist, nothing is loaded and the function returns nil.

150-153: Fix error context prefix for LoadFromReader.

Make source clear in logs.

-		return fmt.Errorf("env/internal.Load: %w", err)
+		return fmt.Errorf("env.LoadFromReader: %w", err)

182-186: Honor scanner errors to preserve transactional guarantee.

Restore env and return on scan/read failure.

-	if err := errors.Join(errs...); err != nil {
+	// capture scanning/reader errors (e.g., token too long, reader error)
+	if scanErr := scanner.Err(); scanErr != nil {
+		return handleError(scanErr)
+	}
+	if err := errors.Join(errs...); err != nil {
 		return handleError(err)
 	}

146-147: Typo: “occurrs” → “occurs”.

-//	error   // any error that occurrs while loading or applying variables
+//	error   // any error that occurs while loading or applying variables
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28421c4 and dca2cf3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (39)
  • .coderabbit-golangci.yml (1 hunks)
  • .coderabbit.yaml (1 hunks)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool.go (1 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/errors.go (1 hunks)
  • as/filename.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (1 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/func_test.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • load.go (2 hunks)
  • load_test.go (1 hunks)
  • parse.go (4 hunks)
  • parse_test.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • as/as_test.go
  • as/funcs.go
  • funcs_test.go
  • funcs.go
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-12T18:30:13.024Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.024Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .coderabbit-golangci.yml
  • .coderabbit.yaml
  • .golangci.yml
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • as/portNo_test.go
  • errors_test.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/func_test.go
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load_test.go
  • load.go
🧬 Code graph analysis (25)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (10-24)
as/errors.go (1)
  • ErrFileDoesNotExist (6-6)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (31-45)
errors.go (1)
  • RangeError (181-184)
vars_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (1)
  • Setenv (20-20)
vars.go (1)
  • Vars (28-59)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseUrl (7-7)
internal/func_test.go (1)
internal/funcs.go (3)
  • FileExists (27-30)
  • Open (14-14)
  • NewFileReader (43-45)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/int_test.go (1)
as/int.go (1)
  • Int (19-25)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (17-17)
parse.go (2)
  • Parse (61-79)
  • ParseInto (101-109)
errors.go (3)
  • ErrNotSet (19-19)
  • InvalidValueError (125-128)
  • ParseError (70-73)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (7-7)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (21-27)
  • DurationIn (49-66)
as/errors.go (1)
  • ErrNotADuration (8-8)
as/portNo.go (1)
errors.go (1)
  • RangeError (181-184)
state.go (1)
internal/funcs.go (1)
  • Setenv (20-20)
vars.go (1)
internal/funcs.go (2)
  • Environ (11-11)
  • LookupEnv (17-17)
parse.go (2)
errors.go (3)
  • ParseError (70-73)
  • InvalidValueError (125-128)
  • ErrNotSet (19-19)
internal/funcs.go (1)
  • LookupEnv (17-17)
env.go (1)
internal/funcs.go (2)
  • LookupEnv (17-17)
  • Unsetenv (23-23)
errors_test.go (1)
errors.go (5)
  • FileError (25-28)
  • NewFileError (30-35)
  • ParseError (70-73)
  • InvalidValueError (125-128)
  • RangeError (181-184)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (10-10)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (27-30)
  • NewFileReader (43-45)
  • Environ (11-11)
  • LookupEnv (17-17)
  • Setenv (20-20)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (69-101)
  • LoadFile (113-129)
  • LoadFromReader (147-187)
internal/consts.go (1)
  • EnvFile (4-4)
errors.go (2)
  • FileError (25-28)
  • ErrInvalidEntry (11-11)
state_test.go (2)
internal/funcs.go (2)
  • Environ (11-11)
  • Setenv (20-20)
state.go (1)
  • State (38-40)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (3)
  • Setenv (20-20)
  • LookupEnv (17-17)
  • Unsetenv (23-23)
env.go (6)
  • Clear (11-13)
  • Get (28-30)
  • IsSet (35-38)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
as/filename.go (1)
as/errors.go (1)
  • ErrFileDoesNotExist (6-6)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (8-8)
load.go (4)
internal/consts.go (1)
  • EnvFile (4-4)
internal/funcs.go (4)
  • FileExists (27-30)
  • NewFileReader (43-45)
  • LookupEnv (17-17)
  • Setenv (20-20)
errors.go (3)
  • NewFileError (30-35)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (15-15)
state.go (1)
  • State (38-40)
errors.go (1)
as/filename.go (1)
  • Filename (10-24)
🪛 GitHub Actions: release
go.mod

[error] 1-1: Run script to extract build information from ./go.mod failed: go.mod file not found (./go.mod).

🔇 Additional comments (12)
go.mod (1)

3-5: Align CI with Go 1.23 and fix release workflow path to go.mod.

  • Confirmed: go.mod is at the repository root; .github/workflows/release.yml delegates to blugnu/.reusable/.github/workflows/[email protected] (no local working-directory or setup-go in release.yml).
  • Action: Update .github/workflows/release.yml to either pass the module path to the reusable workflow (check that workflow's input name — e.g. with: path: . / module-path: .) or set working-directory to the repo root before invoking it; also ensure the workflow uses Go 1.23 (pass go-version: "1.23" or have setup-go read go.mod).
as/string_test.go (1)

11-23: LGTM.

Test reads cleanly and matches as.String behavior.

.golangci.yml (1)

2-19: Invalid key: linters.default; use enable-all in v2.

golangci-lint v2 expects linters.enable-all (or explicit enables). “default: all” won’t be honored.

-version: "2"
-linters:
-  default: all
+version: "2"
+linters:
+  enable-all: true
   disable:
     - err113
     - embeddedstructfieldcheck
     - exhaustruct
     - funlen
     - gochecknoglobals
     - godot
     - inamedparam
     - interfacebloat
     - lll
     - nlreturn
     - noinlineerr
     - paralleltest
     - varnamelen
     - wsl
     - wsl_v5
⛔ Skipped due to learnings
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.024Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.
as/filename_test.go (1)

25-33: LGTM on happy-path assertion.

Checks for existing file and equality are clear and sufficient.

as/portNo.go (1)

32-44: LGTM: clear range check and contextual wrapping.

Using math.MaxUint16 and env.RangeError improves clarity; error wrapping preserves strconv.ErrSyntax for errors.Is in tests.

as/url_test.go (3)

25-31: LGTM on absolute URL success case.

Checks scheme/host directly; concise and robust.


33-39: LGTM on relative-URL error assertion.

Assuming env.InvalidValueError unwraps ErrNotAnAbsoluteURL, this is correct.

If InvalidValueError stops unwrapping in future, adjust the assertion to errors.Is(err, as.ErrNotAnAbsoluteURL).


41-47: LGTM on invalid-URL type assertion.

Type-checking *url.Error keeps the parse-error contract stable.

as/duration_test.go (1)

13-37: Good coverage and assertions for Duration.

The cases align with the new API and error semantics.

env_test.go (1)

104-151: Clarify Unset semantics on partial failures; add a targeted case.

Decide whether Unset should stop on first error (current) and allow partial effects, or attempt all. Add a case to lock this in.

Proposed test addition:

@@
 		Case("error when unsetting", func() {
@@
 			Expect(os.Getenv("VAR2")).To(Equal("value2"))
 		}),
+
+		Case("partial progress when later unset fails", func() {
+			// arrange: first unset succeeds (actually unsets), second fails
+			unsetErr := errors.New("unset error after first")
+			call := 0
+			defer Restore(Original(&internal.Unsetenv).ReplacedBy(func(k string) error {
+				call++
+				if call == 1 {
+					return os.Unsetenv(k) // emulate a real successful unset
+				}
+				return unsetErr
+			}))
+
+			// act
+			err := env.Unset("VAR1", "VAR2")
+
+			// assert
+			Expect(err).Is(unsetErr)
+			Expect(os.Getenv("VAR1")).To(Equal(""))
+			Expect(os.Getenv("VAR2")).To(Equal("value2"))
+		}),
vars_test.go (1)

12-47: Vars tests read well and match the new API.

README.md (1)

16-17: No action required — README badge matches go.mod.
go.mod declares 1.23; README badge alt text is "go version >= 1.23".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
as/url.go (2)

10-13: Doc/code mismatch: IsAbs only checks scheme; doc says scheme, host, and path.

Either enforce host (and path if truly required) or relax the doc. Suggest requiring a non-empty scheme and host.

-// AbsoluteURL converts a string to an absolute URI. An absolute URI is a URI
-// that includes a scheme, host, and path. The function returns an error if the
-// string is not an absolute URI.
+// AbsoluteURL converts a string to an absolute URL.
+// It requires a non-empty scheme and host; otherwise returns an error.

32-39: Enforce host presence (not just scheme).

url.URL.IsAbs() doesn’t require Host. Add a host check to match the contract.

-	u, err := internal.ParseURL(s)
+	u, err := internal.ParseURL(s)
 	if err != nil {
 		return nil, err //nolint:wrapcheck // returns the parse error from [url.Parse]
 	}
-	if !u.IsAbs() {
+	if !u.IsAbs() || u.Host == "" {
 		return nil, env.InvalidValueError{Value: s, Err: ErrNotAnAbsoluteURL}
 	}
state.go (2)

13-18: Docstring still references Reset; should be Restore.

Update the narrative to consistently use Restore().

-// the test has run (by deferring a call to the Reset method on the
+// the test has run (by deferring a call to the Restore method on the

27-30: Example uses Reset; should use Restore for consistency.

Align the example with the new API.

-//		defer env.State().Reset()
+//		defer env.State().Restore()
as/portNo.go (1)

3-9: Trim whitespace before parsing.

Helps with inputs like " 443 " and aligns with other as.* helpers that TrimSpace.

 import (
 	"fmt"
 	"math"
+	"strings"
 	"strconv"
 
 	"github.com/blugnu/env"
 )
-	i, err := strconv.Atoi(s)
+	s = strings.TrimSpace(s)
+	i, err := strconv.Atoi(s)
env.go (1)

56-58: Use internal.LookupEnv in Lookup for seam consistency.

Matches IsSet and improves testability.

-func Lookup(name string) (string, bool) {
-	return os.LookupEnv(name)
-}
+func Lookup(name string) (string, bool) {
+	return internal.LookupEnv(name)
+}
errors.go (3)

115-129: Support pointer targets in ParseError.Is for consistency with FileError.

Improves ergonomics with errors.Is(&env.ParseError{...}, err).

-func (e ParseError) Is(target error) bool {
-	if target, ok := target.(ParseError); ok {
-		return (target.VariableName == "" || e.VariableName == target.VariableName) &&
-			(target.Err == nil || errors.Is(e.Err, target.Err))
-	}
-	return false
-}
+func (e ParseError) Is(target error) bool {
+	switch t := target.(type) {
+	case ParseError:
+		return (t.VariableName == "" || e.VariableName == t.VariableName) &&
+			(t.Err == nil || errors.Is(e.Err, t.Err))
+	case *ParseError:
+		if t == nil {
+			return false
+		}
+		return (t.VariableName == "" || e.VariableName == t.VariableName) &&
+			(t.Err == nil || errors.Is(e.Err, t.Err))
+	default:
+		return false
+	}
+}

170-183: Ditto for InvalidValueError.Is: accept pointer targets.

-func (e InvalidValueError) Is(target error) bool {
-	if target, ok := target.(InvalidValueError); ok {
-		return (target.Value == "" || e.Value == target.Value) &&
-			(target.Err == nil || errors.Is(e.Err, target.Err))
-	}
-	return false
-}
+func (e InvalidValueError) Is(target error) bool {
+	switch t := target.(type) {
+	case InvalidValueError:
+		return (t.Value == "" || e.Value == t.Value) &&
+			(t.Err == nil || errors.Is(e.Err, t.Err))
+	case *InvalidValueError:
+		if t == nil {
+			return false
+		}
+		return (t.Value == "" || e.Value == t.Value) &&
+			(t.Err == nil || errors.Is(e.Err, t.Err))
+	default:
+		return false
+	}
+}

214-225: And for RangeError[T].Is: accept pointer targets.

-func (e RangeError[T]) Is(target error) bool {
-	if target, ok := target.(RangeError[T]); ok {
-		return e == target || (target == RangeError[T]{})
-	}
-	return false
-}
+func (e RangeError[T]) Is(target error) bool {
+	switch t := target.(type) {
+	case RangeError[T]:
+		return e == t || (t == RangeError[T]{})
+	case *RangeError[T]:
+		if t == nil {
+			return false
+		}
+		return e == *t || (*t == RangeError[T]{})
+	default:
+		return false
+	}
+}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dca2cf3 and 2e7fda4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool.go (1 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/errors.go (1 hunks)
  • as/filename.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (1 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/func_test.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • funcs_test.go
  • as/funcs.go
  • funcs.go
  • as/as_test.go
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/func_test.go
  • internal/funcs.go
  • as/filename_test.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/int_test.go
  • as/int.go
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load.go
  • load_test.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • errors_test.go
  • as/portNo_test.go
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • internal/funcs.go
  • state.go
🧬 Code graph analysis (25)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (7-7)
internal/func_test.go (1)
internal/funcs.go (3)
  • FileExists (35-38)
  • Open (16-16)
  • NewFileReader (51-53)
as/internal/funcs.go (1)
parse.go (1)
  • Parse (54-72)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (2)
  • ErrNotADuration (22-22)
  • ErrNotAnInteger (21-21)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (19-19)
  • Setenv (25-25)
  • Unsetenv (28-28)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (21-21)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (6-6)
internal/funcs.go (5)
  • FileExists (35-38)
  • NewFileReader (51-53)
  • ScanLines (22-22)
  • LookupEnv (19-19)
  • Setenv (25-25)
errors.go (3)
  • NewFileError (33-38)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
as/duration.go (1)
as/errors.go (2)
  • ErrNotADuration (22-22)
  • ErrNotAnIntegerOrDuration (25-25)
load_test.go (5)
internal/funcs.go (6)
  • FileExists (35-38)
  • NewFileReader (51-53)
  • Setenv (25-25)
  • Environ (13-13)
  • LookupEnv (19-19)
  • ScanLines (22-22)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (32-71)
  • LoadFile (83-99)
  • LoadFromReader (109-152)
internal/consts.go (1)
  • EnvFile (6-6)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (33-38)
  • ParseError (84-87)
  • InvalidValueError (139-142)
  • RangeError (195-198)
state_test.go (2)
internal/funcs.go (2)
  • Environ (13-13)
  • Setenv (25-25)
state.go (1)
  • State (38-40)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (31-45)
errors.go (1)
  • RangeError (195-198)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (19-19)
parse.go (2)
errors.go (4)
  • ParseError (84-87)
  • InvalidValueError (139-142)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (19-19)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (23-23)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (19-19)
parse.go (2)
  • Parse (54-72)
  • ParseInto (94-106)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (84-87)
  • InvalidValueError (139-142)
  • ErrTargetIsNil (22-22)
vars_test.go (2)
state.go (1)
  • State (38-40)
vars.go (1)
  • Vars (28-59)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (10-32)
as/errors.go (2)
  • ErrFileDoesNotExist (19-19)
  • ErrFilenameIsDirectory (20-20)
as/filename.go (1)
as/errors.go (2)
  • ErrFileDoesNotExist (19-19)
  • ErrFilenameIsDirectory (20-20)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (21-21)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (3)
  • Setenv (25-25)
  • LookupEnv (19-19)
  • Unsetenv (28-28)
env.go (6)
  • Clear (11-13)
  • Get (28-30)
  • IsSet (35-38)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
as/portNo.go (1)
errors.go (1)
  • RangeError (195-198)
state.go (1)
internal/funcs.go (1)
  • Setenv (25-25)
🪛 GitHub Actions: release
go.mod

[error] 1-1: Script step 'a script to extract build information from ./go.mod' failed: file does not exist (./go.mod). Command: if [[ ! -f "$modfile" ]]; then echo "::error::file does not exist ($modfile)".

🔇 Additional comments (34)
as/int.go (1)

22-22: Good: preserves sentinel and cause with multi-%w.

fmt.Errorf with multiple %w is correct here and supported; errors.Is/As will work with ErrNotAnInteger and the original cause.

Please ensure tests assert both chains:

  • errors.Is(err, ErrNotAnInteger)
  • errors.Is(err, strconv.ErrSyntax) (for a representative bad value)
as/filename.go (1)

19-29: Confirm desired symlink semantics.

os.Stat follows symlinks. If you intend to reject symlinks-to-directories or treat broken symlinks distinctly, consider os.Lstat and explicit ModeSymlink checks.

vars_test.go (1)

37-45: Add a case asserting trimming of specified names.

Covers the trimming behavior and prevents regressions.

 		Case("specified variables (including ones not set)", func() {
 			// act
 			result := env.Vars("VAR1", "VAR3")
@@
 			}))
 		}),
+
+		Case("trims whitespace in specified names", func() {
+			// act
+			result := env.Vars("  VAR1\t", "\tVAR3 ")
+
+			// assert
+			Expect(result).To(EqualMap(map[string]string{
+				"VAR1": "value1",
+			}))
+		}),
vars.go (3)

10-28: Document trimming semantics in the API comment.

Clarify that keys are trimmed; aligns docs with behavior.

 // # parameters
 //
-//	names ...string   // (optional) names of environment variables to return;
+//	names ...string   // (optional) names of environment variables to return;
 //	                  // if no names are provided the returned map contains all
-//	                  // current environment variables.
+//	                  // current environment variables. Keys are trimmed of surrounding whitespace.

34-41: Use internal.Environ for consistency/testability.

Keep env sourcing behind the internal seam (mirrors LookupEnv).

 	switch len(names) {
 	case 0: // all environment variables
-		src = os.Environ()
+		src = internal.Environ()
 		fn = func(s string) (string, string, bool) {
 			k, v, _ := strings.Cut(s, "=")
 			return k, v, true
 		}

Also remove the now-unneeded os import.

-import (
-	"os"
-	"strings"
-
-	"github.com/blugnu/env/internal"
-)
+import (
+	"strings"
+
+	"github.com/blugnu/env/internal"
+)

42-55: Trim before lookup and avoid double‑trimming.

Trim the key once in the case handler; drop the loop‑level TrimSpace.

 	default: // only the named variables (if set)
 		src = names
 		fn = func(s string) (string, string, bool) {
-			v, isSet := internal.LookupEnv(s)
-			return s, v, isSet
+			k := strings.TrimSpace(s)
+			v, isSet := internal.LookupEnv(k)
+			return k, v, isSet
 		}
 	}
 
 	result := make(map[string]string, len(src))
 	for _, s := range src {
 		if k, v, isSet := fn(s); isSet {
-			k = strings.TrimSpace(k)
 			result[k] = v
 		}
 	}
#!/bin/bash
# Sanity: find call sites relying on whitespace in names (should be none)
rg -n --type=go 'Vars\([^)]*\)' -C2
go.mod (1)

5-5: Confirm dependency and run go mod tidy with Go 1.23.

Tests across the repo reference the github.com/blugnu/test DSL (e.g. vars_test.go, parse_test.go, errors_test.go, many as/*_test.go), so keeping require github.com/blugnu/test v0.12.0 is correct — run go mod tidy using the Go 1.23 toolchain to update go.sum and ensure CI reproducibility.

as/string_test.go (1)

11-23: LGTM for basic happy-path coverage.

Test is clear and matches the trivial implementation.

as/int_test.go (1)

13-43: LGTM; good coverage of bounds and sentinel.

Assertions for Min/Max int32, ErrSyntax, and ErrNotAnInteger look solid.

internal/funcs.go (2)

51-53: Mark NewFileReader as an intentional global seam.

Add nolint to silence gochecknoglobals for this test seam.

-var NewFileReader = func(path string) (io.ReadCloser, error) {
+//nolint:gochecknoglobals // test seam
+var NewFileReader = func(path string) (io.ReadCloser, error) {
 	return Open(path)
 }

31-38: Fix panic and clarify FileExists contract.

fi may be nil when os.Stat returns a non-ENOENT error, causing fi.IsDir() to panic. Decide on the contract and implement safely. Recommend: return true only on a successful stat of a non-directory.

-// FileExists returns true if a file with the given filename exists.
-// Note that this function returns false for directories and returns
-//
-// true for files, even if the file is not accessible, e.g. due to
-// permission errors.
+// FileExists reports whether the path exists and is a regular file.
+// It returns true only when os.Stat succeeds and the path is not a directory.
 var FileExists = func(filename string) bool {
-	fi, err := os.Stat(filename)
-	return !fi.IsDir() && !errors.Is(err, os.ErrNotExist)
+	fi, err := os.Stat(filename)
+	if err != nil {
+		return false
+	}
+	return !fi.IsDir()
 }
as/bool.go (1)

9-14: Fix GoDoc grammar and make it idiomatic.

Use “a Boolean” and condense to standard GoDoc without headings.

-// Bool converts a string to an Boolean.  Conversion is initially attempted
-//
-// using [strconv.ParseBool].  If that fails, the value is checked for
-// "y"/"yes" or "n"/"no" (case-insensitive).
-//
-// If the value is not supported by [strconv.ParseBool] and is not one of
-// the additional values above, an error is returned.
+// Bool converts a string to a Boolean.
+// It first tries strconv.ParseBool (case-insensitive).
+// Additionally accepts "y"/"yes" and "n"/"no" (case-insensitive).
+// Returns the parsed value or an error for unsupported input.
as/portNo_test.go (3)

23-29: Also assert RangeError bounds using constants.

Strengthen the “-1” case.

-			rangeErr := env.RangeError[int]{}
-			Expect(errors.As(err, &rangeErr)).To(BeTrue())
-			Expect(rangeErr.Min).To(Equal(0))
-			Expect(rangeErr.Max).To(Equal(65535))
+			rangeErr := env.RangeError[int]{}
+			Expect(errors.As(err, &rangeErr)).To(BeTrue())
+			Expect(rangeErr.Min).To(Equal(0))
+			Expect(rangeErr.Max).To(Equal(int(math.MaxUint16)))
 			Expect(result).To(Equal(0))

3-12: Import math to assert the upper bound symbolically.

Use math.MaxUint16 to lock the contract.

 import (
 	"errors"
+	"math"
 	"strconv"
 	"testing"

46-49: Strengthen the overflow case: assert RangeError bounds.

Mirror the “-1” checks here too.

-		Case("65536", func(result int, err error) {
-			Expect(err).Is(env.RangeError[int]{})
-			Expect(result).To(Equal(0))
-		}),
+		Case("65536", func(result int, err error) {
+			Expect(err).Is(env.RangeError[int]{})
+			var rangeErr env.RangeError[int]
+			if errors.As(err, &rangeErr) {
+				Expect(rangeErr.Min).To(Equal(0))
+				Expect(rangeErr.Max).To(Equal(int(math.MaxUint16)))
+			}
+			Expect(result).To(Equal(0))
+		}),
as/portNo.go (1)

36-42: Behavior and error wrapping LGTM; confirm tests use errors.Is/As.

Wrapping strconv and RangeError with %w is correct; ensure tests/assertions rely on errors.Is/As, not string matching.

as/url_test.go (1)

25-49: Solid coverage and assertions for absolute vs relative/invalid.

Tests reflect the wrapped error behavior and parse errors correctly.

as/filename_test.go (1)

28-69: LGTM; platform-robust invalid-path assertion.

Good move to avoid OS-specific syscall expectations.

env.go (1)

32-38: IsSet implementation via internal.LookupEnv looks good.

Consistent with internal seams used elsewhere.

state_test.go (1)

25-42: Restore test exercises the public API correctly.

Assertions validate end state; setup via t.Setenv keeps it clean.

state.go (1)

61-66: Clarify intentional ignoring of Setenv errors during restore.

If best‑effort restore is intended, add a brief comment so linters/readers know this is deliberate; otherwise, consider collecting the first error and exposing it (API change).

  for _, e := range s {
    k, v, _ := strings.Cut(e, "=")
-    _ = os.Setenv(k, v)
+    // best-effort: ignore Setenv errors during restore
+    _ = os.Setenv(k, v)
  }

Note: Respecting project preference to avoid unnecessary seams; not suggesting routing via internal.Setenv here per maintainer guidance.

as/duration_test.go (1)

56-73: Add missing edge cases: "1.5h" and whitespace handling.

Round out coverage per earlier suggestion.

@@
 		Case("ignores unit for valid duration expression", testcase{
 			input: "1h",
 			unit:  time.Second,
 			assert: func(result time.Duration, err error) {
 				Expect(err).IsNil()
 				Expect(result).To(Equal(time.Hour))
 			},
 		}),
+		Case("decimal duration expression is valid", testcase{
+			input: "1.5h",
+			unit:  time.Second,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(time.Hour + 30*time.Minute))
+			},
+		}),
@@
 		Case("with decimal unit", testcase{
 			input: "1.5",
 			unit:  time.Second,
 			assert: func(result time.Duration, err error) {
-				Expect(err).Is(as.ErrNotAnInteger)
+				Expect(err).Is(as.ErrNotAnIntegerOrDuration)
 				Expect(result).To(Equal(time.Duration(0)))
 			},
 		}),
+		Case("trims surrounding whitespace", testcase{
+			input: " 1h ",
+			unit:  time.Minute,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(time.Hour))
+			},
+		}),
+		Case("applies unit to trimmed integer", testcase{
+			input: " 1 ",
+			unit:  time.Minute,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(time.Minute))
+			},
+		}),

Also applies to: 111-119

errors.go (1)

40-74: LGTM: FileError formatting and pointer-friendly Is.

Consistent “env.FileError” prefix, inclusion of filename, and pointer/value target support are all solid.

errors_test.go (1)

104-107: LGTM: pointer-target case and FileError.Unwrap coverage.

Good additions; they validate the extended Is semantics and Unwrap symmetry.

Also applies to: 116-128

parse_test.go (1)

17-21: Deduplicate LookupEnv stubbing with a small helper.

Reduces repetition and centralizes restore handling.

Add helper (outside the selected hunks, near imports):

// withLookupEnv replaces internal.LookupEnv for the test and returns a restore func.
func withLookupEnv(val string, ok bool) func() {
	return Restore(Original(&internal.LookupEnv).ReplacedBy(
		func(string) (string, bool) { return val, ok },
	))
}

Apply pattern; e.g., replace Lines 18-20:

-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()

Repeat for the other stubs in the listed ranges.

Also applies to: 33-37, 51-54, 67-70, 83-86, 100-102, 117-119, 134-136

as/duration.go (1)

4-8: Verify/fix multi-%w fmt.Errorf usages in as/duration.go

Replace any fmt.Errorf calls that use multiple %w with a single %w wrapping errors.Join(...) (add import "errors") so root causes are preserved; capture secondary parse errors and include them in errors.Join if desired. Affected: as/duration.go (imports and error-return sites around lines 4–8, 18–20, 35–46). Repo scan failed (ripgrep returned "No files were searched"); run a search for fmt.Errorf occurrences before landing the change: rg -nP 'fmt.Errorf([^)]%w[^)]%w' -S.

parse.go (3)

59-71: Parse semantics LGTM (defaults, empty-string, error wrapping).

Behavior matches docs; invalid values wrap with InvalidValueError and missing var returns ParseError{ErrNotSet}.


95-97: Nil-target check is correct and fails fast.

Returning ParseError{VariableName, ErrTargetIsNil} is consistent with the package’s error model.


55-57: No action required — ParseError already implements Unwrap().
errors.go defines ParseError (VariableName, Err) and an Unwrap() method returning e.Err, so errors.Is/errors.As will work.

load_test.go (2)

241-248: Good transactional failure test.

Verifies Setenv failures roll back all changes.


250-257: Add test: comments with leading spaces are ignored.

Covers " # comment" path which code handles via TrimSpace.

Apply this diff to add a case:

@@
         Case("ignores commented lines", testcase{
             content: "# VAR1=value1\nVAR2=value2\n",
             assert: func(err error) {
                 Expect(err).IsNil()
                 Expect(os.Environ()).Should(HaveLen(1))
                 Expect(os.Getenv("VAR2")).To(Equal("value2"))
             },
         }),
+
+        Case("ignores commented lines with leading spaces", testcase{
+            content: "   # VAR1=value1\nVAR2=value2\n",
+            assert: func(err error) {
+                Expect(err).IsNil()
+                Expect(os.Environ()).Should(HaveLen(1))
+                Expect(os.Getenv("VAR2")).To(Equal("value2"))
+            },
+        }),
as/errors.go (2)

5-16: Multi-error type is well-formed.

combinedError correctly supports errors.Is/As via Unwrap() []error.


18-26: Error set reads clearly and is consistent (“URL” vs “URI”).

Public error wording matches identifiers; sentinel for integer-or-duration is helpful.

load.go (1)

145-151: Scanner error handling LGTM.

Appending scanner.Err() and joining preserves transactional semantics. (Previously flagged; now fixed.)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
state.go (1)

13-17: Docstring still refers to removed Reset method

Line 16 still tells consumers to defer Reset, but the public API now exposes Restore(). Please update the comment to point to the new method name so the docs don’t misdirect callers.

as/url.go (2)

10-23: Doc/behavior mismatch: AbsoluteURL only checks scheme.

Comment says “scheme, host, and path”, but the code only checks IsAbs (scheme).

-// AbsoluteURL converts a string to an absolute URI. An absolute URI is a URI
-// that includes a scheme, host, and path. The function returns an error if the
-// string is not an absolute URI.
+// AbsoluteURL converts a string to an absolute URL. For this package, an
+// absolute URL is one that includes a scheme and a host. The function returns
+// an error if the string is not an absolute URL.

31-39: Also validate host to enforce “absolute URL”.

Enforce host presence per the updated doc.

 func AbsoluteURL(s string) (*url.URL, error) {
-	u, err := internal.ParseURL(s)
+	u, err := internal.ParseURL(s)
 	if err != nil {
 		return nil, err //nolint:wrapcheck // returns the parse error from [url.Parse]
 	}
-	if !u.IsAbs() {
+	if !u.IsAbs() || u.Host == "" {
 		return nil, env.InvalidValueError{Value: s, Err: ErrNotAnAbsoluteURL}
 	}
 	return u, nil
 }
as/portNo.go (1)

3-9: Trim whitespace before parsing.

Aligns with as.Duration and makes the API tolerant of benign spaces.

 import (
 	"fmt"
 	"math"
 	"strconv"
+	"strings"
 
 	"github.com/blugnu/env"
 )
env.go (2)

56-58: Route through internal.LookupEnv for consistency and testability.

Matches IsSet/Set/Unset seams and simplifies stubbing in tests.

 func Lookup(name string) (string, bool) {
-	return os.LookupEnv(name)
+	return internal.LookupEnv(name)
 }

28-30: Optional: implement Get via internal.LookupEnv for the same seam.

Keeps all env access behind internal for consistent testing.

 func Get(name string) string {
-	return os.Getenv(name)
+	v, _ := internal.LookupEnv(name)
+	return v
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e7fda4 and fce6a27.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool.go (1 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/errors.go (1 hunks)
  • as/filename.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (4 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • internal/funcs_test.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • funcs.go
  • funcs_test.go
  • as/as_test.go
  • as/funcs.go
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • errors_test.go
  • as/duration_test.go
  • as/portNo_test.go
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In as/filename.go, differentiate between empty filename conditions and file-not-existing conditions using distinct sentinel errors for better semantic clarity.

Applied to files:

  • errors_test.go
  • errors.go
  • as/filename.go
  • load.go
  • as/filename_test.go
  • as/errors.go
  • internal/funcs.go
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • state.go
  • vars.go
  • internal/funcs.go
📚 Learning: 2025-09-24T23:04:07.744Z
Learnt from: deltics
PR: blugnu/env#10
File: as/duration_test.go:111-118
Timestamp: 2025-09-24T23:04:07.744Z
Learning: The ErrNotAnIntegerOrDuration error in the as package is implemented as a combinedError that allows testing for any constituent error (ErrNotAnInteger, ErrNotADuration, or strconv.ErrSyntax) using errors.Is(), providing flexible error handling for callers.

Applied to files:

  • as/int_test.go
  • as/duration.go
  • as/int.go
  • as/duration_test.go
  • as/errors.go
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/int_test.go
  • as/int.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T22:24:17.657Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:1-3
Timestamp: 2025-09-24T22:24:17.657Z
Learning: In the blugnu/env project, deltics prefers to rely on golangci-lint's automatic Go version detection from go.mod rather than explicit run.go configuration, as it's more robust and avoids version synchronization issues. The project size doesn't warrant explicit timeout configuration.

Applied to files:

  • .golangci.yml
  • go.mod
📚 Learning: 2025-09-24T22:33:15.369Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:21-24
Timestamp: 2025-09-24T22:33:15.369Z
Learning: In the blugnu/env project, deltics prefers to keep the intrange linter disabled, favoring explicit loop constructs over int ranges for clarity in individual circumstances.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load_test.go
  • load.go
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In the as package, prefer returning underlying filesystem errors directly rather than wrapping them with sentinel errors to avoid "sentinel for sentinel" patterns.

Applied to files:

  • as/filename.go
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • load.go
  • as/filename_test.go
  • internal/funcs_test.go
  • internal/funcs.go
📚 Learning: 2025-09-24T23:20:43.209Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/func_test.go:16-18
Timestamp: 2025-09-24T23:20:43.209Z
Learning: In tests, prefer using actual values that consumers would use (like ".env") rather than internal constants, even when those constants exist. Tests should reflect real-world usage patterns, not implementation details.

Applied to files:

  • internal/funcs_test.go
🧬 Code graph analysis (26)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (33-38)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • RangeError (213-216)
state.go (1)
internal/funcs.go (1)
  • Setenv (21-21)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (32-32)
as/internal/funcs.go (1)
parse.go (1)
  • Parse (54-72)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (30-30)
vars_test.go (2)
state.go (1)
  • State (38-40)
vars.go (1)
  • Vars (29-60)
state_test.go (1)
state.go (1)
  • State (38-40)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (15-15)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (7-7)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (31-37)
  • NewFileReader (50-52)
  • Setenv (21-21)
  • LookupEnv (15-15)
  • ScanLines (18-18)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (32-71)
  • LoadFile (83-99)
  • LoadFromReader (109-155)
internal/consts.go (1)
  • EnvFile (6-6)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (15-15)
parse.go (2)
  • Parse (54-72)
  • ParseInto (95-107)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrTargetIsNil (22-22)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
errors.go (1)
as/filename.go (1)
  • Filename (9-28)
as/filename.go (1)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (15-15)
  • Setenv (21-21)
  • Unsetenv (24-24)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
as/portNo.go (1)
errors.go (1)
  • RangeError (213-216)
parse.go (2)
errors.go (4)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (15-15)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (6-6)
internal/funcs.go (5)
  • FileExists (31-37)
  • NewFileReader (50-52)
  • ScanLines (18-18)
  • LookupEnv (15-15)
  • Setenv (21-21)
errors.go (3)
  • NewFileError (33-38)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (9-28)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (3)
  • ErrNotADuration (30-30)
  • ErrNotAnInteger (29-29)
  • ErrNotAnIntegerOrDuration (31-31)
internal/funcs_test.go (1)
internal/funcs.go (3)
  • FileExists (31-37)
  • Open (12-12)
  • NewFileReader (50-52)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (2)
  • Setenv (21-21)
  • Unsetenv (24-24)
env.go (6)
  • Clear (11-13)
  • IsSet (35-38)
  • Get (28-30)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (31-45)
errors.go (1)
  • RangeError (213-216)
🪛 GitHub Check: release / qa / run tests
as/internal/funcs.go

[failure] 6-6:
godoc should start with symbol name ("ParseURL") (godoclint)

internal/funcs.go

[failure] 23-23:
godoc should start with symbol name ("Unsetenv") (godoclint)


[failure] 20-20:
godoc should start with symbol name ("Setenv") (godoclint)


[failure] 17-17:
godoc should start with symbol name ("ScanLines") (godoclint)


[failure] 14-14:
godoc should start with symbol name ("LookupEnv") (godoclint)


[failure] 11-11:
godoc should start with symbol name ("Open") (godoclint)


[failure] 6-6:
godoc should start with symbol name ("ParseURL") (godoclint)

🪛 GitHub Actions: release
as/internal/funcs.go

[error] 6-6: godoc should start with symbol name ("ParseURL") (godoclint)

go.mod

[error] 1-1: Script error: file does not exist (./go.mod)

internal/funcs.go

[error] 6-6: godoc should start with symbol name ("ParseURL") (godoclint)

🔇 Additional comments (30)
as/bool.go (1)

9-14: Polish the Bool doc comment grammar.

The sentence uses “an Boolean” and double spacing. Tidy this to keep the package docs clean, e.g.:

-// Bool converts a string to an Boolean.  Conversion is initially attempted
+// Bool converts a string to a Boolean. Conversion is initially attempted
as/bool_test.go (1)

28-66: Broad boolean normalization coverage looks great.

Thanks for adding the mixed‑case and whitespace tokens—this aligns perfectly with the lowercasing + TrimSpace behavior in as.Bool.

as/filename.go (1)

14-25: Solid filename validation guards.

The explicit empty-string and directory checks tighten the contract while still surfacing the original os.Stat errors when appropriate.

as/url_test.go (1)

25-47: Happy path and failure modes are well covered.

Exercising absolute, relative, and malformed URLs (including the type assertion on the parse error) gives us high confidence in AbsoluteURL.

state_test.go (1)

25-41: Restore flow verification looks good.

Clearing the environment and asserting both variables post-Restore() confirms the renamed API behaves as intended.

as/errors.go (1)

7-32: Nice multi-error unwrapping utility.

notAnIntegerOrDurationError provides the expected errors.Is coverage across the new sentinels without sacrificing readability.

errors_test.go (1)

130-540: Error semantics are thoroughly exercised.

The expanded tables—especially the pointer-target cases—lock down Is/Unwrap behavior for all custom error types.

parse.go (1)

54-106: Helpful Parse/ParseInto refinements.

The updated docs read cleanly, and the nil target guard with a ParseError sentinel makes ParseInto safer to consume.

as/int.go (1)

12-23: Clearer conversion failure signal.

The added %w context ensures callers can detect ErrNotAnInteger while still seeing the underlying strconv error.

env_test.go (1)

24-26: Good move to black‑box via env.IsSet and per‑test Restore.

Clean isolation and public API usage. Looks solid.

as/filename_test.go (1)

37-43: Platform‑robust invalid/absent filename checks.

Assertions avoid OS‑specific errno brittleness while still validating behavior. LGTM.

as/duration.go (2)

14-21: LGTM: trims input before parsing Duration.

Whitespace tolerance is user‑friendly and low risk.


3-8: Fix: multiple %w in fmt.Errorf causes a runtime panic.

Only one %w is allowed; use errors.Join to wrap multiple causes.

 import (
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
 	"time"
 )
⛔ Skipped due to learnings
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.
parse_test.go (1)

10-12: DRY the repeated LookupEnv stubs with a tiny helper.

Reduces repetition and centralizes restore handling.

Apply this diff to add a helper:

 import (
   "errors"
   "strconv"
   "testing"

   . "github.com/blugnu/test"

   "github.com/blugnu/env"
   "github.com/blugnu/env/internal"
 )

+// withLookupEnv replaces internal.LookupEnv for the duration of the test, returning a restore func.
+func withLookupEnv(val string, ok bool) func() {
+	return Restore(Original(&internal.LookupEnv).ReplacedBy(
+		func(string) (string, bool) { return val, ok },
+	))
+}

Then replace repeated blocks, e.g.:

- defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-   return "123", true
- }))
+ defer withLookupEnv("123", true)()
- defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-   return "", false
- }))
+ defer withLookupEnv("", false)()
go.mod (1)

3-6: Pin toolchain and fix release workflow working directory.

  • Add toolchain to align local/CI builds.
  • The release failure “file does not exist (./go.mod)” indicates the job isn’t running at repo root or didn’t checkout the code.
 module github.com/blugnu/env
 
 go 1.23
+toolchain go1.23.0
 
 require github.com/blugnu/test v0.12.0

Action items:

  • Ensure the release job includes actions/checkout and runs from the repo root (or sets working-directory to the repo root) before running scripts.
as/string_test.go (1)

11-23: LGTM: minimal, clear test.

.golangci.yml (1)

1-51: LGTM: config matches stated project preferences.

Defaults + selective disables look intentional; intrange remains disabled per preference; copyloopvar not disabled.

as/int_test.go (1)

13-43: LGTM: good coverage including error chain assertions.

internal/funcs_test.go (2)

12-18: LGTM: consumer-oriented filename choice and positive/negative checks.


20-56: LGTM: Open patched via indirection; both success/failure paths verified.

load_test.go (2)

258-275: Add test: comment lines with leading spaces are ignored.

You already cover “# …”; add “ # …” to lock behavior. This was previously requested.

@@
-        Case("ignores commented lines", testcase{
+        Case("ignores commented lines", testcase{
           content: "# VAR1=value1\nVAR2=value2\n",
           assert: func(err error) {
             Expect(err).IsNil()
             Expect(os.Environ()).Should(HaveLen(1))
             Expect(os.Getenv("VAR2")).To(Equal("value2"))
           },
         }),
+
+        Case("ignores commented lines with leading spaces", testcase{
+          content: "  # VAR1=value1\nVAR2=value2\n",
+          assert: func(err error) {
+            Expect(err).IsNil()
+            Expect(os.Environ()).Should(HaveLen(1))
+            Expect(os.Getenv("VAR2")).To(Equal("value2"))
+          },
+        }),

112-121: Add test: pre-set empty variables are not overwritten.

We still don’t cover the “existing but empty” case. Suggest a small standalone test to avoid altering this table’s arrange step.

I can add a new Test ensuring a pre-set empty variable remains empty when the file provides a value. Want me to push the test code?

vars_test.go (1)

26-56: Good coverage and state hygiene.

Cases exercise all/selected/trimmed paths and use Restore/T().Setenv correctly.

load.go (2)

13-71: LGTM: transactional load, .env precedence, and error aggregation.

Dedup + existence filtering, per-file FileError context, and full rollback on any error are solid.


110-155: LGTM: reader-based transactional core with strict entry validation.

Scanner error capture, invalid-entry handling, non-overwrite semantics, and contextual wrapping are correct.

as/duration_test.go (1)

82-121: Add missing edge cases: decimals with unit and whitespace handling.

Per prior discussion on combinedError and tolerant parsing, add “1.5h” and whitespace cases.

 		// MARK: edge cases
@@
 		Case("with negative duration expression", testcase{
 			input: "-2h30m",
 			unit:  time.Minute,
 			assert: func(result time.Duration, err error) {
 				Expect(err).IsNil()
 				Expect(result).To(Equal(-2*time.Hour - 30*time.Minute))
 			},
 		}),
 
+		Case("with decimal duration expression", testcase{
+			input: "1.5h",
+			unit:  time.Second, // unit ignored for valid expressions
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(90 * time.Minute))
+			},
+		}),
+
 		Case("with decimal unit", testcase{
 			input: "1.5",
 			unit:  time.Second,
 			assert: func(result time.Duration, err error) {
 				Expect(err).Is(as.ErrNotADuration)
 				Expect(err).Is(as.ErrNotAnInteger)
 				Expect(err).Is(as.ErrNotAnIntegerOrDuration)
 				Expect(result).To(Equal(time.Duration(0)))
 			},
 		}),
+
+		Case("trims whitespace in input (expression)", testcase{
+			input: " 1h ",
+			unit:  time.Second,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(time.Hour))
+			},
+		}),
+
+		Case("trims whitespace in input (plain integer applies unit)", testcase{
+			input: " 1 ",
+			unit:  time.Second,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(1 * time.Second))
+			},
+		}),
as/portNo_test.go (2)

46-49: Strengthen upper-bound assertions to verify RangeError bounds.

Mirror the “-1” case: assert Min/Max via errors.As to lock the 0..65535 contract.

-		Case("65536", func(result int, err error) {
-			Expect(err).Is(env.RangeError[int]{})
-			Expect(result).To(Equal(0))
-		}),
+		Case("65536", func(result int, err error) {
+			var rangeErr env.RangeError[int]
+			Expect(errors.As(err, &rangeErr)).To(BeTrue())
+			Expect(rangeErr.Min).To(Equal(0))
+			Expect(rangeErr.Max).To(Equal(int(math.MaxUint16)))
+			Expect(result).To(Equal(0))
+		}),

23-29: Use math.MaxUint16 for clarity and future‑proofing.

Avoid hard‑coding 65535; assert against int(math.MaxUint16) to mirror implementation.

-			Expect(rangeErr.Max).To(Equal(65535))
+			Expect(rangeErr.Max).To(Equal(int(math.MaxUint16)))

Also add the missing import:

 import (
 	"errors"
+	"math"
 	"strconv"
 	"testing"
README.md (2)

54-61: Revised Parse example looks solid.

The snippet now compiles cleanly and demonstrates default handling without shadowing issues. 👍


16-16: go.mod matches README badge version – go.mod declares go 1.23, matching the “Go ≥ 1.23” badge.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
errors.go (2)

140-143: Fix ParseError.Unwrap doc string.

The comment implies it unwraps an env.Parse call rather than the ParseError itself.

Apply:

-// Unwrap returns the error that caused the env.Parse.
+// Unwrap returns the error that caused the ParseError.

218-225: Align RangeError.Error docs with actual output.

Docs say “out of range...” while the implementation prefixes with “env.RangeError...”.

Apply:

-// Error returns a string representation of the error in the form:
-//
-//	out of range: <Min> <= (x) <= <Max>
-//
-// If Min and Max are both the zero value of T:
-//
-//	out of range
+// Error returns a string representation of the error in the form:
+//
+//	env.RangeError: <Min> <= (x) <= <Max>
+//
+// If Min and Max are both the zero value of T:
+//
+//	env.RangeError
as/url.go (1)

10-13: Doc/behavior mismatch for “absolute” URL.

u.IsAbs() only requires a non-empty scheme; current docs claim scheme, host, and path. Either tighten the check (require u.Host) or fix docs. Recommending doc update to match net/url semantics.

Apply:

-// AbsoluteURL converts a string to an absolute URI. An absolute URI is a URI
-// that includes a scheme, host, and path. The function returns an error if the
-// string is not an absolute URI.
+// AbsoluteURL converts a string to an absolute URL. A URL is considered absolute
+// if it has a non-empty scheme per [url.URL.IsAbs]. The function returns an error
+// if the string is not an absolute URL.
@@
-//   - if the URL is not an absolute URI, the function returns an
-//     env.InvalidValueError
+//   - if the URL is not absolute, the function returns an env.InvalidValueError

Also applies to: 29-31

state.go (1)

13-37: Update docs/examples to use Restore (not Reset).

Top-level State docs still reference Reset and show it in the example.

Apply:

-// the test has run (by deferring a call to the Reset method on the
+// the test has run (by deferring a call to the Restore method on the
 // returned state value).
@@
-//		defer env.State().Reset()
+//		defer env.State().Restore()
env.go (1)

56-58: Route Lookup through internal.LookupEnv for consistency/testability.

Aligns with IsSet and keeps a single seam.

-func Lookup(name string) (string, bool) {
-	return os.LookupEnv(name)
-}
+func Lookup(name string) (string, bool) {
+	return internal.LookupEnv(name)
+}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fce6a27 and 7cfff64.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (34)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (4 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • internal/funcs_test.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • funcs_test.go
  • as/as_test.go
  • funcs.go
  • as/funcs.go
🧰 Additional context used
🧠 Learnings (14)
📚 Learning: 2025-09-24T23:04:07.744Z
Learnt from: deltics
PR: blugnu/env#10
File: as/duration_test.go:111-118
Timestamp: 2025-09-24T23:04:07.744Z
Learning: The ErrNotAnIntegerOrDuration error in the as package is implemented as a combinedError that allows testing for any constituent error (ErrNotAnInteger, ErrNotADuration, or strconv.ErrSyntax) using errors.Is(), providing flexible error handling for callers.

Applied to files:

  • as/duration_test.go
  • as/int.go
  • as/int_test.go
  • as/duration.go
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • as/duration_test.go
  • as/portNo_test.go
  • errors_test.go
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/int.go
  • as/int_test.go
  • as/duration.go
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • state.go
  • internal/funcs.go
  • vars.go
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load_test.go
  • load.go
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2025-09-24T22:24:17.657Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:1-3
Timestamp: 2025-09-24T22:24:17.657Z
Learning: In the blugnu/env project, deltics prefers to rely on golangci-lint's automatic Go version detection from go.mod rather than explicit run.go configuration, as it's more robust and avoids version synchronization issues. The project size doesn't warrant explicit timeout configuration.

Applied to files:

  • go.mod
  • .golangci.yml
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In as/filename.go, differentiate between empty filename conditions and file-not-existing conditions using distinct sentinel errors for better semantic clarity.

Applied to files:

  • internal/funcs.go
  • load.go
  • errors.go
  • errors_test.go
  • as/filename_test.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/funcs.go
  • load.go
  • as/filename_test.go
  • internal/funcs_test.go
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T22:33:15.369Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:21-24
Timestamp: 2025-09-24T22:33:15.369Z
Learning: In the blugnu/env project, deltics prefers to keep the intrange linter disabled, favoring explicit loop constructs over int ranges for clarity in individual circumstances.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T23:20:43.209Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/func_test.go:16-18
Timestamp: 2025-09-24T23:20:43.209Z
Learning: In tests, prefer using actual values that consumers would use (like ".env") rather than internal constants, even when those constants exist. Tests should reflect real-world usage patterns, not implementation details.

Applied to files:

  • internal/funcs_test.go
🧬 Code graph analysis (25)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (3)
  • ErrNotADuration (30-30)
  • ErrNotAnInteger (29-29)
  • ErrNotAnIntegerOrDuration (31-31)
as/portNo.go (1)
errors.go (1)
  • RangeError (213-216)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (7-7)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
state.go (1)
internal/funcs.go (1)
  • Setenv (22-22)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • Setenv (22-22)
  • LookupEnv (16-16)
  • ScanLines (19-19)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (32-71)
  • LoadFile (83-99)
  • LoadFromReader (109-155)
internal/consts.go (1)
  • EnvFile (4-4)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
state_test.go (1)
state.go (1)
  • State (38-40)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
vars_test.go (2)
state.go (1)
  • State (38-40)
vars.go (1)
  • Vars (29-60)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (4-4)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • ScanLines (19-19)
  • LookupEnv (16-16)
  • Setenv (22-22)
errors.go (3)
  • NewFileError (33-38)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
errors.go (1)
as/filename.go (1)
  • Filename (9-28)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (30-30)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (2)
  • Setenv (22-22)
  • Unsetenv (28-28)
env.go (6)
  • Clear (11-13)
  • IsSet (35-38)
  • Get (28-30)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (16-16)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (32-47)
errors.go (1)
  • RangeError (213-216)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
as/internal/funcs.go (1)
parse.go (1)
  • Parse (54-72)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (33-38)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • RangeError (213-216)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (16-16)
parse.go (2)
  • Parse (54-72)
  • ParseInto (95-107)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrTargetIsNil (22-22)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (32-32)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (9-28)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (16-16)
  • Setenv (22-22)
  • Unsetenv (28-28)
parse.go (2)
errors.go (4)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (16-16)
internal/funcs_test.go (1)
internal/funcs.go (4)
  • FileExists (38-48)
  • Stat (25-25)
  • Open (13-13)
  • NewFileReader (61-63)
🔇 Additional comments (37)
errors_test.go (1)

235-262: Pointer-target cases keep errors.Is behavior predictable.

Great to see the *env.ParseError scenarios covered—this aligns the pointer semantics with the value cases and protects future changes.

as/portNo.go (1)

37-43: Whitespace trim + wrapped range errors look solid.

The TrimSpace before Atoi plus the contextual wrapping keeps the converter robust while still surfacing the original error for callers. 👍

as/portNo_test.go (1)

23-54: Great job locking the RangeError details in tests.

Asserting the Min/Max bounds and the ErrSyntax path gives strong confidence this helper stays tight on its contract.

as/filename_test.go (1)

29-68: LGTM: solid, platform-robust checks.

Good coverage including empty, directory, non-existent, and invalid-name cases with OS-agnostic assertions.

vars.go (1)

10-59: LGTM: clear API and trimming semantics.

Implementation and docs align (trimmed keys, values preserved). Sensible use of os.Environ and internal.LookupEnv.

vars_test.go (1)

12-56: LGTM: good coverage including name trimming.

Tests exercise both modes and whitespace in names; setup/restoration looks correct.

as/duration.go (2)

10-21: LGTM: strict duration parsing with clear sentinel.

Trimming input and wrapping both the sentinel and parse error is appropriate.


23-51: LGTM: integer-units parser with duration fallback.

Behavior and error composition (combined sentinel + cause) look good; trimming handled.

internal/funcs.go (2)

31-48: Docs and behavior for FileExists are now aligned—nice.

Explicit ErrNotExist check plus fi!=nil path handles edge OS behaviors.

If godoclint still flags this section, please confirm the comment lines wrap at or before 120 chars per your linter config.


61-63: Mark NewFileReader as an intentional global seam.

Silence gochecknoglobals consistently with the var block above.

Apply:

-var NewFileReader = func(path string) (io.ReadCloser, error) {
+//nolint:gochecknoglobals // test seam
+var NewFileReader = func(path string) (io.ReadCloser, error) {
 	return Open(path)
 }
internal/funcs_test.go (2)

22-59: LGTM: FileExists scenarios well covered.

Good use of the Stat seam to model edge cases.


62-98: LGTM: NewFileReader seam tests are concise and correct.

Resource is closed; success and failure paths verified.

as/duration_test.go (1)

82-121: Add tests for “decimal with unit” and whitespace in DurationIn.

Strengthens coverage for documented behaviors.

Apply:

 		// MARK: edge cases
@@
 		Case("with decimal unit", testcase{
 			input: "1.5",
 			unit:  time.Second,
 			assert: func(result time.Duration, err error) {
 				Expect(err).Is(as.ErrNotADuration)
 				Expect(err).Is(as.ErrNotAnInteger)
 				Expect(err).Is(as.ErrNotAnIntegerOrDuration)
 				Expect(result).To(Equal(time.Duration(0)))
 			},
 		}),
+
+		Case("with decimal value and duration unit specified in input", testcase{
+			input: "1.5h",
+			unit:  time.Second, // ignored when input is a duration expression
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(90 * time.Minute))
+			},
+		}),
+
+		Case("trims whitespace in integer input", testcase{
+			input: "  1  ",
+			unit:  time.Minute,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(1 * time.Minute))
+			},
+		}),
+
+		Case("trims whitespace in duration expression input", testcase{
+			input: "  2h30m  ",
+			unit:  time.Second,
+			assert: func(result time.Duration, err error) {
+				Expect(err).IsNil()
+				Expect(result).To(Equal(2*time.Hour + 30*time.Minute))
+			},
+		}),
errors.go (6)

40-52: FileError message format is now consistent.

Prefix and filename are included; aligns with other env.* errors.


54-74: FileError.Is correctly supports value and pointer targets.

Matching logic and errors.Is delegation look solid.


124-138: ParseError.Is: pointer/value target support is correct.

Matching semantics (empty name/err as wildcards) are preserved.


187-201: InvalidValueError.Is: pointer/value target support is correct.

Works with errors.Is and maintains intended wildcard semantics.


239-256: RangeError[T].Is: generic pointer/value matching is correct.

Zero-value Min/Max used as wildcards is clear and efficient.


9-23: Error docs reference valid exported APIs Load, LoadFile, LoadFromReader, Parse, and ParseInto exist and no stale LoadFiles references remain.

env_test.go (2)

24-26: Tests now use public API (env.IsSet) for assertions.

Good black-box coverage; avoids coupling to internals.


136-149: Error-path test for Unset stubs internal.Unsetenv correctly.

Asserts errors.Is behavior and non-mutation on failure; solid.

as/string_test.go (1)

11-23: Simple, clear golden-path test.

Matches as.String contract and test harness conventions.

state.go (1)

61-66: Direct os.Setenv use is acceptable here.

Consistent with the “test seams only where needed” practice for this repo.

parse_test.go (3)

41-45: Strong error composition assertions — LGTM.

Covers ErrNotSet, ParseError wrapping, and InvalidValueError shapes consistently.

Also applies to: 109-113, 146-149, 158-159


8-12: Deduplicate LookupEnv stubbing with a small helper.

Reduces repetition and centralizes restore handling.

Add a helper after imports:

@@
 	"github.com/blugnu/env"
 	"github.com/blugnu/env/internal"
 )
 
+// withLookupEnv replaces internal.LookupEnv for the test and returns a restore func.
+func withLookupEnv(val string, ok bool) func() {
+	return Restore(Original(&internal.LookupEnv).ReplacedBy(
+		func(string) (string, bool) { return val, ok },
+	))
+}

Use it in tests (apply similarly to other occurrences shown in this file):

@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "", false
-	}))
+	defer withLookupEnv("", false)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "not-a-number", true
-	}))
+	defer withLookupEnv("not-a-number", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "", false
-	}))
+	defer withLookupEnv("", false)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "abc", true
-	}))
+	defer withLookupEnv("abc", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "not a number", true
-	}))
+	defer withLookupEnv("not a number", true)()

Also applies to: 18-20, 34-36, 52-54, 70-72, 86-88, 102-104, 121-123, 138-140


59-63: Assert the exact InvalidValueError inner error in conversion-fails case.

Increases signal by verifying Err equals converr.

  // assert
  Expect(err).Is(converr)
  Expect(err).Is(env.ParseError{VariableName: "NOT_A_NUMBER"})
- Expect(err).Is(env.InvalidValueError{Value: "not-a-number"})
+ Expect(err).Is(env.InvalidValueError{Value: "not-a-number", Err: converr})
  Expect(value).To(Equal(0))
env.go (1)

32-38: IsSet/Set/Unset refactor — LGTM.

Good move to internal seams and consistent error prefixing in Unset.

Also applies to: 73-76, 92-95

load_test.go (2)

58-77: Comprehensive coverage of Load behavior — LGTM.*

Good transactional semantics checks, precedence, non-overwrite, and scanner/set errors.

Also applies to: 79-89, 100-110, 112-121, 152-167, 169-180, 219-228, 258-265, 267-275


258-265: Add a case for comment lines with leading spaces.

Confirms TrimSpace-before-‘#’ handling.

 		Case("ignores commented lines", testcase{
 			content: "# VAR1=value1\nVAR2=value2\n",
 			assert: func(err error) {
 				Expect(err).IsNil()
 				Expect(os.Environ()).Should(HaveLen(1))
 				Expect(os.Getenv("VAR2")).To(Equal("value2"))
 			},
 		}),
+		Case("ignores commented lines with leading spaces", testcase{
+			content: "   # c1\n\t# c2\nVAR=ok\n",
+			assert: func(err error) {
+				Expect(err).IsNil()
+				Expect(os.Getenv("VAR")).To(Equal("ok"))
+			},
+		}),
README.md (1)

16-17: Approve docs changes — go.mod declares Go 1.23, matching the badge and examples.

load.go (3)

13-32: Docs align with behavior; good clarity on atomicity and non-overwrite.
Comment updates about missing files, .env precedence, non-overwrite semantics, and transactional restore are consistent with the implementation.


56-71: All-or-nothing rollback across files looks correct.
Collecting all file errors via errors.Join and restoring initial state preserves atomicity and gives callers complete error context.

Please confirm this all-or-nothing behavior is intended even when only one of several files fails; successful loads from earlier files will be rolled back.


117-151: Scanner error handling and per-file atomicity: LGTM.
Appending scanner.Err() and routing through handleError maintains the transactional guarantee for the reader.

as/int.go (1)

12-23: Error wrapping with multiple %w is correct here.
Maintains both ErrNotAnInteger and the underlying error; tests assert both.

as/internal/funcs.go (1)

5-8: ParseURL naming and doc pass lint; seam is clear.
Identifier uses proper initialism and the doc starts with the name; suitable for testing overrides.

as/int_test.go (1)

22-41: Coverage and assertions look solid.
Validates bounds, zero, and error cases; asserts both strconv.ErrSyntax and as.ErrNotAnInteger.

go.mod (1)

3-5: Pin toolchain to stabilize CI environment.
Add a toolchain directive to align local/CI builds with Go 1.23.

Apply:

 module github.com/blugnu/env

 go 1.23
+toolchain go1.23.0

 require github.com/blugnu/test v0.12.0

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
state.go (1)

13-34: Update State docs to reference Restore.

After renaming the method, the State documentation and example still instruct callers to defer Reset, which no longer exists. Please align the comment with the new Restore name.

-// the test has run (by deferring a call to the Reset method on the
+// the test has run (by deferring a call to the Restore method on the
 // returned state value).
@@
-//		defer env.State().Reset()
+//		defer env.State().Restore()
env.go (1)

55-58: Route Lookup through internal for seam consistency.

Align with IsSet/Set/Unset and improve testability by using internal.LookupEnv.

 func Lookup(name string) (string, bool) {
-	return os.LookupEnv(name)
+	return internal.LookupEnv(name)
 }
errors.go (2)

140-143: Fix doc: Unwrap refers to ParseError, not env.Parse.

Update comment to match the type.

-// Unwrap returns the error that caused the env.Parse.
+// Unwrap returns the error that caused the ParseError.
 func (e ParseError) Unwrap() error {
 	return e.Err
 }

218-225: Fix doc: align RangeError.Error() comment with returned strings.

Prepend env.RangeError in examples to match implementation.

-//	out of range: <Min> <= (x) <= <Max>
+//	env.RangeError: <Min> <= (x) <= <Max>
@@
-//	out of range
+//	env.RangeError
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cfff64 and 0854843.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/pkgdoc.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (4 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • internal/funcs_test.go (1 hunks)
  • internal/pkgdoc.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • pkgdoc.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • funcs_test.go
  • as/funcs.go
  • funcs.go
  • as/as_test.go
🧰 Additional context used
🧠 Learnings (14)
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load.go
  • load_test.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In as/filename.go, differentiate between empty filename conditions and file-not-existing conditions using distinct sentinel errors for better semantic clarity.

Applied to files:

  • load.go
  • internal/funcs.go
  • errors_test.go
  • as/filename_test.go
  • errors.go
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • load.go
  • internal/funcs_test.go
  • internal/funcs.go
  • as/filename_test.go
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • vars.go
  • internal/funcs.go
  • state.go
📚 Learning: 2025-09-24T23:20:43.209Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/func_test.go:16-18
Timestamp: 2025-09-24T23:20:43.209Z
Learning: In tests, prefer using actual values that consumers would use (like ".env") rather than internal constants, even when those constants exist. Tests should reflect real-world usage patterns, not implementation details.

Applied to files:

  • internal/funcs_test.go
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/int.go
  • as/duration.go
  • as/int_test.go
📚 Learning: 2025-09-24T23:04:07.744Z
Learnt from: deltics
PR: blugnu/env#10
File: as/duration_test.go:111-118
Timestamp: 2025-09-24T23:04:07.744Z
Learning: The ErrNotAnIntegerOrDuration error in the as package is implemented as a combinedError that allows testing for any constituent error (ErrNotAnInteger, ErrNotADuration, or strconv.ErrSyntax) using errors.Is(), providing flexible error handling for callers.

Applied to files:

  • as/int.go
  • as/duration.go
  • as/int_test.go
  • as/duration_test.go
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • as/portNo_test.go
  • errors_test.go
  • as/duration_test.go
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2025-09-24T22:24:17.657Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:1-3
Timestamp: 2025-09-24T22:24:17.657Z
Learning: In the blugnu/env project, deltics prefers to rely on golangci-lint's automatic Go version detection from go.mod rather than explicit run.go configuration, as it's more robust and avoids version synchronization issues. The project size doesn't warrant explicit timeout configuration.

Applied to files:

  • go.mod
  • .golangci.yml
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T22:33:15.369Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:21-24
Timestamp: 2025-09-24T22:33:15.369Z
Learning: In the blugnu/env project, deltics prefers to keep the intrange linter disabled, favoring explicit loop constructs over int ranges for clarity in individual circumstances.

Applied to files:

  • .golangci.yml
🧬 Code graph analysis (24)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (5-5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • ScanLines (19-19)
  • LookupEnv (16-16)
  • Setenv (22-22)
errors.go (3)
  • NewFileError (33-38)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (16-16)
internal/funcs_test.go (1)
internal/funcs.go (4)
  • FileExists (38-48)
  • Stat (25-25)
  • Open (13-13)
  • NewFileReader (61-63)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (32-47)
errors.go (1)
  • RangeError (213-216)
state.go (1)
internal/funcs.go (1)
  • Setenv (22-22)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • Setenv (22-22)
  • LookupEnv (16-16)
  • ScanLines (19-19)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (33-74)
  • LoadFile (86-102)
  • LoadFromReader (112-158)
internal/consts.go (1)
  • EnvFile (5-5)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (16-16)
  • Setenv (22-22)
  • Unsetenv (28-28)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (33-38)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • RangeError (213-216)
as/portNo.go (1)
errors.go (1)
  • RangeError (213-216)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (16-16)
parse.go (2)
  • Parse (55-73)
  • ParseInto (97-109)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrTargetIsNil (22-22)
state_test.go (2)
state.go (1)
  • State (38-40)
internal/funcs.go (1)
  • Setenv (22-22)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (30-30)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (9-28)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (10-10)
vars_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (1)
  • Setenv (22-22)
vars.go (1)
  • Vars (29-60)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
errors.go (1)
as/filename.go (1)
  • Filename (9-28)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (3)
  • ErrNotADuration (30-30)
  • ErrNotAnInteger (29-29)
  • ErrNotAnIntegerOrDuration (31-31)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (32-32)
parse.go (2)
errors.go (4)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (16-16)
env_test.go (2)
internal/funcs.go (2)
  • Setenv (22-22)
  • Unsetenv (28-28)
env.go (6)
  • Clear (11-13)
  • IsSet (35-38)
  • Get (28-30)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
🪛 GitHub Actions: release
go.mod

[error] 1-1: CI script error: file does not exist (./go.mod).

🔇 Additional comments (36)
README.md (1)

55-66: Clearer default-handling guidance

The rewritten example and narrative make it obvious how env.Parse behaves when the variable is missing vs. set, which will save readers a double-take. Nice improvement to the docs.

internal/pkgdoc.go (1)

1-11: LGTM – clear internal package doc with SPDX header.

internal/consts.go (1)

3-6: LGTM – simple exported constant with proper doc comment.

as/bool_test.go (1)

13-67: LGTM – cases align with implementation, including whitespace and mixed‑case yes/no.

go.mod (2)

3-3: Pin the Go toolchain for consistent local/CI builds.

Add a toolchain directive to avoid version drift between dev and CI.

 module github.com/blugnu/env

 go 1.23
+toolchain go1.23.0

3-5: Release workflow likely running outside repo root; fix CI to resolve “file does not exist (./go.mod)”.

Ensure the release job:

  • runs actions/checkout first,
  • uses actions/setup-go with go-version-file: go.mod,
  • executes from the repository root (set working-directory: . or remove custom cwd).
as/string_test.go (1)

11-22: Nice, focused coverage for as.String.

The test nails the happy-path contract and uses the shared harness cleanly. No further gaps spotted here.

as/internal/funcs.go (1)

1-11: Doc-comment fix keeps the seam tidy.

The alias (and its docstring) now line up with godoclint while preserving the override seam—looks good.

pkgdoc.go (1)

1-22: Package doc reads well.

Great high-level overview of env’s responsibilities with the SPDX tag in place.

.golangci.yml (1)

1-51: Lint config dialed in.

Defaulting to all, carving out only the project-specific disables, and keeping staticcheck active with a targeted exclusion is a solid balance.

vars_test.go (1)

12-66: Vars scenarios are well covered.

The harness isolation plus the new whitespace/value cases map cleanly onto the API contract.

internal/funcs_test.go (1)

22-107: Thorough exercise of the IO seams.

Thanks for sticking with consumer-style inputs (per your earlier guidance)—the Stat/Open branches and close-on-success paths are all verified.

internal/funcs.go (1)

10-63: IO seam wrappers and FileExists behaviour look right.

Explicitly handling ErrNotExist while still honouring returned FileInfos matches the documented nuance; NewFileReader stays a simple passthrough seam.

as/duration.go (1)

4-48: Duration parsing refactor looks solid.

Whitespace trimming plus the fallback/dual-wrapped error path cover the intended UX without regressing the sentinels.

as/duration_test.go (1)

123-131: Great validation of the combined sentinel behaviour.
Thanks for locking in the expectation that the decimal-path error unwraps to ErrNotADuration, ErrNotAnInteger, and ErrNotAnIntegerOrDuration (per the combined-error design we discussed earlier). This keeps callers free to probe the flavour they care about.

state_test.go (1)

32-39: Appreciate the OS-level safety net.
Capturing and restoring the env with plain os/strings calls avoids the circular dependency on the code under test—exactly the isolation we talked about before. Nicely done.

load.go (1)

41-57: Path cleaning + de-dupe hits the mark.
filepath.Clean followed by the seen filter eliminates ./.env vs .env double loads while still honouring the “.env first” rule. Clean, predictable ordering.

vars.go (1)

35-56: Trimmed keys keep the contract crisp.
Switching both pathways to emit already-trimmed keys means the returned map matches the documented semantics with no downstream surprises.

as/int.go (1)

22-23: Nice double-wrap to preserve context.
The dual %w keeps both ErrNotAnInteger and the underlying strconv error reachable via errors.Is, so callers can branch however they like without losing detail.

as/url.go (1)

32-35: Internal ParseURL seam fits nicely.
Delegating to internal.ParseURL keeps the parsing stub-able while still returning the raw url.Parse error (with the wrapcheck note), which is exactly what downstream code expects.

as/portNo_test.go (1)

23-28: Solid bounds assertion.
Verifying the RangeError via errors.As and checking the recorded min/max nails the public contract for out-of-range ports.

as/int_test.go (1)

37-40: Good dual-sentinel check.
Asserting both strconv.ErrSyntax and ErrNotAnInteger ensures the unwrap chain stays intact for callers matching on either error.

env_test.go (2)

14-26: Good migration to public API and per-test restoration.

Tests now use env.IsSet/Get/Clear and env.State().Restore for isolation. Looks solid.


100-149: Nice error-path seam via internal.Unsetenv.

The stub and errors.Is assertions correctly verify wrapping and rollback behavior.

env.go (1)

92-98: LGTM: error wrapping and Windows semantics documented.

env.Unset wraps errors with context and preserves atomic semantics per key.

load_test.go (3)

100-111: Great precedence test for .env first.

This ensures .env wins regardless of argument order. Nice.


219-239: Add comment-with-leading-space coverage.

Confirms trimmed comments are ignored.

@@
 		Case("contains empty lines", testcase{
@@
 		}),
+
+		Case("comment lines with leading spaces are ignored", testcase{
+			content: "   # c1\n\t # c2\nVAR=ok\n",
+			assert: func(err error) {
+				Expect(err).IsNil()
+				Expect(os.Getenv("VAR")).To(Equal("ok"))
+			},
+		}),

66-77: Add case: pre-set empty vars aren’t overwritten.

Verifies “do not replace existing variables (even if empty)” behavior.

@@
 		Case("no files specified/.env exists with valid entries", testcase{
@@
 		}),
+
+		Case("pre-set empty value is not overwritten", testcase{
+			fileContent: map[string]any{
+				internal.EnvFile: "X=from-file\nY=from-file",
+			},
+			assert: func(err error) {
+				Expect(err).IsNil()
+				Expect(os.Getenv("X")).To(Equal("from-proc"))
+				Expect(os.Getenv("Y")).To(Equal(""))
+			},
+		}),

Seed X/Y in the arrange section:

@@
 			defer env.State().Restore()
 			os.Clearenv()
-			t.Setenv("PRESET", "true")
+			t.Setenv("PRESET", "true")
+			_ = os.Setenv("X", "from-proc")
+			_ = os.Setenv("Y", "")
parse.go (2)

55-73: Parse: clear semantics and correct wrapping.

Correct use of internal.LookupEnv, default handling, and ParseError wrapping.


97-109: ParseInto: nil-target guard and error shape are correct.

Returns ParseError{Err: ErrTargetIsNil} and only assigns on success. Good.

errors.go (1)

40-52: FileError message and Is/Unwrap implementations look good.

Consistent env.* prefixes and robust errors.Is support for pointer/value targets.

as/portNo.go (1)

33-43: Consistent error context and trimming look solid.

Thank you for hardening the parser with TrimSpace and the uniform as.PortNo error prefix; this keeps callers’ diagnostics aligned with the rest of the package.

as/filename_test.go (1)

37-52: Great cross-platform coverage.

The split between non-existent, invalid-byte, directory, and empty-path cases gives us confidence the new API behaves correctly on every OS.

parse_test.go (1)

98-149: Thorough default/error-path assertions appreciated.

Exercising both Parse and ParseInto for success, default fallback, and wrapped error propagation will catch regressions quickly.

as/url_test.go (1)

25-48: Field-wise URL assertions keep the test stable.

Validating Scheme and Host is a nice improvement over struct equality and should survive stdlib changes.

errors_test.go (1)

333-414: Comprehensive pointer/value Is() matrix looks great.

The added pointer-target cases make the InvalidValueError behaviour explicit and symmetric with the other error types.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
state.go (1)

42-66: Update the State doc block to match the Restore rename.

The top-of-file comment still mentions calling Reset; that’s now stale after the rename and will trip readers (and doc tooling). Please switch both the narrative sentence and the example to Restore.

-// the test has run (by deferring a call to the Reset method on the
+// the test has run (by deferring a call to the Restore method on the
 // returned state value).
@@
-//		defer env.State().Reset()
+//		defer env.State().Restore()
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0854843 and 2e981db.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .golangci.yml (1 hunks)
  • README.md (4 hunks)
  • as/as_test.go (0 hunks)
  • as/bool_test.go (1 hunks)
  • as/duration.go (1 hunks)
  • as/duration_test.go (1 hunks)
  • as/filename_test.go (1 hunks)
  • as/funcs.go (0 hunks)
  • as/int.go (1 hunks)
  • as/int_test.go (1 hunks)
  • as/internal/funcs.go (1 hunks)
  • as/pkgdoc.go (1 hunks)
  • as/portNo.go (2 hunks)
  • as/portNo_test.go (1 hunks)
  • as/string_test.go (1 hunks)
  • as/url.go (2 hunks)
  • as/url_test.go (1 hunks)
  • env.go (4 hunks)
  • env_test.go (1 hunks)
  • errors.go (4 hunks)
  • errors_test.go (1 hunks)
  • funcs.go (0 hunks)
  • funcs_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/consts.go (1 hunks)
  • internal/funcs.go (1 hunks)
  • internal/funcs_test.go (1 hunks)
  • internal/pkgdoc.go (1 hunks)
  • load.go (1 hunks)
  • load_test.go (1 hunks)
  • parse.go (2 hunks)
  • parse_test.go (1 hunks)
  • pkgdoc.go (1 hunks)
  • state.go (3 hunks)
  • state_test.go (1 hunks)
  • vars.go (1 hunks)
  • vars_test.go (1 hunks)
💤 Files with no reviewable changes (4)
  • as/funcs.go
  • funcs_test.go
  • as/as_test.go
  • funcs.go
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2025-09-13T00:47:52.975Z
Learnt from: deltics
PR: blugnu/env#10
File: as/int.go:3-6
Timestamp: 2025-09-13T00:47:52.975Z
Learning: fmt.Errorf supports multiple %w verbs in a single format string. When multiple %w verbs are present, the returned error implements an Unwrap method returning a []error containing all the %w operands in the order they appear in the arguments. This allows wrapping multiple errors in a single fmt.Errorf call.

Applied to files:

  • as/int.go
  • as/duration.go
  • as/int_test.go
📚 Learning: 2025-09-24T23:04:07.744Z
Learnt from: deltics
PR: blugnu/env#10
File: as/duration_test.go:111-118
Timestamp: 2025-09-24T23:04:07.744Z
Learning: The ErrNotAnIntegerOrDuration error in the as package is implemented as a combinedError that allows testing for any constituent error (ErrNotAnInteger, ErrNotADuration, or strconv.ErrSyntax) using errors.Is(), providing flexible error handling for callers.

Applied to files:

  • as/int.go
  • as/duration.go
  • as/int_test.go
  • as/duration_test.go
📚 Learning: 2025-09-12T18:30:13.036Z
Learnt from: deltics
PR: blugnu/env#10
File: .coderabbit.yaml:4-8
Timestamp: 2025-09-12T18:30:13.036Z
Learning: CodeRabbit now supports golangci-lint v2, so separate v1 configuration files for CodeRabbit are no longer necessary. Both v1 and v2 versions are supported by CodeRabbit's tooling.

Applied to files:

  • .golangci.yml
📚 Learning: 2025-09-24T22:24:17.657Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:1-3
Timestamp: 2025-09-24T22:24:17.657Z
Learning: In the blugnu/env project, deltics prefers to rely on golangci-lint's automatic Go version detection from go.mod rather than explicit run.go configuration, as it's more robust and avoids version synchronization issues. The project size doesn't warrant explicit timeout configuration.

Applied to files:

  • .golangci.yml
  • go.mod
📚 Learning: 2025-09-24T22:33:15.369Z
Learnt from: deltics
PR: blugnu/env#10
File: .golangci.yml:21-24
Timestamp: 2025-09-24T22:33:15.369Z
Learning: In the blugnu/env project, deltics prefers to keep the intrange linter disabled, favoring explicit loop constructs over int ranges for clarity in individual circumstances.

Applied to files:

  • .golangci.yml
📚 Learning: 2024-07-04T08:06:24.732Z
Learnt from: deltics
PR: blugnu/env#2
File: load_test.go:3-10
Timestamp: 2024-07-04T08:06:24.732Z
Learning: The `io/fs` package is used in the `load_test.go` file, specifically `fs.ErrNotExist`.

Applied to files:

  • internal/funcs_test.go
  • internal/funcs.go
  • load.go
  • as/filename_test.go
📚 Learning: 2025-09-24T23:20:43.209Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/func_test.go:16-18
Timestamp: 2025-09-24T23:20:43.209Z
Learning: In tests, prefer using actual values that consumers would use (like ".env") rather than internal constants, even when those constants exist. Tests should reflect real-world usage patterns, not implementation details.

Applied to files:

  • internal/funcs_test.go
📚 Learning: 2024-07-04T08:27:07.225Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:63-91
Timestamp: 2024-07-04T08:27:07.225Z
Learning: When suggesting improvements for error handling, ensure to verify that all types of errors are adequately covered in the tests, including multiple errors being joined.

Applied to files:

  • errors_test.go
  • as/duration_test.go
  • as/portNo_test.go
📚 Learning: 2025-09-24T23:02:26.831Z
Learnt from: deltics
PR: blugnu/env#10
File: as/filename.go:21-29
Timestamp: 2025-09-24T23:02:26.831Z
Learning: In as/filename.go, differentiate between empty filename conditions and file-not-existing conditions using distinct sentinel errors for better semantic clarity.

Applied to files:

  • errors_test.go
  • internal/funcs.go
  • errors.go
  • load.go
  • as/filename_test.go
📚 Learning: 2025-09-21T03:03:26.651Z
Learnt from: deltics
PR: blugnu/env#10
File: state.go:65-66
Timestamp: 2025-09-21T03:03:26.651Z
Learning: In the blugnu/env codebase, test seams (like internal package wrappers around OS functions) should only be introduced where required for testing, not for consistency alone. The practice is to use seams only where needed rather than universally wrapping all OS calls.

Applied to files:

  • internal/funcs.go
  • state.go
  • vars.go
📚 Learning: 2025-09-21T03:06:50.819Z
Learnt from: deltics
PR: blugnu/env#10
File: state_test.go:25-42
Timestamp: 2025-09-21T03:06:50.819Z
Learning: When testing environment state management functions, don't use those same functions as safety mechanisms in tests. This creates circular dependencies where test infrastructure relies on the system under test, potentially causing cascading failures.

Applied to files:

  • state_test.go
📚 Learning: 2025-09-15T03:59:10.905Z
Learnt from: deltics
PR: blugnu/env#10
File: internal/consts.go:3-6
Timestamp: 2025-09-15T03:59:10.905Z
Learning: In Go, filepath.Join cannot be used in const declarations since it's a function call. For simple relative path prefixes like "./", string concatenation is cross-platform compatible and preferable to using vars with filepath.Join.

Applied to files:

  • internal/consts.go
📚 Learning: 2025-09-25T03:13:16.923Z
Learnt from: deltics
PR: blugnu/env#10
File: as/pkgdoc.go:1-6
Timestamp: 2025-09-25T03:13:16.923Z
Learning: In Go doc comments, when referencing packages within the same module, use the short form [pkg.Symbol] rather than fully qualified links [module.com/pkg.Symbol]. The short form is the preferred and idiomatic approach for intra-module references in Go documentation, while fully qualified paths are typically reserved for external packages or cases where ambiguity might exist.

Applied to files:

  • as/pkgdoc.go
📚 Learning: 2024-07-04T09:04:46.594Z
Learnt from: deltics
PR: blugnu/env#2
File: load.go:94-121
Timestamp: 2024-07-04T09:04:46.594Z
Learning: When suggesting the use of `bufio.Scanner` for reading lines in files, consider the potential impact on testing complexity, especially if efficiency is not critical.

Applied to files:

  • load.go
  • load_test.go
📚 Learning: 2025-09-21T02:35:04.766Z
Learnt from: deltics
PR: blugnu/env#10
File: load.go:163-171
Timestamp: 2025-09-21T02:35:04.766Z
Learning: The default bufio.Scanner uses ScanLines which automatically removes both \r and \n characters when reading lines, so manual CRLF handling is not needed when using scanner.Scan().

Applied to files:

  • load.go
🧬 Code graph analysis (24)
as/portNo.go (1)
errors.go (1)
  • RangeError (213-216)
as/int.go (1)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
as/duration.go (1)
as/errors.go (1)
  • ErrNotADuration (30-30)
as/url_test.go (2)
as/url.go (1)
  • AbsoluteURL (31-40)
as/errors.go (1)
  • ErrNotAnAbsoluteURL (32-32)
internal/funcs_test.go (1)
internal/funcs.go (4)
  • FileExists (38-48)
  • Stat (25-25)
  • Open (13-13)
  • NewFileReader (61-63)
env_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (2)
  • Setenv (22-22)
  • Unsetenv (28-28)
env.go (6)
  • Clear (11-13)
  • IsSet (35-38)
  • Get (28-30)
  • Lookup (56-58)
  • Set (74-76)
  • Unset (91-98)
errors_test.go (1)
errors.go (5)
  • FileError (28-31)
  • NewFileError (33-38)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • RangeError (213-216)
state_test.go (1)
state.go (1)
  • State (38-40)
as/int_test.go (2)
as/int.go (1)
  • Int (19-25)
as/errors.go (1)
  • ErrNotAnInteger (29-29)
vars_test.go (3)
state.go (1)
  • State (38-40)
internal/funcs.go (1)
  • Setenv (22-22)
vars.go (1)
  • Vars (29-60)
errors.go (1)
as/filename.go (1)
  • Filename (9-28)
env.go (1)
internal/funcs.go (3)
  • LookupEnv (16-16)
  • Setenv (22-22)
  • Unsetenv (28-28)
load.go (4)
state.go (1)
  • State (38-40)
internal/consts.go (1)
  • EnvFile (5-5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • ScanLines (19-19)
  • LookupEnv (16-16)
  • Setenv (22-22)
errors.go (3)
  • NewFileError (33-38)
  • ErrInvalidEntry (11-11)
  • ErrSetFailed (19-19)
as/bool_test.go (1)
as/bool.go (1)
  • Bool (15-28)
as/string_test.go (1)
as/string.go (1)
  • String (14-16)
as/duration_test.go (2)
as/duration.go (2)
  • Duration (14-21)
  • DurationIn (32-50)
as/errors.go (3)
  • ErrNotADuration (30-30)
  • ErrNotAnInteger (29-29)
  • ErrNotAnIntegerOrDuration (31-31)
as/filename_test.go (2)
as/filename.go (1)
  • Filename (9-28)
as/errors.go (2)
  • ErrFilenameIsEmpty (27-27)
  • ErrFilenameIsDirectory (28-28)
state.go (1)
internal/funcs.go (1)
  • Setenv (22-22)
load_test.go (5)
internal/funcs.go (5)
  • FileExists (38-48)
  • NewFileReader (61-63)
  • Setenv (22-22)
  • LookupEnv (16-16)
  • ScanLines (19-19)
state.go (1)
  • State (38-40)
load.go (3)
  • Load (33-74)
  • LoadFile (86-102)
  • LoadFromReader (112-158)
internal/consts.go (1)
  • EnvFile (5-5)
errors.go (2)
  • FileError (28-31)
  • ErrInvalidEntry (11-11)
as/url.go (1)
as/internal/funcs.go (1)
  • ParseURL (10-10)
parse.go (2)
errors.go (4)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrNotSet (15-15)
  • ErrTargetIsNil (22-22)
internal/funcs.go (1)
  • LookupEnv (16-16)
vars.go (1)
internal/funcs.go (1)
  • LookupEnv (16-16)
parse_test.go (3)
internal/funcs.go (1)
  • LookupEnv (16-16)
parse.go (2)
  • Parse (55-73)
  • ParseInto (97-109)
errors.go (4)
  • ErrNotSet (15-15)
  • ParseError (84-87)
  • InvalidValueError (148-151)
  • ErrTargetIsNil (22-22)
as/portNo_test.go (2)
as/portNo.go (1)
  • PortNo (32-47)
errors.go (1)
  • RangeError (213-216)
🪛 GitHub Actions: release
go.mod

[error] 1-1: go mod tidy is required; updated go.mod/go.sum must be committed (command: go mod tidy)

🔇 Additional comments (36)
.golangci.yml (1)

48-51: Nice catch keeping staticcheck fully enabled.

Adding all back in before subtracting -QF1008 restores the complete staticcheck suite while still silencing the noisy advisory. Looks good.

as/pkgdoc.go (1)

1-9: Doc comment stays consistent with Go intra-module links.
Retaining the short-form [env.Parse]/[env.ParseInto] references (per the Go doc guidance we discussed earlier) keeps pkgsite happy and matches module conventions.

as/bool_test.go (1)

13-67: Matrix now mirrors Bool converter semantics.
The whitespace variants and empty-token coverage keep these assertions aligned with as.Bool’s trim-and-lowercase behavior.

pkgdoc.go (1)

1-22: Package doc clearly frames the expanded API.
Good overview of parsing, loading, and test-support capabilities, plus the SPDX header.

as/string_test.go (1)

11-23: Straightforward happy-path coverage looks good.
Confirms the converter is an identity function and will catch accidental changes.

vars.go (1)

10-59: Implementation matches the documented trimming semantics.
Both code paths trim keys exactly once and the map preallocation is tidy.

as/int.go (1)

20-23: Error wrapping keeps the sentinel and root cause intact.
The dual %w propagates ErrNotAnInteger alongside the original strconv failure just as intended.

internal/consts.go (1)

3-6: Doc comment succinctly documents the constant.
Enough context for pkgdoc and lint tooling without extra noise.

internal/pkgdoc.go (1)

1-11: Internal package doc is concise and SPDX-complete.
Nicely signals that the internals are module-private and subject to change.

as/int_test.go (1)

22-41: Nice coverage of sentinel unwrapping.

Thanks for asserting both strconv.ErrSyntax and as.ErrNotAnInteger; this locks down the multi-%w contract while keeping the happy path boundaries in check.

as/internal/funcs.go (1)

8-10: Docstring tweak keeps godoclint satisfied.

Appreciate the rename plus comment fix—this keeps the alias lint-clean while still advertising the test seam.

as/portNo_test.go (1)

23-54: Solid boundary assertions on RangeError.

Really like the errors.As probe for Min/Max; that guarantees the wrapped RangeError[int] stays consistent if we ever rejig the formatter.

as/url.go (1)

32-34: Directly surfacing the stdlib parse error is the right call.

Keeping the original *url.Error intact avoids double-wrapping noise and plays nicely with callers already matching on url.Parse failures.

as/portNo.go (1)

33-46: Whitespace trim + shared handler reads well.

The TrimSpace plus handleError helper keeps the branches tidy while guaranteeing every exit path carries the as.PortNo: prefix. Nicely done.

as/filename_test.go (1)

29-68: Good cross-platform guards on the invalid path case.

Checking for “non-nil and not os.ErrNotExist” makes this resilient to the syscall differences we see across CI runners.

as/url_test.go (1)

25-47: Field-level assertions keep this future-proof.

Validating scheme/host instead of the whole struct dodges surprises if url.URL grows new fields. Thanks for tightening that up.

parse_test.go (2)

59-63: Good structured error assertions.

Asserting ParseError + InvalidValueError (and the underlying cause) makes failure modes explicit and stable.


17-21: Deduplicate LookupEnv stubbing with a tiny helper.

Define a withLookupEnv(val string, ok bool) helper returning a restore func, then replace repeated inline stubs. Keeps tests terse and centralizes restore logic.

@@
 import (
   "errors"
   "strconv"
   "testing"
 
   . "github.com/blugnu/test"
 
   "github.com/blugnu/env"
   "github.com/blugnu/env/internal"
 )
 
+// withLookupEnv replaces internal.LookupEnv for a test and returns a restore func.
+func withLookupEnv(val string, ok bool) func() {
+	return Restore(Original(&internal.LookupEnv).ReplacedBy(
+		func(string) (string, bool) { return val, ok },
+	))
+}
@@
-	defer Restore(Original(&internal.LookupEnv).ReplacedBy(func(string) (string, bool) {
-		return "123", true
-	}))
+	defer withLookupEnv("123", true)()

Apply similarly to the other repeated stubs in this file.

Also applies to: 33-36, 52-55, 70-73, 86-89, 102-105, 121-124, 138-141

env_test.go (1)

91-98: LGTM: public API usage and empty-string presence case covered.

Solid isolation via State().Restore and Setenv; Lookup behavior for empty values is locked in.

internal/funcs_test.go (1)

61-69: LGTM: directory, non-nil FileInfo+error, and ENOENT branches are exercised.

Good breadth on FileExists behavior; NewFileReader seams are also well tested.

errors_test.go (1)

235-263: LGTM: pointer-target Is cases included.

Comprehensive coverage for ParseError (and likewise for others elsewhere in the file) ensures consistent semantics across value and pointer targets.

vars_test.go (1)

47-65: LGTM: key trimming and value preservation verified.

This nails the intended Vars contract for both specified names and value whitespace.

env.go (1)

32-38: LGTM: test seams and error context.

  • IsSet uses internal.LookupEnv for stubbability.
  • Set/Unset route via internal seams; Unset error prefix is consistent (“env.Unset: …”).

Also applies to: 71-76, 92-95

internal/funcs.go (1)

10-29: LGTM: seams documented and FileExists behavior clarified.

  • Godoclint-friendly comments and gochecknoglobals rationale look good.
  • FileExists explicitly handles ErrNotExist and directories while tolerating non-nil FileInfo.

Also applies to: 31-48

load_test.go (4)

100-110: Great coverage of .env precedence over explicit files

This verifies ".env" is loaded first and non-overwrite semantics hold. Solid.


138-158: Nice dedup across path variants

The filepath.Clean-based dedup ensures ".env", "./.env", etc., are loaded once. Good test.


255-275: Add case: leading-space comment lines are ignored

Covers " # comment" handling.

@@
 		Case("contains empty lines", testcase{
@@
 		}),
+
+		Case("comment lines (with and without leading spaces) are ignored", testcase{
+			content: "# c1\n  # c2\nVAR=ok\n",
+			assert: func(err error) {
+				Expect(err).IsNil()
+				Expect(os.Getenv("VAR")).To(Equal("ok"))
+			},
+		}),

112-121: Add case: do not overwrite an existing empty value

We also preserve empty-string values; add a case to lock this in.

@@
 		Case("does not override existing environment variables", testcase{
@@
 		}),
+
+		Case("does not override existing empty values", testcase{
+			fileContent: map[string]any{
+				internal.EnvFile: "X=from-file\nY=from-file",
+			},
+			assert: func(err error) {
+				Expect(err).IsNil()
+				_ = os.Setenv("X", "")
+				_ = os.Setenv("Y", "")
+				Expect(os.Getenv("X")).To(Equal(""))
+				Expect(os.Getenv("Y")).To(Equal(""))
+			},
+		}),
state_test.go (1)

26-40: Good safety net: cleanup uses only os APIs

Avoids circular dependency on the SUT. Nicely done.

as/duration.go (1)

45-46: Confirm errors.Is behaviour for notAnIntegerOrDurationError

Ensure callers can do errors.Is(err, ErrNotAnInteger) and errors.Is(err, ErrNotADuration) against the returned error.

Please confirm that notAnIntegerOrDurationError implements errors.Is semantics (e.g., via multi-%w or Unwrap) so tests like errors.Is(err, ErrNotAnIntegerOrDuration), errors.Is(err, ErrNotAnInteger), and errors.Is(err, ErrNotADuration) all work as intended.

parse.go (2)

55-73: Parse error semantics and default handling look correct

  • Wraps InvalidValueError when set-but-invalid.
  • Wraps ErrNotSet when absent and no default.
  • Honors empty-string as “set” (no default). Good.

97-109: Nil target guard in ParseInto is appropriate

Returning ParseError with ErrTargetIsNil avoids panics and is consistent with docs.

load.go (3)

39-57: Path cleaning and dedup before existence checks is spot on

Correctly normalizes, dedups, and ensures .env precedence. Good defensive shape.


135-149: Validation and non-overwrite semantics handled correctly

  • Empty/whitespace/comment lines skipped.
  • Empty variable names flagged as ErrInvalidEntry.
  • Pre-existing vars preserved.
  • Set failures wrapped as ErrSetFailed.

151-157: Scanner error propagation keeps transactional guarantee

Appending scanner.Err() and joining ensures atomic rollback on reader/scanner failures. Good.

README.md (1)

16-17: No changes needed: badge alt text matches go.mod minimum version (1.23).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants