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  }