pulumi/pkg/compiler/parsetree.go
joeduffy d9631f6e75 Retain unrecognized service properties
During unmarshaling, the default behavior of the stock Golang JSON marshaler,
and consequently the YAML one we used which mimics its behavior, is to toss away
unrecognized properties.  This isn't what we want for two reasons:

First, we want to issue errors/warnings on unrecognized fields to aid in diagnostics;
we will set aside some extensible section for 3rd parties to use.  This is not
addressed in this change, however.

Second, and more pertinent, is that we need to retain unrecognized fields for certain
types like services, which are extensible by default.

Until golang/go#6213 is addressed -- imminent, it seems -- we will have to do a
somewhat hacky workaround to this problem.  This change contains what I consider to
be the "least bad" in that we won't introduce a lot of performance overhead, and
just have to deal with the slight annoyance of the ast.Services node type containing
both Public/Private *and* PublicUntyped/PrivateUntyped fields alongside one another.
The marshaler dumps property bags into the *Untyped fields, and the parsetree analyzer
expands them out into a structured ast.Service type.  Subsequent passes can then
ignore the *Untyped fields altogether.

Note that this would cause some marshaling funkiness if we ever wanted to remarshal
the mutated ASTs back into JSON/YAML.  Since we don't do that right now, however, I've
not made any attempt to keep the two pairs in synch.  Post-parsetree analyzer, we
literally just forget about the *Untyped guys.
2016-11-19 09:01:23 -08:00

130 lines
4.1 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"github.com/blang/semver"
"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
// Analyze checks the validity of an entire parse tree (starting with a top-level Stack).
Analyze(doc *diag.Document, stack *ast.Stack)
}
// NewPTAnalayzer allocates a new PTAnalyzer associated with the given Compiler.
func NewPTAnalyzer(c Compiler) PTAnalyzer {
return &ptAnalyzer{c: c}
}
type ptAnalyzer struct {
c Compiler
}
func (a *ptAnalyzer) Diag() diag.Sink {
return a.c.Diag()
}
func (a *ptAnalyzer) Analyze(doc *diag.Document, 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(doc, stack)
}
func (a *ptAnalyzer) VisitMetadata(doc *diag.Document, kind string, meta *ast.Metadata) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
meta.Kind = kind
// Metadata names are required.
if meta.Name == "" {
a.Diag().Errorf(errors.MissingMetadataName.WithDocument(doc), kind)
}
// Metadata versions must be valid semantic versions (and specifically, not ranges). In other words, we need
// a concrete version number like "1.3.9-beta2" and *not* a range like ">1.3.9".
// TODO: should we require a version number?
if meta.Version != "" {
if _, err := semver.Parse(string(meta.Version)); err != nil {
a.Diag().Errorf(errors.IllegalMetadataSemVer.WithDocument(doc), kind, meta.Version)
}
}
}
func (a *ptAnalyzer) VisitStack(doc *diag.Document, stack *ast.Stack) {
}
func (a *ptAnalyzer) VisitParameter(doc *diag.Document, name string, param *ast.Parameter) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
param.Name = name
}
func (a *ptAnalyzer) VisitDependency(doc *diag.Document, name ast.Name, dep *ast.Dependency) {
// Dependency versions must be valid semantic versions *or* ranges.
// TODO: should we require dependencies to have versions?
ver := *dep
if ver != "" {
if _, err := semver.ParseRange(string(ver)); err != nil {
a.Diag().Errorf(errors.IllegalDependencySemVer.WithDocument(doc), name, ver)
}
}
}
func (a *ptAnalyzer) VisitServices(doc *diag.Document, 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.
svcs.Public = make(ast.ServiceMap)
for _, name := range ast.StableUntypedServices(svcs.PublicUntyped) {
svcs.Public[name] = a.untypedServiceToTyped(doc, name, true, svcs.PublicUntyped[name])
}
svcs.Private = make(ast.ServiceMap)
for _, name := range ast.StableUntypedServices(svcs.PrivateUntyped) {
svcs.Private[name] = a.untypedServiceToTyped(doc, name, false, svcs.PrivateUntyped[name])
}
}
func (a *ptAnalyzer) untypedServiceToTyped(doc *diag.Document,
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.IllegalMufileSyntax.WithDocument(doc), "service type must be a string")
}
}
return ast.Service{
Name: name,
Type: typ,
Public: public,
Extra: bag,
}
}
func (a *ptAnalyzer) VisitService(doc *diag.Document, name ast.Name, public bool, svc *ast.Service) {
}
func (a *ptAnalyzer) VisitTarget(doc *diag.Document, name string, target *ast.Target) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
target.Name = name
}