Skip to content

[linter-miner] feat(linters): add logfatallibrary linter to flag log.Fatal calls in library packages#45115

Merged
pelikhan merged 2 commits into
mainfrom
linter-miner/logfatallibrary-34272152660ce94b
Jul 13, 2026
Merged

[linter-miner] feat(linters): add logfatallibrary linter to flag log.Fatal calls in library packages#45115
pelikhan merged 2 commits into
mainfrom
linter-miner/logfatallibrary-34272152660ce94b

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new logfatallibrary Go static analysis linter that flags log.Fatal, log.Fatalf, and log.Fatalln calls inside library packages (pkg/). These functions call os.Exit(1) internally, bypassing deferred cleanup and making packages untestable in isolation. This extends the existing process-termination linter family (osexitinlibrary, panicinlibrarycode).

Changes

File Change
pkg/linters/logfatallibrary/logfatallibrary.go New analysis pass detecting log.Fatal* in non-cmd/ packages
pkg/linters/logfatallibrary/logfatallibrary_test.go analysistest-based unit test
pkg/linters/logfatallibrary/testdata/src/logfatallibrary/logfatallibrary.go Testdata fixtures (bad: Fatal/Fatalf/Fatalln; ok: log.Print, nolint suppressions)
cmd/linters/main.go Registered logfatallibrary.Analyzer in the linter binary
docs/adr/45115-logfatallibrary-linter-for-pkg-packages.md Draft ADR-45115 documenting the decision

Behaviour

  • Flagged: log.Fatal, log.Fatalf, log.Fatalln in packages whose import path does not contain /cmd/ and does not end with /main.
  • Exempted: packages under cmd/ or with suffix /main (entry-point executables).
  • Skipped: test files (_test.go) and test packages (path suffix .test).
  • Suppression: //nolint:logfatallibrary directive on the same or preceding line.

Diagnostic

log.Fatal called in library package ...; use error returns instead to avoid implicit os.Exit

Alternatives considered

  • Extend osexitinlibrary: rejected — keeps conceptually distinct constructs separate, limits blast radius.
  • Documentation + code review only: rejected — inconsistent enforcement; automated analysis provides unconditional coverage.

References

  • ADR: docs/adr/45115-logfatallibrary-linter-for-pkg-packages.md
  • Related linters: osexitinlibrary, panicinlibrarycode

Generated by PR Description Updater for #45115 · 37 AIC · ⌖ 4.51 AIC · ⊞ 4.7K ·

Detects log.Fatal, log.Fatalf, and log.Fatalln calls in library (pkg/)
packages. These functions call os.Exit(1) internally, which:
- Bypasses deferred cleanup (e.g., open files, db connections)
- Makes the calling package untestable in isolation
- Prevents callers from handling errors gracefully

The linter mirrors osexitinlibrary but targets the log package's fatal
functions. cmd/ entry-points are excluded, as are test files and calls
suppressed with //nolint:logfatallibrary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Great addition! 🎉 The logfatallibrary linter is a natural complement to the existing osexitinlibrary and panicinlibrarycode linters — detecting log.Fatal* calls in pkg/ packages closes a real gap since those functions silently call os.Exit(1) and cause the same testability and cleanup problems.

The PR is well-structured: the analyzer, unit tests with analysistest, and fixture data are all present, the description clearly explains the "why", and registration in cmd/linters/main.go is included. This looks ready for review.

Generated by ✅ Contribution Check · 89.5 AIC · ⌖ 11.5 AIC · ⊞ 6.2K ·

@pelikhan pelikhan marked this pull request as ready for review July 13, 2026 06:44
Copilot AI review requested due to automatic review settings July 13, 2026 06:44
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 13, 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 Go analyzer intended to prevent process termination from library code.

Changes:

  • Detects standard-library log.Fatal* calls.
  • Supports nolint suppression and tests core cases.
  • Registers the analyzer with the multichecker.
Show a summary per file
File Description
cmd/linters/main.go Registers the analyzer.
pkg/linters/logfatallibrary/logfatallibrary.go Implements detection and reporting.
pkg/linters/logfatallibrary/logfatallibrary_test.go Runs analyzer tests.
pkg/linters/logfatallibrary/testdata/src/logfatallibrary/logfatallibrary.go Provides test fixtures.

Review details

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment thread cmd/linters/main.go
httprespbodyclose.Analyzer,
httpstatuscode.Analyzer,
largefunc.Analyzer,
logfatallibrary.Analyzer,
Comment on lines +26 to +30
// Analyzer is the log-fatal-in-library analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "logfatallibrary",
Doc: "reports log.Fatal, log.Fatalf, and log.Fatalln calls inside library packages where they implicitly call os.Exit and bypass deferred cleanup",
URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/logfatallibrary",
Comment on lines +57 to +58
if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
return

@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.

The implementation is correct and consistent with the existing osexitinlibrary linter pattern. No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 16.1 AIC · ⌖ 5.13 AIC · ⊞ 4.8K

@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 violations.

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Coverage
TestLogFatalLibrary logfatallibrary_test.go:13 Behavioral contract via black-box testdata ✅ All 5 scenarios (3 error, 2 suppression/non-fatal)

✅ Verdict

Passed. 0% implementation tests (threshold: 30%).

The test uses Go's standard analysistest framework with comprehensive testdata fixtures:

  • ✅ Covers all 3 fatal variants (Fatal, Fatalf, Fatalln)
  • ✅ Verifies nolint suppression (both styles)
  • ✅ Non-fatal log calls correctly pass
  • ✅ Build tag compliant (//go:build !integration)
  • ✅ Black-box design testing via expectations
  • ✅ Minimal test inflation (0.21:1 ratio)

Excellent test quality: behavioral design contract well-supported by edge-case and suppression coverage.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 18.4 AIC · ⌖ 10.3 AIC · ⊞ 6.9K ·
Comment /review to run again

@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. 0% implementation tests (threshold: 30%). Behavioral design contract well-supported with comprehensive edge-case and suppression coverage.

@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 Matt Pocock's skills by Matt Pocock Skills Reviewer · 28.9 AIC · ⌖ 4.42 AIC · ⊞ 6.6K
Comment /matt to run again

// ok: nolint suppression on same line.
func suppressedSameLine() {
log.Fatal("suppressed") //nolint:logfatallibrary
}

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] Missing alias-import test case — osexitinlibrary has a dedicated osexitinlibrary_alias.go fixture that proves aliased imports (e.g. xlog "log") are still flagged and local shadowed vars are not. Without it, the alias-handling path of IsPkgSelector is untested for this linter.

💡 Suggested addition: `testdata/src/logfatallibrary/logfatallibrary_alias.go`
package logfatallibrary

import xlog "log"

// flagged: aliased log import still resolves to log package
func stopWithAliasedFatal() {
	xlog.Fatal("aliased") // want `log.Fatal called in library package`
}

// not flagged: local type named "log"
type fakeLog struct{}

func (fakeLog) Fatal(args ...any) {}

func notFlaggedShadowedLog() {
	var log fakeLog
	log.Fatal("not the stdlib log")
}

This mirrors pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_alias.go exactly.

@copilot please address this.

@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.

🔎 Code quality review by PR Code Quality Reviewer · 50.2 AIC · ⌖ 4.56 AIC · ⊞ 5.4K
Comment /review to run again

if !ok {
return
}
if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {

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.

Redundant per-node evaluation of a constant check: strings.HasSuffix(pkgPath, ".test") is re-evaluated on every AST node visit, but pkgPath is constant for the entire analysis pass.

💡 Suggested fix

Hoist the package-level skip guards to the top of run(), alongside the existing cmd/ guard:

func run(pass *analysis.Pass) (any, error) {
    pkgPath := pass.Pkg.Path()
    if strings.HasSuffix(pkgPath, "/main") ||
        strings.Contains(pkgPath, "/cmd/") ||
        strings.HasSuffix(pkgPath, ".test") {
        return nil, nil
    }
    // ...
    insp.Preorder(nodeFilter, func(n ast.Node) {
        call, ok := n.(*ast.CallExpr)
        if !ok {
            return
        }
        if filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
            return
        }
        // ...
    })
}

filecheck.IsTestFile still needs to stay inside the closure because it is node-dependent (per filename), but the package-path suffix check is package-scoped and executing it per-node is wasteful. The same pattern would be a fix for the sibling osexitinlibrary linter too.

if !astutil.IsPkgSelector(pass, sel, "log") {
return
}
position := pass.Fset.PositionFor(call.Pos(), false)

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.

Inconsistent position resolution between test-file guard and nolint check: line 57 uses pass.Fset.Position() (adjusted=true) while line 70 uses pass.Fset.PositionFor(call.Pos(), false) (adjusted=false). In files with //line directives (e.g. generated code), these resolve to different physical lines — so the two guards could disagree about which line is the 'real' one.

💡 Suggested fix

Use PositionFor(..., false) consistently in both places so physical-line resolution is always used:

// line 57 — change Position to PositionFor
if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.PositionFor(call.Pos(), false).Filename) {
    return
}

The sibling osgetenvlibrary linter already uses PositionFor(..., false) in both its node-level guard and its nolint check, so this aligns the new linter with the established pattern.

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 (128 new lines in pkg/ directories) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/45115-logfatallibrary-linter-for-pkg-packages.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-45115: Add logfatallibrary Linter to Flag log.Fatal Calls in Library Packages

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

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

📋 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., 45115-logfatallibrary-linter-for-pkg-packages.md for PR #45115).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 50.8 AIC · ⌖ 10.1 AIC · ⊞ 8.5K ·
Comment /review to run again

@pelikhan pelikhan merged commit ac1cc6a into main Jul 13, 2026
36 checks passed
@pelikhan pelikhan deleted the linter-miner/logfatallibrary-34272152660ce94b branch July 13, 2026 14:32
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.82.9

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