From 2f3862d46e340aaba556aa38d86388c19fa6de65 Mon Sep 17 00:00:00 2001 From: Walter Scott Date: Tue, 5 Aug 2025 07:42:07 -0500 Subject: [PATCH] docs: clarify loader limitations with yaml-backed ast --- taskfile/loader.go | 43 ++++++++++++++++++++++++++++++++ taskfile/loader_test.go | 23 ++++++++++++++++++ taskfile/reader.go | 54 ++++++++++++++++++++++++++++------------- 3 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 taskfile/loader.go create mode 100644 taskfile/loader_test.go diff --git a/taskfile/loader.go b/taskfile/loader.go new file mode 100644 index 0000000000..9fa6805e58 --- /dev/null +++ b/taskfile/loader.go @@ -0,0 +1,43 @@ +package taskfile + +import ( + stdErrors "errors" + + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/internal/filepathext" + "github.com/go-task/task/v3/taskfile/ast" +) + +// Loader defines the behavior required to load a Taskfile from raw data. +// +// Note: the returned [ast.Taskfile] is still backed by YAML-specific +// unmarshalling logic within the ast package. Loaders for alternative +// formats must populate the AST structures directly without relying on the +// YAML-only helpers. Future work will extract those bindings out of the ast +// package so it becomes truly format agnostic. +type Loader interface { + Load(data []byte, location string) (*ast.Taskfile, error) +} + +// YAMLLoader implements [Loader] using YAML as the configuration format. +type YAMLLoader struct{} + +// Load parses the given data as YAML into a Taskfile structure. +func (YAMLLoader) Load(data []byte, location string) (*ast.Taskfile, error) { + var tf ast.Taskfile + if err := yaml.Unmarshal(data, &tf); err != nil { + taskfileDecodeErr := &errors.TaskfileDecodeError{} + if stdErrors.As(err, &taskfileDecodeErr) { + snippet := NewSnippet(data, + WithLine(taskfileDecodeErr.Line), + WithColumn(taskfileDecodeErr.Column), + WithPadding(2), + ) + return nil, taskfileDecodeErr.WithFileInfo(location, snippet.String()) + } + return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(location), Err: err} + } + return &tf, nil +} diff --git a/taskfile/loader_test.go b/taskfile/loader_test.go new file mode 100644 index 0000000000..9002e4bde1 --- /dev/null +++ b/taskfile/loader_test.go @@ -0,0 +1,23 @@ +package taskfile + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/taskfile/ast" +) + +func TestYAMLLoader(t *testing.T) { + t.Parallel() + + data := []byte("version: '3'\n\ntasks:\n default:\n cmds:\n - echo hello\n") + loader := YAMLLoader{} + tf, err := loader.Load(data, "Taskfile.yml") + require.NoError(t, err) + + var expected ast.Taskfile + require.NoError(t, yaml.Unmarshal(data, &expected)) + require.Equal(t, &expected, tf) +} diff --git a/taskfile/reader.go b/taskfile/reader.go index 3f36ad62b2..e2c8dbc3b5 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -4,16 +4,16 @@ import ( "context" "fmt" "os" + "path/filepath" + "strings" "sync" "time" "github.com/dominikbraun/graph" "golang.org/x/sync/errgroup" - "gopkg.in/yaml.v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" - "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) @@ -40,6 +40,7 @@ type ( // [ast.TaskfileGraph] from them. Reader struct { graph *ast.TaskfileGraph + loaders map[string]Loader insecure bool download bool offline bool @@ -55,7 +56,11 @@ type ( // options. func NewReader(opts ...ReaderOption) *Reader { r := &Reader{ - graph: ast.NewTaskfileGraph(), + graph: ast.NewTaskfileGraph(), + loaders: map[string]Loader{ + ".yml": YAMLLoader{}, + ".yaml": YAMLLoader{}, + }, insecure: false, download: false, offline: false, @@ -119,6 +124,24 @@ func (o *offlineOption) ApplyToReader(r *Reader) { r.offline = o.offline } +// WithLoader registers a new [Loader] for the given file extension. If a loader +// already exists for the extension, it will be replaced. +func WithLoader(ext string, loader Loader) ReaderOption { + return &loaderOption{ext: strings.ToLower(ext), loader: loader} +} + +type loaderOption struct { + ext string + loader Loader +} + +func (o *loaderOption) ApplyToReader(r *Reader) { + if r.loaders == nil { + r.loaders = map[string]Loader{} + } + r.loaders[o.ext] = o.loader +} + // WithTempDir sets the temporary directory that will be used by the [Reader]. // By default, the reader uses [os.TempDir]. func WithTempDir(tempDir string) ReaderOption { @@ -324,19 +347,16 @@ func (r *Reader) readNode(ctx context.Context, node Node) (*ast.Taskfile, error) return nil, err } - var tf ast.Taskfile - if err := yaml.Unmarshal(b, &tf); err != nil { - // Decode the taskfile and add the file info the any errors - taskfileDecodeErr := &errors.TaskfileDecodeError{} - if errors.As(err, &taskfileDecodeErr) { - snippet := NewSnippet(b, - WithLine(taskfileDecodeErr.Line), - WithColumn(taskfileDecodeErr.Column), - WithPadding(2), - ) - return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String()) - } - return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} + ext := strings.ToLower(filepath.Ext(node.Location())) + loader, ok := r.loaders[ext] + if !ok { + // Fallback to YAML loader if no loader is registered for the extension + loader = YAMLLoader{} + } + + tf, err := loader.Load(b, node.Location()) + if err != nil { + return nil, err } // Check that the Taskfile is set and has a schema version @@ -357,7 +377,7 @@ func (r *Reader) readNode(ctx context.Context, node Node) (*ast.Taskfile, error) } } - return &tf, nil + return tf, nil } func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error) {