You've already forked postgres_exporter
mirror of
https://github.com/prometheus-community/postgres_exporter.git
synced 2025-08-09 15:42:47 +03:00
This commit implements a massive refactor of the repository, and moves the build system over to use Mage (magefile.org) which should allow seamless building across multiple platforms.
342 lines
7.9 KiB
Go
342 lines
7.9 KiB
Go
package parse
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/doc"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
mgTypes "github.com/magefile/mage/types"
|
|
)
|
|
|
|
type PkgInfo struct {
|
|
Funcs []Function
|
|
DefaultIsError bool
|
|
DefaultIsContext bool
|
|
DefaultName string
|
|
DefaultFunc Function
|
|
Aliases map[string]string
|
|
}
|
|
|
|
// Function represented a job function from a mage file
|
|
type Function struct {
|
|
Name string
|
|
IsError bool
|
|
IsContext bool
|
|
Synopsis string
|
|
Comment string
|
|
}
|
|
|
|
// TemplateString 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) TemplateString() string {
|
|
if f.IsContext && f.IsError {
|
|
out := `wrapFn := func(ctx context.Context) error {
|
|
return %s(ctx)
|
|
}
|
|
err := runTarget(wrapFn)`
|
|
return fmt.Sprintf(out, f.Name)
|
|
}
|
|
if f.IsContext && !f.IsError {
|
|
out := `wrapFn := func(ctx context.Context) error {
|
|
%s(ctx)
|
|
return nil
|
|
}
|
|
err := runTarget(wrapFn)`
|
|
return fmt.Sprintf(out, f.Name)
|
|
}
|
|
if !f.IsContext && f.IsError {
|
|
out := `wrapFn := func(ctx context.Context) error {
|
|
return %s()
|
|
}
|
|
err := runTarget(wrapFn)`
|
|
return fmt.Sprintf(out, f.Name)
|
|
}
|
|
if !f.IsContext && !f.IsError {
|
|
out := `wrapFn := func(ctx context.Context) error {
|
|
%s()
|
|
return nil
|
|
}
|
|
err := runTarget(wrapFn)`
|
|
return fmt.Sprintf(out, f.Name)
|
|
}
|
|
return `fmt.Printf("Error formatting job code\n")
|
|
os.Exit(1)`
|
|
}
|
|
|
|
// Package parses a package
|
|
func Package(path string, files []string) (*PkgInfo, error) {
|
|
fset := token.NewFileSet()
|
|
|
|
pkg, err := getPackage(path, files, fset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info, err := makeInfo(path, fset, pkg.Files)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pi := &PkgInfo{}
|
|
|
|
p := doc.New(pkg, "./", 0)
|
|
for _, f := range p.Funcs {
|
|
if f.Recv != "" {
|
|
// skip methods
|
|
continue
|
|
}
|
|
if !ast.IsExported(f.Name) {
|
|
// skip non-exported functions
|
|
continue
|
|
}
|
|
if typ := voidOrError(f.Decl.Type, info); typ != mgTypes.InvalidType {
|
|
pi.Funcs = append(pi.Funcs, Function{
|
|
Name: f.Name,
|
|
Comment: f.Doc,
|
|
Synopsis: sanitizeSynopsis(f),
|
|
IsError: typ == mgTypes.ErrorType || typ == mgTypes.ContextErrorType,
|
|
IsContext: typ == mgTypes.ContextVoidType || typ == mgTypes.ContextErrorType,
|
|
})
|
|
}
|
|
}
|
|
|
|
setDefault(p, pi, info)
|
|
setAliases(p, pi, info)
|
|
|
|
return pi, nil
|
|
}
|
|
|
|
// 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(p *doc.Package, pi *PkgInfo, info types.Info) {
|
|
for _, v := range p.Vars {
|
|
for x, name := range v.Names {
|
|
if name != "Default" {
|
|
continue
|
|
}
|
|
spec := v.Decl.Specs[x].(*ast.ValueSpec)
|
|
if len(spec.Values) != 1 {
|
|
log.Println("warning: default declaration has multiple values")
|
|
}
|
|
id, ok := spec.Values[0].(*ast.Ident)
|
|
if !ok {
|
|
log.Println("warning: default declaration is not a function name")
|
|
}
|
|
for _, f := range pi.Funcs {
|
|
if f.Name == id.Name {
|
|
pi.DefaultName = f.Name
|
|
pi.DefaultIsError = f.IsError
|
|
pi.DefaultIsContext = f.IsContext
|
|
pi.DefaultFunc = f
|
|
return
|
|
}
|
|
}
|
|
log.Println("warning: default declaration does not reference a mage target")
|
|
}
|
|
}
|
|
}
|
|
|
|
func setAliases(p *doc.Package, pi *PkgInfo, info types.Info) {
|
|
for _, v := range p.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 = make(map[string]string)
|
|
for _, elem := range comp.Elts {
|
|
kv, ok := elem.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
log.Println("warning: alias declaration is not a map element")
|
|
return
|
|
}
|
|
k, ok := kv.Key.(*ast.BasicLit)
|
|
if !ok || k.Kind != token.STRING {
|
|
log.Println("warning: alias is not a string")
|
|
return
|
|
}
|
|
v, ok := kv.Value.(*ast.Ident)
|
|
if !ok {
|
|
log.Println("warning: alias target is not a function")
|
|
return
|
|
}
|
|
alias := strings.Trim(k.Value, "\"")
|
|
valid := false
|
|
for _, f := range pi.Funcs {
|
|
valid = valid || f.Name == v.Name
|
|
}
|
|
if !valid {
|
|
log.Printf("warning: alias declaration (%s) does not reference a mage target", alias)
|
|
}
|
|
pi.Aliases[alias] = v.Name
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// getPackage returns the non-test package at the given path.
|
|
func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) {
|
|
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: %v", err)
|
|
}
|
|
|
|
for name, pkg := range pkgs {
|
|
if !strings.HasSuffix(name, "_test") {
|
|
return pkg, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no non-test packages found in %s", path)
|
|
}
|
|
|
|
func makeInfo(dir string, fset *token.FileSet, files map[string]*ast.File) (types.Info, error) {
|
|
goroot := os.Getenv("GOROOT")
|
|
if goroot == "" {
|
|
c := exec.Command("go", "env", "GOROOT")
|
|
b, err := c.Output()
|
|
if err != nil {
|
|
return types.Info{}, fmt.Errorf("failed to get GOROOT from 'go env': %v", err)
|
|
}
|
|
goroot = strings.TrimSpace(string(b))
|
|
if goroot == "" {
|
|
return types.Info{}, fmt.Errorf("could not determine GOROOT")
|
|
}
|
|
}
|
|
|
|
build.Default.GOROOT = goroot
|
|
|
|
cfg := types.Config{
|
|
Importer: getImporter(fset),
|
|
}
|
|
|
|
info := types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
}
|
|
|
|
fs := make([]*ast.File, 0, len(files))
|
|
for _, v := range files {
|
|
fs = append(fs, v)
|
|
}
|
|
|
|
_, err := cfg.Check(dir, fset, fs, &info)
|
|
if err != nil {
|
|
return info, fmt.Errorf("failed to check types in directory: %v", err)
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// errorOrVoid filters the list of functions to only those that return only an
|
|
// error or have no return value, and have no parameters.
|
|
func errorOrVoid(fns []*ast.FuncDecl, info types.Info) []*ast.FuncDecl {
|
|
fds := []*ast.FuncDecl{}
|
|
|
|
for _, fn := range fns {
|
|
if voidOrError(fn.Type, info) != mgTypes.InvalidType {
|
|
fds = append(fds, fn)
|
|
}
|
|
}
|
|
return fds
|
|
}
|
|
|
|
func hasContextParam(ft *ast.FuncType, info types.Info) bool {
|
|
if ft.Params.NumFields() == 1 {
|
|
ret := ft.Params.List[0]
|
|
t := info.TypeOf(ret.Type)
|
|
if t != nil && t.String() == "context.Context" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasVoidReturn(ft *ast.FuncType, info types.Info) bool {
|
|
res := ft.Results
|
|
if res.NumFields() == 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasErrorReturn(ft *ast.FuncType, info types.Info) bool {
|
|
res := ft.Results
|
|
if res.NumFields() == 1 {
|
|
ret := res.List[0]
|
|
if len(ret.Names) > 1 {
|
|
return false
|
|
}
|
|
t := info.TypeOf(ret.Type)
|
|
if t != nil && t.String() == "error" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func voidOrError(ft *ast.FuncType, info types.Info) mgTypes.FuncType {
|
|
if hasContextParam(ft, info) {
|
|
if hasVoidReturn(ft, info) {
|
|
return mgTypes.ContextVoidType
|
|
}
|
|
if hasErrorReturn(ft, info) {
|
|
return mgTypes.ContextErrorType
|
|
}
|
|
}
|
|
if ft.Params.NumFields() == 0 {
|
|
if hasVoidReturn(ft, info) {
|
|
return mgTypes.VoidType
|
|
}
|
|
if hasErrorReturn(ft, info) {
|
|
return mgTypes.ErrorType
|
|
}
|
|
}
|
|
return mgTypes.InvalidType
|
|
}
|