github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/doc/pkg.go (about)

     1  package doc
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/doc"
     7  	"go/parser"
     8  	"go/token"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  )
    13  
    14  type pkgData struct {
    15  	name      string
    16  	dir       bfsDir
    17  	fset      *token.FileSet
    18  	files     []*ast.File
    19  	testFiles []*ast.File
    20  	symbols   []symbolData
    21  }
    22  
    23  const (
    24  	symbolDataValue byte = iota
    25  	symbolDataType
    26  	symbolDataFunc
    27  	symbolDataMethod
    28  	symbolDataStructField
    29  	symbolDataInterfaceMethod
    30  )
    31  
    32  type symbolData struct {
    33  	symbol     string
    34  	accessible string
    35  	typ        byte
    36  }
    37  
    38  func newPkgData(dir bfsDir, unexported bool) (*pkgData, error) {
    39  	files, err := os.ReadDir(dir.dir)
    40  	if err != nil {
    41  		return nil, fmt.Errorf("commands/doc: open %q: %w", dir.dir, err)
    42  	}
    43  	pkg := &pkgData{
    44  		dir:  dir,
    45  		fset: token.NewFileSet(),
    46  	}
    47  	for _, file := range files {
    48  		n := file.Name()
    49  		// Ignore files with prefix . or _ like go tools do.
    50  		// Ignore _filetest.gno, but not _test.gno, as we use those to compute
    51  		// examples.
    52  		if file.IsDir() ||
    53  			!strings.HasSuffix(n, ".gno") ||
    54  			strings.HasPrefix(n, ".") ||
    55  			strings.HasPrefix(n, "_") ||
    56  			strings.HasSuffix(n, "_filetest.gno") {
    57  			continue
    58  		}
    59  		fullPath := filepath.Join(dir.dir, n)
    60  		err := pkg.parseFile(fullPath, unexported)
    61  		if err != nil {
    62  			return nil, fmt.Errorf("commands/doc: parse file %q: %w", fullPath, err)
    63  		}
    64  	}
    65  
    66  	if len(pkg.files) == 0 {
    67  		return nil, fmt.Errorf("commands/doc: no valid gno files in %q", dir.dir)
    68  	}
    69  	pkgName := pkg.files[0].Name.Name
    70  	for _, file := range pkg.files[1:] {
    71  		if file.Name.Name != pkgName {
    72  			return nil, fmt.Errorf("commands/doc: multiple packages (%q / %q) in dir %q", pkgName, file.Name.Name, dir.dir)
    73  		}
    74  	}
    75  	pkg.name = pkgName
    76  
    77  	return pkg, nil
    78  }
    79  
    80  func (pkg *pkgData) parseFile(fileName string, unexported bool) error {
    81  	f, err := os.Open(fileName)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	defer f.Close()
    86  	astf, err := parser.ParseFile(pkg.fset, filepath.Base(fileName), f, parser.ParseComments)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	if strings.HasSuffix(fileName, "_test.gno") {
    91  		// add test files separately - we should not add their symbols to the package.
    92  		pkg.testFiles = append(pkg.testFiles, astf)
    93  		return nil
    94  	}
    95  	pkg.files = append(pkg.files, astf)
    96  
    97  	// add symbols
    98  	for _, decl := range astf.Decls {
    99  		switch x := decl.(type) {
   100  		case *ast.FuncDecl:
   101  			// prepend receiver if this is a method
   102  			sd := symbolData{
   103  				symbol: x.Name.Name,
   104  				typ:    symbolDataFunc,
   105  			}
   106  			if x.Recv != nil {
   107  				sd.symbol, sd.accessible = typeExprString(x.Recv.List[0].Type), sd.symbol
   108  				if !unexported && !token.IsExported(sd.symbol) {
   109  					continue
   110  				}
   111  				sd.typ = symbolDataMethod
   112  			}
   113  			pkg.symbols = append(pkg.symbols, sd)
   114  		case *ast.GenDecl:
   115  			for _, spec := range x.Specs {
   116  				pkg.appendSpec(spec, unexported)
   117  			}
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  func (pkg *pkgData) appendSpec(spec ast.Spec, unexported bool) {
   124  	switch s := spec.(type) {
   125  	case *ast.TypeSpec:
   126  		if !unexported && !token.IsExported(s.Name.Name) {
   127  			return
   128  		}
   129  		pkg.symbols = append(pkg.symbols, symbolData{symbol: s.Name.Name, typ: symbolDataType})
   130  		switch st := s.Type.(type) {
   131  		case *ast.StructType:
   132  			pkg.appendFieldList(s.Name.Name, st.Fields, unexported, symbolDataStructField)
   133  		case *ast.InterfaceType:
   134  			pkg.appendFieldList(s.Name.Name, st.Methods, unexported, symbolDataInterfaceMethod)
   135  		}
   136  	case *ast.ValueSpec:
   137  		for _, name := range s.Names {
   138  			if !unexported && !token.IsExported(name.Name) {
   139  				continue
   140  			}
   141  			pkg.symbols = append(pkg.symbols, symbolData{symbol: name.Name, typ: symbolDataValue})
   142  		}
   143  	}
   144  }
   145  
   146  func (pkg *pkgData) appendFieldList(tName string, fl *ast.FieldList, unexported bool, typ byte) {
   147  	if fl == nil {
   148  		return
   149  	}
   150  	for _, field := range fl.List {
   151  		if field.Names == nil {
   152  			if typ == symbolDataInterfaceMethod {
   153  				continue
   154  			}
   155  			embName := typeExprString(field.Type)
   156  			if !unexported && !token.IsExported(embName) {
   157  				continue
   158  			}
   159  			// embedded struct
   160  			pkg.symbols = append(pkg.symbols, symbolData{symbol: tName, accessible: embName, typ: typ})
   161  			continue
   162  		}
   163  		for _, name := range field.Names {
   164  			if !unexported && !token.IsExported(name.Name) {
   165  				continue
   166  			}
   167  			pkg.symbols = append(pkg.symbols, symbolData{symbol: tName, accessible: name.Name, typ: typ})
   168  		}
   169  	}
   170  }
   171  
   172  func typeExprString(expr ast.Expr) string {
   173  	if expr == nil {
   174  		return ""
   175  	}
   176  
   177  	switch t := expr.(type) {
   178  	case *ast.Ident:
   179  		return t.Name
   180  	case *ast.StarExpr:
   181  		return typeExprString(t.X)
   182  	}
   183  	return ""
   184  }
   185  
   186  func (pkg *pkgData) docPackage(opts *WriteDocumentationOptions) (*ast.Package, *doc.Package, error) {
   187  	// largely taken from go/doc.NewFromFiles source
   188  
   189  	// Collect .gno files in a map for ast.NewPackage.
   190  	fileMap := make(map[string]*ast.File)
   191  	for i, file := range pkg.files {
   192  		f := pkg.fset.File(file.Pos())
   193  		if f == nil {
   194  			return nil, nil, fmt.Errorf("commands/doc: file pkg.files[%d] is not found in the provided file set", i)
   195  		}
   196  		fileMap[f.Name()] = file
   197  	}
   198  
   199  	// from cmd/doc/pkg.go:
   200  	// go/doc does not include typed constants in the constants
   201  	// list, which is what we want. For instance, time.Sunday is of type
   202  	// time.Weekday, so it is defined in the type but not in the
   203  	// Consts list for the package. This prevents
   204  	//	go doc time.Sunday
   205  	// from finding the symbol. This is why we always have AllDecls.
   206  	mode := doc.AllDecls
   207  	if opts.Source {
   208  		mode |= doc.PreserveAST
   209  	}
   210  
   211  	// Compute package documentation.
   212  	// Assign to blank to ignore errors that can happen due to unresolved identifiers.
   213  	astpkg, _ := ast.NewPackage(pkg.fset, fileMap, simpleImporter, nil)
   214  	p := doc.New(astpkg, pkg.dir.importPath, mode)
   215  	// TODO: classifyExamples(p, Examples(testGoFiles...))
   216  
   217  	return astpkg, p, nil
   218  }
   219  
   220  func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
   221  	pkg := imports[path]
   222  	if pkg == nil {
   223  		// note that strings.LastIndex returns -1 if there is no "/"
   224  		pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
   225  		pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
   226  		imports[path] = pkg
   227  	}
   228  	return pkg, nil
   229  }