github.com/dkischenko/gomarkdoc@v0.0.0-20230516135336-e40deae8a495/lang/package.go (about) 1 package lang 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/build" 7 "go/doc" 8 "go/parser" 9 "go/token" 10 "io/ioutil" 11 "os" 12 "path" 13 "path/filepath" 14 "regexp" 15 "strings" 16 17 "github.com/dkischenko/gomarkdoc/logger" 18 ) 19 20 type ( 21 // Package holds documentation information for a package and all of the 22 // symbols contained within it. 23 Package struct { 24 cfg *Config 25 doc *doc.Package 26 examples []*doc.Example 27 } 28 29 // PackageOptions holds options related to the configuration of the package 30 // and its documentation on creation. 31 PackageOptions struct { 32 includeUnexported bool 33 repositoryOverrides *Repo 34 } 35 36 // PackageOption configures one or more options for the package. 37 PackageOption func(opts *PackageOptions) error 38 ) 39 40 // NewPackage creates a representation of a package's documentation from the 41 // raw documentation constructs provided by the standard library. This is only 42 // recommended for advanced scenarios. Most consumers will find it easier to use 43 // NewPackageFromBuild instead. 44 func NewPackage(cfg *Config, examples []*doc.Example) *Package { 45 return &Package{cfg, cfg.Pkg, examples} 46 } 47 48 // NewPackageFromBuild creates a representation of a package's documentation 49 // from the build metadata for that package. It can be configured using the 50 // provided options. 51 func NewPackageFromBuild(log logger.Logger, pkg *build.Package, opts ...PackageOption) (*Package, error) { 52 var options PackageOptions 53 for _, opt := range opts { 54 if err := opt(&options); err != nil { 55 return nil, err 56 } 57 } 58 59 wd, err := os.Getwd() 60 if err != nil { 61 return nil, err 62 } 63 64 cfg, err := NewConfig(log, wd, pkg.Dir, ConfigWithRepoOverrides(options.repositoryOverrides)) 65 if err != nil { 66 return nil, err 67 } 68 69 cfg.Pkg, err = getDocPkg(pkg, cfg.FileSet, options.includeUnexported) 70 if err != nil { 71 return nil, err 72 } 73 74 sym := PackageSymbols(cfg.Pkg) 75 cfg.Symbols = sym 76 77 examples := doc.Examples(cfg.Files...) 78 79 return NewPackage(cfg, examples), nil 80 } 81 82 // PackageWithUnexportedIncluded can be used along with the NewPackageFromBuild 83 // function to specify that all symbols, including unexported ones, should be 84 // included in the documentation for the package. 85 func PackageWithUnexportedIncluded() PackageOption { 86 return func(opts *PackageOptions) error { 87 opts.includeUnexported = true 88 return nil 89 } 90 } 91 92 // PackageWithRepositoryOverrides can be used along with the NewPackageFromBuild 93 // function to define manual overrides to the automatic repository detection 94 // logic. 95 func PackageWithRepositoryOverrides(repo *Repo) PackageOption { 96 return func(opts *PackageOptions) error { 97 opts.repositoryOverrides = repo 98 return nil 99 } 100 } 101 102 // Level provides the default level that headers for the package's root 103 // documentation should be rendered. 104 func (pkg *Package) Level() int { 105 return pkg.cfg.Level 106 } 107 108 // Dir provides the name of the full directory in which the package is located. 109 func (pkg *Package) Dir() string { 110 return pkg.cfg.PkgDir 111 } 112 113 // Dirname provides the name of the leaf directory in which the package is 114 // located. 115 func (pkg *Package) Dirname() string { 116 return filepath.Base(pkg.cfg.PkgDir) 117 } 118 119 // Name provides the name of the package as it would be seen from another 120 // package importing it. 121 func (pkg *Package) Name() string { 122 return pkg.doc.Name 123 } 124 125 // Import provides the raw text for the import declaration that is used to 126 // import code from the package. If your package's documentation is generated 127 // from a local path and does not use Go Modules, this will typically print 128 // `import "."`. 129 func (pkg *Package) Import() string { 130 return fmt.Sprintf(`import "%s"`, pkg.doc.ImportPath) 131 } 132 133 // ImportPath provides the identifier used for the package when installing or 134 // importing the package. If your package's documentation is generated from a 135 // local path and does not use Go Modules, this will typically print `.`. 136 func (pkg *Package) ImportPath() string { 137 return pkg.doc.ImportPath 138 } 139 140 // Summary provides the one-sentence summary of the package's documentation 141 // comment. 142 func (pkg *Package) Summary() string { 143 return extractSummary(pkg.doc.Doc) 144 } 145 146 // Doc provides the structured contents of the documentation comment for the 147 // package. 148 func (pkg *Package) Doc() *Doc { 149 // TODO: level should only be + 1, but we have special knowledge for rendering 150 return NewDoc(pkg.cfg.Inc(2), pkg.doc.Doc) 151 } 152 153 // Consts lists the top-level constants provided by the package. 154 func (pkg *Package) Consts() (consts []*Value) { 155 for _, c := range pkg.doc.Consts { 156 consts = append(consts, NewValue(pkg.cfg.Inc(1), c)) 157 } 158 159 return 160 } 161 162 // Vars lists the top-level variables provided by the package. 163 func (pkg *Package) Vars() (vars []*Value) { 164 for _, v := range pkg.doc.Vars { 165 vars = append(vars, NewValue(pkg.cfg.Inc(1), v)) 166 } 167 168 return 169 } 170 171 // Funcs lists the top-level functions provided by the package. 172 func (pkg *Package) Funcs() (funcs []*Func) { 173 for _, fn := range pkg.doc.Funcs { 174 funcs = append(funcs, NewFunc(pkg.cfg.Inc(1), fn, pkg.examples)) 175 } 176 177 return 178 } 179 180 // Types lists the top-level types provided by the package. 181 func (pkg *Package) Types() (types []*Type) { 182 for _, typ := range pkg.doc.Types { 183 types = append(types, NewType(pkg.cfg.Inc(1), typ, pkg.examples)) 184 } 185 186 return 187 } 188 189 // Examples provides the package-level examples that have been defined. This 190 // does not include examples that are associated with symbols contained within 191 // the package. 192 func (pkg *Package) Examples() (examples []*Example) { 193 for _, example := range pkg.examples { 194 var name string 195 switch { 196 case example.Name == "": 197 name = "" 198 case strings.HasPrefix(example.Name, "_"): 199 name = example.Name[1:] 200 default: 201 // TODO: better filtering 202 continue 203 } 204 205 examples = append(examples, NewExample(pkg.cfg.Inc(1), name, example)) 206 } 207 208 return 209 } 210 211 var goModRegex = regexp.MustCompile(`^\s*module ([^\s]+)`) 212 213 // findImportPath attempts to find an import path for the contents of the 214 // provided dir by walking up to the nearest go.mod file and constructing an 215 // import path from it. If the directory is not in a Go Module, the second 216 // return value will be false. 217 func findImportPath(dir string) (string, bool) { 218 absDir, err := filepath.Abs(dir) 219 if err != nil { 220 return "", false 221 } 222 223 f, ok := findFileInParent(absDir, "go.mod", false) 224 if !ok { 225 return "", false 226 } 227 defer f.Close() 228 229 b, err := ioutil.ReadAll(f) 230 if err != nil { 231 return "", false 232 } 233 234 m := goModRegex.FindSubmatch(b) 235 if m == nil { 236 return "", false 237 } 238 239 relative, err := filepath.Rel(filepath.Dir(f.Name()), absDir) 240 if err != nil { 241 return "", false 242 } 243 244 relative = filepath.ToSlash(relative) 245 246 return path.Join(string(m[1]), relative), true 247 } 248 249 // findFileInParent looks for a file or directory of the given name within the 250 // provided dir. The returned os.File is opened and must be closed by the 251 // caller to avoid a memory leak. 252 func findFileInParent(dir, filename string, fileIsDir bool) (*os.File, bool) { 253 initial := dir 254 current := initial 255 256 for { 257 p := filepath.Join(current, filename) 258 if f, err := os.Open(p); err == nil { 259 if s, err := f.Stat(); err == nil && (fileIsDir && s.Mode().IsDir() || !fileIsDir && s.Mode().IsRegular()) { 260 return f, true 261 } 262 } 263 264 // Walk up a dir 265 next := filepath.Join(current, "..") 266 267 // If we didn't change dirs, there's no more to search 268 if current == next { 269 break 270 } 271 272 current = next 273 } 274 275 return nil, false 276 } 277 278 func getDocPkg(pkg *build.Package, fs *token.FileSet, includeUnexported bool) (*doc.Package, error) { 279 pkgs, err := parser.ParseDir( 280 fs, 281 pkg.Dir, 282 func(info os.FileInfo) bool { 283 for _, name := range pkg.GoFiles { 284 if name == info.Name() { 285 return true 286 } 287 } 288 289 for _, name := range pkg.CgoFiles { 290 if name == info.Name() { 291 return true 292 } 293 } 294 295 return false 296 }, 297 parser.ParseComments, 298 ) 299 300 if err != nil { 301 return nil, fmt.Errorf("gomarkdoc: failed to parse package: %w", err) 302 } 303 304 if len(pkgs) == 0 { 305 return nil, fmt.Errorf("gomarkdoc: no source-code package in directory %s", pkg.Dir) 306 } 307 308 if len(pkgs) > 1 { 309 return nil, fmt.Errorf("gomarkdoc: multiple packages in directory %s", pkg.Dir) 310 } 311 312 astPkg := pkgs[pkg.Name] 313 314 if !includeUnexported { 315 ast.PackageExports(astPkg) 316 } 317 318 importPath := pkg.ImportPath 319 if pkg.ImportComment != "" { 320 importPath = pkg.ImportComment 321 } 322 323 if importPath == "." { 324 if modPath, ok := findImportPath(pkg.Dir); ok { 325 importPath = modPath 326 } 327 } 328 329 return doc.New(astPkg, importPath, doc.AllDecls), nil 330 }