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  }