Skip to content

[linter-miner] linter: add osgetenvlibrary — flag os.Getenv/LookupEnv in library packages#42115

Merged
pelikhan merged 2 commits into
mainfrom
linter-miner/osgetenvlibrary-7686a9a9cc3db8d1
Jun 28, 2026
Merged

[linter-miner] linter: add osgetenvlibrary — flag os.Getenv/LookupEnv in library packages#42115
pelikhan merged 2 commits into
mainfrom
linter-miner/osgetenvlibrary-7686a9a9cc3db8d1

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a new osgetenvlibrary Go static analysis linter that flags os.Getenv and os.LookupEnv calls in non-main, non-test library packages. This extends the existing environment-boundary enforcement story alongside ossetenvlibrary (which covers writes) to also cover reads.

Motivation

Library packages that call os.Getenv/os.LookupEnv directly couple themselves to the process environment. This hides configuration dependencies from function signatures, makes packages harder to test (tests must manipulate environment variables as side effects), and reduces caller visibility. A code scan of pkg/ identified six existing call sites in non-main library packages. This linter enforces the boundary at CI/IDE time rather than relying on convention.

Changes

New: pkg/linters/osgetenvlibrary/

  • osgetenvlibrary.go — Go analysis pass using type-aware *types.Func matching to identify os.Getenv and os.LookupEnv calls.
    • Skips packages named main, paths containing /cmd/, paths ending /main, .test packages, and test files.
    • Supports //nolint:osgetenvlibrary inline suppression for exceptional cases.
    • Uses the same internal helpers as ossetenvlibrary: astutil.Inspector, filecheck.IsTestFile, nolint.BuildLineIndex, nolint.HasDirective.
    • Diagnostic messages:
      • os.Getenv couples the library to the process environment; pass configuration explicitly instead
      • os.LookupEnv couples the library to the process environment; pass configuration explicitly instead
  • osgetenvlibrary_test.go — Unit test using analysistest.Run covering a library package (positive/negative/suppressed cases) and a main package (exemption).
  • testdata/src/osgetenvlibrary/osgetenvlibrary.go — Test fixture: flags os.Getenv, os.LookupEnv; does not flag os.Setenv, a method on a local struct named os, or a call with //nolint:osgetenvlibrary.
  • testdata/src/mainpkg/main.go — Test fixture: os.Getenv/os.LookupEnv in a main package are not flagged.

Modified: cmd/linters/main.go

  • Imported osgetenvlibrary package and registered osgetenvlibrary.Analyzer in the linter binary, ordered between osexitinlibrary and ossetenvlibrary.

New: docs/adr/42115-add-osgetenvlibrary-linter-flag-env-reads-in-libraries.md

  • Draft ADR documenting the decision, two alternatives considered (documentation-only; extending ossetenvlibrary), and consequences (positive: testability, explicit config APIs, complete boundary enforcement; negative: six existing call sites require refactoring).

Scope and Exemptions

Context Flagged
Non-main, non-test library package ✅ Yes
Package named main or path containing /cmd/ ❌ No
Test file (_test.go or .test package) ❌ No
Call with //nolint:osgetenvlibrary ❌ No

Follow-up

Six existing os.Getenv/os.LookupEnv call sites in pkg/ library packages must be refactored to pass configuration through explicit parameters, constructor arguments, or config structs.

Generated by PR Description Updater for #42115 · 55.3 AIC · ⌖ 9.51 AIC · ⊞ 4.7K ·

…ckages

Introduces the `osgetenvlibrary` linter — the natural complement to the
existing `ossetenvlibrary` linter.  While `ossetenvlibrary` flags
environment *writes* (os.Setenv/Unsetenv), this new linter flags environment
*reads* (os.Getenv, os.LookupEnv) in non-main, non-test packages.

Library packages that read environment variables directly couple themselves to
the process environment, making them harder to test in isolation and surprising
to callers that do not expect hidden configuration sources.  Configuration
should instead be passed explicitly through parameters.

Evidence from static scan of pkg/:
- pkg/logger/logger.go:31
- pkg/console/accessibility.go:16
- pkg/parser/github.go:84
- pkg/workflow/action_mode.go:40
- pkg/workflow/features.go:59
- pkg/constants/constants.go:382

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! go-linters labels Jun 28, 2026
@pelikhan pelikhan marked this pull request as ready for review June 28, 2026 18:26
Copilot AI review requested due to automatic review settings June 28, 2026 18:26
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel analysis for PR #42115 already completed in a prior session of this same workflow run (28331792274). add_comment and submit_pull_request_review were both successfully called and their limits (1 of 1 each) are exhausted. Score: 100/100 Excellent, verdict: APPROVE.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new go/analysis analyzer (osgetenvlibrary) to the gh aw linter suite to discourage library packages from directly reading process environment variables via os.Getenv / os.LookupEnv, complementing the existing ossetenvlibrary rule.

Changes:

  • Introduces pkg/linters/osgetenvlibrary analyzer to report os.Getenv/os.LookupEnv usage in non-main, non-test packages (with nolint support).
  • Adds analysistest-based tests and initial test fixtures for library vs main package behavior.
  • Registers the new analyzer in cmd/linters/main.go so it runs with the linter driver.
Show a summary per file
File Description
pkg/linters/osgetenvlibrary/osgetenvlibrary.go New analyzer implementation to detect os.Getenv / os.LookupEnv calls in library packages (type-aware matching + nolint support).
pkg/linters/osgetenvlibrary/osgetenvlibrary_test.go Adds analysistest coverage for the new analyzer.
pkg/linters/osgetenvlibrary/testdata/src/osgetenvlibrary/osgetenvlibrary.go Adds positive/negative fixture cases for library packages (including shadowing + nolint).
pkg/linters/osgetenvlibrary/testdata/src/mainpkg/main.go Adds fixture verifying main packages are exempt.
cmd/linters/main.go Registers osgetenvlibrary.Analyzer with the multichecker driver.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines +13 to +15
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), osgetenvlibrary.Analyzer, "osgetenvlibrary", "mainpkg")
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 52.2 AIC · ⌖ 9.08 AIC · ⊞ 4.9K

)

func TestAnalyzer(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), osgetenvlibrary.Analyzer, "osgetenvlibrary", "mainpkg")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ossetenvlibrary test includes "fixtures/cmd/tool" to verify the /cmd/ path exclusion, but this linter is missing an equivalent fixture. Without it, the strings.Contains(pkgPath, "/cmd/") guard in run() is an untested code path.

Consider adding a testdata/src/fixtures/cmd/tool/main.go fixture and expanding the analysistest.Run call:

analysistest.Run(t, analysistest.TestData(), osgetenvlibrary.Analyzer, "osgetenvlibrary", "mainpkg", "fixtures/cmd/tool")

@copilot please address this.

Comment on lines +61 to +65
switch fn.Name() {
case "Getenv":
pass.ReportRangef(call, "os.Getenv couples the library to the process environment; pass configuration explicitly instead")
case "LookupEnv":
pass.ReportRangef(call, "os.LookupEnv couples the library to the process environment; pass configuration explicitly instead")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both Getenv and LookupEnv cases produce structurally identical messages differing only in the function name. The switch can be collapsed into a single ReportRangef call, removing the duplication:

pass.ReportRangef(call, "os.%s couples the library to the process environment; pass configuration explicitly instead", fn.Name())

This also means that if a new function name is ever added to calledOSFunc, the report will still fire rather than falling through silently.

@copilot please address this.

Captures the architectural decision to add a static analysis linter
that flags os.Getenv/LookupEnv calls in non-main, non-test Go library
packages, complementing the existing ossetenvlibrary rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (148 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/42115-add-osgetenvlibrary-linter-flag-env-reads-in-libraries.md — review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-42115: Add osgetenvlibrary Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

Library boundary decisions like this one — choosing to prohibit os.Getenv reads in non-main packages — affect API design across the codebase. An ADR captures the rationale for that choice before it becomes invisible tribal knowledge.

Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 42115-add-osgetenvlibrary-linter-flag-env-reads-in-libraries.md for PR #42115).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 49.4 AIC · ⌖ 10.1 AIC · ⊞ 8.4K ·

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test: 1 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestAnalyzer pkg/linters/osgetenvlibrary/osgetenvlibrary_test.go:13 ✅ Design

Go: 1 (*_test.go); JavaScript: 0. No other languages detected.

Test inflation: 15 test lines vs 91 production lines (ratio ≈ 0.16). No inflation.

Edge cases covered via testdata (six scenarios exercised by analysistest.Run):

  • BadGetenvos.Getenv in library → correctly flagged
  • BadLookupEnvos.LookupEnv in library → correctly flagged
  • OkSetenvos.Setenv (different function) → correctly not flagged
  • LocalVarNamedOS — local variable named os with custom method → correctly not flagged (type disambiguation)
  • SuppressedGetenv//nolint:osgetenvlibrary directive → correctly suppressed
  • mainpkg package — os.Getenv/os.LookupEnv in main package → correctly not flagged

Note on assertions: analysistest.Run drives all assertions through // want annotations in testdata — testify-style message requirements do not apply here, as this is the idiomatic Go analysis testing pattern.

Verdict

Check passed. 0% implementation tests (threshold: 30%). The single TestAnalyzer function uses analysistest.Run against carefully crafted testdata covering both positive (flag) and negative (do-not-flag) cases across six distinct scenarios, including nolint suppression, type disambiguation, and main-package exemption.

🧪 Test quality analysis by Test Quality Sentinel · 86.7 AIC · ⌖ 17.4 AIC · ⊞ 8.2K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd, /grill-with-docs, and /improve-codebase-architecture — requesting changes on test coverage gaps before merge.

📋 Key Themes & Highlights

Key Themes

  • Test coverage parity gap: The sibling ossetenvlibrary ships alias.go, dotimport.go, and a fixtures/cmd/tool fixture; this linter is missing all three. The implementation should handle these cases correctly (type-aware *types.Func matching is import-alias-safe), but without fixtures there is no regression protection.
  • Untested exemption branch: The /cmd/ path guard at line 29 is never exercised by the current analysistest run.
  • DRY: calledOSFunc is a near-copy of the identically-named helper in ossetenvlibrary; a shared internal helper would avoid future drift.
  • Minor: The two switch cases produce identical messages except for the function name — collapsible to a single ReportRangef with fn.Name().

Positive Highlights

  • ✅ Correct use of type-aware *types.Func matching — immune to import aliases by design
  • nolint:osgetenvlibrary escape hatch tested in fixture
  • ✅ Local-variable shadow case (os := fakeOS{}) correctly handled and tested
  • ✅ Clean registration in cmd/linters/main.go, alphabetically ordered
  • ✅ Diagnostic messages are actionable and consistent with the existing linter vocabulary

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 59.2 AIC · ⌖ 9.74 AIC · ⊞ 6.6K

)

func TestAnalyzer(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), osgetenvlibrary.Analyzer, "osgetenvlibrary", "mainpkg")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] The sibling ossetenvlibrary has alias.go and dotimport.go fixtures that confirm the linter catches calls through aliased (import o "os") and dot (import . "os") imports — this linter is missing both. Since calledOSFunc uses type-aware *types.Func matching these should work, but without fixtures there is no regression net.

💡 Suggested additions

Add testdata/src/osgetenvlibrary/alias.go:

package osgetenvlibrary

import o "os"

func BadAliasGetenv() string {
	return o.Getenv("KEY") // want "os.Getenv couples the library"
}

func BadAliasLookupEnv() (string, bool) {
	return o.LookupEnv("KEY") // want "os.LookupEnv couples the library"
}

Add testdata/src/osgetenvlibrary/dotimport.go:

package osgetenvlibrary

import . "os"

func BadDotGetenv() string {
	return Getenv("KEY") // want "os.Getenv couples the library"
}

Mirrors ossetenvlibrary/testdata/src/ossetenvlibrary/alias.go and dotimport.go.

@copilot please address this.


func run(pass *analysis.Pass) (any, error) {
pkgPath := pass.Pkg.Path()
if pass.Pkg.Name() == "main" || strings.HasSuffix(pkgPath, "/main") || strings.Contains(pkgPath, "/cmd/") {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] The /cmd/ path exemption (strings.Contains(pkgPath, "/cmd/")) has no test fixture to exercise it. The sibling linter covers this with a fixtures/cmd/tool package — without a counterpart here, a future refactor could silently break this exemption.

💡 Suggested fix

Add a fixture at testdata/src/fixtures/cmd/tool/main.go:

package main

import "os"

func main() {
	_ = os.Getenv("KEY")
	_, _ = os.LookupEnv("KEY")
}

And wire it into the test:

analysistest.Run(t, analysistest.TestData(), osgetenvlibrary.Analyzer,
	"osgetenvlibrary", "mainpkg", "fixtures/cmd/tool")

@copilot please address this.

return nil, nil
}

func calledOSFunc(pass *analysis.Pass, call *ast.CallExpr) (*types.Func, bool) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/improve-codebase-architecture] calledOSFunc is nearly identical to the same-named helper in ossetenvlibrary — the only difference is the two function names it accepts. This duplication means bug-fixes or improvements (e.g., nil-safety on obj) have to be applied in both places.

💡 Refactoring idea

Consider extracting to a shared internal helper, e.g. pkg/linters/internal/osfunccheck:

// CalledOSFunc returns the *types.Func if the call targets one of the named
// os-package functions, or (nil, false) otherwise.
func CalledOSFunc(pass *analysis.Pass, call *ast.CallExpr, names ...string) (*types.Func, bool)

Both osgetenvlibrary and ossetenvlibrary could then delegate to this, and the nil-guard on obj becomes a single, tested code path.

@copilot please address this.

}
switch fn.Name() {
case "Getenv":
pass.ReportRangef(call, "os.Getenv couples the library to the process environment; pass configuration explicitly instead")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/grill-with-docs] The two switch cases produce messages that are identical except for the function name — extracting the name via fn.Name() removes the duplication and ensures message consistency if the wording ever changes.

💡 Simplified form
// Before (two near-identical strings)
case "Getenv":
    pass.ReportRangef(call, "os.Getenv couples the library...")
case "LookupEnv":
    pass.ReportRangef(call, "os.LookupEnv couples the library...")

// After
pass.ReportRangef(call,
    "os.%s couples the library to the process environment; pass configuration explicitly instead",
    fn.Name())

The switch can then be dropped entirely — calledOSFunc already guarantees the name is one of the two valid values.

@copilot please address this.


// BadGetenv calls os.Getenv and should be flagged.
func BadGetenv() string {
return os.Getenv("CONFIG_KEY") // want "os.Getenv couples the library to the process environment"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[/tdd] os.ExpandEnv and os.Environ also read from the process environment but are not flagged. The fixture currently only confirms the two explicit API entries. If the scope is intentionally limited to Getenv/LookupEnv, it would be useful to add a // not flagged comment in the fixture documenting that decision explicitly so future contributors know it was intentional.

💡 Suggested documentation comment
// os.ExpandEnv and os.Environ are intentionally out of scope for this rule;
// only direct Getenv/LookupEnv calls are targeted.
func OkExpandEnv() string {
	return os.ExpandEnv("${KEY}") // not flagged by this rule
}

@copilot please address this.

@pelikhan pelikhan merged commit 0431a53 into main Jun 28, 2026
29 checks passed
@pelikhan pelikhan deleted the linter-miner/osgetenvlibrary-7686a9a9cc3db8d1 branch June 28, 2026 19:27
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.82.0

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

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants