github.com/huner2/gomarkdoc@v0.3.6/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/huner2/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, doc *doc.Package, examples []*doc.Example) *Package { 45 return &Package{cfg, doc, 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 docPkg, err := getDocPkg(pkg, cfg.FileSet, options.includeUnexported) 70 if err != nil { 71 return nil, err 72 } 73 74 files, err := parsePkgFiles(pkg, cfg.FileSet) 75 if err != nil { 76 return nil, err 77 } 78 79 examples := doc.Examples(files...) 80 81 return NewPackage(cfg, docPkg, examples), nil 82 } 83 84 // PackageWithUnexportedIncluded can be used along with the NewPackageFromBuild 85 // function to specify that all symbols, including unexported ones, should be 86 // included in the documentation for the package. 87 func PackageWithUnexportedIncluded() PackageOption { 88 return func(opts *PackageOptions) error { 89 opts.includeUnexported = true 90 return nil 91 } 92 } 93 94 // PackageWithRepositoryOverrides can be used along with the NewPackageFromBuild 95 // function to define manual overrides to the automatic repository detection 96 // logic. 97 func PackageWithRepositoryOverrides(repo *Repo) PackageOption { 98 return func(opts *PackageOptions) error { 99 opts.repositoryOverrides = repo 100 return nil 101 } 102 } 103 104 // Level provides the default level that headers for the package's root 105 // documentation should be rendered. 106 func (pkg *Package) Level() int { 107 return pkg.cfg.Level 108 } 109 110 // Dir provides the name of the full directory in which the package is located. 111 func (pkg *Package) Dir() string { 112 return pkg.cfg.PkgDir 113 } 114 115 // Dirname provides the name of the leaf directory in which the package is 116 // located. 117 func (pkg *Package) Dirname() string { 118 return filepath.Base(pkg.cfg.PkgDir) 119 } 120 121 // Name provides the name of the package as it would be seen from another 122 // package importing it. 123 func (pkg *Package) Name() string { 124 return pkg.doc.Name 125 } 126 127 // Import provides the raw text for the import declaration that is used to 128 // import code from the package. If your package's documentation is generated 129 // from a local path and does not use Go Modules, this will typically print 130 // `import "."`. 131 func (pkg *Package) Import() string { 132 return fmt.Sprintf(`import "%s"`, pkg.doc.ImportPath) 133 } 134 135 // Summary provides the one-sentence summary of the package's documentation 136 // comment. 137 func (pkg *Package) Summary() string { 138 return extractSummary(pkg.doc.Doc) 139 } 140 141 // Doc provides the structured contents of the documentation comment for the 142 // package. 143 func (pkg *Package) Doc() *Doc { 144 // TODO: level should only be + 1, but we have special knowledge for rendering 145 return NewDoc(pkg.cfg.Inc(2), pkg.doc.Doc) 146 } 147 148 // Consts lists the top-level constants provided by the package. 149 func (pkg *Package) Consts() (consts []*Value) { 150 for _, c := range pkg.doc.Consts { 151 consts = append(consts, NewValue(pkg.cfg.Inc(1), c)) 152 } 153 154 return 155 } 156 157 // Vars lists the top-level variables provided by the package. 158 func (pkg *Package) Vars() (vars []*Value) { 159 for _, v := range pkg.doc.Vars { 160 vars = append(vars, NewValue(pkg.cfg.Inc(1), v)) 161 } 162 163 return 164 } 165 166 // Funcs lists the top-level functions provided by the package. 167 func (pkg *Package) Funcs() (funcs []*Func) { 168 for _, fn := range pkg.doc.Funcs { 169 funcs = append(funcs, NewFunc(pkg.cfg.Inc(1), fn, pkg.examples)) 170 } 171 172 return 173 } 174 175 // Types lists the top-level types provided by the package. 176 func (pkg *Package) Types() (types []*Type) { 177 for _, typ := range pkg.doc.Types { 178 types = append(types, NewType(pkg.cfg.Inc(1), typ, pkg.examples)) 179 } 180 181 return 182 } 183 184 // Examples provides the package-level examples that have been defined. This 185 // does not include examples that are associated with symbols contained within 186 // the package. 187 func (pkg *Package) Examples() (examples []*Example) { 188 for _, example := range pkg.examples { 189 var name string 190 switch { 191 case example.Name == "": 192 name = "" 193 case strings.HasPrefix(example.Name, "_"): 194 name = example.Name[1:] 195 default: 196 // TODO: better filtering 197 continue 198 } 199 200 examples = append(examples, NewExample(pkg.cfg.Inc(1), name, example)) 201 } 202 203 return 204 } 205 206 var goModRegex = regexp.MustCompile(`^\s*module ([^\s]+)`) 207 208 // findImportPath attempts to find an import path for the contents of the 209 // provided dir by walking up to the nearest go.mod file and constructing an 210 // import path from it. If the directory is not in a Go Module, the second 211 // return value will be false. 212 func findImportPath(dir string) (string, bool) { 213 absDir, err := filepath.Abs(dir) 214 if err != nil { 215 return "", false 216 } 217 218 f, ok := findFileInParent(absDir, "go.mod", false) 219 if !ok { 220 return "", false 221 } 222 defer f.Close() 223 224 b, err := ioutil.ReadAll(f) 225 if err != nil { 226 return "", false 227 } 228 229 m := goModRegex.FindSubmatch(b) 230 if m == nil { 231 return "", false 232 } 233 234 relative, err := filepath.Rel(filepath.Dir(f.Name()), absDir) 235 if err != nil { 236 return "", false 237 } 238 239 relative = filepath.ToSlash(relative) 240 241 return path.Join(string(m[1]), relative), true 242 } 243 244 // findFileInParent looks for a file or directory of the given name within the 245 // provided dir. The returned os.File is opened and must be closed by the 246 // caller to avoid a memory leak. 247 func findFileInParent(dir, filename string, fileIsDir bool) (*os.File, bool) { 248 initial := dir 249 current := initial 250 251 for { 252 p := filepath.Join(current, filename) 253 if f, err := os.Open(p); err == nil { 254 if s, err := f.Stat(); err == nil && (fileIsDir && s.Mode().IsDir() || !fileIsDir && s.Mode().IsRegular()) { 255 return f, true 256 } 257 } 258 259 // Walk up a dir 260 next := filepath.Join(current, "..") 261 262 // If we didn't change dirs, there's no more to search 263 if current == next { 264 break 265 } 266 267 current = next 268 } 269 270 return nil, false 271 } 272 273 func getDocPkg(pkg *build.Package, fs *token.FileSet, includeUnexported bool) (*doc.Package, error) { 274 pkgs, err := parser.ParseDir( 275 fs, 276 pkg.Dir, 277 func(info os.FileInfo) bool { 278 for _, name := range pkg.GoFiles { 279 if name == info.Name() { 280 return true 281 } 282 } 283 284 for _, name := range pkg.CgoFiles { 285 if name == info.Name() { 286 return true 287 } 288 } 289 290 return false 291 }, 292 parser.ParseComments, 293 ) 294 295 if err != nil { 296 return nil, fmt.Errorf("gomarkdoc: failed to parse package: %w", err) 297 } 298 299 if len(pkgs) == 0 { 300 return nil, fmt.Errorf("gomarkdoc: no source-code package in directory %s", pkg.Dir) 301 } 302 303 if len(pkgs) > 1 { 304 return nil, fmt.Errorf("gomarkdoc: multiple packages in directory %s", pkg.Dir) 305 } 306 307 astPkg := pkgs[pkg.Name] 308 309 if !includeUnexported { 310 ast.PackageExports(astPkg) 311 } 312 313 importPath := pkg.ImportPath 314 if pkg.ImportComment != "" { 315 importPath = pkg.ImportComment 316 } 317 318 if importPath == "." { 319 if modPath, ok := findImportPath(pkg.Dir); ok { 320 importPath = modPath 321 } 322 } 323 324 return doc.New(astPkg, importPath, doc.AllDecls), nil 325 } 326 327 func parsePkgFiles(pkg *build.Package, fs *token.FileSet) ([]*ast.File, error) { 328 rawFiles, err := ioutil.ReadDir(pkg.Dir) 329 if err != nil { 330 return nil, fmt.Errorf("gomarkdoc: error reading package dir: %w", err) 331 } 332 333 var files []*ast.File 334 for _, f := range rawFiles { 335 if !strings.HasSuffix(f.Name(), ".go") && !strings.HasSuffix(f.Name(), ".cgo") { 336 continue 337 } 338 339 p := path.Join(pkg.Dir, f.Name()) 340 341 fi, err := os.Stat(p) 342 if err != nil || !fi.Mode().IsRegular() { 343 continue 344 } 345 346 parsed, err := parser.ParseFile(fs, p, nil, parser.ParseComments) 347 if err != nil { 348 return nil, fmt.Errorf("gomarkdoc: failed to parse package file %s", f.Name()) 349 } 350 351 files = append(files, parsed) 352 } 353 354 return files, nil 355 }