From 0ed63eff298e9f51c82b9f9a27893b7f695ada29 Mon Sep 17 00:00:00 2001 From: Jamie Gennis Date: Mon, 30 Jun 2014 18:07:17 -0700 Subject: [PATCH] Fix scoping logic to respect the caller's package. This change makes the ModuleContext and SingletonContext methods respect the caller's Go package when performing name lookups. It does this by re-parenting the module or singleton's scope to the scope of the caller's package, thus allowing a Module or Singleton to pass its context to a function defined in another Go package and have the called function access variables and rules defined in its own Go package. Change-Id: Ifdec87ba3095a453b36fb00e38c0bb3a928a2b9b --- blueprint/globals.go | 48 +++++++++++++++++++++++++------------- blueprint/module_ctx.go | 9 +++++++ blueprint/scope.go | 22 +++++++++++++++++ blueprint/singleton_ctx.go | 9 +++++++ 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/blueprint/globals.go b/blueprint/globals.go index 2b2a62c..22e2700 100644 --- a/blueprint/globals.go +++ b/blueprint/globals.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "reflect" - "regexp" "runtime" "strings" ) @@ -18,8 +17,6 @@ type pkg struct { var pkgs = map[string]*pkg{} -var pkgRegexp = regexp.MustCompile(`(.*)\.init(·[0-9]+)?`) - var Phony Rule = &builtinRule{ name_: "phony", } @@ -37,6 +34,36 @@ func pkgPathToName(pkgPath string) string { return strings.Replace(pkgPath, "/", ".", -1) } +// callerName returns the package path and function name of the calling +// function. The skip argument has the same meaning as the skip argument of +// runtime.Callers. +func callerName(skip int) (pkgPath, funcName string) { + var pc [1]uintptr + n := runtime.Callers(skip+1, pc[:]) + if n != 1 { + panic("unable to get caller pc") + } + + f := runtime.FuncForPC(pc[0]) + fullName := f.Name() + + lastDotIndex := strings.LastIndex(fullName, ".") + if lastDotIndex == -1 { + panic("unable to distinguish function name from package") + } + + if fullName[lastDotIndex-1] == ')' { + // The caller is a method on some type, so it's name looks like + // "pkg/path.(type).method". We need to go back one dot farther to get + // to the package name. + lastDotIndex = strings.LastIndex(fullName[:lastDotIndex], ".") + } + + pkgPath = fullName[:lastDotIndex] + funcName = fullName[lastDotIndex+1:] + return +} + // callerPackage returns the pkg of the function that called the caller of // callerPackage. The caller of callerPackage must have been called from an // init function of the package or callerPackage will panic. @@ -46,23 +73,12 @@ func pkgPathToName(pkgPath string) string { // implementation details. However, it allows us to ensure that it's easy to // determine where a definition in a .ninja file came from. func callerPackage() *pkg { - var pc [1]uintptr - n := runtime.Callers(3, pc[:]) - if n != 1 { - panic("unable to get caller pc") - } + pkgPath, funcName := callerName(3) - f := runtime.FuncForPC(pc[0]) - callerName := f.Name() - - submatches := pkgRegexp.FindSubmatch([]byte(callerName)) - if submatches == nil { - println(callerName) + if funcName != "init" && !strings.HasPrefix(funcName, "init·") { panic("not called from an init func") } - pkgPath := string(submatches[1]) - pkgName := pkgPathToName(pkgPath) err := validateNinjaName(pkgName) if err != nil { diff --git a/blueprint/module_ctx.go b/blueprint/module_ctx.go index 0029d11..0de8c4e 100644 --- a/blueprint/module_ctx.go +++ b/blueprint/module_ctx.go @@ -93,6 +93,9 @@ func (m *moduleContext) OtherModuleErrorf(module Module, format string, } func (m *moduleContext) Variable(name, value string) { + const skip = 2 + m.scope.ReparentToCallerPackage(skip) + v, err := m.scope.AddLocalVariable(name, value) if err != nil { panic(err) @@ -104,6 +107,9 @@ func (m *moduleContext) Variable(name, value string) { func (m *moduleContext) Rule(name string, params RuleParams, argNames ...string) Rule { + const skip = 2 + m.scope.ReparentToCallerPackage(skip) + r, err := m.scope.AddLocalRule(name, ¶ms, argNames...) if err != nil { panic(err) @@ -115,6 +121,9 @@ func (m *moduleContext) Rule(name string, params RuleParams, } func (m *moduleContext) Build(params BuildParams) { + const skip = 2 + m.scope.ReparentToCallerPackage(skip) + def, err := parseBuildParams(m.scope, ¶ms) if err != nil { panic(err) diff --git a/blueprint/scope.go b/blueprint/scope.go index dac7a60..c9b8aca 100644 --- a/blueprint/scope.go +++ b/blueprint/scope.go @@ -237,6 +237,28 @@ func newLocalScope(parent *basicScope, namePrefix string) *localScope { } } +// ReparentToCallerPackage sets the localScope's parent scope to the scope of +// the Go package of the caller. This allows a ModuleContext and +// SingletonContext to be passed to a function defined in a different Go package +// with that function having access to all of the package-scoped variables of +// its own package. +// +// The skip argument has the same meaning as the skip argument of +// runtime.Callers. +func (s *localScope) ReparentToCallerPackage(skip int) { + var pkgScope *basicScope + + pkgPath, _ := callerName(skip + 1) + pkg, ok := pkgs[pkgPath] + if ok { + pkgScope = pkg.scope + } else { + pkgScope = newScope(nil) + } + + s.scope.parent = pkgScope +} + func (s *localScope) LookupVariable(name string) (Variable, error) { return s.scope.LookupVariable(name) } diff --git a/blueprint/singleton_ctx.go b/blueprint/singleton_ctx.go index aa1626a..d25bd06 100644 --- a/blueprint/singleton_ctx.go +++ b/blueprint/singleton_ctx.go @@ -87,6 +87,9 @@ func (s *singletonContext) Errorf(format string, args ...interface{}) { } func (s *singletonContext) Variable(name, value string) { + const skip = 2 + s.scope.ReparentToCallerPackage(skip) + v, err := s.scope.AddLocalVariable(name, value) if err != nil { panic(err) @@ -98,6 +101,9 @@ func (s *singletonContext) Variable(name, value string) { func (s *singletonContext) Rule(name string, params RuleParams, argNames ...string) Rule { + const skip = 2 + s.scope.ReparentToCallerPackage(skip) + r, err := s.scope.AddLocalRule(name, ¶ms, argNames...) if err != nil { panic(err) @@ -109,6 +115,9 @@ func (s *singletonContext) Rule(name string, params RuleParams, } func (s *singletonContext) Build(params BuildParams) { + const skip = 2 + s.scope.ReparentToCallerPackage(skip) + def, err := parseBuildParams(s.scope, ¶ms) if err != nil { panic(err)