internal/report: refactor - make Summary its own type

Convert Summary (previously a plain string) to a named type so that
methods can be defined on it. Move logic to lint summaries to a new
method, Summary.lint.

Change-Id: I97afb15369dec19a5ed9e2b1865da59d90bae940
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/542356
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/vulnreport/main.go b/cmd/vulnreport/main.go
index 55227a4..c6ad84b 100644
--- a/cmd/vulnreport/main.go
+++ b/cmd/vulnreport/main.go
@@ -719,7 +719,7 @@
 	if any(r.CVEs) || any(r.GHSAs) {
 		return true
 	}
-	return is(r.Summary) || is(r.Description) || any(r.Credits)
+	return is(r.Summary.String()) || is(r.Description) || any(r.Credits)
 }
 
 // addReferenceTODOs adds a TODO for each important reference type not
diff --git a/cmd/vulnreport/suggest.go b/cmd/vulnreport/suggest.go
index ee12982..5dc7895 100644
--- a/cmd/vulnreport/suggest.go
+++ b/cmd/vulnreport/suggest.go
@@ -66,7 +66,7 @@
 			}
 			switch choice {
 			case "a":
-				r.Summary = s.Summary
+				r.Summary = report.Summary(s.Summary)
 				r.Description = s.Description
 				if err := r.Write(filename); err != nil {
 					errlog.Println(err)
diff --git a/internal/genericosv/report.go b/internal/genericosv/report.go
index e271edb..12a1aef 100644
--- a/internal/genericosv/report.go
+++ b/internal/genericosv/report.go
@@ -26,7 +26,7 @@
 func (osv *Entry) ToReport(goID string, pc *proxy.Client) *report.Report {
 	r := &report.Report{
 		ID:          goID,
-		Summary:     osv.Summary,
+		Summary:     report.Summary(osv.Summary),
 		Description: osv.Details,
 	}
 	addAlias := func(alias string) {
diff --git a/internal/palmapi/gen_examples/main.go b/internal/palmapi/gen_examples/main.go
index b766831..cb53679 100644
--- a/internal/palmapi/gen_examples/main.go
+++ b/internal/palmapi/gen_examples/main.go
@@ -132,7 +132,7 @@
 				Description: v.ghsa.Details,
 			},
 			Suggestion: palmapi.Suggestion{
-				Summary:     removeNewlines(v.r.Summary),
+				Summary:     removeNewlines(v.r.Summary.String()),
 				Description: removeNewlines(v.r.Description),
 			},
 		}
diff --git a/internal/report/cve5.go b/internal/report/cve5.go
index af54f64..fa36095 100644
--- a/internal/report/cve5.go
+++ b/internal/report/cve5.go
@@ -45,7 +45,7 @@
 		ProviderMetadata: cveschema5.ProviderMetadata{
 			OrgID: GoOrgUUID,
 		},
-		Title: removeNewlines(r.Summary),
+		Title: removeNewlines(r.Summary.String()),
 		Descriptions: []cveschema5.Description{
 			{
 				Lang:  "en",
diff --git a/internal/report/fix.go b/internal/report/fix.go
index 8e99e0c..86de67c 100644
--- a/internal/report/fix.go
+++ b/internal/report/fix.go
@@ -26,7 +26,7 @@
 	fixLines := func(sp *string) {
 		*sp = fixLineLength(*sp, maxLineLength)
 	}
-	fixLines(&r.Summary)
+	fixLines((*string)(&r.Summary))
 	fixLines(&r.Description)
 	if r.CVEMetadata != nil {
 		fixLines(&r.CVEMetadata.Description)
diff --git a/internal/report/ghsa.go b/internal/report/ghsa.go
index 998bc73..c489ec9 100644
--- a/internal/report/ghsa.go
+++ b/internal/report/ghsa.go
@@ -15,7 +15,7 @@
 // GHSAToReport creates a Report struct from a given GHSA SecurityAdvisory and modulePath.
 func GHSAToReport(sa *ghsa.SecurityAdvisory, modulePath string, pc *proxy.Client) *Report {
 	r := &Report{
-		Summary:     sa.Summary,
+		Summary:     Summary(sa.Summary),
 		Description: sa.Description,
 	}
 	var cves, ghsas []string
diff --git a/internal/report/lint.go b/internal/report/lint.go
index 6d24875..ee05761 100644
--- a/internal/report/lint.go
+++ b/internal/report/lint.go
@@ -294,6 +294,22 @@
 	}
 }
 
+func (s *Summary) lint(addIssue func(string)) {
+	summary := s.String()
+	if len(summary) == 0 {
+		addIssue("missing summary")
+	}
+	if strings.HasPrefix(summary, "TODO") {
+		addIssue("summary contains a TODO")
+	}
+	if l := len(summary); l > 100 {
+		addIssue(fmt.Sprintf("summary is too long: %d characters (max 100)", l))
+	}
+	if strings.HasSuffix(summary, ".") {
+		addIssue("summary should not end in a period (should be a phrase, not a sentence)")
+	}
+}
+
 func (r *Report) IsExcluded() bool {
 	return r.Excluded != ""
 }
@@ -392,18 +408,7 @@
 			addIssue("no modules")
 		}
 		r.lintDescription(addIssue)
-		if r.Summary == "" {
-			addIssue("missing summary")
-		}
-		if strings.HasPrefix(r.Summary, "TODO") {
-			addIssue("summary contains a TODO")
-		}
-		if l := len(r.Summary); l > 100 {
-			addIssue(fmt.Sprintf("summary is too long: %d characters (max 100)", l))
-		}
-		if strings.HasSuffix(r.Summary, ".") {
-			addIssue("summary should not end in a period (should be a phrase, not a sentence)")
-		}
+		r.Summary.lint(addIssue)
 	}
 
 	isFirstParty := false
diff --git a/internal/report/osv.go b/internal/report/osv.go
index a9f7e8e..54f83a1 100644
--- a/internal/report/osv.go
+++ b/internal/report/osv.go
@@ -50,7 +50,7 @@
 	// govulncheck can robustly display summaries in place of details.
 	details := r.Description
 	if details == "" {
-		details = r.Summary
+		details = r.Summary.String()
 	}
 
 	entry := osv.Entry{
@@ -59,7 +59,7 @@
 		Modified:         osv.Time{Time: lastModified},
 		Withdrawn:        withdrawn,
 		Related:          r.Related,
-		Summary:          toParagraphs(r.Summary),
+		Summary:          toParagraphs(r.Summary.String()),
 		Details:          toParagraphs(details),
 		Credits:          credits,
 		SchemaVersion:    SchemaVersion,
diff --git a/internal/report/report.go b/internal/report/report.go
index aa9ce42..cd9f389 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -189,7 +189,7 @@
 	Modules []*Module `yaml:",omitempty"`
 
 	// Summary is a short phrase describing the vulnerability.
-	Summary string `yaml:",omitempty"`
+	Summary Summary `yaml:",omitempty"`
 
 	// Description is the CVE description from an existing CVE. If we are
 	// assigning a CVE ID ourselves, use CVEMetadata.Description instead.
@@ -223,6 +223,12 @@
 	Notes []*Note `yaml:",omitempty"`
 }
 
+type Summary string
+
+func (s *Summary) String() string {
+	return string(*s)
+}
+
 // GoCVE returns the CVE assigned to this report by the Go CNA,
 // or the empty string if not applicable.
 func (r *Report) GoCVE() string {