Skip to content

[linter-miner] feat: add panic-in-library-code linter#34268

Merged
pelikhan merged 1 commit into
mainfrom
linter-miner/panic-in-library-code-eb1938bef622a3af
May 23, 2026
Merged

[linter-miner] feat: add panic-in-library-code linter#34268
pelikhan merged 1 commit into
mainfrom
linter-miner/panic-in-library-code-eb1938bef622a3af

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds a new static analysis linter panic-in-library-code that detects panic() calls in library code under pkg/ that should return errors instead.

Evidence

Found 6+ occurrences in the codebase during automated mining (Run #16):

  • pkg/workflow/domains.go (lines 324, 785)
  • pkg/workflow/strings.go (line 176)
  • pkg/workflow/pi_engine.go (line 139)
  • pkg/workflow/engine_definition_loader.go (line 137)
  • pkg/workflow/cache.go (line 48)

Rationale

This follows Go best practices where library code should return errors to allow callers to handle failures gracefully, rather than using panic() which terminates the program. This pattern is similar to the existing osexitinlibrary linter that prevents os.Exit() in library code.

From the Uber Go Style Guide:

Code running in production must avoid panics. Panics are a major source of cascading failures. If an error occurs, the function must return an error and allow the caller to decide how to handle it.

Implementation Details

The linter:

  • ✅ Detects builtin panic() calls in pkg/ packages
  • ✅ Excludes test files (allowed to panic in tests)
  • ✅ Excludes cmd/ entry-points (main packages can panic)
  • ✅ Distinguishes builtin panic from user-defined functions named panic
  • ✅ Reports: "avoid panic in library code; return an error instead"

Files created:

  • pkg/linters/panic-in-library-code/panic-in-library-code.go - Main analyzer
  • pkg/linters/panic-in-library-code/panic-in-library-code_test.go - Tests
  • pkg/linters/panic-in-library-code/testdata/src/panicinlibrarycode/panicinlibrarycode.go - Test fixtures
  • Updated cmd/linters/main.go - Registered new analyzer

Testing

✅ Unit tests passing:

go test ./pkg/linters/panic-in-library-code/
# PASS (0.38s)

✅ Compilation successful:

go build ./cmd/linters
# Build successful (9.5MB binary)

✅ Real-world validation: Correctly detects panic calls in actual codebase files

Mining Process

This linter was discovered through automated daily mining:

  1. Discussion mining: Analyzed 14 days of GitHub Issues/Discussions
  2. Code pattern scanning: Used Serena LSP to scan pkg/ and cmd/ for error-prone patterns
  3. Selection criteria: Chose panic-in-library-code based on:
    • ✅ Specific and actionable diagnostic
    • ✅ High signal-to-noise ratio (6+ real occurrences)
    • ✅ Not covered by standard golangci-lint rules
    • ✅ Clear violation of Go best practices

Related Linters

  • osexitinlibrary - Prevents os.Exit() in library code (similar pattern)
  • largefunc - Reference implementation for linter structure
  • ctxbackground - Example of context-aware linting

Automated by: Linter Miner workflow (Run #16 - 26339347390)

Generated by Linter Miner · ● 3.8M ·

  • expires on May 30, 2026, 5:58 PM UTC

Add new static analysis linter to detect panic() calls in library code
under pkg/ that should return errors instead.

Evidence: Found 6+ occurrences in the codebase:
- pkg/workflow/domains.go (lines 324, 785)
- pkg/workflow/strings.go (line 176)
- pkg/workflow/pi_engine.go (line 139)
- pkg/workflow/engine_definition_loader.go (line 137)
- pkg/workflow/cache.go (line 48)

This follows Go best practices where library code should return errors
to allow callers to handle failures gracefully, rather than using panic
which terminates the program. Similar to the existing osexitinlibrary
linter that prevents os.Exit() in library code.

The linter:
- Detects builtin panic() calls in pkg/ packages
- Excludes test files and cmd/ entry-points
- Distinguishes builtin panic from user-defined functions
- Reports: 'avoid panic in library code; return an error instead'

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 May 23, 2026
@pelikhan pelikhan marked this pull request as ready for review May 23, 2026 18:40
Copilot AI review requested due to automatic review settings May 23, 2026 18:40
@pelikhan pelikhan merged commit 9ba9b8a into main May 23, 2026
19 of 21 checks passed
@pelikhan pelikhan deleted the linter-miner/panic-in-library-code-eb1938bef622a3af branch May 23, 2026 18:40
@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 23, 2026

Design Decision Gate 🏗️ failed during design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 23, 2026

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 23, 2026

Test Quality Sentinel failed during test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 23, 2026

🧠 Matt Pocock Skills Reviewer failed during the skills-based review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 custom go/analysis analyzer to the cmd/linters multichecker that reports calls to the builtin panic() in non-cmd/ (library) packages, with analysistest-based fixtures.

Changes:

  • Added panicinlibrarycode analyzer that scans for builtin panic() calls and reports a diagnostic.
  • Added analysistest unit test + testdata fixture for expected diagnostics.
  • Registered the analyzer in cmd/linters/main.go.
Show a summary per file
File Description
pkg/linters/panic-in-library-code/panic-in-library-code.go Implements the analyzer and filtering logic (skip cmd/ and tests) and reports on builtin panic() calls.
pkg/linters/panic-in-library-code/panic-in-library-code_test.go Runs analysistest for the new analyzer.
pkg/linters/panic-in-library-code/testdata/src/panicinlibrarycode/panicinlibrarycode.go Provides fixtures demonstrating “bad” builtin panic() and “ok” non-builtin cases.
cmd/linters/main.go Registers the new analyzer in the multichecker binary.

Copilot's findings

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 3

Comment on lines +56 to +61
// Verify it's the builtin panic, not a user-defined function
if obj := pass.TypesInfo.Uses[ident]; obj != nil {
if _, ok := obj.(*types.Builtin); !ok {
return // Not the builtin panic
}
}
Comment on lines +20 to +24
// ok: user-defined panic function (not the builtin)
type myType struct{}

func (m myType) panic(msg string) {
// This is a custom method, not builtin panic
Comment thread cmd/linters/main.go
Comment on lines 24 to 28
"github.com/github/gh-aw/pkg/linters/largefunc"
"github.com/github/gh-aw/pkg/linters/manualmutexunlock"
"github.com/github/gh-aw/pkg/linters/osexitinlibrary"
panicinlibrarycode "github.com/github/gh-aw/pkg/linters/panic-in-library-code"
"github.com/github/gh-aw/pkg/linters/rawloginlib"
Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

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 and /grill-with-docs — overall solid implementation that mirrors osexitinlibrary, with a few gaps worth addressing before merging.

📋 Key Themes & Highlights

Key Themes

  • Doc/implementation scope mismatch: Doc says pkg/ but the linter fires on all non-cmd/ packages — aligning these avoids surprises.
  • Must* false positives: Common Go library pattern (MustCompile, MustParse) uses intentional panics and will generate noise. This is the highest-risk issue for adoption.
  • Thin test fixture: Edge cases (closures, init(), panic(nil)) are undocumented — it's unclear whether they should be flagged or silently allowed.
  • IsTestFile called per-node: Minor efficiency issue, but easy to fix by collecting test-file names once upfront.

Positive Highlights

  • ✅ Correctly distinguishes builtin panic from user-defined functions with the same name via types.Builtin check
  • ✅ Build tag on test file, analysistest harness, correct package naming
  • ✅ Clean structural match with osexitinlibrary — easy to review and maintain
  • URL field populated — good for tooling that surfaces linter docs

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 1.5M


// Analyzer is the panic-in-library-code analysis pass.
var Analyzer = &analysis.Analyzer{
Name: "panicinlibrarycode",
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 Doc says under pkg/ but the implementation flags all non-cmd/ packages — any top-level package (e.g. internal/, tools/) would also be checked.

💡 Suggestion

Either tighten the guard to match the documented scope:

if !strings.Contains(pkgPath, "/pkg/") {
    return nil, nil
}

...or update the Doc string to reflect the actual behavior (e.g. "reports panic() calls in non-entrypoint packages"). The osexitinlibrary linter has the same pattern, so aligning wording with it would keep the codebase consistent.

func run(pass *analysis.Pass) (any, error) {
pkgPath := pass.Pkg.Path()
// Skip packages under cmd/ entry-points — they are allowed to call panic.
if 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] No allowance for the idiomatic Go Must* pattern (MustCompile, MustParse, etc.). These functions intentionally panic on invalid input, are common in library packages, and are considered good style — the linter will generate false positives for them.

💡 Suggestion

Add a check to skip calls inside functions whose names start with Must:

// findEnclosingFuncName returns the name of the innermost named function
// containing pos, or "" for anonymous functions.
func findEnclosingFuncName(file *ast.File, pos token.Pos) string {
    for _, decl := range file.Decls {
        fd, ok := decl.(*ast.FuncDecl)
        if !ok || fd.Body == nil {
            continue
        }
        if fd.Body.Pos() <= pos && pos <= fd.Body.End() {
            return fd.Name.Name
        }
    }
    return ""
}

Then inside the callback:

if strings.HasPrefix(findEnclosingFuncName(file, call.Pos()), "Must") {
    return
}

Alternatively, explicitly document the expected false-positive pattern and show an //nolint:panicinlibrarycode example in the testdata.

}

insp.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
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] IsTestFile and pass.Fset.Position(call.Pos()).Filename are called on every CallExpr node, not once per file. For packages with many call expressions, this is repeated work.

💡 Suggestion

Collect the set of non-test filenames once before entering the walk, then check membership:

testFiles := make(map[string]bool)
for _, f := range pass.Files {
    name := pass.Fset.Position(f.Pos()).Filename
    if filecheck.IsTestFile(name) {
        testFiles[name] = true
    }
}

insp.Preorder(nodeFilter, func(n ast.Node) {
    call := n.(*ast.CallExpr)
    filename := pass.Fset.Position(call.Pos()).Filename
    if testFiles[filename] {
        return
    }
    // ...
})

This follows a pattern common in go/analysis passes and avoids the repeated string computation.

import "errors"

// bad: panic in a pkg/ package.
func riskyFunction() {
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 fixture is missing several edge-case scenarios that are important to validate for a panic linter.

💡 Missing test cases
// Should this be flagged? panic inside init() is common for "fail-fast" startup validation.
func init() {
    panic("startup failure") // want `avoid panic...` (or explicitly allowed?)
}

// Should be flagged: panic inside an anonymous function in pkg/ code.
func withClosure() {
    fn := func() {
        panic("inside closure") // want `avoid panic...`
    }
    fn()
}

// Should be flagged: panic(nil) — less common but valid Go.
func nilPanic() {
    panic(nil) // want `avoid panic...`
}

Declaring intent (even if the answer is "yes, flag all of these") prevents future contributors from being surprised.

@github-actions
Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test: 1 design test, 0 implementation tests, 0 guideline violations.

📊 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 Classification Details

Test File Classification Issues Detected
TestPanicInLibraryCode pkg/linters/panic-in-library-code/panic-in-library-code_test.go:13 ✅ Design None — excellent analyzer test

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)

Test Analysis

TestPanicInLibraryCode is a design test that verifies the linter's behavioral contract:

  • ✅ Uses standard analysistest framework for Go static analyzer testing
  • ✅ Comprehensive testdata covering error cases (panic with string, panic with error)
  • ✅ Edge case coverage (user-defined panic method that should NOT be flagged)
  • ✅ Proper build tag (//go:build !integration)
  • ✅ Test-to-production ratio: 0.24:1 (16 lines test / 67 lines production) — no inflation

What design invariant does this enforce? The linter correctly identifies builtin panic() calls in library code and distinguishes them from user-defined panic methods.

What would break if deleted? The linter could regress by failing to detect panic calls, incorrectly flagging custom panic methods, or missing edge cases.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.


References:

🧪 Test quality analysis by Test Quality Sentinel · ● 947.7K ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

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%).

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