Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion pkg/linters/panic-in-library-code/panic-in-library-code.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,35 @@ func stringPrefix(pass *analysis.Pass, expr ast.Expr) (string, bool) {
if len(e.Args) == 0 {
return "", false
}
// Only inspect the format argument of fmt.Sprintf to avoid false negatives
// from arbitrary user functions that happen to receive a "BUG:" string.
if !isFmtSprintf(pass, e) {
return "", false
}
return stringPrefix(pass, e.Args[0])
default:
return "", false
}
}

// isFmtSprintf reports whether call is an invocation of the fmt.Sprintf function.
func isFmtSprintf(pass *analysis.Pass, call *ast.CallExpr) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Sprintf" {
return false
}
if obj := pass.TypesInfo.Uses[sel.Sel]; obj != nil {
return obj.Pkg() != nil && obj.Pkg().Path() == "fmt"
}
return false
}

// isInInitFunction reports whether the panic is inside a top-level init()
// function. Only top-level (no receiver) init functions are recognized;
// methods named init are ordinary methods and are not exempt.
func isInInitFunction(stack []ast.Node) bool {
decl := enclosingFuncDecl(stack)
return decl != nil && decl.Name != nil && decl.Name.Name == "init"
return decl != nil && decl.Recv == nil && decl.Name != nil && decl.Name.Name == "init"
}

func hasDocumentedPanicContract(stack []ast.Node) bool {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,79 @@
package panicinlibrarycode

import (
"errors"
"fmt"
"sync"
"errors"
"fmt"
"sync"
)

// bad: panic in a pkg/ package.
func riskyFunction() {
panic("something went wrong") // want `avoid panic in library code; return an error instead`
panic("something went wrong") // want `avoid panic in library code; return an error instead`
}

// bad: panic with a value
func anotherRiskyFunction() {
panic(errors.New("error")) // want `avoid panic in library code; return an error instead`
panic(errors.New("error")) // want `avoid panic in library code; return an error instead`
}

// bad: panic with fmt.Sprintf that does not start with BUG:
func yetAnotherRiskyFunction(n int) {
panic(fmt.Sprintf("unexpected value: %d", n)) // want `avoid panic in library code; return an error instead`
}

// ok: function that returns an error instead of panicking.
func safeFunction() error {
return nil
return nil
}

// 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
// This is a custom method, not builtin panic
}

func callCustomPanic() {
m := myType{}
m.panic("this is ok") // Should not be flagged
m := myType{}
m.panic("this is ok") // Should not be flagged
}

// ok: panic in top-level init() — init() cannot return an error.
func init() {
panic("startup registration failure") // should not be flagged
}

// ok: panic inside a sync.Once.Do callback.
var once sync.Once

func allowedSyncOncePanic() {
once.Do(func() {
panic("lazy init failure")
})
once.Do(func() {
panic("lazy init failure") // should not be flagged
})
}

// ok: panic whose message starts with "BUG:" — invariant violation.
func allowedBUGPanic() {
panic(fmt.Sprintf("BUG: unreachable: %v", errors.New("boom")))
panic(fmt.Sprintf("BUG: unreachable: %v", errors.New("boom"))) // should not be flagged
}

func init() {
panic("startup registration failure")
// ok: panic with plain "BUG:" string literal.
func invariantCheck(x int) {
if x < 0 {
panic("BUG: x must be non-negative") // should not be flagged
}
}

// documentedPreconditionPanics panics if the caller passes an invalid mode.
func documentedPreconditionPanics(mode string) {
if mode == "" {
panic("invalid mode")
}
if mode == "" {
panic("invalid mode") // should not be flagged — documented panic contract
}
}

// ok: method named init is NOT a top-level init; its panic should be flagged.
type myInitType struct{}

func (m myInitType) init() {
panic("method init panic") // want `avoid panic in library code; return an error instead`
}
2 changes: 1 addition & 1 deletion pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ func NewEngineRegistry() *EngineRegistry {
}
for _, engine := range builtins {
if err := registry.Register(engine); err != nil {
panic(fmt.Sprintf("failed to register built-in engine: %v", err))
panic(fmt.Sprintf("BUG: failed to register built-in engine: %v", err))
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/claude_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (e *ClaudeEngine) computeAllowedClaudeToolsString(tools map[string]any, saf
// Enforce that only neutral tools are provided - fail if claude section is present
if _, hasClaudeSection := tools["claude"]; hasClaudeSection {
claudeToolsLog.Print("ERROR: Claude section found in input tools, should only contain neutral tools")
panic("computeAllowedClaudeToolsString should only receive neutral tools, not claude section tools")
panic("BUG: computeAllowedClaudeToolsString should only receive neutral tools, not claude section tools")
}

// Convert neutral tools to Claude-specific tools
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/model_aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type builtinModelAliasesFile struct {
func BuiltinModelAliases() map[string][]string {
var data builtinModelAliasesFile
if err := json.Unmarshal(builtinModelAliasesJSON, &data); err != nil {
panic(fmt.Sprintf("workflow: failed to parse embedded model_aliases.json: %v (try 'make build' to rebuild with the latest data)", err))
panic(fmt.Sprintf("BUG: workflow: failed to parse embedded model_aliases.json: %v (try 'make build' to rebuild with the latest data)", err))
}
// Return a fresh copy so callers may freely modify it.
result := make(map[string][]string, len(data.Aliases))
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func GenerateHeredocDelimiterFromSeed(name string, seed string) string {
} else {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
panic("crypto/rand failed: " + err.Error())
panic("BUG: crypto/rand failed: " + err.Error())
}
tag = hex.EncodeToString(b)
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-wasm-golden.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ function normalizeAWFImageTagDigests(content) {
// Mirrors normalizeOutput() in pkg/workflow/wasm_golden_test.go.
function normalizeCopilotDefaultModel(content) {
return content.replace(
/\|\| 'claude-sonnet-4\.5'/g,
/\|\| 'claude-sonnet-\d+\.\d+'/g,
"|| 'default'"
);
}
Expand Down
Loading