github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/doc/doc.go (about) 1 // Package doc implements support for documentation of Gno packages and realms, 2 // in a similar fashion to `go doc`. 3 // As a reference, the [official implementation] for `go doc` is used. 4 // 5 // [official implementation]: https://github.com/golang/go/tree/90dde5dec1126ddf2236730ec57511ced56a512d/src/cmd/doc 6 package doc 7 8 import ( 9 "errors" 10 "fmt" 11 "go/ast" 12 "go/doc" 13 "go/token" 14 "io" 15 "log" 16 "os" 17 "path/filepath" 18 "strings" 19 20 "go.uber.org/multierr" 21 ) 22 23 // WriteDocumentationOptions represents the possible options when requesting 24 // documentation through Documentable. 25 type WriteDocumentationOptions struct { 26 // ShowAll shows all symbols when displaying documentation about a package. 27 ShowAll bool 28 // Source shows the source code when documenting a symbol. 29 Source bool 30 // Unexported shows unexported symbols as well as exported. 31 Unexported bool 32 // Short shows a one-line representation for each symbol. 33 Short bool 34 35 w io.Writer 36 } 37 38 // Documentable is a package, symbol, or accessible which can be documented. 39 type Documentable interface { 40 WriteDocumentation(w io.Writer, opts *WriteDocumentationOptions) error 41 } 42 43 // static implementation check 44 var _ Documentable = (*documentable)(nil) 45 46 type documentable struct { 47 bfsDir 48 symbol string 49 accessible string 50 pkgData *pkgData 51 } 52 53 func (d *documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOptions) error { 54 if o == nil { 55 o = &WriteDocumentationOptions{} 56 } 57 o.w = w 58 59 var err error 60 // pkgData may already be initialised if we already had to look to see 61 // if it had the symbol we wanted; otherwise initialise it now. 62 if d.pkgData == nil { 63 d.pkgData, err = newPkgData(d.bfsDir, o.Unexported) 64 if err != nil { 65 return err 66 } 67 } 68 69 astpkg, pkg, err := d.pkgData.docPackage(o) 70 if err != nil { 71 return err 72 } 73 74 // copied from go source - map vars, constants and constructors to their respective types. 75 typedValue := make(map[*doc.Value]bool) 76 constructor := make(map[*doc.Func]bool) 77 for _, typ := range pkg.Types { 78 pkg.Consts = append(pkg.Consts, typ.Consts...) 79 pkg.Vars = append(pkg.Vars, typ.Vars...) 80 pkg.Funcs = append(pkg.Funcs, typ.Funcs...) 81 if !o.Unexported && !token.IsExported(typ.Name) { 82 continue 83 } 84 for _, value := range typ.Consts { 85 typedValue[value] = true 86 } 87 for _, value := range typ.Vars { 88 typedValue[value] = true 89 } 90 for _, fun := range typ.Funcs { 91 // We don't count it as a constructor bound to the type 92 // if the type itself is not exported. 93 constructor[fun] = true 94 } 95 } 96 97 pp := &pkgPrinter{ 98 name: d.pkgData.name, 99 pkg: astpkg, 100 file: ast.MergePackageFiles(astpkg, 0), 101 doc: pkg, 102 typedValue: typedValue, 103 constructor: constructor, 104 fs: d.pkgData.fset, 105 opt: o, 106 importPath: d.importPath, 107 } 108 pp.buf.pkg = pp 109 110 return d.output(pp) 111 } 112 113 func (d *documentable) output(pp *pkgPrinter) (err error) { 114 defer func() { 115 // handle the case of errFatal. 116 // this will have been generated by pkg.Fatalf, so get the error 117 // from pp.err. 118 e := recover() 119 ee, ok := e.(error) 120 if e != nil && ok && errors.Is(ee, errFatal) { 121 panic(e) 122 } 123 124 flushErr := pp.flush() 125 if pp.err == nil { 126 err = pp.err 127 } 128 if flushErr != nil { 129 err = multierr.Combine(err, fmt.Errorf("error flushing: %w", err)) 130 } 131 }() 132 133 switch { 134 case d.symbol == "" && d.accessible == "": 135 if pp.opt.ShowAll { 136 pp.allDoc() 137 return 138 } 139 pp.packageDoc() 140 case d.symbol != "" && d.accessible == "": 141 pp.symbolDoc(d.symbol) 142 default: // both non-empty 143 if pp.methodDoc(d.symbol, d.accessible) { 144 return 145 } 146 if pp.fieldDoc(d.symbol, d.accessible) { 147 return 148 } 149 } 150 151 return 152 } 153 154 // set as a variable so it can be changed by testing. 155 var fpAbs = filepath.Abs 156 157 // ResolveDocumentable returns a Documentable from the given arguments. 158 // Refer to the documentation of gno doc for the formats accepted (in general 159 // the same as the go doc command). 160 // An error may be returned even if documentation was resolved in case some 161 // packages in dirs could not be parsed correctly. 162 // 163 // dirs specifies the gno system directories to scan which specify full import paths 164 // in their directories, such as @/examples and @/gnovm/stdlibs; modDirs specifies 165 // directories which contain a gno.mod file. 166 func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (Documentable, error) { 167 d := newDirs(dirs, modDirs) 168 169 parsed, ok := parseArgs(args) 170 if !ok { 171 return nil, fmt.Errorf("commands/doc: invalid arguments: %v", args) 172 } 173 return resolveDocumentable(d, parsed, unexported) 174 } 175 176 func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (Documentable, error) { 177 var candidates []bfsDir 178 179 // if we have a candidate package name, search dirs for a dir that matches it. 180 // prefer directories whose import path match precisely the package 181 if s, err := os.Stat(parsed.pkg); err == nil && s.IsDir() { 182 // expand to full path - fpAbs is filepath.Abs except in test 183 absVal, err := fpAbs(parsed.pkg) 184 if err == nil { 185 candidates = dirs.findDir(absVal) 186 } else { 187 // this is very rare - generally syscall failure or os.Getwd failing 188 log.Printf("warning: could not determine abs path: %v", err) 189 } 190 } else if err != nil && !os.IsNotExist(err) { 191 // also quite rare, generally will be permission errors (in reading cwd) 192 log.Printf("warning: tried showing documentation for directory %q, error: %v", parsed.pkg, err) 193 } 194 // arg is either not a dir, or if it matched a local dir it was not 195 // valid (ie. not scanned by dirs). try parsing as a package 196 if len(candidates) == 0 { 197 candidates = dirs.findPackage(parsed.pkg) 198 } 199 200 if len(candidates) == 0 { 201 // there are no candidates. 202 // if this is ambiguous, remove ambiguity and try parsing args using pkg as the symbol. 203 if !parsed.pkgAmbiguous { 204 return nil, fmt.Errorf("commands/doc: package not found: %q", parsed.pkg) 205 } 206 parsed = docArgs{pkg: ".", sym: parsed.pkg, acc: parsed.sym} 207 return resolveDocumentable(dirs, parsed, unexported) 208 } 209 // we wanted documentation about a package, and we found one! 210 if parsed.sym == "" { 211 return &documentable{bfsDir: candidates[0]}, nil 212 } 213 214 // we also have a symbol, and maybe accessible. 215 // search for the symbol through the candidates 216 217 doc := &documentable{ 218 symbol: parsed.sym, 219 accessible: parsed.acc, 220 } 221 222 var matchFunc func(s symbolData) bool 223 if parsed.acc == "" { 224 matchFunc = func(s symbolData) bool { 225 return (s.accessible == "" && symbolMatch(parsed.sym, s.symbol)) || 226 (s.typ == symbolDataMethod && symbolMatch(parsed.sym, s.accessible)) 227 } 228 } else { 229 matchFunc = func(s symbolData) bool { 230 return symbolMatch(parsed.sym, s.symbol) && symbolMatch(parsed.acc, s.accessible) 231 } 232 } 233 234 var errs []error 235 for _, candidate := range candidates { 236 pd, err := newPkgData(candidate, unexported) 237 if err != nil { 238 // report errors as warning, but don't fail because of them 239 // likely ast/parsing errors. 240 errs = append(errs, err) 241 continue 242 } 243 for _, sym := range pd.symbols { 244 if !matchFunc(sym) { 245 continue 246 } 247 doc.bfsDir = candidate 248 doc.pkgData = pd 249 // match found. return this as documentable. 250 return doc, multierr.Combine(errs...) 251 } 252 } 253 return nil, multierr.Append( 254 fmt.Errorf("commands/doc: could not resolve arguments: %+v", parsed), 255 multierr.Combine(errs...), 256 ) 257 } 258 259 // docArgs represents the parsed args of the doc command. 260 // sym could be a symbol, but the accessibles of types should also be shown if they match sym. 261 type docArgs struct { 262 pkg string // always set 263 sym string 264 acc string // short for "accessible". only set if sym is also set 265 266 // pkg could be a symbol in the local dir. 267 // if that is the case, and sym != "", then sym, acc = pkg, sym 268 pkgAmbiguous bool 269 } 270 271 func parseArgs(args []string) (docArgs, bool) { 272 switch len(args) { 273 case 0: 274 return docArgs{pkg: "."}, true 275 case 1: 276 // allowed syntaxes (acc is method or field, [] marks optional): 277 // <pkg> 278 // [<pkg>.]<sym>[.<acc>] 279 // [<pkg>.][<sym>.]<acc> 280 // if the (part) argument contains a slash, then it is most certainly 281 // a pkg. 282 // note: pkg can be a relative path. this is mostly problematic for ".." and 283 // ".". so we count full stops from the last slash. 284 slash := strings.LastIndexByte(args[0], '/') 285 if args[0] == "." || args[0] == ".." || 286 (slash != -1 && args[0][slash+1:] == "..") { 287 // special handling for common ., .. and <dir>/.. 288 // these will generally work poorly if you try to use the one-argument 289 // syntax to access a symbol/accessible. 290 return docArgs{pkg: args[0]}, true 291 } 292 switch strings.Count(args[0][slash+1:], ".") { 293 case 0: 294 if slash != -1 { 295 return docArgs{pkg: args[0]}, true 296 } 297 return docArgs{pkg: args[0], pkgAmbiguous: true}, true 298 case 1: 299 pos := strings.IndexByte(args[0][slash+1:], '.') + slash + 1 300 if slash != -1 { 301 return docArgs{pkg: args[0][:pos], sym: args[0][pos+1:]}, true 302 } 303 if token.IsExported(args[0]) { 304 // See rationale here: 305 // https://github.com/golang/go/blob/90dde5dec1126ddf2236730ec57511ced56a512d/src/cmd/doc/main.go#L265 306 return docArgs{pkg: ".", sym: args[0][:pos], acc: args[0][pos+1:]}, true 307 } 308 return docArgs{pkg: args[0][:pos], sym: args[0][pos+1:], pkgAmbiguous: true}, true 309 case 2: 310 // pkg.sym.acc 311 parts := strings.Split(args[0][slash+1:], ".") 312 return docArgs{ 313 pkg: args[0][:slash+1] + parts[0], 314 sym: parts[1], 315 acc: parts[2], 316 }, true 317 default: 318 return docArgs{}, false 319 } 320 case 2: 321 switch strings.Count(args[1], ".") { 322 case 0: 323 return docArgs{pkg: args[0], sym: args[1]}, true 324 case 1: 325 pos := strings.IndexByte(args[1], '.') 326 return docArgs{pkg: args[0], sym: args[1][:pos], acc: args[1][pos+1:]}, true 327 default: 328 return docArgs{}, false 329 } 330 default: 331 return docArgs{}, false 332 } 333 }