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 }