Skip to content
Draft
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
10 changes: 10 additions & 0 deletions passes/unclosetx/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,13 @@ func f6(ctx context.Context, client *spanner.Client) error {
}
return nil
}

func usetx(tx *spanner.ReadOnlyTransaction) {
_ = tx // use tx
}

// see https://github.com/gcpug/zagane/issues/49
func f7(ctx context.Context, client *spanner.Client) {
tx := client.ReadOnlyTransaction() // want "transaction must be closed"
usetx(tx)
}
78 changes: 77 additions & 1 deletion passes/unclosetx/unclosetx.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
// skip this
continue
}
instrs := analysisutil.NotCalledIn(f, txTyp, methods...)
instrs := detectUnclosedTx(f, txTyp, methods)
for _, instr := range instrs {
pos := instr.Pos()
if pos == token.NoPos {
Expand All @@ -77,6 +77,82 @@ func run(pass *analysis.Pass) (interface{}, error) {
return nil, nil
}

func detectUnclosedTx(f *ssa.Function, txTyp types.Type, methods []*types.Func) []ssa.Instruction {
instrs := analysisutil.NotCalledIn(f, txTyp, methods...)
argInstrs := findArgPassedUnclosedTx(f, txTyp, methods)

seen := map[ssa.Instruction]bool{}
for _, instr := range instrs {
seen[instr] = true
}
for _, instr := range argInstrs {
if !seen[instr] {
instrs = append(instrs, instr)
seen[instr] = true
}
}
return instrs
}

// findArgPassedUnclosedTx detects cases where a *ReadOnlyTransaction value is passed as an argument to another function but Close() is never called.
// analysisutil.NotCalledIn skips these cases due to its internal isArg check, so this function complements it.
func findArgPassedUnclosedTx(f *ssa.Function, txTyp types.Type, methods []*types.Func) []ssa.Instruction {
var result []ssa.Instruction
for _, b := range f.Blocks {
for _, instr := range b.Instrs {
v, ok := instr.(ssa.Value)
if !ok || v == nil {
continue
}
if !types.Identical(v.Type(), txTyp) {
continue
}
refs := v.Referrers()
if refs == nil {
continue
}
hasClose := false
isPassedAsArg := false
for _, ref := range *refs {
for _, m := range methods {
if analysisutil.Called(ref, v, m) {
hasClose = true
}
}
if isArgOf(ref, v) {
isPassedAsArg = true
}
}
if isPassedAsArg && !hasClose {
result = append(result, instr)
}
}
}
return result
}

// isArgOf reports whether instr is a call instruction and v is passed as one of its arguments.
func isArgOf(instr ssa.Instruction, v ssa.Value) bool {
call, ok := instr.(ssa.CallInstruction)
if !ok {
return false
}
common := call.Common()
if common == nil {
return false
}
args := common.Args
if common.Signature().Recv() != nil && len(args) > 0 {
args = args[1:]
}
for _, arg := range args {
if arg == v {
return true
}
}
return false
}

func isSingle(instr ssa.Instruction, single *types.Func) bool {
call, ok := instr.(ssa.CallInstruction)
if !ok {
Expand Down