pulumi/pkg/compiler/parsetree.go
joeduffy db80229899 Fix a few type binding mistakes
* Persue the default/optional checking if a property value == nil.

* Use the Interface() function to convert a reflect.Type to its underlying
  interface{} value.  This is required for typechecking to check out.

* Also, unrelated to the above, change type assertions to use nil rather than
  allocating real objects.  Although minimal, this incurs less GC pressure.
2016-12-09 13:12:57 -08:00

213 lines
6.9 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"strings"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/ast"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
)
// PTAnalyzer knows how to walk and validate parse trees.
type PTAnalyzer interface {
core.Visitor
// AnalyzeStack checks the validity of an entire Stack parse tree.
AnalyzeStack(stack *ast.Stack)
// AnalyzeWorkspace checks the validity of an entire Workspace parse tree.
AnalyzeWorkspace(w *ast.Workspace)
}
// NewPTAnalayzer allocates a new PTAnalyzer associated with the given Compiler.
func NewPTAnalyzer(c Compiler) PTAnalyzer {
return &ptAnalyzer{c: c}
}
type ptAnalyzer struct {
c Compiler
}
var _ PTAnalyzer = (*ptAnalyzer)(nil) // compile-time assert that binder implements PTAnalyzer.
func (a *ptAnalyzer) Diag() diag.Sink {
return a.c.Diag()
}
func (a *ptAnalyzer) AnalyzeStack(stack *ast.Stack) {
glog.Infof("Parsetree analyzing Mu Stack: %v", stack.Name)
if glog.V(2) {
defer func() {
glog.V(2).Infof("Parsetree analysis for Mu Stack %v completed w/ %v warnings and %v errors",
stack.Name, a.Diag().Warnings(), a.Diag().Errors())
}()
}
// Use an InOrderVisitor to walk the tree in-order; this handles determinism for us.
v := core.NewInOrderVisitor(a, nil)
v.VisitStack(stack)
}
func (a *ptAnalyzer) AnalyzeWorkspace(w *ast.Workspace) {
glog.Infof("Parsetree analyzing workspace file: %v", w.Doc.File)
if glog.V(2) {
defer func() {
glog.V(2).Infof("Parsetree analysis for workspace %v completed w/ %v warnings and %v errors",
w.Doc.File, a.Diag().Warnings(), a.Diag().Errors())
}()
}
// Use an InOrderVisitor to walk the tree in-order; this handles determinism for us.
v := core.NewInOrderVisitor(a, nil)
v.VisitWorkspace(w)
}
func (a *ptAnalyzer) VisitWorkspace(w *ast.Workspace) {
}
func (a *ptAnalyzer) VisitCluster(name string, cluster *ast.Cluster) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
cluster.Name = name
}
func (a *ptAnalyzer) VisitDependency(parent *ast.Workspace, ref ast.Ref, dep *ast.Dependency) {
}
func (a *ptAnalyzer) VisitStack(stack *ast.Stack) {
}
func (a *ptAnalyzer) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas) {
}
func (a *ptAnalyzer) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name, public bool,
schema *ast.Schema) {
// Decorate the AST with contextual information.
schema.Name = name
schema.Public = public
// If the schema has a base type listed, parse it to the best of our ability.
if schema.Base != "" {
schema.BoundBase = a.parseType(schema.Base)
}
}
func (a *ptAnalyzer) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
prop.Name = name
// Parse the property type to the best of our ability at this phase in the compiler.
prop.BoundType = a.parseType(prop.Type)
}
func (a *ptAnalyzer) VisitServices(parent *ast.Stack, svcs *ast.Services) {
// We need to expand the UntypedServiceMaps into strongly typed ServiceMaps. As part of this, we also decorate the
// AST with extra contextual information so subsequent passes can operate context-free.
// TODO[marapongo/mu#4]: once we harden the marshalers, we should be able to largely eliminate this.
svcs.Public = make(ast.ServiceMap)
for _, name := range ast.StableUntypedServices(svcs.PublicUntyped) {
svcs.Public[name] = a.untypedServiceToTyped(parent, name, true, svcs.PublicUntyped[name])
}
svcs.Private = make(ast.ServiceMap)
for _, name := range ast.StableUntypedServices(svcs.PrivateUntyped) {
svcs.Private[name] = a.untypedServiceToTyped(parent, name, false, svcs.PrivateUntyped[name])
}
}
func (a *ptAnalyzer) untypedServiceToTyped(parent *ast.Stack, name ast.Name, public bool,
bag map[string]interface{}) *ast.Service {
var typ ast.Name
t, has := bag["type"]
if has {
// If the bag contains a type, ensure that it is a string.
ts, ok := t.(string)
if ok {
typ = ast.Name(ts)
} else {
a.Diag().Errorf(errors.ErrorIllegalMufileSyntax.At(parent), "service type must be a string")
}
// Delete the type property so it's not considered semantically meaningful for the target.
delete(bag, "type")
}
return &ast.Service{
Name: name,
Type: ast.Ref(typ),
Public: public,
Properties: bag,
}
}
func (a *ptAnalyzer) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool,
svc *ast.Service) {
}
// parseType produces an ast.Type. This will not have been bound yet, so for example, we won't know whether
// an arbitrary non-primitive reference name references a stack or a schema, however at least this is a start.
func (a *ptAnalyzer) parseType(ref ast.Ref) *ast.Type {
refs := string(ref)
mix := strings.Index(refs, ast.TypeDecorsMapPrefix)
if mix == 0 {
// If we have a map, find the separator, and then parse the key and value parts.
rest := refs[mix+len(ast.TypeDecorsMapPrefix):]
if sep := strings.Index(rest, ast.TypeDecorsMapSeparator); sep != -1 {
keyn := ast.Ref(rest[:sep])
valn := ast.Ref(rest[sep+len(ast.TypeDecorsMapSeparator):])
if keyn != "" && valn != "" {
keyt := a.parseType(keyn)
valt := a.parseType(valn)
if keyt != nil && valt != nil {
return ast.NewMapType(keyt, valt)
}
}
}
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
} else if aix := strings.LastIndex(refs, ast.TypeDecorsArraySuffix); aix != -1 {
if aix == len(refs)-len(ast.TypeDecorsArraySuffix) {
// If we have an array, peel off the front and keep going.
rest := refs[:aix]
if rest != "" {
if elem := a.parseType(ast.Ref(rest)); elem != nil {
return ast.NewArrayType(elem)
}
}
}
// The array part was in the wrong position. Issue an error. Maybe they did T[] instead of []T?
a.Diag().Errorf(errors.ErrorIllegalArrayLikeSyntax, refs)
} else if mix != -1 {
// The map part was in the wrong position. Issue an error.
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
} else {
// Otherwise, there are no decorators. Parse the result as either a primitive type or unresolved name.
switch ast.PrimitiveType(refs) {
case ast.PrimitiveTypeAny:
return ast.NewAnyType()
case ast.PrimitiveTypeString:
return ast.NewStringType()
case ast.PrimitiveTypeNumber:
return ast.NewNumberType()
case ast.PrimitiveTypeBool:
return ast.NewBoolType()
case ast.PrimitiveTypeService:
return ast.NewServiceType()
}
// If we didn't recognize anything thus far, it's a simple name. We don't yet know what it references --
// it could be a stack, schema, or even a completely bogus, missing name -- so just store it as it is.
if _, err := ref.Parse(); err != nil {
a.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax, refs, err)
} else {
return ast.NewUnresolvedRefType(&ref)
}
}
return nil
}