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  }