func ErrorAbsolutePos(err error, pos token.Position) error {
	list, ok := err.(scanner.ErrorList)
	if !ok {
		return err
	}
	for i, err := range list {
		err.Pos.Filename = pos.Filename
		err.Pos.Line += pos.Line
		err.Pos.Line--  
		err.Pos.Column += pos.Column
		err.Pos.Column--  
		list[i] = err
	}
	return list
}
ErrorAbsolutePos modifies all positions in err, considered to be relative to
pos. This is useful so that the position information of syntax tree nodes
parsed from a comment are relative to the entire file, and not only relative
to the comment containing the source.
package loader
import (
	"encoding/hex"
	"fmt"
	"go/ast"
	"go/constant"
	"go/parser"
	"go/scanner"
	"go/token"
	"go/types"
	"html/template"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"time"
	"github.com/gunk/gunk/assets"
	"github.com/gunk/gunk/log"
	"golang.org/x/tools/go/packages"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/descriptorpb"
)
type Loader struct {
	Dir  string
	Fset *token.FileSet
	Types bool
	cache map[string]*GunkPackage  
}
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
func hexRand(enclen int) string {
	p := make([]byte, hex.DecodedLen(enclen))
	_, _ = seededRand.Read(p)
	return hex.EncodeToString(p)
}
func (l *Loader) addTempGoFiles() (undo func(), _ error) {
	root := "."
	cmd := exec.Command("go", "list", "-m", "-f={{.Dir}}")
	cmd.Dir = l.Dir
	if out, err := cmd.Output(); err == nil {
		root = strings.TrimSpace(string(out))
	}
	if root == "" {
		return nil, fmt.Errorf("empty module root; missing module?")
	}
	var toDelete []string
	if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return nil
		}
		if strings.Contains(path, "@v") {
			return filepath.SkipDir
		}
		infos, err := ioutil.ReadDir(path)
		if err != nil {
			return err
		}
		pkgName := info.Name()  
		anyGunk := false
		for _, info := range infos {
			name := info.Name()
			if strings.HasSuffix(name, ".go") {
				return nil
			}
			if strings.HasSuffix(name, ".gunk") {
				f, err := parser.ParseFile(token.NewFileSet(),
					filepath.Join(path, name), nil, parser.PackageClauseOnly)
				if err == nil {
					pkgName = f.Name.Name
				}
				anyGunk = true
				break
			}
		}
		if !anyGunk {
			return nil
		}
		tmpPath := filepath.Join(path, "gunkpkg-"+hexRand(8)+".go")
		if err := ioutil.WriteFile(tmpPath, []byte("package "+pkgName), 0o666); err != nil {
			return err
		}
		toDelete = append(toDelete, tmpPath)
		return nil
	}); err != nil {
		return nil, err
	}
	return func() {
		anyErr := false
		for _, path := range toDelete {
			if err := os.Remove(path); err != nil {
				anyErr = true
				fmt.Fprintf(os.Stderr, "could not delete gunkpkg file: %v", err)
			}
		}
		if anyErr {
			panic("could not delete some of the gunkpkg files")
		}
	}, nil
}
func (l *Loader) Load(patterns ...string) ([]*GunkPackage, error) {
	if len(patterns) == 1 {
		pkgPath := patterns[0]
		if pkg := l.cache[pkgPath]; pkg != nil {
			return []*GunkPackage{pkg}, nil
		}
	}
	var pkgs []*GunkPackage
	loadFiles := len(patterns) > 0 && strings.HasSuffix(patterns[0], ".gunk")
	if loadFiles {
		pkgs = append(pkgs, &GunkPackage{
			Package: packages.Package{
				ID:      "command-line-arguments",
				Name:    "",  
				PkgPath: "command-line-arguments",
			},
			GunkFiles: patterns,
		})
	} else {
		undo, err := l.addTempGoFiles()
		if err != nil {
			return nil, err
		}
		defer undo()
		cfg := &packages.Config{
			Dir:  l.Dir,
			Mode: packages.LoadFiles,
		}
		lpkgs, err := packages.Load(cfg, patterns...)
		if err != nil {
			return nil, err
		}
		for _, lpkg := range lpkgs {
			pkg := &GunkPackage{Package: *lpkg}
			findGunkFiles(pkg)
			if len(pkg.GunkFiles) == 0 {
				continue
			}
			pkgs = append(pkgs, pkg)
		}
	}
	for _, pkg := range pkgs {
		l.parseGunkPackage(pkg)
		l.validatePackage(pkg)
		if l.cache == nil {
			l.cache = make(map[string]*GunkPackage)
		}
		l.cache[pkg.PkgPath] = pkg
	}
	return pkgs, nil
}
func findGunkFiles(pkg *GunkPackage) {
	for _, gofile := range pkg.GoFiles {
		dir := filepath.Dir(gofile)
		if pkg.Dir == "" {
			pkg.Dir = dir
		} else if dir != pkg.Dir {
			pkg.addError(ListError, 0, nil, "multiple dirs for %s: %s %s",
				pkg.PkgPath, pkg.Dir, dir)
			return  
		}
	}
	matches, err := filepath.Glob(filepath.Join(pkg.Dir, "*.gunk"))
	if err != nil {
		panic(err.Error())
	}
	pkg.GunkFiles = matches
}
const (
	UnknownError = packages.UnknownError
	ListError    = packages.ListError
	ParseError   = packages.ParseError
	TypeError    = packages.TypeError
	ValidateError = packages.TypeError + 10 + iota
)
func (l *Loader) Import(path string) (*types.Package, error) {
	if !strings.Contains(path, ".") {
		cfg := &packages.Config{Mode: packages.LoadTypes}
		pkgs, err := packages.Load(cfg, path)
		if err != nil {
			return nil, err
		}
		if len(pkgs) != 1 {
			panic("expected go/packages.Load to return exactly one package")
		}
		return pkgs[0].Types, nil
	}
	pkgs, err := l.Load(path)
	if err != nil {
		return nil, err
	}
	if len(pkgs) != 1 {
		panic("expected Loader.Load to return exactly one package")
	}
	return pkgs[0].Types, nil
}
type GunkPackage struct {
	packages.Package
	Dir        string       
	GunkFiles  []string     
	GunkSyntax []*ast.File  
	GunkNames []string
	GunkTags  map[ast.Node][]GunkTag
	Imports   map[string]*GunkPackage
	ProtoName string  
}
func (g *GunkPackage) addError(kind packages.ErrorKind, tokenPos token.Pos, fset *token.FileSet, format string, args ...interface{}) {
	pos := ""
	if tokenPos > 0 && fset != nil {
		pos = fset.Position(tokenPos).String()
	}
	g.Errors = append(g.Errors, packages.Error{
		Pos:  pos,
		Msg:  fmt.Sprintf(format, args...),
		Kind: kind,
	})
}
type GunkTag struct {
	ast.Expr                 
	Type     types.Type      
	Value    constant.Value  
}
func (l *Loader) parseGunkPackage(pkg *GunkPackage) {
	for _, fpath := range pkg.GunkFiles {
		file, err := parser.ParseFile(l.Fset, fpath, nil, parser.ParseComments)
		if err != nil {
			pkg.addError(ParseError, 0, nil, "%s", err)
			continue
		}
		relPath := pkg.PkgPath + "/" + filepath.Base(fpath)
		pkg.GunkNames = append(pkg.GunkNames, relPath)
		pkg.GunkSyntax = append(pkg.GunkSyntax, file)
		if name := file.Name.Name; pkg.Name == "" {
			pkg.Name = name
		} else if pkg.Name != name && l.Types {
			pkg.addError(ValidateError, 0, nil, "gunk package name mismatch: %q %q",
				pkg.Name, name)
		}
		name, err := protoPackageName(l.Fset, file)
		if err != nil {
			pkg.addError(ParseError, 0, nil, "%s", err)
			continue
		}
		if pkg.ProtoName == "" {
			pkg.ProtoName = name
		} else if name != "" && l.Types {
			pkg.addError(ValidateError, 0, nil, "proto package name mismatch: %q %q",
				pkg.ProtoName, name)
			continue
		}
	}
	if pkg.ProtoName == "" {
		pkg.ProtoName = pkg.Name
	}
	if len(pkg.Errors) > 0 {
		return
	}
	if !l.Types {
		return
	}
	pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name)
	tconfig := &types.Config{
		DisableUnusedImportCheck: true,
		Importer:                 l,
	}
	pkg.TypesInfo = &types.Info{
		Types:      make(map[ast.Expr]types.TypeAndValue),
		Defs:       make(map[*ast.Ident]types.Object),
		Uses:       make(map[*ast.Ident]types.Object),
		Implicits:  make(map[ast.Node]types.Object),
		Scopes:     make(map[ast.Node]*types.Scope),
		Selections: make(map[*ast.SelectorExpr]*types.Selection),
	}
	check := types.NewChecker(tconfig, l.Fset, pkg.Types, pkg.TypesInfo)
	if err := check.Files(pkg.GunkSyntax); err != nil {
		pkg.addError(TypeError, 0, nil, "%s", err)
		return
	}
	pkg.Imports = make(map[string]*GunkPackage)
	for _, file := range pkg.GunkSyntax {
		l.splitGunkTags(pkg, file)
		for _, spec := range file.Imports {
			pkgPath, _ := strconv.Unquote(spec.Path.Value)
			pkgs, err := l.Load(pkgPath)
			if err != nil {
				panic(err)
			}
			if len(pkgs) == 1 {
				pkg.Imports[pkgPath] = pkgs[0]
			}
		}
	}
}
func (l *Loader) validatePackage(pkg *GunkPackage) {
	for _, file := range pkg.GunkSyntax {
		ast.Inspect(file, func(node ast.Node) bool {
			st, ok := node.(*ast.StructType)
			if !ok || st.Fields == nil {
				return true
			}
			for _, field := range st.Fields.List {
				if len(field.Names) < 1 {
					pkg.addError(ParseError, st.Pos(), l.Fset, "anonymous struct fields are not supported")
					return false
				}
			}
			usedSequences := make(map[int]bool, len(st.Fields.List))
			jsonNamesSeen := map[string]bool{}
			for _, f := range st.Fields.List {
				if f.Tag == nil {
					continue
				}
				fieldName := f.Names[0].Name
				str, _ := strconv.Unquote(f.Tag.Value)
				if err := validateStructTag(str); err != nil {
					pkg.addError(ValidateError, st.Pos(), l.Fset, "error in struct tag on %s: %v", fieldName, err)
					continue
				}
				stag := reflect.StructTag(str)
				val, ok := stag.Lookup("pb")
				if !ok || val == "" {
					continue
				}
				valJson, ok := stag.Lookup("json")
				if ok && valJson != "" {
					if jsonNamesSeen[valJson] {
						err := fmt.Errorf("json tag %q seen twice", valJson)
						pkg.addError(ValidateError, st.Pos(), l.Fset, "error in struct tag on %s: %v", fieldName, err)
						continue
					}
					jsonNamesSeen[valJson] = true
				}
				sequence, err := strconv.Atoi(val)
				if err != nil {
					pkg.addError(ValidateError, st.Pos(), l.Fset, "unable to convert tag to number on %s: %v", fieldName, err)
					continue
				}
				if usedSequences[sequence] {
					pkg.addError(ValidateError, st.Pos(), l.Fset, "sequence %q on %s has already been used in this struct", val, fieldName)
					continue
				}
				usedSequences[sequence] = true
			}
			return true
		})
	}
}
const protoCommentPrefix = "// proto "
func protoPackageName(fset *token.FileSet, file *ast.File) (string, error) {
	packageLine := fset.Position(file.Package).Line
allComments:
	for _, cgroup := range file.Comments {
		for _, comment := range cgroup.List {
			cline := fset.Position(comment.Pos()).Line
			if cline < packageLine {
				continue  
			} else if cline > packageLine {
				break allComments  
			}
			quoted := strings.TrimPrefix(comment.Text, protoCommentPrefix)
			if quoted == comment.Text {
				continue  
			}
			unquoted, err := strconv.Unquote(quoted)
			return unquoted, err
		}
	}
	return "", nil
}
type ProtoLoader struct {
	Dir        string
	ProtocPath string
}
func (l *ProtoLoader) LoadProto(names ...string) ([]*descriptorpb.FileDescriptorProto, error) {
	tmpl := template.Must(template.New("letter").Parse(`
syntax = "proto3";
{{range $_, $name := .}}import "{{$name}}";
{{end}}
`))
	generatedFilesToLoad := []string{}
	filteredNames := make([]string, 0, len(names))
	for _, n := range names {
		switch n {
		case "google/api/annotations.proto":
			generatedFilesToLoad = append(generatedFilesToLoad, "google_api_annotations.fdp")
		case "google/protobuf/empty.proto":
			generatedFilesToLoad = append(generatedFilesToLoad, "google_protobuf_empty.fdp")
		case "google/protobuf/timestamp.proto":
			generatedFilesToLoad = append(generatedFilesToLoad, "google_protobuf_timestamp.fdp")
		case "google/protobuf/duration.proto":
			generatedFilesToLoad = append(generatedFilesToLoad, "google_protobuf_duration.fdp")
		case "protoc-gen-openapiv2/options/annotations.proto":
			generatedFilesToLoad = append(generatedFilesToLoad, "protoc-gen-openapiv2_options_annotations.fdp")
		default:
			filteredNames = append(filteredNames, n)
		}
	}
	var combinedFset descriptorpb.FileDescriptorSet
	if len(filteredNames) > 0 {
		gunkProtoFile := "gunk-proto"
		if l.Dir != "" {
			gunkProtoFile = filepath.Join(l.Dir, gunkProtoFile)
		}
		importsFile, err := os.Create(gunkProtoFile)
		if err != nil {
			return nil, err
		}
		if err := tmpl.Execute(importsFile, filteredNames); err != nil {
			return nil, err
		}
		if err := importsFile.Close(); err != nil {
			return nil, err
		}
		defer os.Remove(gunkProtoFile)
		args := []string{
			"-o/dev/stdout",
			"--include_imports",
			gunkProtoFile,
		}
		if l.Dir != "" {
			args = append(args, "-I"+l.Dir)
		}
		protocPath := "protoc"
		if l.ProtocPath != "" {
			protocPath = l.ProtocPath
		}
		cmd := log.ExecCommand(protocPath, args...)
		out, err := cmd.Output()
		if err != nil {
			if e, ok := err.(*exec.ExitError); ok {
				return nil, fmt.Errorf("protoc %s: %s", e, e.Stderr)
			}
			return nil, err
		}
		var fset descriptorpb.FileDescriptorSet
		if err := proto.Unmarshal(out, &fset); err != nil {
			return nil, err
		}
		for i := 0; i < len(fset.File); i++ {
			if *fset.File[i].Name != "gunk-proto" {
				continue
			}
			combinedFset.File = append(fset.File[:i], fset.File[i+1:]...)
		}
	}
	for _, fileToLoad := range generatedFilesToLoad {
		buf, err := assets.ReadFile(fileToLoad)
		if err != nil {
			return nil, err
		}
		var fset descriptorpb.FileDescriptorSet
		if err := proto.Unmarshal(buf, &fset); err != nil {
			return nil, err
		}
		combinedFset.File = append(combinedFset.File, fset.File...)
	}
	return combinedFset.File, nil
}
func (l *Loader) splitGunkTags(pkg *GunkPackage, file *ast.File) {
	hadError := false
	ast.Inspect(file, func(node ast.Node) bool {
		if gd, ok := node.(*ast.GenDecl); ok {
			if len(gd.Specs) != 1 {
				return true
			}
			if doc := nodeDoc(gd.Specs[0]); doc != nil {
				*doc = gd.Doc
			}
			return true
		}
		doc := nodeDoc(node)
		if doc == nil {
			return true
		}
		docText, exprs, err := SplitGunkTag(pkg, l.Fset, *doc)
		if err != nil {
			hadError = true
			pkg.addError(ParseError, (*doc).Pos(), l.Fset, "%s", err)
			return false
		}
		if len(exprs) > 0 {
			if pkg.GunkTags == nil {
				pkg.GunkTags = make(map[ast.Node][]GunkTag)
			}
			pkg.GunkTags[node] = exprs
			**doc = *CommentFromText(*doc, docText)
		}
		return true
	})
	if !hadError {
		for _, cg := range file.Comments {
			for _, c := range cg.List {
				if strings.Contains(c.Text, "+gunk") {
					pkg.addError(ParseError, c.Pos(), l.Fset, "gunk tag without declaration: %s", c.Text)
				}
			}
		}
	}
}
func nodeDoc(node ast.Node) **ast.CommentGroup {
	switch node := node.(type) {
	case *ast.File:
		return &node.Doc
	case *ast.Field:
		return &node.Doc
	case *ast.TypeSpec:
		return &node.Doc
	case *ast.ValueSpec:
		return &node.Doc
	}
	return nil
}
func CommentFromText(orig ast.Node, text string) *ast.CommentGroup {
	group := &ast.CommentGroup{}
	lines := strings.Split(text, "\n")
	for i, line := range lines {
		comment := &ast.Comment{Text: "// " + line}
		switch i {
		case 0:
			comment.Slash = orig.Pos()
		case len(lines) - 1:
			comment.Slash = orig.End()
		}
		group.List = append(group.List, comment)
	}
	return group
}
func SplitGunkTag(pkg *GunkPackage, fset *token.FileSet, comment *ast.CommentGroup) (string, []GunkTag, error) {
	docLines := strings.Split(comment.Text(), "\n")
	var gunkTagLines []string
	var gunkTagPos []int
	var commentLines []string
	foundGunkTag := false
	for i, line := range docLines {
		if strings.HasPrefix(line, "+gunk ") {
			gunkTagLine := strings.Replace(line, "+gunk", "     ", 1)
			gunkTagLines = append(gunkTagLines, gunkTagLine)
			gunkTagPos = append(gunkTagPos, i)
			foundGunkTag = true
		} else if foundGunkTag {
			gunkTagLines[len(gunkTagLines)-1] += "\n" + line
		} else {
			line = strings.TrimSpace(line)
			if line == "" {
				continue
			}
			commentLines = append(commentLines, line)
		}
	}
	if len(gunkTagLines) == 0 {
		return comment.Text(), nil, nil
	}
	var tags []GunkTag
	for i, gunkTag := range gunkTagLines {
		expr, err := parser.ParseExprFrom(fset, "", gunkTag, 0)
		if err != nil {
			tagPos := fset.Position(comment.Pos())
			tagPos.Line += gunkTagPos[i]  
			tagPos.Column += len("// ")   
			return "", nil, ErrorAbsolutePos(err, tagPos)
		}
		tag := GunkTag{Expr: expr}
		if pkg != nil {
			tv, err := types.Eval(fset, pkg.Types, comment.Pos(), gunkTag)
			if err != nil {
				return "", nil, err
			}
			tag.Type, tag.Value = tv.Type, tv.Value
		}
		tags = append(tags, tag)
	}
	return strings.Join(commentLines, "\n"), tags, nil
}