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  }