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
This commit is contained in:
Jamie Gennis 2014-06-30 18:07:17 -07:00
parent 3159cb7673
commit 0ed63eff29
4 changed files with 72 additions and 16 deletions

View file

@ -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 {

View file

@ -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, &params, 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, &params)
if err != nil {
panic(err)

View file

@ -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)
}

View file

@ -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, &params, 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, &params)
if err != nil {
panic(err)