This adds a for statement which is used to iterate over a list with a body of statements. This is an important data transformation tool which should be used sparingly, but is important to have. An import statement inside of a for loop is not currently supported. We have a simple hack to detect the obvious cases, but more deeply nested scenarios probably won't be caught, and you'll get an obscure error message if you try to do this. This was incredibly challenging to get right, and it's all thanks to Sam for his brilliance. Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
594 lines
17 KiB
Go
594 lines
17 KiB
Go
// Mgmt
|
|
// Copyright (C) James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program 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.
|
|
//
|
|
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
package ast
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/purpleidea/mgmt/lang/funcs"
|
|
"github.com/purpleidea/mgmt/lang/funcs/simple"
|
|
"github.com/purpleidea/mgmt/lang/funcs/vars"
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/lang/types"
|
|
"github.com/purpleidea/mgmt/util"
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
)
|
|
|
|
// FuncPrefixToFunctionsScope is a helper function to return the functions
|
|
// portion of the scope from a function prefix lookup. Basically this wraps the
|
|
// implementation in the Func interface in the *ExprFunc struct.
|
|
func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr {
|
|
fns := funcs.LookupPrefix(prefix) // map[string]func() interfaces.Func
|
|
exprs := make(map[string]interfaces.Expr)
|
|
for name, f := range fns {
|
|
|
|
x := f() // inspect
|
|
// We can pass in Fns []*types.FuncValue for the simple and
|
|
// simplepoly API's and avoid the double wrapping from the
|
|
// simple/simplepoly API's to the main function api and back.
|
|
if st, ok := x.(*simple.WrappedFunc); simple.DirectInterface && ok {
|
|
fn := &ExprFunc{
|
|
Title: name,
|
|
|
|
Values: []*types.FuncValue{st.Fn}, // just one!
|
|
}
|
|
// XXX: should we run fn.SetType(st.Fn.Type()) ?
|
|
exprs[name] = fn
|
|
continue
|
|
}
|
|
|
|
//if st, ok := x.(*simplepoly.WrappedFunc); simplepoly.DirectInterface && ok {
|
|
// fn := &ExprFunc{
|
|
// Title: name,
|
|
|
|
// Values: st.Fns,
|
|
// }
|
|
// exprs[name] = fn
|
|
// continue
|
|
//}
|
|
|
|
fn := &ExprFunc{
|
|
Title: name,
|
|
// We need to pass in the constructor function, because
|
|
// we'll need more than one copy of this function if it
|
|
// is used in more than one place so we can build more.
|
|
Function: f, // func() interfaces.Func
|
|
}
|
|
exprs[name] = fn
|
|
}
|
|
|
|
// Wrap every Expr in ExprPoly, so that the function can be used with
|
|
// different types. Those functions are all builtins, so they don't need
|
|
// to access the surrounding scope.
|
|
exprPolys := make(map[string]interfaces.Expr)
|
|
for name, expr := range exprs {
|
|
exprPolys[name] = &ExprPoly{
|
|
Definition: &ExprTopLevel{
|
|
Definition: expr,
|
|
CapturedScope: interfaces.EmptyScope(),
|
|
},
|
|
}
|
|
}
|
|
|
|
return exprPolys
|
|
}
|
|
|
|
// VarPrefixToVariablesScope is a helper function to return the variables
|
|
// portion of the scope from a variable prefix lookup. Basically this is useful
|
|
// to pull out a portion of the variables we've defined by API.
|
|
// TODO: pass `data` into here so we can plumb it into Init for Expr's ?
|
|
func VarPrefixToVariablesScope(prefix string) map[string]interfaces.Expr {
|
|
fns := vars.LookupPrefix(prefix) // map[string]func() interfaces.Var
|
|
exprs := make(map[string]interfaces.Expr)
|
|
for name, f := range fns {
|
|
x := f() // inspect
|
|
expr, err := ValueToExpr(x)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not build expr: %+v", err))
|
|
}
|
|
exprs[name] = &ExprTopLevel{
|
|
Definition: &ExprSingleton{
|
|
Definition: expr,
|
|
|
|
mutex: &sync.Mutex{}, // TODO: call Init instead
|
|
},
|
|
CapturedScope: interfaces.EmptyScope(),
|
|
}
|
|
}
|
|
return exprs
|
|
}
|
|
|
|
// MergeExprMaps merges the two maps of Expr's, and errors if any overwriting
|
|
// would occur. If any prefix string is specified, that is added to the keys of
|
|
// the second "extra" map before doing the merge. This doesn't change the input
|
|
// maps.
|
|
func MergeExprMaps(m, extra map[string]interfaces.Expr, prefix ...string) (map[string]interfaces.Expr, error) {
|
|
p := strings.Join(prefix, "") // hack to have prefix be optional
|
|
|
|
result := map[string]interfaces.Expr{}
|
|
for k, v := range m {
|
|
result[k] = v // copy
|
|
}
|
|
|
|
for k, v := range extra {
|
|
name := p + k
|
|
if _, exists := result[name]; exists {
|
|
return nil, fmt.Errorf("duplicate variable: %s", name)
|
|
}
|
|
result[name] = v
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ValueToExpr converts a Value into the equivalent Expr.
|
|
// FIXME: Add some tests for this function.
|
|
func ValueToExpr(val types.Value) (interfaces.Expr, error) {
|
|
var expr interfaces.Expr
|
|
|
|
switch x := val.(type) {
|
|
case *types.BoolValue:
|
|
expr = &ExprBool{
|
|
V: x.Bool(),
|
|
}
|
|
|
|
case *types.StrValue:
|
|
expr = &ExprStr{
|
|
V: x.Str(),
|
|
}
|
|
|
|
case *types.IntValue:
|
|
expr = &ExprInt{
|
|
V: x.Int(),
|
|
}
|
|
|
|
case *types.FloatValue:
|
|
expr = &ExprFloat{
|
|
V: x.Float(),
|
|
}
|
|
|
|
case *types.ListValue:
|
|
exprs := []interfaces.Expr{}
|
|
|
|
for _, v := range x.List() {
|
|
e, err := ValueToExpr(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exprs = append(exprs, e)
|
|
}
|
|
|
|
expr = &ExprList{
|
|
Elements: exprs,
|
|
}
|
|
|
|
case *types.MapValue:
|
|
kvs := []*ExprMapKV{}
|
|
|
|
for k, v := range x.Map() {
|
|
kx, err := ValueToExpr(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vx, err := ValueToExpr(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kv := &ExprMapKV{
|
|
Key: kx,
|
|
Val: vx,
|
|
}
|
|
kvs = append(kvs, kv)
|
|
}
|
|
|
|
expr = &ExprMap{
|
|
KVs: kvs,
|
|
}
|
|
|
|
case *types.StructValue:
|
|
fields := []*ExprStructField{}
|
|
|
|
for k, v := range x.Struct() {
|
|
fx, err := ValueToExpr(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
field := &ExprStructField{
|
|
Name: k,
|
|
Value: fx,
|
|
}
|
|
fields = append(fields, field)
|
|
}
|
|
|
|
expr = &ExprStruct{
|
|
Fields: fields,
|
|
}
|
|
|
|
//case *types.FuncValue:
|
|
// // TODO: this particular case is particularly untested!
|
|
// expr = &ExprFunc{
|
|
// Title: "<func from ValueToExpr>", // TODO: change this?
|
|
// // TODO: symmetrically, it would have used x.Func() here
|
|
// Values: []*types.FuncValue{
|
|
// x, // just one!
|
|
// },
|
|
// }
|
|
|
|
case *types.VariantValue:
|
|
// TODO: should this be allowed, or should we unwrap them?
|
|
return nil, fmt.Errorf("variant values are not supported")
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown type (%T) for value: %+v", val, val)
|
|
}
|
|
|
|
return expr, expr.SetType(val.Type())
|
|
}
|
|
|
|
// CollectFiles collects all the files used in the AST. You will see more files
|
|
// based on how many compiling steps have run. In general, this is useful for
|
|
// collecting all the files needed to store in our file system for a deploy.
|
|
func CollectFiles(ast interfaces.Stmt) ([]string, error) {
|
|
// collect the list of files
|
|
fileList := []string{}
|
|
fn := func(node interfaces.Node) error {
|
|
// redundant check for example purposes
|
|
stmt, ok := node.(interfaces.Stmt)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
body, ok := stmt.(*StmtProg)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
// collect into global
|
|
fileList = append(fileList, body.importFiles...)
|
|
return nil
|
|
}
|
|
if err := ast.Apply(fn); err != nil {
|
|
return nil, errwrap.Wrapf(err, "can't retrieve paths")
|
|
}
|
|
return fileList, nil
|
|
}
|
|
|
|
// CopyNodeMapping copies the map of string to node and is used in Ordering.
|
|
func CopyNodeMapping(in map[string]interfaces.Node) map[string]interfaces.Node {
|
|
out := make(map[string]interfaces.Node)
|
|
for k, v := range in {
|
|
out[k] = v // copy the map, not the Node's
|
|
}
|
|
return out
|
|
}
|
|
|
|
// getScope pulls the local stored scope out of an Expr, without needing to add
|
|
// a similarly named method to the Expr interface. This is private and not part
|
|
// of the interface, because it is only used internally.
|
|
// TODO: we could extend this to include Stmt's if it was ever useful
|
|
func getScope(node interfaces.Expr) (*interfaces.Scope, error) {
|
|
//if _, ok := node.(interfaces.Expr); !ok {
|
|
// return nil, fmt.Errorf("unexpected: %+v", node)
|
|
//}
|
|
|
|
switch expr := node.(type) {
|
|
case *ExprBool:
|
|
return expr.scope, nil
|
|
case *ExprStr:
|
|
return expr.scope, nil
|
|
case *ExprInt:
|
|
return expr.scope, nil
|
|
case *ExprFloat:
|
|
return expr.scope, nil
|
|
case *ExprList:
|
|
return expr.scope, nil
|
|
case *ExprMap:
|
|
return expr.scope, nil
|
|
case *ExprStruct:
|
|
return expr.scope, nil
|
|
case *ExprFunc:
|
|
return expr.scope, nil
|
|
case *ExprCall:
|
|
return expr.scope, nil
|
|
case *ExprVar:
|
|
return expr.scope, nil
|
|
case *ExprIf:
|
|
return expr.scope, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unexpected: %+v", node)
|
|
}
|
|
}
|
|
|
|
// trueCallee is a helper function because ExprTopLevel and ExprSingleton are
|
|
// sometimes added around builtins. This makes it difficult for the type checker
|
|
// to check if a particular builtin is the callee or not. This function removes
|
|
// the ExprTopLevel and ExprSingleton wrappers, if they exist.
|
|
func trueCallee(apparentCallee interfaces.Expr) interfaces.Expr {
|
|
switch x := apparentCallee.(type) {
|
|
case *ExprTopLevel:
|
|
return trueCallee(x.Definition)
|
|
case *ExprSingleton:
|
|
return trueCallee(x.Definition)
|
|
case *ExprIterated:
|
|
return trueCallee(x.Definition)
|
|
case *ExprPoly: // XXX: Did we want this one added too?
|
|
return trueCallee(x.Definition)
|
|
|
|
default:
|
|
return apparentCallee
|
|
}
|
|
}
|
|
|
|
// findExprPoly is a helper used in SetScope.
|
|
func findExprPoly(apparentCallee interfaces.Expr) *ExprPoly {
|
|
switch x := apparentCallee.(type) {
|
|
case *ExprTopLevel:
|
|
return findExprPoly(x.Definition)
|
|
case *ExprSingleton:
|
|
return findExprPoly(x.Definition)
|
|
case *ExprIterated:
|
|
return findExprPoly(x.Definition)
|
|
case *ExprPoly:
|
|
return x // found it!
|
|
default:
|
|
return nil // not found!
|
|
}
|
|
}
|
|
|
|
// newExprParam is a helper function to create an ExprParam with the internal
|
|
// key set to the pointer of the thing we're creating.
|
|
func newExprParam(name string, typ *types.Type) *ExprParam {
|
|
expr := &ExprParam{
|
|
Name: name,
|
|
typ: typ,
|
|
}
|
|
expr.envKey = expr
|
|
return expr
|
|
}
|
|
|
|
// newExprIterated is a helper function to create an ExprIterated with the
|
|
// internal key set to the pointer of the thing we're creating.
|
|
func newExprIterated(name string, definition interfaces.Expr) *ExprIterated {
|
|
expr := &ExprIterated{
|
|
Name: name,
|
|
Definition: definition,
|
|
}
|
|
expr.envKey = expr
|
|
return expr
|
|
}
|
|
|
|
// variableScopeFeedback logs some messages about what is actually in scope so
|
|
// that the user gets a hint about what's going on. This is useful for catching
|
|
// bugs in our programming or in user code!
|
|
func variableScopeFeedback(scope *interfaces.Scope, logf func(format string, v ...interface{})) {
|
|
logf("variables in scope:")
|
|
names := []string{}
|
|
for name := range scope.Variables {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
logf("$%s", name)
|
|
}
|
|
}
|
|
|
|
// functionScopeFeedback logs some messages about what is actually in scope so
|
|
// that the user gets a hint about what's going on. This is useful for catching
|
|
// bugs in our programming or in user code!
|
|
func functionScopeFeedback(scope *interfaces.Scope, logf func(format string, v ...interface{})) {
|
|
logf("functions in scope:")
|
|
names := []string{}
|
|
for name := range scope.Functions {
|
|
if strings.HasPrefix(name, "_") { // hidden function
|
|
continue
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
logf("%s(...)", name)
|
|
}
|
|
}
|
|
|
|
// lambdaScopeFeedback logs some messages about what is actually in scope so
|
|
// that the user gets a hint about what's going on. This is useful for catching
|
|
// bugs in our programming or in user code!
|
|
func lambdaScopeFeedback(scope *interfaces.Scope, logf func(format string, v ...interface{})) {
|
|
logf("lambdas in scope:")
|
|
names := []string{}
|
|
for name, val := range scope.Variables {
|
|
// XXX: Is this a valid way to filter?
|
|
if _, ok := trueCallee(val).(*ExprFunc); !ok {
|
|
continue
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
logf("$%s(...)", name)
|
|
}
|
|
}
|
|
|
|
// Textarea stores the coordinates of a statement or expression in the form of a
|
|
// starting line/column and ending line/column.
|
|
type Textarea struct {
|
|
// debug represents if we're running in debug mode or not.
|
|
debug bool
|
|
|
|
// logf is a logger which should be used.
|
|
logf func(format string, v ...interface{})
|
|
|
|
// sf is the SourceFinder function implementation that maps a filename
|
|
// to the source.
|
|
sf interfaces.SourceFinderFunc
|
|
|
|
// path is the full path/filename where this text area exists.
|
|
path string
|
|
|
|
// This data is zero-based. (Eg: first line of file is 0)
|
|
startLine int // first
|
|
startColumn int // left
|
|
endLine int // last
|
|
endColumn int // right
|
|
|
|
isSet bool
|
|
|
|
// Bug5819 works around issue https://github.com/golang/go/issues/5819
|
|
Bug5819 interface{} // XXX: workaround
|
|
}
|
|
|
|
// Setup is used during AST initialization in order to store in each AST node
|
|
// the name of the source file from which it was generated.
|
|
func (obj *Textarea) Setup(data *interfaces.Data) {
|
|
obj.debug = data.Debug
|
|
obj.logf = data.Logf
|
|
obj.sf = data.SourceFinder
|
|
obj.path = data.AbsFilename()
|
|
}
|
|
|
|
// IsSet returns if the position was already set with Locate already.
|
|
func (obj *Textarea) IsSet() bool {
|
|
return obj.isSet
|
|
}
|
|
|
|
// Locate is used by the parser to store the token positions in AST nodes. The
|
|
// path will be filled during AST node initialization usually, because the
|
|
// parser does not know the name of the file it is processing.
|
|
func (obj *Textarea) Locate(line int, col int, endline int, endcol int) {
|
|
obj.startLine = line
|
|
obj.startColumn = col
|
|
obj.endLine = endline
|
|
obj.endColumn = endcol
|
|
obj.isSet = true
|
|
}
|
|
|
|
// Pos returns the starting line/column of an AST node.
|
|
func (obj *Textarea) Pos() (int, int) {
|
|
return obj.startLine, obj.startColumn
|
|
}
|
|
|
|
// End returns the end line/column of an AST node.
|
|
func (obj *Textarea) End() (int, int) {
|
|
return obj.endLine, obj.endColumn
|
|
}
|
|
|
|
// Path returns the name of the source file that holds the code for an AST node.
|
|
func (obj *Textarea) Path() string {
|
|
return obj.path
|
|
}
|
|
|
|
// Filename returns the printable filename that we'd like to display. It tries
|
|
// to return a relative version if possible.
|
|
func (obj *Textarea) Filename() string {
|
|
if obj.path == "" {
|
|
return "<unknown>" // TODO: should this be <stdin> ?
|
|
}
|
|
|
|
wd, _ := os.Getwd() // ignore error since "" would just pass through
|
|
wd += "/" // it's a dir
|
|
if s, err := util.RemoveBasePath(obj.path, wd); err == nil {
|
|
return s
|
|
}
|
|
|
|
return obj.path
|
|
}
|
|
|
|
// Byline gives a succinct representation of the Textarea, but is useful only in
|
|
// debugging. In order to generate pretty error messages, see HighlightText.
|
|
func (obj *Textarea) Byline() string {
|
|
// We convert to 1-based for user display.
|
|
return fmt.Sprintf("%s @ %d:%d-%d:%d", obj.Filename(), obj.startLine+1, obj.startColumn+1, obj.endLine+1, obj.endColumn+1)
|
|
}
|
|
|
|
// HighlightText generates a generic description that just visually indicates
|
|
// part of the line described by a Textarea. If the coordinates that are passed
|
|
// span multiple lines, don't show those lines, but just a description of the
|
|
// area. If it can't generate a valid snippet, then it returns the empty string.
|
|
func (obj *Textarea) HighlightText() string {
|
|
b, err := obj.sf(obj.path) // source finder!
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
contents := string(b)
|
|
|
|
result := &strings.Builder{}
|
|
|
|
result.WriteString(obj.Byline())
|
|
|
|
lines := strings.Split(contents, "\n")
|
|
if len(lines) < obj.endLine-1 {
|
|
// XXX: out of bounds?
|
|
return ""
|
|
}
|
|
|
|
result.WriteString("\n--\n")
|
|
|
|
if obj.startLine == obj.endLine {
|
|
line := lines[obj.startLine] + "\n"
|
|
text := strings.TrimLeft(line, " \t")
|
|
indent := strings.TrimSuffix(line, text)
|
|
offset := len(indent)
|
|
|
|
result.WriteString(line)
|
|
result.WriteString(indent)
|
|
result.WriteString(strings.Repeat(" ", obj.startColumn-offset))
|
|
// TODO: add on the width of the second element as well
|
|
result.WriteString(strings.Repeat("^", obj.endColumn-obj.startColumn+1))
|
|
result.WriteString("\n")
|
|
|
|
return result.String()
|
|
}
|
|
|
|
line := lines[obj.startLine] + "\n"
|
|
text := strings.TrimLeft(line, " \t")
|
|
indent := strings.TrimSuffix(line, text)
|
|
offset := len(indent)
|
|
|
|
result.WriteString(line)
|
|
result.WriteString(indent)
|
|
result.WriteString(strings.Repeat(" ", obj.startColumn-offset))
|
|
result.WriteString("^ from here ...\n")
|
|
|
|
line = lines[obj.endLine] + "\n"
|
|
text = strings.TrimLeft(line, " \t")
|
|
indent = strings.TrimSuffix(line, text)
|
|
offset = len(indent)
|
|
|
|
result.WriteString(line)
|
|
result.WriteString(indent)
|
|
result.WriteString(strings.Repeat(" ", obj.startColumn-offset))
|
|
result.WriteString("^ ... to here\n")
|
|
|
|
return result.String()
|
|
}
|