From 3fb25b9de7f5673fe2af04606c7b35bb53f1f2bf Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Thu, 18 May 2023 05:55:01 +0200 Subject: Rename package to "capturedrefrace" I decided I don't like the "go" prefix for a Go package name. It doesn't make sense. Rename the module and package accordingly. I do like the "go" prefix in the command line program, so leave it there. --- capturedrefrace.go | 226 ++++++++++++++++++++++++++++++++++++++++++ capturedrefrace_test.go | 33 ++++++ cmd/gocapturedrefrace/main.go | 4 +- go.mod | 2 +- gocapturedrefrace.go | 226 ------------------------------------------ gocapturedrefrace_test.go | 33 ------ 6 files changed, 262 insertions(+), 262 deletions(-) create mode 100644 capturedrefrace.go create mode 100644 capturedrefrace_test.go delete mode 100644 gocapturedrefrace.go delete mode 100644 gocapturedrefrace_test.go diff --git a/capturedrefrace.go b/capturedrefrace.go new file mode 100644 index 0000000..21a8b2d --- /dev/null +++ b/capturedrefrace.go @@ -0,0 +1,226 @@ +// Copyright (c) 2023 Teddy Wing +// +// This file is part of Gocapturedrefrace. +// +// Gocapturedrefrace is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// Gocapturedrefrace is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Gocapturedrefrace. If not, see +// . + +// TODO: package documentation. +package capturedrefrace + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +var version = "0.0.1" + +var Analyzer = &analysis.Analyzer{ + Name: "capturedrefrace", + Doc: "reports captured references in goroutine closures", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.GoStmt)(nil), + } + + inspect.Preorder( + nodeFilter, + func(node ast.Node) { + // Find `go` statements. + goStmt, ok := node.(*ast.GoStmt) + if !ok { + return + } + + // Look for a function literal after the `go` statement. + funcLit, ok := goStmt.Call.Fun.(*ast.FuncLit) + if !ok { + return + } + + // Inspect closure argument list. + for _, arg := range funcLit.Type.Params.List { + // Report reference arguments. + _, ok := arg.Type.(*ast.StarExpr) + if !ok { + continue + } + + pass.Reportf( + arg.Pos(), + "reference %s in goroutine closure", + arg.Names[0], + ) + } + + checkClosure(pass, funcLit) + }, + ) + + return nil, nil +} + +// checkClosure reports variables used in funcLit that are captured from an +// outer scope. +func checkClosure(pass *analysis.Pass, funcLit *ast.FuncLit) { + // Get the closure's scope. + funcScope := pass.TypesInfo.Scopes[funcLit.Type] + + // Build a list of assignments local to funcLit. These will be ignored as + // shadowed variables. + localVarDeclarations := findLocalVarDeclarations(funcLit) + + ast.Inspect( + funcLit, + func(node ast.Node) bool { + ident, ok := node.(*ast.Ident) + if !ok { + return true + } + + if ident.Obj == nil { + return true + } + + // Ignore shadowed variables. + for _, declarationIdent := range localVarDeclarations { + if ident == declarationIdent { + return true + } + } + + // Find out whether `ident` was defined in an outer scope. + scope, scopeObj := funcScope.LookupParent(ident.Name, ident.NamePos) + + // Identifier is local to the closure. + if scope == nil && scopeObj == nil { + return true + } + + // Ignore non-variable identifiers. + variable, ok := scopeObj.(*types.Var) + if !ok { + return true + } + + // Ignore captured callable variables, like function arguments. + _, isVariableTypeSignature := variable.Type().(*types.Signature) + if isVariableTypeSignature { + return true + } + + // Identifier was defined in a different scope. + if funcScope != scope { + pass.Reportf( + ident.Pos(), + "captured reference %s in goroutine closure", + ident, + ) + } + + return true + }, + ) +} + +// findLocalVarDeclarations returns a list of all variable declarations in +// funcLit. +func findLocalVarDeclarations( + funcLit *ast.FuncLit, +) (declarations []*ast.Ident) { + declarations = []*ast.Ident{} + + ast.Inspect( + funcLit, + func(node ast.Node) bool { + switch node := node.(type) { + case *ast.AssignStmt: + assignments := assignmentDefinitions(node) + declarations = append(declarations, assignments...) + + case *ast.GenDecl: + decls := varDeclarations(node) + if decls != nil { + declarations = append(declarations, decls...) + } + } + + return true + }, + ) + + return declarations +} + +// assignmentDefinitions returns the identifiers corresponding to variable +// assignments in assignStmt, or nil if assignStmt does not declare any +// variables. +func assignmentDefinitions( + assignStmt *ast.AssignStmt, +) (assignments []*ast.Ident) { + assignments = []*ast.Ident{} + + if assignStmt.Tok != token.DEFINE { + return nil + } + + for _, lhs := range assignStmt.Lhs { + ident, ok := lhs.(*ast.Ident) + if !ok { + continue + } + + if ident == nil { + continue + } + + assignments = append(assignments, ident) + } + + return assignments +} + +// varDeclarations returns the identifiers corresponding to variable +// declarations in decl, or nil if decl is not a variable declaration. +func varDeclarations(decl *ast.GenDecl) (declarations []*ast.Ident) { + if decl.Tok != token.VAR { + return nil + } + + declarations = []*ast.Ident{} + + for _, spec := range decl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + return declarations + } + + for _, ident := range valueSpec.Names { + declarations = append(declarations, ident) + } + } + + return declarations +} diff --git a/capturedrefrace_test.go b/capturedrefrace_test.go new file mode 100644 index 0000000..f4bb43d --- /dev/null +++ b/capturedrefrace_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Teddy Wing +// +// This file is part of Gocapturedrefrace. +// +// Gocapturedrefrace is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// Gocapturedrefrace is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Gocapturedrefrace. If not, see +// . + + +package capturedrefrace_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "gopkg.teddywing.com/capturedrefrace" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + + analysistest.Run(t, testdata, capturedrefrace.Analyzer, ".") +} diff --git a/cmd/gocapturedrefrace/main.go b/cmd/gocapturedrefrace/main.go index bab8032..60c7838 100644 --- a/cmd/gocapturedrefrace/main.go +++ b/cmd/gocapturedrefrace/main.go @@ -21,9 +21,9 @@ package main import ( "golang.org/x/tools/go/analysis/singlechecker" - "gopkg.teddywing.com/gocapturedrefrace" + "gopkg.teddywing.com/capturedrefrace" ) func main() { - singlechecker.Main(gocapturedrefrace.Analyzer) + singlechecker.Main(capturedrefrace.Analyzer) } diff --git a/go.mod b/go.mod index 2a93e77..5d08fa1 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gopkg.teddywing.com/gocapturedrefrace +module gopkg.teddywing.com/capturedrefrace go 1.20 diff --git a/gocapturedrefrace.go b/gocapturedrefrace.go deleted file mode 100644 index a3c1699..0000000 --- a/gocapturedrefrace.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) 2023 Teddy Wing -// -// This file is part of Gocapturedrefrace. -// -// Gocapturedrefrace is free software: you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// Gocapturedrefrace is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Gocapturedrefrace. If not, see -// . - -// TODO: package documentation. -package gocapturedrefrace - -import ( - "go/ast" - "go/token" - "go/types" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" -) - -var version = "0.0.1" - -var Analyzer = &analysis.Analyzer{ - Name: "gocapturedrefrace", - Doc: "reports captured references in goroutine closures", - Run: run, - Requires: []*analysis.Analyzer{inspect.Analyzer}, -} - -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - - nodeFilter := []ast.Node{ - (*ast.GoStmt)(nil), - } - - inspect.Preorder( - nodeFilter, - func(node ast.Node) { - // Find `go` statements. - goStmt, ok := node.(*ast.GoStmt) - if !ok { - return - } - - // Look for a function literal after the `go` statement. - funcLit, ok := goStmt.Call.Fun.(*ast.FuncLit) - if !ok { - return - } - - // Inspect closure argument list. - for _, arg := range funcLit.Type.Params.List { - // Report reference arguments. - _, ok := arg.Type.(*ast.StarExpr) - if !ok { - continue - } - - pass.Reportf( - arg.Pos(), - "reference %s in goroutine closure", - arg.Names[0], - ) - } - - checkClosure(pass, funcLit) - }, - ) - - return nil, nil -} - -// checkClosure reports variables used in funcLit that are captured from an -// outer scope. -func checkClosure(pass *analysis.Pass, funcLit *ast.FuncLit) { - // Get the closure's scope. - funcScope := pass.TypesInfo.Scopes[funcLit.Type] - - // Build a list of assignments local to funcLit. These will be ignored as - // shadowed variables. - localVarDeclarations := findLocalVarDeclarations(funcLit) - - ast.Inspect( - funcLit, - func(node ast.Node) bool { - ident, ok := node.(*ast.Ident) - if !ok { - return true - } - - if ident.Obj == nil { - return true - } - - // Ignore shadowed variables. - for _, declarationIdent := range localVarDeclarations { - if ident == declarationIdent { - return true - } - } - - // Find out whether `ident` was defined in an outer scope. - scope, scopeObj := funcScope.LookupParent(ident.Name, ident.NamePos) - - // Identifier is local to the closure. - if scope == nil && scopeObj == nil { - return true - } - - // Ignore non-variable identifiers. - variable, ok := scopeObj.(*types.Var) - if !ok { - return true - } - - // Ignore captured callable variables, like function arguments. - _, isVariableTypeSignature := variable.Type().(*types.Signature) - if isVariableTypeSignature { - return true - } - - // Identifier was defined in a different scope. - if funcScope != scope { - pass.Reportf( - ident.Pos(), - "captured reference %s in goroutine closure", - ident, - ) - } - - return true - }, - ) -} - -// findLocalVarDeclarations returns a list of all variable declarations in -// funcLit. -func findLocalVarDeclarations( - funcLit *ast.FuncLit, -) (declarations []*ast.Ident) { - declarations = []*ast.Ident{} - - ast.Inspect( - funcLit, - func(node ast.Node) bool { - switch node := node.(type) { - case *ast.AssignStmt: - assignments := assignmentDefinitions(node) - declarations = append(declarations, assignments...) - - case *ast.GenDecl: - decls := varDeclarations(node) - if decls != nil { - declarations = append(declarations, decls...) - } - } - - return true - }, - ) - - return declarations -} - -// assignmentDefinitions returns the identifiers corresponding to variable -// assignments in assignStmt, or nil if assignStmt does not declare any -// variables. -func assignmentDefinitions( - assignStmt *ast.AssignStmt, -) (assignments []*ast.Ident) { - assignments = []*ast.Ident{} - - if assignStmt.Tok != token.DEFINE { - return nil - } - - for _, lhs := range assignStmt.Lhs { - ident, ok := lhs.(*ast.Ident) - if !ok { - continue - } - - if ident == nil { - continue - } - - assignments = append(assignments, ident) - } - - return assignments -} - -// varDeclarations returns the identifiers corresponding to variable -// declarations in decl, or nil if decl is not a variable declaration. -func varDeclarations(decl *ast.GenDecl) (declarations []*ast.Ident) { - if decl.Tok != token.VAR { - return nil - } - - declarations = []*ast.Ident{} - - for _, spec := range decl.Specs { - valueSpec, ok := spec.(*ast.ValueSpec) - if !ok { - return declarations - } - - for _, ident := range valueSpec.Names { - declarations = append(declarations, ident) - } - } - - return declarations -} diff --git a/gocapturedrefrace_test.go b/gocapturedrefrace_test.go deleted file mode 100644 index fd5fc53..0000000 --- a/gocapturedrefrace_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2023 Teddy Wing -// -// This file is part of Gocapturedrefrace. -// -// Gocapturedrefrace is free software: you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// Gocapturedrefrace is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Gocapturedrefrace. If not, see -// . - - -package gocapturedrefrace_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" - "gopkg.teddywing.com/gocapturedrefrace" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() - - analysistest.Run(t, testdata, gocapturedrefrace.Analyzer, ".") -} -- cgit v1.2.3