22// Use of this source code is governed by a BSD-style
33// license that can be found in the LICENSE file.
44
5- // The evaldoc command takes a local directory path, loads the module,
6- // and prints symbols and whether they have documentation to standard output.
5+ // The evaldoc command takes a module_path@version or a local directory
6+ // path, loads the module, and prints symbols and whether they have
7+ // documentation to standard output.
78//
89// It can be used to better understand the "documentation coverage" score
910// on a package's evaluations page (pkg.go.dev/IMPORT/PATH?tab=evals).
1213package main
1314
1415import (
16+ "context"
1517 "errors"
1618 "flag"
1719 "fmt"
@@ -20,18 +22,25 @@ import (
2022 "go/token"
2123 "io/fs"
2224 "log"
25+ "net/http"
2326 "os"
2427 "path"
2528 "path/filepath"
2629 "strings"
2730
2831 "golang.org/x/mod/modfile"
2932 "golang.org/x/pkgsite/internal/frontend"
33+ "golang.org/x/pkgsite/internal/proxy"
34+ )
35+
36+ var (
37+ proxyURL = flag .String ("proxy" , "" , "module proxy URL (defaults to GOPROXY or https://proxy.golang.org)" )
3038)
3139
3240func main () {
3341 flag .Usage = func () {
34- fmt .Fprintf (flag .CommandLine .Output (), "usage: %s local_dir_path\n " , os .Args [0 ])
42+ fmt .Fprintf (flag .CommandLine .Output (), "usage: %s [flags] (module_path@version | local_dir_path)\n " , os .Args [0 ])
43+ fmt .Fprintln (flag .CommandLine .Output (), "local_dir_path must start with one of: / . ~" )
3544 flag .PrintDefaults ()
3645 }
3746 flag .Parse ()
@@ -65,29 +74,79 @@ func resolveLocalPath(arg string) (string, error) {
6574 return filepath .Abs (arg )
6675}
6776
68- // getContentDir uses arg to find a module at localPath , and returns an fs.FS whose root is the
77+ // getContentDir uses arg to find a module, and returns an fs.FS whose root is the
6978// content directory of that module (the directory containing the go.mod file).
79+ // It also returns the module path.
7080func getContentDir (arg string ) (modulePath string , contentDir fs.FS , err error ) {
7181 if len (arg ) == 0 {
7282 return "" , nil , errors .New ("empty argument" )
7383 }
74- localPath , err := resolveLocalPath (arg )
84+ if arg [0 ] == '/' || arg [0 ] == '.' || arg [0 ] == '~' {
85+ localPath , err := resolveLocalPath (arg )
86+ if err != nil {
87+ return "" , nil , fmt .Errorf ("failed to resolve path %s: %w" , arg , err )
88+ }
89+ fi , err := os .Stat (localPath )
90+ if err != nil || ! fi .IsDir () {
91+ return "" , nil , fmt .Errorf ("%s is not a directory" , localPath )
92+ }
93+ modBytes , err := os .ReadFile (filepath .Join (localPath , "go.mod" ))
94+ if err != nil {
95+ return "" , nil , fmt .Errorf ("failed to read go.mod in %s: %w" , localPath , err )
96+ }
97+ modulePath = modfile .ModulePath (modBytes )
98+ if modulePath == "" {
99+ return "" , nil , fmt .Errorf ("go.mod in %s contains no module path" , localPath )
100+ }
101+ return modulePath , os .DirFS (localPath ), nil
102+ }
103+
104+ var reqVer string
105+ var found bool
106+ modulePath , reqVer , found = strings .Cut (arg , "@" )
107+ if ! found || reqVer == "" {
108+ reqVer = "latest"
109+ }
110+
111+ pURL := * proxyURL
112+ if pURL == "" {
113+ pURL = os .Getenv ("GOPROXY" )
114+ }
115+ if pURL == "off" {
116+ return "" , nil , errors .New ("GOPROXY is off" )
117+ }
118+ if pURL == "" {
119+ pURL = "https://proxy.golang.org"
120+ }
121+ // Take the first URL from the GOPROXY list.
122+ if idx := strings .IndexAny (pURL , ",|" ); idx != - 1 {
123+ pURL = pURL [:idx ]
124+ }
125+
126+ ctx := context .Background ()
127+ proxyClient , err := proxy .New (pURL , http .DefaultTransport )
75128 if err != nil {
76- return "" , nil , fmt .Errorf ("failed to resolve path %s : %w" , arg , err )
129+ return "" , nil , fmt .Errorf ("proxy.New(%q) : %w" , pURL , err )
77130 }
78- fi , err := os .Stat (localPath )
79- if err != nil || ! fi .IsDir () {
80- return "" , nil , fmt .Errorf ("%s is not a directory" , localPath )
131+ proxyClient = proxyClient .WithFetchDisabled ()
132+
133+ verInfo , err := proxyClient .Info (ctx , modulePath , reqVer )
134+ if err != nil {
135+ return "" , nil , fmt .Errorf ("proxyClient.Info(%q, %q): %w" , modulePath , reqVer , err )
81136 }
82- modBytes , err := os .ReadFile (filepath .Join (localPath , "go.mod" ))
137+ resolvedVersion := verInfo .Version
138+
139+ zipReader , err := proxyClient .Zip (ctx , modulePath , resolvedVersion )
83140 if err != nil {
84- return "" , nil , fmt .Errorf ("failed to read go.mod in %s : %w" , localPath , err )
141+ return "" , nil , fmt .Errorf ("proxyClient.Zip(%q, %q) : %w" , modulePath , resolvedVersion , err )
85142 }
86- modulePath = modfile .ModulePath (modBytes )
87- if modulePath == "" {
88- return "" , nil , fmt .Errorf ("go.mod in %s contains no module path" , localPath )
143+
144+ contentDir , err = fs .Sub (zipReader , modulePath + "@" + resolvedVersion )
145+ if err != nil {
146+ return "" , nil , fmt .Errorf ("fs.Sub: %w" , err )
89147 }
90- return modulePath , os .DirFS (localPath ), nil
148+
149+ return modulePath , contentDir , nil
91150}
92151
93152// packageDirs walks contentDir to find directories corresponding to valid import paths,
0 commit comments