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  }