From b0c792f09ee504a5fbecea869c9068af6588a1d2 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Mon, 10 Aug 2026 21:11:49 +0200 Subject: [PATCH] feat(templater): report template deprecations through the verbose logger sprout logs its deprecation notices to stdout by default, which would corrupt the JSON and group output styles. Route them to the Executor's logger at verbose level instead, deduplicated so that a single deprecated call is not reported once per compilation pass. --- internal/templater/funcs.go | 2 +- internal/templater/notices.go | 57 +++++++++++++++++++++++++++++++++++ setup.go | 4 +++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 internal/templater/notices.go diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go index 7622a53c..5d8b7d1a 100644 --- a/internal/templater/funcs.go +++ b/internal/templater/funcs.go @@ -63,7 +63,7 @@ var legacySprigAliases = sprout.FunctionAliasMap{ func init() { handler := sprout.New( - sprout.WithLogger(slog.New(slog.DiscardHandler)), + sprout.WithLogger(slog.New(noticeHandler{})), sprout.WithRegistries( taskfuncs.NewRegistry(), backward.NewRegistry(), diff --git a/internal/templater/notices.go b/internal/templater/notices.go new file mode 100644 index 00000000..5f03baf9 --- /dev/null +++ b/internal/templater/notices.go @@ -0,0 +1,57 @@ +package templater + +import ( + "context" + "log/slog" + "sync" + "sync/atomic" +) + +// NoticeFunc receives the deprecation notices that sprout emits when a +// template calls a deprecated function name or uses a deprecated argument +// order. +type NoticeFunc func(format string, args ...any) + +// noticeSink is set once the Executor has built its logger. Until then — and +// the function map is built in an init(), long before that — notices are +// dropped rather than written to sprout's default stdout handler, which would +// corrupt the JSON and group output styles. +var noticeSink atomic.Pointer[NoticeFunc] + +// SetNoticeSink installs the destination for template deprecation notices. +// Passing nil silences them again. +func SetNoticeSink(fn NoticeFunc) { + noticeSeen.Clear() + if fn == nil { + noticeSink.Store(nil) + return + } + noticeSink.Store(&fn) +} + +type noticeHandler struct{} + +func (noticeHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= slog.LevelWarn && noticeSink.Load() != nil +} + +// noticeSeen keeps each distinct notice to a single line. Task renders the +// same templates several times per run — once per compilation pass — so +// without this a lone deprecated call would be reported over and over. +var noticeSeen sync.Map + +func (noticeHandler) Handle(_ context.Context, record slog.Record) error { + fn := noticeSink.Load() + if fn == nil { + return nil + } + if _, dup := noticeSeen.LoadOrStore(record.Message, struct{}{}); dup { + return nil + } + (*fn)("task: %s\n", record.Message) + return nil +} + +func (h noticeHandler) WithAttrs([]slog.Attr) slog.Handler { return h } + +func (h noticeHandler) WithGroup(string) slog.Handler { return h } diff --git a/setup.go b/setup.go index f24f8b8e..b705d675 100644 --- a/setup.go +++ b/setup.go @@ -18,6 +18,7 @@ import ( "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" + "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" @@ -192,6 +193,9 @@ func (e *Executor) setupLogger() { AssumeYes: e.AssumeYes, AssumeTerm: e.AssumeTerm, } + templater.SetNoticeSink(func(format string, args ...any) { + e.Logger.VerboseErrf(logger.Yellow, format, args...) + }) } func (e *Executor) setupOutput() error {