internal: types and functions for the raw latest version
Add RawLatestInfo, which describes the raw latest version of a module,
and provide functions to compute deprecation and retractions from it.
For golang/go#44437
Change-Id: I2f1aa03bf190b961642828fdc7b708e2ee47a20a
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/295196
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/raw_latest.go b/internal/raw_latest.go
new file mode 100644
index 0000000..c38daff
--- /dev/null
+++ b/internal/raw_latest.go
@@ -0,0 +1,58 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package internal
+
+import (
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/semver"
+)
+
+// RawLatestInfo describes the "raw" latest version of a module:
+// the latest version without considering retractions or the like.
+// The go.mod file of the raw latest version establishes whether
+// the module is deprecated, and what versions are retracted.
+type RawLatestInfo struct {
+ ModulePath string
+ Version string
+ GoModFile *modfile.File
+}
+
+// PopulateModule uses the RawLatestInfo to populate fields of the given module.
+func (r *RawLatestInfo) PopulateModule(m *Module) {
+ m.Deprecated, m.DeprecationComment = isDeprecated(r.GoModFile)
+ m.Retracted, m.RetractionRationale = isRetracted(r.GoModFile, m.Version)
+}
+
+// isDeprecated reports whether the go.mod deprecates this module.
+// It looks for "Deprecated" comments in the line comments before and next to
+// the module declaration. If it finds one, it returns true along with the text
+// after "Deprecated:". Otherwise it returns false, "".
+func isDeprecated(mf *modfile.File) (bool, string) {
+ const prefix = "Deprecated:"
+
+ if mf.Module == nil {
+ return false, ""
+ }
+ for _, comment := range append(mf.Module.Syntax.Before, mf.Module.Syntax.Suffix...) {
+ text := strings.TrimSpace(strings.TrimPrefix(comment.Token, "//"))
+ if strings.HasPrefix(text, prefix) {
+ return true, strings.TrimSpace(text[len(prefix):])
+ }
+ }
+ return false, ""
+}
+
+// isRetracted reports whether the go.mod file retracts the version.
+// If so, it returns true along with the rationale for the retraction.
+func isRetracted(mf *modfile.File, resolvedVersion string) (bool, string) {
+ for _, r := range mf.Retract {
+ if semver.Compare(resolvedVersion, r.Low) >= 0 && semver.Compare(resolvedVersion, r.High) <= 0 {
+ return true, r.Rationale
+ }
+ }
+ return false, ""
+}
diff --git a/internal/raw_latest_test.go b/internal/raw_latest_test.go
new file mode 100644
index 0000000..a5c2b42
--- /dev/null
+++ b/internal/raw_latest_test.go
@@ -0,0 +1,93 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package internal
+
+import (
+ "testing"
+
+ "golang.org/x/mod/modfile"
+)
+
+func TestIsDeprecated(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ in string
+ wantIs bool
+ wantComment string
+ }{
+ {"no comment", `module m`, false, ""},
+ {"valid comment",
+ `
+ // Deprecated: use v2
+ module m
+ `, true, "use v2"},
+ {"take first",
+ `
+ // Deprecated: use v2
+ // Deprecated: use v3
+ module m
+ `, true, "use v2"},
+ {"ignore others",
+ `
+ // c1
+ // Deprecated: use v2
+ // c2
+ module m
+ `, true, "use v2"},
+ {"must be capitalized",
+ `
+ // c1
+ // deprecated: use v2
+ // c2
+ module m
+ `, false, ""},
+ {"suffix",
+ `
+ // c1
+ module m // Deprecated: use v2
+ `, true, "use v2",
+ },
+ } {
+ mf, err := modfile.Parse("test", []byte(test.in), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotIs, gotComment := isDeprecated(mf)
+ if gotIs != test.wantIs || gotComment != test.wantComment {
+ t.Errorf("%s: got (%t, %q), want(%t, %q)", test.name, gotIs, gotComment, test.wantIs, test.wantComment)
+ }
+ }
+}
+
+func TestIsRetracted(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ file string
+ wantIs bool
+ wantRationale string
+ }{
+ {"no retract", "module M", false, ""},
+ {"retracted", "module M\nretract v1.2.3", true, ""},
+ {"retracted with comment", "module M\nretract v1.2.3 // bad ", true, "bad"},
+ {"retracted range", "module M\nretract [v1.2.0, v1.3.0] // bad", true, "bad"},
+ {
+ "not retracted", `
+ module M
+ retract [v1.2.0, v1.2.2]
+ retract [v1.4.0, v1.99.0]
+ `,
+ false, "",
+ },
+ } {
+ mf, err := modfile.Parse("test", []byte(test.file), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ gotIs, gotRationale := isRetracted(mf, "v1.2.3")
+ if gotIs != test.wantIs || gotRationale != test.wantRationale {
+ t.Errorf("%s: got (%t, %q), want(%t, %q)", test.name, gotIs, gotRationale, test.wantIs, test.wantRationale)
+ }
+ }
+}