Skip to content

Commit a9c847c

Browse files
committed
internal/fetch: extract "Deprecated" module comment
When processing a module, parse its go.mod file and extract the comment that deprecates the module, if there is one. For golang/go#41321 Change-Id: I24a458ee706e14c3d77fca86413ae2587866a59c Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/290097 Trust: Jonathan Amsterdam <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Julie Qiu <[email protected]>
1 parent 3c4365e commit a9c847c

File tree

3 files changed

+112
-15
lines changed

3 files changed

+112
-15
lines changed

internal/discovery.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ type ModuleInfo struct {
4545
Version string
4646
CommitTime time.Time
4747
IsRedistributable bool
48-
HasGoMod bool // whether the module zip has a go.mod file
48+
// Whether the module zip has a go.mod file.
49+
HasGoMod bool
50+
// The text after the Deprecated comment (could be empty), or nil if no comment.
51+
DeprecatedComment *string
4952
SourceInfo *source.Info
5053
}
5154

internal/fetch/fetch.go

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path"
1515
"sort"
1616
"strconv"
17+
"strings"
1718
"sync"
1819
"time"
1920

@@ -184,7 +185,8 @@ func FetchModule(ctx context.Context, modulePath, requestedVersion string, proxy
184185

185186
// getGoModPath may return a non-empty goModPath even if the error is
186187
// non-nil, if the module version is an alternative module.
187-
fr.GoModPath, fr.Error = getGoModPath(ctx, modulePath, fr.ResolvedVersion, proxyClient)
188+
var goModBytes []byte
189+
fr.GoModPath, goModBytes, fr.Error = getGoModPath(ctx, modulePath, fr.ResolvedVersion, proxyClient)
188190
if fr.Error != nil {
189191
return fr
190192
}
@@ -213,6 +215,12 @@ func FetchModule(ctx context.Context, modulePath, requestedVersion string, proxy
213215
fr.Error = err
214216
return fr
215217
}
218+
if goModBytes != nil {
219+
if err := processGoModFile(goModBytes, mod); err != nil {
220+
fr.Error = fmt.Errorf("%v: %w", err.Error(), derrors.BadModule)
221+
return fr
222+
}
223+
}
216224
fr.Module = mod
217225
fr.PackageVersionStates = pvs
218226
if modulePath == stdlib.ModulePath {
@@ -252,26 +260,27 @@ func getZipSize(ctx context.Context, modulePath, resolvedVersion string, proxyCl
252260
return proxyClient.GetZipSize(ctx, modulePath, resolvedVersion)
253261
}
254262

255-
func getGoModPath(ctx context.Context, modulePath, resolvedVersion string, proxyClient *proxy.Client) (_ string, err error) {
263+
// getGoModPath returns the module path from the go.mod file, as well as the contents of the file obtained from the proxy.
264+
// If modulePath is the standardl library, then the contents will be nil.
265+
func getGoModPath(ctx context.Context, modulePath, resolvedVersion string, proxyClient *proxy.Client) (string, []byte, error) {
256266
if modulePath == stdlib.ModulePath {
257-
return stdlib.ModulePath, nil
267+
return stdlib.ModulePath, nil, nil
258268
}
259-
260269
goModBytes, err := proxyClient.GetMod(ctx, modulePath, resolvedVersion)
261270
if err != nil {
262-
return "", err
271+
return "", nil, err
263272
}
264273
goModPath := modfile.ModulePath(goModBytes)
265274
if goModPath == "" {
266-
return "", fmt.Errorf("go.mod has no module path: %w", derrors.BadModule)
275+
return "", nil, fmt.Errorf("go.mod has no module path: %w", derrors.BadModule)
267276
}
268277
if goModPath != modulePath {
269278
// The module path in the go.mod file doesn't match the path of the
270279
// zip file. Don't insert the module. Store an AlternativeModule
271280
// status in module_version_states.
272-
return goModPath, fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, goModPath, derrors.AlternativeModule)
281+
return goModPath, goModBytes, fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, goModPath, derrors.AlternativeModule)
273282
}
274-
return goModPath, nil
283+
return goModPath, goModBytes, nil
275284
}
276285

277286
// processZipFile extracts information from the module version zip.
@@ -301,8 +310,7 @@ func processZipFile(ctx context.Context, modulePath string, resolvedVersion stri
301310
if err != nil {
302311
return nil, nil, fmt.Errorf("extractPackagesFromZip(%q, %q, zipReader, %v): %v", modulePath, resolvedVersion, allLicenses, err)
303312
}
304-
hasGoMod := zipContainsFilename(zipReader, path.Join(moduleVersionDir(modulePath, resolvedVersion), "go.mod"))
305-
313+
hasGoMod := zipFile(zipReader, path.Join(moduleVersionDir(modulePath, resolvedVersion), "go.mod")) != nil
306314
return &internal.Module{
307315
ModuleInfo: internal.ModuleInfo{
308316
ModulePath: modulePath,
@@ -317,20 +325,54 @@ func processZipFile(ctx context.Context, modulePath string, resolvedVersion stri
317325
}, packageVersionStates, nil
318326
}
319327

328+
// processGoModFile populates mod with information extracted from the contents of the go.mod file.
329+
func processGoModFile(goModBytes []byte, mod *internal.Module) (err error) {
330+
defer derrors.Wrap(&err, "processGoModFile")
331+
332+
mf, err := modfile.Parse("go.mod", goModBytes, nil)
333+
if err != nil {
334+
return err
335+
}
336+
hasDepComment, depComment := extractDeprecatedComment(mf)
337+
if hasDepComment {
338+
mod.DeprecatedComment = &depComment
339+
}
340+
return nil
341+
}
342+
343+
// extractDeprecatedComment looks for "Deprecated" comments in the line comments
344+
// before the module declaration. If it finds one, it returns true along with
345+
// the text after "Deprecated:". Otherwise it returns false, "".
346+
func extractDeprecatedComment(mf *modfile.File) (bool, string) {
347+
const prefix = "Deprecated:"
348+
349+
if mf.Module == nil {
350+
return false, ""
351+
}
352+
for _, comment := range append(mf.Module.Syntax.Before, mf.Module.Syntax.Suffix...) {
353+
text := strings.TrimSpace(strings.TrimPrefix(comment.Token, "//"))
354+
if strings.HasPrefix(text, prefix) {
355+
return true, strings.TrimSpace(text[len(prefix):])
356+
}
357+
}
358+
return false, ""
359+
}
360+
320361
// moduleVersionDir formats the content subdirectory for the given
321362
// modulePath and version.
322363
func moduleVersionDir(modulePath, version string) string {
323364
return fmt.Sprintf("%s@%s", modulePath, version)
324365
}
325366

326-
// zipContainsFilename reports whether there is a file with the given name in the zip.
327-
func zipContainsFilename(r *zip.Reader, name string) bool {
367+
// zipFile returns the file in r whose name matches the given name, or nil
368+
// if there isn't one.
369+
func zipFile(r *zip.Reader, name string) *zip.File {
328370
for _, f := range r.File {
329371
if f.Name == name {
330-
return true
372+
return f
331373
}
332374
}
333-
return false
375+
return nil
334376
}
335377

336378
type FetchInfo struct {

internal/fetch/fetch_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/google/go-cmp/cmp"
1919
"github.com/google/go-cmp/cmp/cmpopts"
2020
"github.com/google/safehtml/template"
21+
"golang.org/x/mod/modfile"
2122
"golang.org/x/pkgsite/internal"
2223
"golang.org/x/pkgsite/internal/derrors"
2324
"golang.org/x/pkgsite/internal/godoc"
@@ -186,3 +187,54 @@ func TestFetchModule_Errors(t *testing.T) {
186187
}
187188
}
188189
}
190+
191+
func TestExtractDeprecatedComment(t *testing.T) {
192+
for _, test := range []struct {
193+
name string
194+
in string
195+
wantHas bool
196+
wantComment string
197+
}{
198+
{"no comment", `module m`, false, ""},
199+
{"valid comment",
200+
`
201+
// Deprecated: use v2
202+
module m
203+
`, true, "use v2"},
204+
{"take first",
205+
`
206+
// Deprecated: use v2
207+
// Deprecated: use v3
208+
module m
209+
`, true, "use v2"},
210+
{"ignore others",
211+
`
212+
// c1
213+
// Deprecated: use v2
214+
// c2
215+
module m
216+
`, true, "use v2"},
217+
{"must be capitalized",
218+
`
219+
// c1
220+
// deprecated: use v2
221+
// c2
222+
module m
223+
`, false, ""},
224+
{"suffix",
225+
`
226+
// c1
227+
module m // Deprecated: use v2
228+
`, true, "use v2",
229+
},
230+
} {
231+
mf, err := modfile.Parse("test", []byte(test.in), nil)
232+
if err != nil {
233+
t.Fatal(err)
234+
}
235+
gotHas, gotComment := extractDeprecatedComment(mf)
236+
if gotHas != test.wantHas || gotComment != test.wantComment {
237+
t.Errorf("%s: got (%t, %q), want(%t, %q)", test.name, gotHas, gotComment, test.wantHas, test.wantComment)
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)