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