modfile: parse deprecation notices in module comments

Deprecation notices start with "Deprecated:" at the beginning of a
line and run until the end of the paragraph.

This CL reuses text extraction code for retraction rationale, so the
same rules apply: comment text may be from the comments above a
"module" directive or as a suffix on the same line.

For golang/go#40357

Change-Id: Id5524149c6bbda3effc64c6b668b701b5cf428af
Reviewed-on: https://go-review.googlesource.com/c/mod/+/301089
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/modfile/rule.go b/modfile/rule.go
index 8843aee..3f603fa 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -47,8 +47,9 @@
 
 // A Module is the module statement.
 type Module struct {
-	Mod    module.Version
-	Syntax *Line
+	Mod        module.Version
+	Deprecated string
+	Syntax     *Line
 }
 
 // A Go is the go statement.
@@ -278,7 +279,11 @@
 			errorf("repeated module statement")
 			return
 		}
-		f.Module = &Module{Syntax: line}
+		deprecated := parseDeprecation(block, line)
+		f.Module = &Module{
+			Syntax:     line,
+			Deprecated: deprecated,
+		}
 		if len(args) != 1 {
 			errorf("usage: module module/path")
 			return
@@ -392,7 +397,7 @@
 		})
 
 	case "retract":
-		rationale := parseRetractRationale(block, line)
+		rationale := parseDirectiveComment(block, line)
 		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
 		if err != nil {
 			if strict {
@@ -619,10 +624,29 @@
 	return t, nil
 }
 
-// parseRetractRationale extracts the rationale for a retract directive from the
-// surrounding comments. If the line does not have comments and is part of a
-// block that does have comments, the block's comments are used.
-func parseRetractRationale(block *LineBlock, line *Line) string {
+var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`)
+
+// parseDeprecation extracts the text of comments on a "module" directive and
+// extracts a deprecation message from that.
+//
+// A deprecation message is contained in a paragraph within a block of comments
+// that starts with "Deprecated:" (case sensitive). The message runs until the
+// end of the paragraph and does not include the "Deprecated:" prefix. If the
+// comment block has multiple paragraphs that start with "Deprecated:",
+// parseDeprecation returns the message from the first.
+func parseDeprecation(block *LineBlock, line *Line) string {
+	text := parseDirectiveComment(block, line)
+	m := deprecatedRE.FindStringSubmatch(text)
+	if m == nil {
+		return ""
+	}
+	return m[1]
+}
+
+// parseDirectiveComment extracts the text of comments on a directive.
+// If the directive's line does not have comments and is part of a block that
+// does have comments, the block's comments are used.
+func parseDirectiveComment(block *LineBlock, line *Line) string {
 	comments := line.Comment()
 	if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
 		comments = block.Comment()
diff --git a/modfile/rule_test.go b/modfile/rule_test.go
index 96ef036..d721c71 100644
--- a/modfile/rule_test.go
+++ b/modfile/rule_test.go
@@ -499,6 +499,118 @@
 	},
 }
 
+var moduleDeprecatedTests = []struct {
+	desc, in, want string
+}{
+	// retractRationaleTests exercises some of the same code, so these tests
+	// don't exhaustively cover comment extraction.
+	{
+		`no_comment`,
+		`module m`,
+		``,
+	},
+	{
+		`other_comment`,
+		`// yo
+		module m`,
+		``,
+	},
+	{
+		`deprecated_no_colon`,
+		`//Deprecated
+		module m`,
+		``,
+	},
+	{
+		`deprecated_no_space`,
+		`//Deprecated:blah
+		module m`,
+		`blah`,
+	},
+	{
+		`deprecated_simple`,
+		`// Deprecated: blah
+		module m`,
+		`blah`,
+	},
+	{
+		`deprecated_lowercase`,
+		`// deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_multiline`,
+		`// Deprecated: one
+		// two
+		module m`,
+		"one\ntwo",
+	},
+	{
+		`deprecated_mixed`,
+		`// some other comment
+		// Deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_middle`,
+		`// module m is Deprecated: blah
+		module m`,
+		``,
+	},
+	{
+		`deprecated_multiple`,
+		`// Deprecated: a
+		// Deprecated: b
+		module m`,
+		"a\nDeprecated: b",
+	},
+	{
+		`deprecated_paragraph`,
+		`// Deprecated: a
+		// b
+		//
+		// c
+		module m`,
+		"a\nb",
+	},
+	{
+		`deprecated_paragraph_space`,
+		`// Deprecated: the next line has a space
+		// 
+		// c
+		module m`,
+		"the next line has a space",
+	},
+	{
+		`deprecated_suffix`,
+		`module m // Deprecated: blah`,
+		`blah`,
+	},
+	{
+		`deprecated_mixed_suffix`,
+		`// some other comment
+		module m // Deprecated: blah`,
+		``,
+	},
+	{
+		`deprecated_mixed_suffix_paragraph`,
+		`// some other comment
+		//
+		module m // Deprecated: blah`,
+		`blah`,
+	},
+	{
+		`deprecated_block`,
+		`// Deprecated: blah
+		module (
+			m
+		)`,
+		`blah`,
+	},
+}
+
 var sortBlocksTests = []struct {
 	desc, in, out string
 	strict        bool
@@ -848,6 +960,20 @@
 	}
 }
 
+func TestModuleDeprecated(t *testing.T) {
+	for _, tt := range moduleDeprecatedTests {
+		t.Run(tt.desc, func(t *testing.T) {
+			f, err := Parse("in", []byte(tt.in), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if f.Module.Deprecated != tt.want {
+				t.Errorf("got %q; want %q", f.Module.Deprecated, tt.want)
+			}
+		})
+	}
+}
+
 func TestSortBlocks(t *testing.T) {
 	for _, tt := range sortBlocksTests {
 		t.Run(tt.desc, func(t *testing.T) {