Repository: gohugoio/hugo (language: go)

Issue:
Make build.writeStats a struct
E.g. 

```go
writeStats struct {
  tags bool
  classes bool
  ids bool
}
```


Discussion / hints:

Fix patch:
diff --git a/config/commonConfig.go b/config/commonConfig.go
index 09f81c1ba05..0374994654e 100644
--- a/config/commonConfig.go
+++ b/config/commonConfig.go
@@ -82,7 +82,7 @@ type LoadConfigResult struct {
 
 var defaultBuild = BuildConfig{
 	UseResourceCacheWhen: "fallback",
-	WriteStats:           false,
+	WriteStats:           WriteStats{},
 
 	CacheBusters: []CacheBuster{
 		{
@@ -111,7 +111,8 @@ type BuildConfig struct {
 
 	// When enabled, will collect and write a hugo_stats.json with some build
 	// related aggregated data (e.g. CSS class names).
-	WriteStats bool
+	// Note that this was a bool <= v0.115.0.
+	WriteStats WriteStats
 
 	// Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js
 	// file.
@@ -121,6 +122,17 @@ type BuildConfig struct {
 	CacheBusters []CacheBuster
 }
 
+// WriteStats configures what to write to the hugo_stats.json file.
+type WriteStats struct {
+	Tags    bool
+	Classes bool
+	IDs     bool
+}
+
+func (w WriteStats) Enabled() bool {
+	return w.Tags || w.Classes || w.IDs
+}
+
 func (b BuildConfig) clone() BuildConfig {
 	b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
 	return b
@@ -171,14 +183,26 @@ func (b *BuildConfig) CompileConfig(logger loggers.Logger) error {
 
 func DecodeBuildConfig(cfg Provider) BuildConfig {
 	m := cfg.GetStringMap("build")
+
 	b := defaultBuild.clone()
 	if m == nil {
 		return b
 	}
 
+	// writeStats was a bool <= v0.115.0.
+	if writeStats, ok := m["writestats"]; ok {
+		if bb, ok := writeStats.(bool); ok {
+			m["writestats"] = WriteStats{
+				Tags:    bb,
+				Classes: bb,
+				IDs:     bb,
+			}
+		}
+	}
+
 	err := mapstructure.WeakDecode(m, &b)
 	if err != nil {
-		return defaultBuild
+		return b
 	}
 
 	b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen)
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 9a645f3a56f..2c8ca0aae90 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -475,7 +475,7 @@ func (h *HugoSites) writeBuildStats() error {
 	if h.ResourceSpec == nil {
 		panic("h.ResourceSpec is nil")
 	}
-	if !h.ResourceSpec.BuildConfig().WriteStats {
+	if !h.ResourceSpec.BuildConfig().WriteStats.Enabled() {
 		return nil
 	}
 
diff --git a/publisher/htmlElementsCollector.go b/publisher/htmlElementsCollector.go
index c3b88c4cc14..0805023528a 100644
--- a/publisher/htmlElementsCollector.go
+++ b/publisher/htmlElementsCollector.go
@@ -24,6 +24,7 @@ import (
 
 	"golang.org/x/net/html"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/helpers"
 )
 
@@ -46,8 +47,9 @@ var (
 	}
 )
 
-func newHTMLElementsCollector() *htmlElementsCollector {
+func newHTMLElementsCollector(conf config.WriteStats) *htmlElementsCollector {
 	return &htmlElementsCollector{
+		conf:       conf,
 		elementSet: make(map[string]bool),
 	}
 }
@@ -93,6 +95,8 @@ type htmlElement struct {
 }
 
 type htmlElementsCollector struct {
+	conf config.WriteStats
+
 	// Contains the raw HTML string. We will get the same element
 	// several times, and want to avoid costly reparsing when this
 	// is used for aggregated data only.
@@ -113,7 +117,9 @@ func (c *htmlElementsCollector) getHTMLElements() HTMLElements {
 	for _, el := range c.elements {
 		classes = append(classes, el.Classes...)
 		ids = append(ids, el.IDs...)
-		tags = append(tags, el.Tag)
+		if c.conf.Tags {
+			tags = append(tags, el.Tag)
+		}
 	}
 
 	classes = helpers.UniqueStringsSorted(classes)
@@ -246,7 +252,7 @@ func (w *htmlElementsCollectorWriter) lexElementInside(resolve htmlCollectorStat
 			}
 
 			// Parse each collected element.
-			el, err := parseHTMLElement(s)
+			el, err := w.parseHTMLElement(s)
 			if err != nil {
 				w.err = err
 				return resolve
@@ -363,7 +369,13 @@ func htmlLexToEndOfComment(w *htmlElementsCollectorWriter) htmlCollectorStateFun
 	return htmlLexToEndOfComment
 }
 
-func parseHTMLElement(elStr string) (el htmlElement, err error) {
+func (w *htmlElementsCollectorWriter) parseHTMLElement(elStr string) (el htmlElement, err error) {
+	conf := w.collector.conf
+
+	if !conf.IDs && !conf.Classes {
+		// Nothing to do.
+		return
+	}
 
 	tagName := parseStartTag(elStr)
 
@@ -390,8 +402,13 @@ func parseHTMLElement(elStr string) (el htmlElement, err error) {
 				switch {
 				case strings.EqualFold(a.Key, "id"):
 					// There should be only one, but one never knows...
-					el.IDs = append(el.IDs, a.Val)
+					if conf.IDs {
+						el.IDs = append(el.IDs, a.Val)
+					}
 				default:
+					if !conf.Classes {
+						continue
+					}
 					if classAttrRe.MatchString(a.Key) {
 						el.Classes = append(el.Classes, strings.Fields(a.Val)...)
 					} else {
diff --git a/publisher/publisher.go b/publisher/publisher.go
index 970c93e6c88..37d8b36e408 100644
--- a/publisher/publisher.go
+++ b/publisher/publisher.go
@@ -81,8 +81,8 @@ func NewDestinationPublisher(rs *resources.Spec, outputFormats output.Formats, m
 	fs := rs.BaseFs.PublishFs
 	cfg := rs.Cfg
 	var classCollector *htmlElementsCollector
-	if rs.BuildConfig().WriteStats {
-		classCollector = newHTMLElementsCollector()
+	if rs.BuildConfig().WriteStats.Enabled() {
+		classCollector = newHTMLElementsCollector(rs.BuildConfig().WriteStats)
 	}
 	pub = DestinationPublisher{fs: fs, htmlElementsCollector: classCollector}
 	pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)


Test patch:
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 3910e2b978b..ada01b6eefc 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -147,6 +147,15 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
 			if match == "" || strings.HasPrefix(match, "#") {
 				continue
 			}
+			var negate bool
+			if strings.HasPrefix(match, "! ") {
+				negate = true
+				match = strings.TrimPrefix(match, "! ")
+			}
+			if negate {
+				s.Assert(content, qt.Not(qt.Contains), match, qt.Commentf(m))
+				continue
+			}
 			s.Assert(content, qt.Contains, match, qt.Commentf(m))
 		}
 	}
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index be59b17a7f2..8265c06d073 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -1162,6 +1162,89 @@ Some text.
 	}
 }
 
+func TestClassCollectorConfigWriteStats(t *testing.T) {
+	r := func(writeStatsConfig string) *IntegrationTestBuilder {
+		files := `
+-- hugo.toml --
+WRITE_STATS_CONFIG
+-- layouts/_default/list.html --
+<div id="myid" class="myclass">Foo</div>
+
+`
+		files = strings.Replace(files, "WRITE_STATS_CONFIG", writeStatsConfig, 1)
+
+		b := NewIntegrationTestBuilder(
+			IntegrationTestConfig{
+				T:           t,
+				TxtarString: files,
+				NeedsOsFS:   true,
+			},
+		).Build()
+
+		return b
+	}
+
+	// Legacy config.
+	b := r(`
+[build]
+writeStats = true
+`)
+
+	b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
+
+	b = r(`
+[build]
+writeStats = false
+	`)
+
+	b.AssertDestinationExists("hugo_stats.json", false)
+
+	b = r(`
+[build.writeStats]
+tags = true
+classes = true
+ids = true
+	`)
+
+	b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
+
+	b = r(`
+[build.writeStats]
+tags = true
+classes = true
+ids = false
+`)
+
+	b.AssertFileContent("hugo_stats.json", "myclass", "div", "! myid")
+
+	b = r(`
+[build.writeStats]
+tags = true
+classes = false
+ids = true
+`)
+
+	b.AssertFileContent("hugo_stats.json", "! myclass", "div", "myid")
+
+	b = r(`
+[build.writeStats]
+tags = false
+classes = true
+ids = true
+	`)
+
+	b.AssertFileContent("hugo_stats.json", "myclass", "! div", "myid")
+
+	b = r(`
+[build.writeStats]
+tags = false
+classes = false
+ids = false
+	`)
+	b.AssertDestinationExists("hugo_stats.json", false)
+
+}
+
 func TestClassCollectorStress(t *testing.T) {
 	statsFilename := "hugo_stats.json"
 	defer os.Remove(statsFilename)
diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go
index 7aeda0dafb0..51b34a3d629 100644
--- a/publisher/htmlElementsCollector_test.go
+++ b/publisher/htmlElementsCollector_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/config/testconfig"
 	"github.com/gohugoio/hugo/media"
 	"github.com/gohugoio/hugo/minifiers"
@@ -136,7 +137,13 @@ func TestClassCollector(t *testing.T) {
 		} {
 
 			c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) {
-				w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
+				w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
+					config.WriteStats{
+						Tags:    true,
+						Classes: true,
+						IDs:     true,
+					},
+				))
 				if variant.minify {
 					if skipMinifyTest[test.name] {
 						c.Skip("skip minify test")
@@ -240,7 +247,13 @@ func BenchmarkElementsCollectorWriter(b *testing.B) {
 </html>
 `
 	for i := 0; i < b.N; i++ {
-		w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
+		w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
+			config.WriteStats{
+				Tags:    true,
+				Classes: true,
+				IDs:     true,
+			},
+		))
 		fmt.Fprint(w, benchHTML)
 
 	}
@@ -262,7 +275,13 @@ func BenchmarkElementsCollectorWriterPre(b *testing.B) {
 <div class="foo"></div>
 
 `
-	w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
+	w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
+		config.WriteStats{
+			Tags:    true,
+			Classes: true,
+			IDs:     true,
+		},
+	))
 	for i := 0; i < b.N; i++ {
 		fmt.Fprint(w, benchHTML)