lang: Factor out the base function table

Whichever function were responsible for instantiating this map would
inevitably end up scaling in length based on the number of funtions and
thus violate the function length linter.

As a compromise, we'll factor out the function table into its own function
that is exempt from that linter but focused only on constructing the base
function table, and then Scope.Functions can be shorter by dealing only
with the scope-specific customizations of the returned table.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins 2025-01-03 15:59:03 -08:00
parent 69bf43dd56
commit 972324e5ea
2 changed files with 157 additions and 144 deletions

View File

@ -33,6 +33,83 @@ const CoreNamespace = addrs.FunctionNamespaceCore + "::"
func (s *Scope) Functions() map[string]function.Function {
s.funcsLock.Lock()
if s.funcs == nil {
s.funcs = makeBaseFunctionTable(s.BaseDir)
if s.ConsoleMode {
// The type function is only available in OpenTofu console.
s.funcs["type"] = funcs.TypeFunc
} else {
// The plantimestamp function doesn't make sense in the OpenTofu
// console.
s.funcs["plantimestamp"] = funcs.MakeStaticTimestampFunc(s.PlanTimestamp)
}
if s.PureOnly {
// Force our few impure functions to return unknown so that we
// can defer evaluating them until a later pass.
for _, name := range impureFunctions {
s.funcs[name] = function.Unpredictable(s.funcs[name])
}
}
coreNames := make([]string, 0)
// Add a description to each function and parameter based on the
// contents of descriptionList.
// One must create a matching description entry whenever a new
// function is introduced.
for name, f := range s.funcs {
s.funcs[name] = funcs.WithDescription(name, f)
coreNames = append(coreNames, name)
}
// Copy all stdlib funcs into core:: namespace
for _, name := range coreNames {
s.funcs[CoreNamespace+name] = s.funcs[name]
}
}
s.funcsLock.Unlock()
return s.funcs
}
// experimentalFunction checks whether the given experiment is enabled for
// the receiving scope. If so, it will return the given function verbatim.
// If not, it will return a placeholder function that just returns an
// error explaining that the function requires the experiment to be enabled.
//
//lint:ignore U1000 Ignore unused function error for now
func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn function.Function) function.Function {
if s.activeExperiments.Has(experiment) {
return fn
}
err := fmt.Errorf(
"this function is experimental and available only when the experiment keyword %s is enabled for the current module",
experiment.Keyword(),
)
return function.New(&function.Spec{
Params: fn.Params(),
VarParam: fn.VarParam(),
Type: func(args []cty.Value) (cty.Type, error) {
return cty.DynamicPseudoType, err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// It would be weird to get here because the Type function always
// fails, but we'll return an error here too anyway just to be
// robust.
return cty.DynamicVal, err
},
})
}
// makeBaseFunctionTable constructs the initial table of functions that we uses as
// the basis for the function table in each scope.
//
// This function intentionally always returns a fresh map on each call because the
// caller is expected to modify it further before storing it as part of a
// particular [Scope], based on the unique settings of that scope.
//
//nolint:funlen // The length of this function naturally scales with the number of functions in the OpenTofu language.
func makeBaseFunctionTable(baseDir string) map[string]function.Function {
// Some of our functions are just directly the cty stdlib functions.
// Others are implemented in the subdirectory "funcs" here in this
// repository. New functions should generally start out their lives
@ -40,7 +117,7 @@ func (s *Scope) Functions() map[string]function.Function {
// later if the functionality seems to be something domain-agnostic
// that would be useful to all applications using cty functions.
s.funcs = map[string]function.Function{
ret := map[string]function.Function{
"abs": stdlib.AbsoluteFunc,
"abspath": funcs.AbsPathFunc,
"alltrue": funcs.AllTrueFunc,
@ -72,16 +149,16 @@ func (s *Scope) Functions() map[string]function.Function {
"element": stdlib.ElementFunc,
"endswith": funcs.EndsWithFunc,
"chunklist": stdlib.ChunklistFunc,
"file": funcs.MakeFileFunc(s.BaseDir, false),
"fileexists": funcs.MakeFileExistsFunc(s.BaseDir),
"fileset": funcs.MakeFileSetFunc(s.BaseDir),
"filebase64": funcs.MakeFileFunc(s.BaseDir, true),
"filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir),
"filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir),
"filemd5": funcs.MakeFileMd5Func(s.BaseDir),
"filesha1": funcs.MakeFileSha1Func(s.BaseDir),
"filesha256": funcs.MakeFileSha256Func(s.BaseDir),
"filesha512": funcs.MakeFileSha512Func(s.BaseDir),
"file": funcs.MakeFileFunc(baseDir, false),
"fileexists": funcs.MakeFileExistsFunc(baseDir),
"fileset": funcs.MakeFileSetFunc(baseDir),
"filebase64": funcs.MakeFileFunc(baseDir, true),
"filebase64sha256": funcs.MakeFileBase64Sha256Func(baseDir),
"filebase64sha512": funcs.MakeFileBase64Sha512Func(baseDir),
"filemd5": funcs.MakeFileMd5Func(baseDir),
"filesha1": funcs.MakeFileSha1Func(baseDir),
"filesha256": funcs.MakeFileSha256Func(baseDir),
"filesha512": funcs.MakeFileSha512Func(baseDir),
"flatten": stdlib.FlattenFunc,
"floor": stdlib.FloorFunc,
"format": stdlib.FormatFunc,
@ -162,83 +239,17 @@ func (s *Scope) Functions() map[string]function.Function {
"zipmap": stdlib.ZipmapFunc,
}
s.funcs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, func() map[string]function.Function {
ret["templatefile"] = funcs.MakeTemplateFileFunc(baseDir, func() map[string]function.Function {
// The templatefile function prevents recursive calls to itself
// by copying this map and overwriting the "templatefile" entry.
return s.funcs
return ret
})
// Registers "templatestring" function in function map.
s.funcs["templatestring"] = funcs.MakeTemplateStringFunc(s.BaseDir, func() map[string]function.Function {
ret["templatestring"] = funcs.MakeTemplateStringFunc(baseDir, func() map[string]function.Function {
// This anonymous function returns the existing map of functions for initialization.
return s.funcs
return ret
})
if s.ConsoleMode {
// The type function is only available in OpenTofu console.
s.funcs["type"] = funcs.TypeFunc
}
if !s.ConsoleMode {
// The plantimestamp function doesn't make sense in the OpenTofu
// console.
s.funcs["plantimestamp"] = funcs.MakeStaticTimestampFunc(s.PlanTimestamp)
}
if s.PureOnly {
// Force our few impure functions to return unknown so that we
// can defer evaluating them until a later pass.
for _, name := range impureFunctions {
s.funcs[name] = function.Unpredictable(s.funcs[name])
}
}
coreNames := make([]string, 0)
// Add a description to each function and parameter based on the
// contents of descriptionList.
// One must create a matching description entry whenever a new
// function is introduced.
for name, f := range s.funcs {
s.funcs[name] = funcs.WithDescription(name, f)
coreNames = append(coreNames, name)
}
// Copy all stdlib funcs into core:: namespace
for _, name := range coreNames {
s.funcs[CoreNamespace+name] = s.funcs[name]
}
}
s.funcsLock.Unlock()
return s.funcs
}
// experimentalFunction checks whether the given experiment is enabled for
// the receiving scope. If so, it will return the given function verbatim.
// If not, it will return a placeholder function that just returns an
// error explaining that the function requires the experiment to be enabled.
//
//lint:ignore U1000 Ignore unused function error for now
func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn function.Function) function.Function {
if s.activeExperiments.Has(experiment) {
return fn
}
err := fmt.Errorf(
"this function is experimental and available only when the experiment keyword %s is enabled for the current module",
experiment.Keyword(),
)
return function.New(&function.Spec{
Params: fn.Params(),
VarParam: fn.VarParam(),
Type: func(args []cty.Value) (cty.Type, error) {
return cty.DynamicPseudoType, err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// It would be weird to get here because the Type function always
// fails, but we'll return an error here too anyway just to be
// robust.
return cty.DynamicVal, err
},
})
return ret
}

View File

@ -43,6 +43,8 @@ import (
// it really is registered correctly) and possibly a small set of additional
// functions showing real-world use-cases that rely on type conversion
// behaviors.
//
//nolint:gocognit // This test intentionally embraces mainloop complexity so that maintenence can primarily focus on the declarative test table rather than the actual test logic.
func TestFunctions(t *testing.T) {
// used in `pathexpand()` test
homePath, err := homedir.Dir()