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