mirror of
https://github.com/magefile/mage.git
synced 2026-10-24 06:19:18 +03:00
1141 lines
28 KiB
Go
1141 lines
28 KiB
Go
// Package parse provides parsing of Go source files to extract mage targets.
|
|
package parse
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/magefile/mage/internal"
|
|
)
|
|
|
|
const importTag = "mage:import"
|
|
|
|
const multilineTag = "mage:multiline"
|
|
|
|
var debug = log.New(io.Discard, "DEBUG: ", log.Ltime|log.Lmicroseconds)
|
|
|
|
// EnableDebug turns on debug logging.
|
|
func EnableDebug() {
|
|
debug.SetOutput(os.Stderr)
|
|
}
|
|
|
|
// PkgInfo contains information about a package of files according to mage's
|
|
// parsing rules.
|
|
type PkgInfo struct {
|
|
AstPkg *ast.Package
|
|
DocPkg *doc.Package
|
|
Description string
|
|
Funcs Functions
|
|
DefaultFunc *Function
|
|
Aliases map[string]*Function
|
|
Imports Imports
|
|
Multiline bool
|
|
}
|
|
|
|
// Function represents a job function from a mage file.
|
|
type Function struct {
|
|
PkgAlias string
|
|
Package string
|
|
ImportPath string
|
|
Name string
|
|
Receiver string
|
|
IsError bool
|
|
IsContext bool
|
|
Synopsis string
|
|
Comment string
|
|
Args []Arg
|
|
}
|
|
|
|
var _ sort.Interface = Functions(nil)
|
|
|
|
// Functions implements sort interface to optimize compiled output with
|
|
// deterministic generated mainfile.
|
|
type Functions []*Function
|
|
|
|
func (s Functions) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
func (s Functions) Less(i, j int) bool {
|
|
return s[i].TargetName() < s[j].TargetName()
|
|
}
|
|
|
|
func (s Functions) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
// Arg is an argument to a Function.
|
|
type Arg struct {
|
|
Name, Type string
|
|
Optional bool
|
|
Comment string
|
|
}
|
|
|
|
// ID returns user-readable information about where this function is defined.
|
|
func (f Function) ID() string {
|
|
path := "<current>"
|
|
if f.ImportPath != "" {
|
|
path = f.ImportPath
|
|
}
|
|
receiver := ""
|
|
if f.Receiver != "" {
|
|
receiver = f.Receiver + "."
|
|
}
|
|
return fmt.Sprintf("%s.%s%s", path, receiver, f.Name)
|
|
}
|
|
|
|
// TargetName returns the name of the target as it should appear when used from
|
|
// the mage cli. It is always lowercase.
|
|
func (f Function) TargetName() string {
|
|
var names []string
|
|
|
|
for _, s := range []string{f.PkgAlias, f.Receiver, f.Name} {
|
|
if s != "" {
|
|
names = append(names, s)
|
|
}
|
|
}
|
|
return strings.Join(names, ":")
|
|
}
|
|
|
|
// NumRequiredArgs returns the number of non-optional arguments.
|
|
func (f Function) NumRequiredArgs() int {
|
|
n := 0
|
|
for _, a := range f.Args {
|
|
if !a.Optional {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// RequiredArgs returns only the non-optional arguments.
|
|
func (f Function) RequiredArgs() []Arg {
|
|
var out []Arg
|
|
for _, a := range f.Args {
|
|
if !a.Optional {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// OptionalArgs returns only the optional arguments.
|
|
func (f Function) OptionalArgs() []Arg {
|
|
var out []Arg
|
|
for _, a := range f.Args {
|
|
if a.Optional {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HasOptionalArgs reports whether the function has any optional arguments.
|
|
func (f Function) HasOptionalArgs() bool {
|
|
for _, a := range f.Args {
|
|
if a.Optional {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MultipleOptionalArgs reports whether the function has more than one optional argument.
|
|
func (f Function) MultipleOptionalArgs() bool {
|
|
n := 0
|
|
for _, a := range f.Args {
|
|
if a.Optional {
|
|
n++
|
|
if n > 1 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ShowFlagDocs reports whether the Flags section should be displayed.
|
|
// This is true when there are multiple optional args (since they are
|
|
// condensed to [<flags>] in the usage line) or when any optional arg
|
|
// has a doc comment.
|
|
func (f Function) ShowFlagDocs() bool {
|
|
if f.MultipleOptionalArgs() {
|
|
return true
|
|
}
|
|
for _, a := range f.Args {
|
|
if a.Optional && a.Comment != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FlagDocsString returns a formatted string documenting optional arguments.
|
|
// It aligns comments to the same column based on the longest flag name.
|
|
func (f Function) FlagDocsString() string {
|
|
opts := f.OptionalArgs()
|
|
if len(opts) == 0 {
|
|
return ""
|
|
}
|
|
// Compute flag labels and find max width for alignment
|
|
type entry struct {
|
|
label string
|
|
comment string
|
|
}
|
|
var entries []entry
|
|
maxLen := 0
|
|
for _, a := range opts {
|
|
label := fmt.Sprintf("-%s=<%s>", a.Name, a.Type)
|
|
if len(label) > maxLen {
|
|
maxLen = len(label)
|
|
}
|
|
entries = append(entries, entry{label: label, comment: a.Comment})
|
|
}
|
|
|
|
var buf strings.Builder
|
|
_, _ = buf.WriteString("Flags:\n\n")
|
|
for _, e := range entries {
|
|
if e.comment != "" {
|
|
_, _ = fmt.Fprintf(&buf, "\t%-*s %s\n", maxLen, e.label, e.comment)
|
|
} else {
|
|
_, _ = fmt.Fprintf(&buf, "\t%s\n", e.label)
|
|
}
|
|
}
|
|
_, _ = buf.WriteString("\n")
|
|
return buf.String()
|
|
}
|
|
|
|
// ExecCode returns code for the template switch to run the target.
|
|
// It wraps each target call to match the func(context.Context) error that
|
|
// runTarget requires.
|
|
func (f Function) ExecCode() string {
|
|
name := f.Name
|
|
if f.Receiver != "" {
|
|
name = f.Receiver + "{}." + name
|
|
}
|
|
if f.Package != "" {
|
|
name = f.Package + "." + name
|
|
}
|
|
|
|
var parseargs strings.Builder
|
|
|
|
// Phase 1: Parse positional (required) arguments
|
|
for x, arg := range f.Args {
|
|
if arg.Optional {
|
|
continue
|
|
}
|
|
switch arg.Type {
|
|
case "string":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
arg%d := args.Args[x]
|
|
x++`, x)
|
|
case "int":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
arg%d, err := strconv.Atoi(args.Args[x])
|
|
if err != nil {
|
|
logger.Printf("can't convert argument %%q to int\n", args.Args[x])
|
|
os.Exit(2)
|
|
}
|
|
x++`, x)
|
|
case "float64":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
arg%d, err := strconv.ParseFloat(args.Args[x], 64)
|
|
if err != nil {
|
|
logger.Printf("can't convert argument %%q to float64\n", args.Args[x])
|
|
os.Exit(2)
|
|
}
|
|
x++`, x)
|
|
case "bool":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
arg%d, err := strconv.ParseBool(args.Args[x])
|
|
if err != nil {
|
|
logger.Printf("can't convert argument %%q to bool\n", args.Args[x])
|
|
os.Exit(2)
|
|
}
|
|
x++`, x)
|
|
case "time.Duration":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
arg%d, err := time.ParseDuration(args.Args[x])
|
|
if err != nil {
|
|
logger.Printf("can't convert argument %%q to time.Duration\n", args.Args[x])
|
|
os.Exit(2)
|
|
}
|
|
x++`, x)
|
|
default:
|
|
// unsupported type, skip
|
|
}
|
|
}
|
|
|
|
// Phase 2: Declare optional argument variables (nil by default)
|
|
for x, arg := range f.Args {
|
|
if !arg.Optional {
|
|
continue
|
|
}
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
var arg%d *%s`, x, arg.Type)
|
|
}
|
|
|
|
// Phase 3: Parse optional arguments from -name=value flags
|
|
if f.HasOptionalArgs() {
|
|
// Collect lowercase names of bool optional args for bare-flag support
|
|
var boolOptNames []string
|
|
for _, arg := range f.Args {
|
|
if arg.Optional && arg.Type == "bool" {
|
|
boolOptNames = append(boolOptNames, strings.ToLower(arg.Name))
|
|
}
|
|
}
|
|
|
|
_, _ = fmt.Fprint(&parseargs, `
|
|
for x < len(args.Args) && _strings.HasPrefix(args.Args[x], "-") {
|
|
_optArg := args.Args[x]
|
|
_eqIdx := _strings.Index(_optArg, "=")
|
|
var _optName, _optVal string
|
|
if _eqIdx < 0 {
|
|
_optName = _strings.ToLower(_optArg[1:])
|
|
switch _optName {`)
|
|
// Generate cases for each bool optional arg
|
|
for _, bname := range boolOptNames {
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_optVal = "true"`, bname)
|
|
}
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
default:
|
|
logger.Printf("invalid option %%q for target \"%s\", expected -name=value format\n", _optArg)
|
|
os.Exit(2)
|
|
}
|
|
} else {
|
|
_optName = _strings.ToLower(_optArg[1:_eqIdx])
|
|
_optVal = _optArg[_eqIdx+1:]
|
|
}
|
|
switch _optName {`, f.TargetName())
|
|
for x, arg := range f.Args {
|
|
if !arg.Optional {
|
|
continue
|
|
}
|
|
lowerName := strings.ToLower(arg.Name)
|
|
switch arg.Type {
|
|
case "string":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_tmp%d := _optVal
|
|
arg%d = &_tmp%d`, lowerName, x, x, x)
|
|
case "int":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_tmp%d, err := strconv.Atoi(_optVal)
|
|
if err != nil {
|
|
logger.Printf("can't convert option %%q value %%q to int\n", _optName, _optVal)
|
|
os.Exit(2)
|
|
}
|
|
arg%d = &_tmp%d`, lowerName, x, x, x)
|
|
case "float64":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_tmp%d, err := strconv.ParseFloat(_optVal, 64)
|
|
if err != nil {
|
|
logger.Printf("can't convert option %%q value %%q to float64\n", _optName, _optVal)
|
|
os.Exit(2)
|
|
}
|
|
arg%d = &_tmp%d`, lowerName, x, x, x)
|
|
case "bool":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_tmp%d, err := strconv.ParseBool(_optVal)
|
|
if err != nil {
|
|
logger.Printf("can't convert option %%q value %%q to bool\n", _optName, _optVal)
|
|
os.Exit(2)
|
|
}
|
|
arg%d = &_tmp%d`, lowerName, x, x, x)
|
|
case "time.Duration":
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
case %q:
|
|
_tmp%d, err := time.ParseDuration(_optVal)
|
|
if err != nil {
|
|
logger.Printf("can't convert option %%q value %%q to time.Duration\n", _optName, _optVal)
|
|
os.Exit(2)
|
|
}
|
|
arg%d = &_tmp%d`, lowerName, x, x, x)
|
|
default:
|
|
// unsupported type, skip
|
|
}
|
|
}
|
|
_, _ = fmt.Fprintf(&parseargs, `
|
|
default:
|
|
logger.Printf("unknown option %%q for target \"%s\"\n", _optName)
|
|
os.Exit(2)
|
|
}
|
|
x++
|
|
}`, f.TargetName())
|
|
}
|
|
|
|
out := parseargs.String() + `
|
|
wrapFn := func(ctx context.Context) error {
|
|
`
|
|
if f.IsError {
|
|
out += "return "
|
|
}
|
|
out += name + "("
|
|
var args []string
|
|
if f.IsContext {
|
|
args = append(args, "ctx")
|
|
}
|
|
for x := 0; x < len(f.Args); x++ {
|
|
args = append(args, fmt.Sprintf("arg%d", x))
|
|
}
|
|
out += strings.Join(args, ", ")
|
|
out += ")"
|
|
if !f.IsError {
|
|
out += `
|
|
return nil`
|
|
}
|
|
out += `
|
|
}
|
|
ret := runTarget(logger, wrapFn)`
|
|
return out
|
|
}
|
|
|
|
// PrimaryPackage parses a package. If files is non-empty, it will only parse the files given.
|
|
func PrimaryPackage(gocmd, path string, files []string, multiline bool) (*PkgInfo, error) {
|
|
info, err := Package(path, files, multiline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := setImports(gocmd, info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
setDefault(info)
|
|
setAliases(info)
|
|
return info, nil
|
|
}
|
|
|
|
func checkDupes(info *PkgInfo, imports []*Import) error {
|
|
funcs := map[string][]*Function{}
|
|
for _, f := range info.Funcs {
|
|
funcs[strings.ToLower(f.TargetName())] = append(funcs[strings.ToLower(f.TargetName())], f)
|
|
}
|
|
for _, imp := range imports {
|
|
for _, f := range imp.Info.Funcs {
|
|
target := strings.ToLower(f.TargetName())
|
|
funcs[target] = append(funcs[target], f)
|
|
}
|
|
}
|
|
for alias, f := range info.Aliases {
|
|
if len(funcs[alias]) != 0 {
|
|
var ids []string
|
|
for _, f := range funcs[alias] {
|
|
ids = append(ids, f.ID())
|
|
}
|
|
return fmt.Errorf("alias %q duplicates existing target(s): %s", alias, strings.Join(ids, ", "))
|
|
}
|
|
funcs[alias] = append(funcs[alias], f)
|
|
}
|
|
var dupes []string
|
|
for target, list := range funcs {
|
|
if len(list) > 1 {
|
|
dupes = append(dupes, target)
|
|
}
|
|
}
|
|
if len(dupes) == 0 {
|
|
return nil
|
|
}
|
|
errs := make([]string, 0, len(dupes))
|
|
for _, d := range dupes {
|
|
var ids []string
|
|
for _, f := range funcs[d] {
|
|
ids = append(ids, f.ID())
|
|
}
|
|
sort.Strings(ids)
|
|
errs = append(errs, fmt.Sprintf("%q target has multiple definitions: %s\n", d, strings.Join(ids, ", ")))
|
|
}
|
|
sort.Strings(errs)
|
|
return errors.New(strings.Join(errs, "\n"))
|
|
}
|
|
|
|
// Package compiles information about a mage package.
|
|
func Package(path string, files []string, multiline bool) (*PkgInfo, error) {
|
|
start := time.Now()
|
|
defer func() {
|
|
debug.Println("time parse Magefiles:", time.Since(start))
|
|
}()
|
|
fset := token.NewFileSet()
|
|
pkg, err := getPackage(path, files, fset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasComment(pkg, multilineTag) {
|
|
debug.Printf("found %s tag, using multiline descriptions", multilineTag)
|
|
multiline = true
|
|
}
|
|
p := doc.New(pkg, "./", doc.PreserveAST)
|
|
|
|
// Build a map from AST fields to their inline comments. We use
|
|
// ast.NewCommentMap because the Go parser does not populate
|
|
// ast.Field.Comment for function parameters (only for struct fields).
|
|
fieldComments := make(map[*ast.Field]string)
|
|
for _, f := range pkg.Files {
|
|
cmap := ast.NewCommentMap(fset, f, f.Comments)
|
|
for node, groups := range cmap {
|
|
field, ok := node.(*ast.Field)
|
|
if !ok || len(groups) == 0 {
|
|
continue
|
|
}
|
|
fieldComments[field] = strings.TrimSpace(groups[0].Text())
|
|
}
|
|
}
|
|
|
|
pi := &PkgInfo{
|
|
AstPkg: pkg,
|
|
DocPkg: p,
|
|
Multiline: multiline,
|
|
}
|
|
if multiline {
|
|
pi.Description = strings.TrimSuffix(p.Doc, "\n")
|
|
} else {
|
|
pi.Description = toOneLine(p.Doc)
|
|
}
|
|
|
|
setNamespaces(pi, fieldComments)
|
|
setFuncs(pi, fieldComments)
|
|
|
|
hasDupes, names := checkDupeTargets(pi)
|
|
if hasDupes {
|
|
var msg strings.Builder
|
|
_, _ = fmt.Fprint(&msg, "Build targets must be case insensitive, thus the following targets conflict:\n")
|
|
for _, v := range names {
|
|
if len(v) > 1 {
|
|
_, _ = fmt.Fprint(&msg, " "+strings.Join(v, ", ")+"\n")
|
|
}
|
|
}
|
|
return nil, errors.New(msg.String())
|
|
}
|
|
|
|
return pi, nil
|
|
}
|
|
|
|
func getNamedImports(gocmd string, pkgs map[string]string, multiline bool) ([]*Import, error) {
|
|
var imports []*Import
|
|
for pkg, alias := range pkgs {
|
|
debug.Printf("getting import package %q, alias %q", pkg, alias)
|
|
imp, err := getImport(gocmd, pkg, alias, multiline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imports = append(imports, imp)
|
|
}
|
|
return imports, nil
|
|
}
|
|
|
|
// getImport returns the metadata about a package that has been mage:import'ed.
|
|
func getImport(gocmd, importpath, alias string, multiline bool) (*Import, error) {
|
|
out, err := internal.OutputDebug(gocmd, "list", "-f", "{{.Dir}}||{{.Name}}", importpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts := strings.Split(out, "||")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("incorrect data from go list: %s", out)
|
|
}
|
|
dir, name := parts[0], parts[1]
|
|
debug.Printf("parsing imported package %q from dir %q", importpath, dir)
|
|
|
|
// we use go list to get the list of files, since go/parser doesn't differentiate between
|
|
// go files with build tags etc, and go list does. This prevents weird problems if you
|
|
// have more than one package in a folder because of build tags.
|
|
out, err = internal.OutputDebug(gocmd, "list", "-f", `{{join .GoFiles "||"}}`, importpath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files := strings.Split(out, "||")
|
|
|
|
info, err := Package(dir, files, multiline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range info.Funcs {
|
|
debug.Printf("setting alias %q and package %q on func %v", alias, name, info.Funcs[i].Name)
|
|
info.Funcs[i].PkgAlias = alias
|
|
info.Funcs[i].ImportPath = importpath
|
|
}
|
|
return &Import{Alias: alias, Name: name, Path: importpath, Info: *info}, nil
|
|
}
|
|
|
|
// Import represents the data about a mage:import package.
|
|
type Import struct {
|
|
Alias string
|
|
Name string
|
|
UniqueName string // a name unique across all imports
|
|
Path string
|
|
Info PkgInfo
|
|
}
|
|
|
|
var _ sort.Interface = Imports(nil)
|
|
|
|
// Imports implements sort interface to optimize compiled output with
|
|
// deterministic generated mainfile.
|
|
type Imports []*Import
|
|
|
|
func (s Imports) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
func (s Imports) Less(i, j int) bool {
|
|
return s[i].UniqueName < s[j].UniqueName
|
|
}
|
|
|
|
func (s Imports) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
func setFuncs(pi *PkgInfo, fieldComments map[*ast.Field]string) {
|
|
for _, f := range pi.DocPkg.Funcs {
|
|
if f.Recv != "" {
|
|
debug.Printf("skipping method %s.%s", f.Recv, f.Name)
|
|
// skip methods
|
|
continue
|
|
}
|
|
fn, ok := funcFromDoc(f, pi.DocPkg.ImportPath, f.Name, pi.Multiline, fieldComments)
|
|
if !ok {
|
|
continue
|
|
}
|
|
pi.Funcs = append(pi.Funcs, fn)
|
|
}
|
|
}
|
|
|
|
func setNamespaces(pi *PkgInfo, fieldComments map[*ast.Field]string) {
|
|
for _, t := range pi.DocPkg.Types {
|
|
if !isNamespace(t) {
|
|
continue
|
|
}
|
|
debug.Printf("found namespace %s %s", pi.DocPkg.ImportPath, t.Name)
|
|
for _, f := range t.Methods {
|
|
fn, ok := funcFromDoc(f, pi.DocPkg.ImportPath, t.Name+"."+f.Name, pi.Multiline, fieldComments)
|
|
if !ok {
|
|
continue
|
|
}
|
|
fn.Receiver = t.Name
|
|
pi.Funcs = append(pi.Funcs, fn)
|
|
}
|
|
}
|
|
}
|
|
|
|
func funcFromDoc(f *doc.Func, importpath, funcname string, multiline bool, fieldComments map[*ast.Field]string) (*Function, bool) {
|
|
if !ast.IsExported(f.Name) {
|
|
return nil, false
|
|
}
|
|
fn, err := funcType(f.Decl.Type, fieldComments)
|
|
if err != nil {
|
|
debug.Printf("skipping invalid method %s %s: %v", importpath, funcname, err)
|
|
return nil, false
|
|
}
|
|
debug.Printf("found method %s %s", importpath, funcname)
|
|
fn.Name = f.Name
|
|
if multiline {
|
|
fn.Comment = strings.TrimSuffix(f.Doc, "\n")
|
|
} else {
|
|
fn.Comment = toOneLine(f.Doc)
|
|
}
|
|
fn.Synopsis = sanitizeSynopsis(f)
|
|
return fn, true
|
|
}
|
|
|
|
func setImports(gocmd string, pi *PkgInfo) error {
|
|
importNames := map[string]string{}
|
|
rootImports := []string{}
|
|
for _, f := range pi.AstPkg.Files {
|
|
for _, d := range f.Decls {
|
|
gen, ok := d.(*ast.GenDecl)
|
|
if !ok || gen.Tok != token.IMPORT {
|
|
continue
|
|
}
|
|
for j := 0; j < len(gen.Specs); j++ {
|
|
spec := gen.Specs[j]
|
|
impspec, ok := spec.(*ast.ImportSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(gen.Specs) == 1 && gen.Lparen == token.NoPos && impspec.Doc == nil {
|
|
impspec.Doc = gen.Doc
|
|
}
|
|
name, alias, ok := getImportPath(impspec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if alias != "" {
|
|
debug.Printf("found %s: %s (%s)", importTag, name, alias)
|
|
importNames[name] = alias
|
|
} else {
|
|
debug.Printf("found %s: %s", importTag, name)
|
|
rootImports = append(rootImports, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
imports, err := getNamedImports(gocmd, importNames, pi.Multiline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, s := range rootImports {
|
|
imp, err := getImport(gocmd, s, "", pi.Multiline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
imports = append(imports, imp)
|
|
}
|
|
if err := checkDupes(pi, imports); err != nil {
|
|
return err
|
|
}
|
|
|
|
// have to set unique package names on imports
|
|
used := map[string]bool{}
|
|
for _, imp := range imports {
|
|
unique := imp.Name + "_mageimport"
|
|
x := 1
|
|
for used[unique] {
|
|
unique = fmt.Sprintf("%s_mageimport%d", imp.Name, x)
|
|
x++
|
|
}
|
|
used[unique] = true
|
|
imp.UniqueName = unique
|
|
for _, f := range imp.Info.Funcs {
|
|
f.Package = unique
|
|
}
|
|
}
|
|
pi.Imports = imports
|
|
return nil
|
|
}
|
|
|
|
func getImportPath(imp *ast.ImportSpec) (path, alias string, ok bool) {
|
|
leadingVals := getImportPathFromCommentGroup(imp.Doc)
|
|
trailingVals := getImportPathFromCommentGroup(imp.Comment)
|
|
|
|
var vals []string
|
|
switch {
|
|
case len(leadingVals) > 0:
|
|
vals = leadingVals
|
|
if len(trailingVals) > 0 {
|
|
log.Println("warning:", importTag, "specified both before and after, picking first")
|
|
}
|
|
case len(trailingVals) > 0:
|
|
vals = trailingVals
|
|
default:
|
|
return "", "", false
|
|
}
|
|
path, ok = lit2string(imp.Path)
|
|
if !ok {
|
|
return "", "", false
|
|
}
|
|
|
|
switch len(vals) {
|
|
case 1:
|
|
// just the import tag, this is a root import
|
|
return path, "", true
|
|
case 2:
|
|
// also has an alias
|
|
return path, vals[1], true
|
|
default:
|
|
log.Println("warning: ignoring malformed", importTag, "for import", path)
|
|
return "", "", false
|
|
}
|
|
}
|
|
|
|
func getImportPathFromCommentGroup(comments *ast.CommentGroup) []string {
|
|
if comments == nil || len(comments.List) == 0 {
|
|
return nil
|
|
}
|
|
// import is always the last comment
|
|
s := comments.List[len(comments.List)-1].Text
|
|
|
|
// trim comment start and normalize for anyone who has spaces or not between
|
|
// "//"" and the text
|
|
vals := strings.Fields(strings.ToLower(s[2:]))
|
|
if len(vals) == 0 {
|
|
return nil
|
|
}
|
|
if vals[0] != importTag {
|
|
return nil
|
|
}
|
|
return vals
|
|
}
|
|
|
|
func isNamespace(t *doc.Type) bool {
|
|
if len(t.Decl.Specs) != 1 {
|
|
return false
|
|
}
|
|
id, ok := t.Decl.Specs[0].(*ast.TypeSpec)
|
|
if !ok {
|
|
return false
|
|
}
|
|
sel, ok := id.Type.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
ident, ok := sel.X.(*ast.Ident)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return ident.Name == "mg" && sel.Sel.Name == "Namespace"
|
|
}
|
|
|
|
// checkDupeTargets checks a package for duplicate target names.
|
|
func checkDupeTargets(info *PkgInfo) (hasDupes bool, names map[string][]string) {
|
|
names = map[string][]string{}
|
|
lowers := map[string]bool{}
|
|
for _, f := range info.Funcs {
|
|
low := strings.ToLower(f.Name)
|
|
if f.Receiver != "" {
|
|
low = strings.ToLower(f.Receiver) + ":" + low
|
|
}
|
|
if lowers[low] {
|
|
hasDupes = true
|
|
}
|
|
lowers[low] = true
|
|
names[low] = append(names[low], f.Name)
|
|
}
|
|
return hasDupes, names
|
|
}
|
|
|
|
// sanitizeSynopsis sanitizes function Doc to create a summary.
|
|
func sanitizeSynopsis(f *doc.Func) string {
|
|
synopsis := doc.Synopsis(f.Doc)
|
|
|
|
// If the synopsis begins with the function name, remove it. This is done to
|
|
// not repeat the text.
|
|
// From:
|
|
// clean Clean removes the temporarily generated files
|
|
// To:
|
|
// clean removes the temporarily generated files
|
|
if syns := strings.Split(synopsis, " "); strings.EqualFold(f.Name, syns[0]) {
|
|
return strings.Join(syns[1:], " ")
|
|
}
|
|
|
|
return synopsis
|
|
}
|
|
|
|
func setDefault(pi *PkgInfo) {
|
|
for _, v := range pi.DocPkg.Vars {
|
|
for x, name := range v.Names {
|
|
if name != "Default" {
|
|
continue
|
|
}
|
|
spec, ok := v.Decl.Specs[x].(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(spec.Values) != 1 {
|
|
log.Println("warning: default declaration has multiple values")
|
|
}
|
|
|
|
f, err := getFunction(spec.Values[0], pi)
|
|
if err != nil {
|
|
log.Println("warning: default declaration malformed:", err)
|
|
return
|
|
}
|
|
pi.DefaultFunc = f
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func lit2string(l *ast.BasicLit) (string, bool) {
|
|
if !strings.HasPrefix(l.Value, `"`) || !strings.HasSuffix(l.Value, `"`) {
|
|
return "", false
|
|
}
|
|
return strings.Trim(l.Value, `"`), true
|
|
}
|
|
|
|
func setAliases(pi *PkgInfo) {
|
|
for _, v := range pi.DocPkg.Vars {
|
|
for x, name := range v.Names {
|
|
if name != "Aliases" {
|
|
continue
|
|
}
|
|
spec, ok := v.Decl.Specs[x].(*ast.ValueSpec)
|
|
if !ok {
|
|
log.Println("warning: aliases declaration is not a value")
|
|
return
|
|
}
|
|
if len(spec.Values) != 1 {
|
|
log.Println("warning: aliases declaration has multiple values")
|
|
}
|
|
comp, ok := spec.Values[0].(*ast.CompositeLit)
|
|
if !ok {
|
|
log.Println("warning: aliases declaration is not a map")
|
|
return
|
|
}
|
|
pi.Aliases = map[string]*Function{}
|
|
for _, elem := range comp.Elts {
|
|
kv, ok := elem.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
log.Printf("warning: alias declaration %q is not a map element", elem)
|
|
continue
|
|
}
|
|
k, ok := kv.Key.(*ast.BasicLit)
|
|
if !ok || k.Kind != token.STRING {
|
|
log.Printf("warning: alias key is not a string literal %q", elem)
|
|
continue
|
|
}
|
|
|
|
alias, ok := lit2string(k)
|
|
if !ok {
|
|
log.Println("warning: malformed name for alias", elem)
|
|
continue
|
|
}
|
|
f, err := getFunction(kv.Value, pi)
|
|
if err != nil {
|
|
log.Printf("warning, alias malformed: %v", err)
|
|
continue
|
|
}
|
|
pi.Aliases[alias] = f
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func getFunction(exp ast.Expr, pi *PkgInfo) (*Function, error) {
|
|
// selector expressions are in LIFO format.
|
|
// So, in foo.bar.baz the first selector.Name is
|
|
// actually "baz", the second is "bar", and the last is "foo"
|
|
|
|
var pkg, receiver, funcname string
|
|
switch v := exp.(type) {
|
|
case *ast.Ident:
|
|
// "foo" : Bar
|
|
funcname = v.Name
|
|
case *ast.SelectorExpr:
|
|
// need to handle
|
|
// namespace.Func
|
|
// import.Func
|
|
// import.namespace.Func
|
|
|
|
// "foo" : ?.bar
|
|
funcname = v.Sel.Name
|
|
switch x := v.X.(type) {
|
|
case *ast.Ident:
|
|
// "foo" : baz.bar
|
|
// this is either a namespace or package
|
|
firstname := x.Name
|
|
for _, f := range pi.Funcs {
|
|
if firstname == f.Receiver && funcname == f.Name {
|
|
return f, nil
|
|
}
|
|
}
|
|
// not a namespace, let's try imported packages
|
|
for _, imp := range pi.Imports {
|
|
if firstname == imp.Name {
|
|
for _, f := range imp.Info.Funcs {
|
|
if funcname == f.Name && f.Receiver == "" {
|
|
return f, nil
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("%q is not a known target", exp)
|
|
case *ast.SelectorExpr:
|
|
// "foo" : bar.Baz.Bat
|
|
// must be package.Namespace.Func
|
|
sel, ok := v.X.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X)
|
|
}
|
|
receiver = sel.Sel.Name
|
|
id, ok := sel.X.(*ast.Ident)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X)
|
|
}
|
|
pkg = id.Name
|
|
default:
|
|
return nil, fmt.Errorf("%q is not valid", exp)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("target %s is not a function", exp)
|
|
}
|
|
if pkg == "" {
|
|
for _, f := range pi.Funcs {
|
|
if f.Name == funcname && f.Receiver == receiver {
|
|
return f, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("unknown function %s.%s", receiver, funcname)
|
|
}
|
|
for _, imp := range pi.Imports {
|
|
if imp.Name == pkg {
|
|
for _, f := range imp.Info.Funcs {
|
|
if f.Name == funcname && f.Receiver == receiver {
|
|
return f, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("unknown function %s.%s.%s", pkg, receiver, funcname)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("unknown package for function %q", exp)
|
|
}
|
|
|
|
// getPackage returns the importable package at the given path.
|
|
func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) {
|
|
var filter func(f os.FileInfo) bool
|
|
if len(files) > 0 {
|
|
fm := make(map[string]bool, len(files))
|
|
for _, f := range files {
|
|
fm[f] = true
|
|
}
|
|
|
|
filter = func(f os.FileInfo) bool {
|
|
return fm[f.Name()]
|
|
}
|
|
}
|
|
|
|
pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse directory: %w", err)
|
|
}
|
|
|
|
switch len(pkgs) {
|
|
case 1:
|
|
for _, pkg := range pkgs {
|
|
return pkg, nil
|
|
}
|
|
return nil, fmt.Errorf("should be impossible while parsing %s", path)
|
|
case 0:
|
|
return nil, fmt.Errorf("no importable packages found in %s", path)
|
|
default:
|
|
var names []string
|
|
for name := range pkgs {
|
|
names = append(names, name)
|
|
}
|
|
return nil, fmt.Errorf("multiple packages found in %s: %v", path, strings.Join(names, ", "))
|
|
}
|
|
}
|
|
|
|
// hasContextParams returns whether or not the first parameter is a context.Context. If it
|
|
// determines that hte first parameter makes this function invalid for mage, it'll return a non-nil
|
|
// error.
|
|
func hasContextParam(ft *ast.FuncType) (bool, error) {
|
|
if ft.Params.NumFields() < 1 {
|
|
return false, nil
|
|
}
|
|
param := ft.Params.List[0]
|
|
sel, ok := param.Type.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
pkg, ok := sel.X.(*ast.Ident)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
if pkg.Name != "context" {
|
|
return false, nil
|
|
}
|
|
if sel.Sel.Name != "Context" {
|
|
return false, nil
|
|
}
|
|
if len(param.Names) > 1 {
|
|
// something like foo, bar context.Context
|
|
return false, errors.New("ETOOMANYCONTEXTS")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func hasErrorReturn(ft *ast.FuncType) (bool, error) {
|
|
res := ft.Results
|
|
if res.NumFields() == 0 {
|
|
// void return is ok
|
|
return false, nil
|
|
}
|
|
if res.NumFields() > 1 {
|
|
return false, errors.New("ETOOMANYRETURNS")
|
|
}
|
|
ret := res.List[0]
|
|
if len(ret.Names) > 1 {
|
|
return false, errors.New("ETOOMANYERRORS")
|
|
}
|
|
if fmt.Sprint(ret.Type) == "error" {
|
|
return true, nil
|
|
}
|
|
return false, errors.New("EBADRETURNTYPE")
|
|
}
|
|
|
|
func funcType(ft *ast.FuncType, fieldComments map[*ast.Field]string) (*Function, error) {
|
|
var err error
|
|
f := &Function{}
|
|
f.IsContext, err = hasContextParam(ft)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.IsError, err = hasErrorReturn(ft)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
x := 0
|
|
if f.IsContext {
|
|
x++
|
|
}
|
|
for ; x < len(ft.Params.List); x++ {
|
|
param := ft.Params.List[x]
|
|
optional := false
|
|
paramType := param.Type
|
|
// Check for pointer types (optional arguments)
|
|
if star, ok := param.Type.(*ast.StarExpr); ok {
|
|
optional = true
|
|
paramType = star.X
|
|
}
|
|
t := fmt.Sprint(paramType)
|
|
typ, ok := argTypes[t]
|
|
if !ok {
|
|
if optional {
|
|
return nil, fmt.Errorf("unsupported argument type: *%s", t)
|
|
}
|
|
return nil, fmt.Errorf("unsupported argument type: %s", t)
|
|
}
|
|
comment := fieldComments[param]
|
|
// support for foo, bar string
|
|
for _, name := range param.Names {
|
|
f.Args = append(f.Args, Arg{Name: name.Name, Type: typ, Optional: optional, Comment: comment})
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func toOneLine(s string) string {
|
|
return strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
|
|
}
|
|
|
|
// hasComment reports whether any file in the package contains a comment
|
|
// matching the given tag (e.g. "mage:multiline").
|
|
func hasComment(pkg *ast.Package, tag string) bool {
|
|
for _, f := range pkg.Files {
|
|
for _, cg := range f.Comments {
|
|
for _, c := range cg.List {
|
|
vals := strings.Fields(strings.ToLower(c.Text[2:]))
|
|
if len(vals) > 0 && vals[0] == tag {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var argTypes = map[string]string{
|
|
"string": "string",
|
|
"int": "int",
|
|
"float64": "float64",
|
|
"&{time Duration}": "time.Duration",
|
|
"bool": "bool",
|
|
}
|