Repository: gohugoio/hugo (language: go)

Issue:
Add --printUnusedTemplates
I'm doing some cleanup in an old Hugo site, and it would save me lots of work if Hugo had a way of printing the templates not in use.


Discussion / hints:
While you are at that, could you have a re-look at #8375, please? It feels connected to what files/procedures you will touch here.

Fix patch:
diff --git a/commands/commands.go b/commands/commands.go
index 63707e368be..10b7a6649c5 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -306,6 +306,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
 	cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
 	cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
 	cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
+	cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
 	cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
 	cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
 	cmd.Flags().BoolVarP(&cc.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
diff --git a/commands/hugo.go b/commands/hugo.go
index ce3c4ab7ba3..8c5294f006d 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -31,6 +31,7 @@ import (
 	"time"
 
 	"github.com/gohugoio/hugo/hugofs/files"
+	"github.com/gohugoio/hugo/tpl"
 
 	"github.com/gohugoio/hugo/common/types"
 
@@ -217,6 +218,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
 		"force",
 		"gc",
 		"printI18nWarnings",
+		"printUnusedTemplates",
 		"invalidateCDN",
 		"layoutDir",
 		"logFile",
@@ -501,7 +503,6 @@ func (c *commandeer) build() error {
 		return err
 	}
 
-	// TODO(bep) Feedback?
 	if !c.h.quiet {
 		fmt.Println()
 		c.hugo().PrintProcessingStats(os.Stdout)
@@ -513,6 +514,11 @@ func (c *commandeer) build() error {
 				c.logger.Warnln("Duplicate target paths:", dupes)
 			}
 		}
+
+		unusedTemplates := c.hugo().Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
+		for _, unusedTemplate := range unusedTemplates {
+			c.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
+		}
 	}
 
 	if c.h.buildWatch {
diff --git a/docs/content/en/commands/hugo.md b/docs/content/en/commands/hugo.md
index 088a8102f37..0aa81719a7e 100644
--- a/docs/content/en/commands/hugo.md
+++ b/docs/content/en/commands/hugo.md
@@ -53,6 +53,7 @@ hugo [flags]
       --printI18nWarnings          print missing translations
       --printMemoryUsage           print memory usage to screen at intervals
       --printPathWarnings          print warnings on duplicate target paths etc.
+      --printUnusedTemplates       print warnings on unused templates.
       --quiet                      build in quiet mode
       --renderToMemory             render to memory (only useful for benchmark testing)
   -s, --source string              filesystem path to read files relative from
diff --git a/docs/content/en/commands/hugo_mod.md b/docs/content/en/commands/hugo_mod.md
index b0860f29990..afcda982378 100644
--- a/docs/content/en/commands/hugo_mod.md
+++ b/docs/content/en/commands/hugo_mod.md
@@ -49,6 +49,7 @@ See https://gohugo.io/hugo-modules/ for more information.
       --printI18nWarnings      print missing translations
       --printMemoryUsage       print memory usage to screen at intervals
       --printPathWarnings      print warnings on duplicate target paths etc.
+      --printUnusedTemplates   print warnings on unused templates.
       --templateMetrics        display metrics about template executions
       --templateMetricsHints   calculate some improvement hints when combined with --templateMetrics
   -t, --theme strings          themes to use (located in /themes/THEMENAME/)
diff --git a/docs/content/en/commands/hugo_new.md b/docs/content/en/commands/hugo_new.md
index e8a7d3b0802..a8b08496338 100644
--- a/docs/content/en/commands/hugo_new.md
+++ b/docs/content/en/commands/hugo_new.md
@@ -50,6 +50,7 @@ hugo new [path] [flags]
       --printI18nWarnings      print missing translations
       --printMemoryUsage       print memory usage to screen at intervals
       --printPathWarnings      print warnings on duplicate target paths etc.
+      --printUnusedTemplates   print warnings on unused templates.
       --templateMetrics        display metrics about template executions
       --templateMetricsHints   calculate some improvement hints when combined with --templateMetrics
   -t, --theme strings          themes to use (located in /themes/THEMENAME/)
diff --git a/docs/content/en/commands/hugo_server.md b/docs/content/en/commands/hugo_server.md
index abdb64a5eed..c224c4a52c4 100644
--- a/docs/content/en/commands/hugo_server.md
+++ b/docs/content/en/commands/hugo_server.md
@@ -63,6 +63,7 @@ hugo server [flags]
       --printI18nWarnings      print missing translations
       --printMemoryUsage       print memory usage to screen at intervals
       --printPathWarnings      print warnings on duplicate target paths etc.
+      --printUnusedTemplates   print warnings on unused templates.
       --renderToDisk           render to Destination path (default is render to memory & serve from there)
       --templateMetrics        display metrics about template executions
       --templateMetricsHints   calculate some improvement hints when combined with --templateMetrics
diff --git a/tpl/template.go b/tpl/template.go
index 0375b4a1776..c5a6a44c0c4 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -44,6 +44,11 @@ type TemplateFinder interface {
 	TemplateLookupVariant
 }
 
+// UnusedTemplatesProvider lists unused templates if the build is configured to track those.
+type UnusedTemplatesProvider interface {
+	UnusedTemplates() []FileInfo
+}
+
 // TemplateHandler finds and executes templates.
 type TemplateHandler interface {
 	TemplateFinder
diff --git a/tpl/template_info.go b/tpl/template_info.go
index d9b438138bf..c21c0ae7d5f 100644
--- a/tpl/template_info.go
+++ b/tpl/template_info.go
@@ -27,6 +27,11 @@ type Info interface {
 	identity.Provider
 }
 
+type FileInfo interface {
+	Name() string
+	Filename() string
+}
+
 type InfoManager interface {
 	ParseInfo() ParseInfo
 
@@ -65,10 +70,6 @@ func (info ParseInfo) IsZero() bool {
 	return info.Config.Version == 0
 }
 
-// Info holds some info extracted from a parsed template.
-type Info1 struct {
-}
-
 type ParseConfig struct {
 	Version int
 }
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 66ef7fc21b1..9d2911ee95b 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -67,10 +67,11 @@ var embeddedTemplatesAliases = map[string][]string{
 }
 
 var (
-	_ tpl.TemplateManager    = (*templateExec)(nil)
-	_ tpl.TemplateHandler    = (*templateExec)(nil)
-	_ tpl.TemplateFuncGetter = (*templateExec)(nil)
-	_ tpl.TemplateFinder     = (*templateExec)(nil)
+	_ tpl.TemplateManager         = (*templateExec)(nil)
+	_ tpl.TemplateHandler         = (*templateExec)(nil)
+	_ tpl.TemplateFuncGetter      = (*templateExec)(nil)
+	_ tpl.TemplateFinder          = (*templateExec)(nil)
+	_ tpl.UnusedTemplatesProvider = (*templateExec)(nil)
 
 	_ tpl.Template = (*templateState)(nil)
 	_ tpl.Info     = (*templateState)(nil)
@@ -130,6 +131,11 @@ func newTemplateExec(d *deps.Deps) (*templateExec, error) {
 		funcMap[k] = v.Interface()
 	}
 
+	var templateUsageTracker map[string]templateInfo
+	if d.Cfg.GetBool("printUnusedTemplates") {
+		templateUsageTracker = make(map[string]templateInfo)
+	}
+
 	h := &templateHandler{
 		nameBaseTemplateName: make(map[string]string),
 		transformNotFound:    make(map[string]*templateState),
@@ -146,6 +152,8 @@ func newTemplateExec(d *deps.Deps) (*templateExec, error) {
 		layoutHandler:       output.NewLayoutHandler(),
 		layoutsFs:           d.BaseFs.Layouts.Fs,
 		layoutTemplateCache: make(map[layoutCacheKey]tpl.Template),
+
+		templateUsageTracker: templateUsageTracker,
 	}
 
 	if err := h.loadEmbedded(); err != nil {
@@ -225,13 +233,72 @@ func (t *templateExec) Execute(templ tpl.Template, wr io.Writer, data interface{
 		defer t.Metrics.MeasureSince(templ.Name(), time.Now())
 	}
 
+	if t.templateUsageTracker != nil {
+		if ts, ok := templ.(*templateState); ok {
+			t.templateUsageTrackerMu.Lock()
+			if _, found := t.templateUsageTracker[ts.Name()]; !found {
+				t.templateUsageTracker[ts.Name()] = ts.info
+			}
+
+			if !ts.baseInfo.IsZero() {
+				if _, found := t.templateUsageTracker[ts.baseInfo.name]; !found {
+					t.templateUsageTracker[ts.baseInfo.name] = ts.baseInfo
+				}
+			}
+			t.templateUsageTrackerMu.Unlock()
+		}
+	}
+
 	execErr := t.executor.Execute(templ, wr, data)
 	if execErr != nil {
 		execErr = t.addFileContext(templ, execErr)
 	}
+
 	return execErr
 }
 
+// TODO1
+func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
+	if t.templateUsageTracker == nil {
+		return nil
+	}
+	var unused []tpl.FileInfo
+
+	for _, ti := range t.needsBaseof {
+		if _, found := t.templateUsageTracker[ti.name]; !found {
+			unused = append(unused, ti)
+		}
+	}
+
+	for _, ti := range t.baseof {
+		if _, found := t.templateUsageTracker[ti.name]; !found {
+			unused = append(unused, ti)
+		}
+	}
+
+	for _, ts := range t.main.templates {
+		ti := ts.info
+		if strings.HasPrefix(ti.name, "_internal/") {
+			continue
+		}
+		if strings.HasPrefix(ti.name, "partials/inline/pagination") {
+			// TODO(bep) we need to fix this. These are internal partials, but
+			// they may also be defined in the project, which currently could
+			// lead to some false negatives.
+			continue
+		}
+		if _, found := t.templateUsageTracker[ti.name]; !found {
+			unused = append(unused, ti)
+		}
+	}
+
+	sort.Slice(unused, func(i, j int) bool {
+		return unused[i].Name() < unused[j].Name()
+	})
+
+	return unused
+}
+
 func (t *templateExec) GetFunc(name string) (reflect.Value, bool) {
 	v, found := t.funcs[name]
 	return v, found
@@ -285,6 +352,10 @@ type templateHandler struct {
 	// Note that for shortcodes that same information is embedded in the
 	// shortcodeTemplates type.
 	templateInfo map[string]tpl.Info
+
+	// May be nil.
+	templateUsageTracker   map[string]templateInfo
+	templateUsageTrackerMu sync.Mutex
 }
 
 // AddTemplate parses and adds a template to the collection.
diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go
index df80726f5d6..06d89553693 100644
--- a/tpl/tplimpl/template_errors.go
+++ b/tpl/tplimpl/template_errors.go
@@ -34,6 +34,14 @@ type templateInfo struct {
 	realFilename string
 }
 
+func (t templateInfo) Name() string {
+	return t.name
+}
+
+func (t templateInfo) Filename() string {
+	return t.realFilename
+}
+
 func (t templateInfo) IsZero() bool {
 	return t.name == ""
 }


Test patch:
diff --git a/commands/commands_test.go b/commands/commands_test.go
index 9e623e9a27c..b4fb8962134 100644
--- a/commands/commands_test.go
+++ b/commands/commands_test.go
@@ -197,6 +197,7 @@ func TestFlags(t *testing.T) {
 				"--renderToDisk",
 				"--source=mysource",
 				"--printPathWarnings",
+				"--printUnusedTemplates",
 			},
 			check: func(c *qt.C, sc *serverCmd) {
 				c.Assert(sc, qt.Not(qt.IsNil))
diff --git a/tpl/tplimpl/integration_test.go b/tpl/tplimpl/integration_test.go
new file mode 100644
index 00000000000..f71e28bc1f2
--- /dev/null
+++ b/tpl/tplimpl/integration_test.go
@@ -0,0 +1,61 @@
+package tplimpl_test
+
+import (
+	"path/filepath"
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/hugolib"
+	"github.com/gohugoio/hugo/tpl"
+)
+
+func TestPrintUnusedTemplates(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+printUnusedTemplates=true
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< usedshortcode >}}
+-- layouts/baseof.html --
+{{ block "main" . }}{{ end }}
+-- layouts/baseof.json --
+{{ block "main" . }}{{ end }}
+-- layouts/index.html --
+{{ define "main" }}FOO{{ end }}
+-- layouts/_default/single.json --
+-- layouts/_default/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/post/single.html --
+{{ define "main" }}MAIN{{ end }}
+-- layouts/partials/usedpartial.html --
+-- layouts/partials/unusedpartial.html --
+-- layouts/shortcodes/usedshortcode.html --
+{{ partial "usedpartial.html" }}
+-- layouts/shortcodes/unusedshortcode.html --
+
+	`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+		},
+	)
+	b.Build()
+
+	unused := b.H.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
+
+	var names []string
+	for _, tmpl := range unused {
+		names = append(names, tmpl.Name())
+	}
+
+	b.Assert(names, qt.DeepEquals, []string{"_default/single.json", "baseof.json", "partials/unusedpartial.html", "post/single.html", "shortcodes/unusedshortcode.html"})
+	b.Assert(unused[0].Filename(), qt.Equals, filepath.Join(b.Cfg.WorkingDir, "layouts/_default/single.json"))
+}