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 {