initial rework of xgotext

This commit is contained in:
Benjamin Böhmke 2020-02-22 22:35:39 +01:00
parent 2b59b30398
commit 75a3d22c53
8 changed files with 525 additions and 336 deletions

View file

@ -4,6 +4,8 @@ import (
"fmt"
"github.com/leonelquinteros/gotext"
alias "github.com/leonelquinteros/gotext"
"github.com/leonelquinteros/gotext/cli/xgotext/fixtures/pkg"
)
// Fake object with methods similar to gotext
@ -15,12 +17,26 @@ func (f Fake) Get(id int) int {
return 42
}
// Fake object with same methods as gotext
type Fake2 struct {
}
// Get by str
func (f Fake2) Get(s string) string {
return s
}
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Translate text from default domain
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// same as before
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// same with alias package name
fmt.Println(alias.Get("alias call"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
@ -43,6 +59,24 @@ func main() {
l.GetDC("domain2", "string", "ctx")
l.GetNDC("translations", "ndc", "ndcs", 7, "NDC-CTX")
// try fake structs
f := Fake{}
f.Get(3)
f2 := Fake2{}
f2.Get("3")
// use translator of sub object
t := pkg.Translate{}
t.L.Get("translate package")
t.S.L.Get("translate sub package")
// redefine alias with fake struct
alias := Fake2{}
alias.Get("3")
}
// dummy function
func dummy(locale *gotext.Locale) {
locale.Get("inside dummy")
}

View file

@ -0,0 +1,16 @@
package pkg
import "github.com/leonelquinteros/gotext"
type SubTranslate struct {
L gotext.Locale
}
type Translate struct {
L gotext.Locale
S SubTranslate
}
func test() {
gotext.Get("inside sub package")
}

View file

@ -2,24 +2,16 @@ package main
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path"
"strconv"
"path/filepath"
"github.com/leonelquinteros/gotext/cli/xgotext/parser"
)
var (
debug = flag.Bool("debug", false, "enable debug mode and print AST")
dirName = flag.String("in", "", "input dir: /path/to/go/pkg")
outputDir = flag.String("out", "", "output dir: /path/to/i18n/files")
fset *token.FileSet
domainFiles map[string]*os.File
currentDomain = "default"
currentFile string
dirName = flag.String("in", "", "input dir: /path/to/go/pkg")
outputDir = flag.String("out", "", "output dir: /path/to/i18n/files")
)
func main() {
@ -28,44 +20,24 @@ func main() {
// Init logger
log.SetFlags(0)
// Init domain files
domainFiles = make(map[string]*os.File)
// Check if dir name parameter is valid
log.Println(*dirName)
f, err := os.Stat(*dirName)
data, err := parser.ParseDirRec(*dirName)
if err != nil {
log.Fatal(err)
}
// Process file or dir
if f.IsDir() {
parseDir(*dirName)
} else {
parseFile(*dirName)
}
}
func getDomainFile(domain string) *os.File {
// Return existent when available
if f, ok := domainFiles[domain]; ok {
return f
}
// If the file doesn't exist, create it.
filePath := path.Join(*outputDir, domain+".po")
f, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
err = os.MkdirAll(*outputDir, os.ModePerm)
if err != nil {
log.Fatal(err)
log.Fatalf("failed to create output dir: %s", err)
}
domainFiles[domain] = f
writePoHeader(f)
return f
}
for name, domain := range data {
outFile := filepath.Join(*outputDir, name+".po")
file, err := os.Create(outFile)
if err != nil {
log.Fatalf("failed to save po file for %s: %s", name, err)
}
func writePoHeader(f *os.File) {
h := `msgid ""
file.WriteString(`msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
@ -74,298 +46,8 @@ msgstr ""
"Language: \n"
"X-Generator: xgotext\n"
`
f.Write([]byte(h))
}
func write(dom, msgid string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgid " + msgid))
f.Write([]byte("\nmsgstr \"\""))
f.Write([]byte("\n"))
}
func writePlural(dom, msgid, msgidPlural string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgid " + msgid))
f.Write([]byte("\nmsgid_plural " + msgidPlural))
f.Write([]byte("\nmsgstr[0] \"\""))
f.Write([]byte("\nmsgstr[1] \"\""))
f.Write([]byte("\n"))
}
func writeContext(dom, ctx string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgctxt " + ctx))
}
func writeComments(dom, file, call string) {
f := getDomainFile(dom)
f.Write([]byte("\n#: " + file))
f.Write([]byte("\n#. " + call))
}
func parseDir(dirName string) error {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dirName, nil, parser.AllErrors)
if err != nil {
log.Fatal(err)
}
for _, pkg := range pkgs {
for fn := range pkg.Files {
parseFile(fn)
}
}
return nil
}
func parseFile(fileName string) error {
// Remember current file to write comments on .po file
currentFile = fileName
// Parse AST
fset = token.NewFileSet()
node, err := parser.ParseFile(fset, fileName, nil, parser.AllErrors)
if err != nil {
log.Fatal(err)
return err
}
// Debug mode
if *debug {
ast.Print(fset, node)
}
ast.Inspect(node, inspectFile)
return nil
}
func inspectFile(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
inspectCallExpr(x)
}
return true
}
func inspectCallExpr(n *ast.CallExpr) {
if se, ok := n.Fun.(*ast.SelectorExpr); ok {
switch se.Sel.String() {
case "Get":
parseGet(n)
case "GetN":
parseGetN(n)
case "GetD":
parseGetD(n)
case "GetND":
parseGetND(n)
case "GetC":
parseGetC(n)
case "GetNC":
parseGetNC(n)
case "GetDC":
parseGetDC(n)
case "GetNDC":
parseGetNDC(n)
case "SetDomain":
parseSetDomain(n)
}
}
}
func parseGet(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 0 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
write(currentDomain, lit.Value)
}
}
}
}
func parseGetN(call *ast.CallExpr) {
if call.Args == nil || len(call.Args) < 3 {
return
}
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok1 := call.Args[1].(*ast.BasicLit); ok1 {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
switch x := call.Args[2].(type) {
case *ast.BasicLit:
if x.Kind != token.INT {
return
}
case *ast.Ident:
if x.Obj.Kind != ast.Var && x.Obj.Kind != ast.Con {
return
}
default:
return
}
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writePlural(currentDomain, lit.Value, lit1.Value)
}
}
}
}
func parseGetD(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
write(dom, lit1.Value)
}
}
}
}
}
func parseGetND(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 2 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writePlural(dom, lit1.Value, lit2.Value)
}
}
}
}
}
}
func parseGetC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(currentDomain, lit1.Value)
write(currentDomain, lit.Value)
}
}
}
}
}
func parseGetNC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 3 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit3, ok := call.Args[3].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit3.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(currentDomain, lit3.Value)
writePlural(currentDomain, lit.Value, lit1.Value)
}
}
}
}
}
}
func parseGetDC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 2 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(dom, lit2.Value)
write(dom, lit1.Value)
}
}
}
}
}
}
func parseGetNDC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 4 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit4, ok := call.Args[4].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING && lit4.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(dom, lit4.Value)
writePlural(dom, lit1.Value, lit2.Value)
}
}
}
}
}
}
}
func parseSetDomain(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) == 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit.Kind == token.STRING {
cd, err := strconv.Unquote(lit.Value)
if err == nil {
currentDomain = cd
}
}
}
`)
file.WriteString(domain.Dump())
file.Close()
}
}

View file

@ -0,0 +1,129 @@
package parser
import (
"sort"
"strings"
)
// Translation for a text to translate
type Translation struct {
MsgId string
MsgIdPlural string
Context string
SourceLocations []string
}
// AddLocations to translation
func (t *Translation) AddLocations(locations []string) {
if t.SourceLocations == nil {
t.SourceLocations = locations
} else {
t.SourceLocations = append(t.SourceLocations, locations...)
}
}
// Dump translation as string
func (t *Translation) Dump() string {
data := make([]string, 0, len(t.SourceLocations)+5)
for _, location := range t.SourceLocations {
data = append(data, "#: "+location)
}
if t.Context != "" {
data = append(data, "msgctxt "+t.Context)
}
data = append(data, "msgid "+t.MsgId)
if t.MsgIdPlural == "" {
data = append(data, "msgstr \"\"")
} else {
data = append(data,
"msgid_plural "+t.MsgIdPlural,
"msgstr[0] \"\"",
"msgstr[1] \"\"")
}
return strings.Join(data, "\n")
}
// TranslationMap contains a map of translations with the ID as key
type TranslationMap map[string]*Translation
// Dump the translation map as string
func (m TranslationMap) Dump() string {
// sort by translation id for consistence output
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
data := make([]string, 0, len(m))
for _, key := range keys {
data = append(data, (m)[key].Dump())
}
return strings.Join(data, "\n\n")
}
// Domain holds all translations of one domain
type Domain struct {
Translations TranslationMap
ContextTranslations map[string]TranslationMap
}
// AddTranslation to the domain
func (d *Domain) AddTranslation(translation *Translation) {
if d.Translations == nil {
d.Translations = make(TranslationMap)
d.ContextTranslations = make(map[string]TranslationMap)
}
if translation.Context == "" {
if t, ok := d.Translations[translation.MsgId]; ok {
t.AddLocations(translation.SourceLocations)
} else {
d.Translations[translation.MsgId] = translation
}
} else {
if _, ok := d.ContextTranslations[translation.Context]; !ok {
d.ContextTranslations[translation.Context] = make(TranslationMap)
}
if t, ok := d.ContextTranslations[translation.Context][translation.MsgId]; ok {
t.AddLocations(translation.SourceLocations)
} else {
d.ContextTranslations[translation.Context][translation.MsgId] = translation
}
}
}
// Dump the domain as string
func (d *Domain) Dump() string {
data := make([]string, 0, len(d.ContextTranslations)+1)
data = append(data, d.Translations.Dump())
// sort context translations by context for consistence output
keys := make([]string, 0, len(d.ContextTranslations))
for k := range d.ContextTranslations {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
data = append(data, d.ContextTranslations[key].Dump())
}
return strings.Join(data, "\n\n")
}
// DomainMap contains multiple domains as map with name as key
type DomainMap map[string]*Domain
// AddTranslation to domain map
func (m *DomainMap) AddTranslation(domain string, translation *Translation) {
if _, ok := (*m)[domain]; !ok {
(*m)[domain] = new(Domain)
}
(*m)[domain].AddTranslation(translation)
}

View file

@ -0,0 +1,255 @@
package parser
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"log"
"path/filepath"
"strconv"
"golang.org/x/tools/go/packages"
)
// GetterDef describes a getter
type GetterDef struct {
Id int
Plural int
Context int
Domain int
}
// maxArgIndex returns the largest argument index
func (d *GetterDef) maxArgIndex() int {
m := d.Id
if d.Plural > m {
m = d.Plural
}
if d.Context > m {
m = d.Context
}
if d.Domain > m {
m = d.Domain
}
return m
}
// list of supported getter
var gotextGetter = map[string]GetterDef{
"Get": {0, -1, -1, -1},
"GetN": {0, 1, -1, -1},
"GetD": {1, -1, -1, 0},
"GetND": {1, 2, -1, 0},
"GetC": {0, -1, 1, -1},
"GetNC": {0, 1, 3, -1},
"GetDC": {1, -1, 2, 0},
"GetNDC": {1, 2, 4, 0},
}
// register go parser
func init() {
AddParser(goParser)
}
// parse go package
func goParser(dirPath, basePath string, data DomainMap) error {
fileSet := token.NewFileSet()
conf := packages.Config{
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedSyntax |
packages.NeedTypes |
packages.NeedTypesInfo,
Fset: fileSet,
Dir: basePath,
}
// load package from path
pkgs, err := packages.Load(&packages.Config{
Mode: conf.Mode,
Fset: fileSet,
Dir: dirPath,
})
if err != nil || len(pkgs) == 0 {
// not a go package
return nil
}
// handle each file
for _, node := range pkgs[0].Syntax {
file := GoFile{
pkgConf: &conf,
filePath: fileSet.Position(node.Package).Filename,
basePath: basePath,
data: data,
fileSet: fileSet,
importedPackages: map[string]*packages.Package{
pkgs[0].Name: pkgs[0],
},
}
ast.Inspect(node, file.inspectFile)
}
return nil
}
// GoFile handles the parsing of one go file
type GoFile struct {
filePath string
basePath string
data DomainMap
fileSet *token.FileSet
pkgConf *packages.Config
importedPackages map[string]*packages.Package
}
// getPackage loads module by name
func (g *GoFile) getPackage(name string) (*packages.Package, error) {
pkgs, err := packages.Load(g.pkgConf, name)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, nil
}
return pkgs[0], nil
}
// getType from ident object
func (g *GoFile) getType(ident *ast.Ident) types.Object {
for _, pkg := range g.importedPackages {
if obj, ok := pkg.TypesInfo.Uses[ident]; ok {
return obj
}
}
return nil
}
func (g *GoFile) inspectFile(n ast.Node) bool {
switch x := n.(type) {
// get names of imported packages
case *ast.ImportSpec:
packageName, _ := strconv.Unquote(x.Path.Value)
pkg, err := g.getPackage(packageName)
if err != nil {
log.Printf("failed to load package %s: %s", packageName, err)
} else {
if x.Name == nil {
g.importedPackages[pkg.Name] = pkg
} else {
g.importedPackages[x.Name.Name] = pkg
}
}
// check each function call
case *ast.CallExpr:
g.inspectCallExpr(x)
default:
print()
}
return true
}
// checkType for gotext object
func (g *GoFile) checkType(rawType types.Type) bool {
switch t := rawType.(type) {
case *types.Pointer:
return g.checkType(t.Elem())
case *types.Named:
if t.Obj().Pkg().Path() != "github.com/leonelquinteros/gotext" {
return false
}
default:
return false
}
return true
}
func (g *GoFile) inspectCallExpr(n *ast.CallExpr) {
// must be a selector expression otherwise it is a local function call
expr, ok := n.Fun.(*ast.SelectorExpr)
if !ok {
return
}
switch e := expr.X.(type) {
// direct call
case *ast.Ident:
// object is a package if the Obj is not set
if e.Obj == nil {
pkg, ok := g.importedPackages[e.Name]
if !ok || pkg.PkgPath != "github.com/leonelquinteros/gotext" {
return
}
} else {
// validate type of object
if !g.checkType(g.getType(e).Type()) {
return
}
}
// call to attribute
case *ast.SelectorExpr:
// validate type of object
if !g.checkType(g.getType(e.Sel).Type()) {
return
}
default:
return
}
// convert args
args := make([]*ast.BasicLit, len(n.Args))
for idx, arg := range n.Args {
args[idx], _ = arg.(*ast.BasicLit)
}
// get position
path, _ := filepath.Rel(g.basePath, g.filePath)
position := fmt.Sprintf("%s:%d", path, g.fileSet.Position(n.Lparen).Line)
// handle getters
if def, ok := gotextGetter[expr.Sel.String()]; ok {
g.parseGetter(def, args, position)
return
}
}
func (g *GoFile) parseGetter(def GetterDef, args []*ast.BasicLit, pos string) {
// check if enough arguments are given
if len(args) < def.maxArgIndex() {
return
}
// get domain
var domain string
if def.Domain == -1 {
domain = "default" // TODO
} else {
domain, _ = strconv.Unquote(args[def.Domain].Value)
}
trans := Translation{
MsgId: args[def.Id].Value,
SourceLocations: []string{pos},
}
if def.Plural > 0 {
trans.MsgIdPlural = args[def.Plural].Value
}
if def.Context > 0 {
trans.Context = args[def.Context].Value
}
g.data.AddTranslation(domain, &trans)
}

View file

@ -0,0 +1,55 @@
package parser
import (
"os"
"path/filepath"
)
// ParseDirFunc parses one directory
type ParseDirFunc func(filePath, basePath string, data DomainMap) error
var knownParser []ParseDirFunc
// AddParser to the known parser list
func AddParser(parser ParseDirFunc) {
if knownParser == nil {
knownParser = []ParseDirFunc{parser}
} else {
knownParser = append(knownParser, parser)
}
}
// ParseDir calls all known parser for each directory
func ParseDir(dirPath, basePath string, data DomainMap) error {
dirPath, _ = filepath.Abs(dirPath)
basePath, _ = filepath.Abs(basePath)
for _, parser := range knownParser {
err := parser(dirPath, basePath, data)
if err != nil {
return err
}
}
return nil
}
// ParseDirRec calls all known parser for each directory
func ParseDirRec(dirPath string) (DomainMap, error) {
data := make(DomainMap)
dirPath, _ = filepath.Abs(dirPath)
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
err := ParseDir(path, dirPath, data)
if err != nil {
return err
}
}
return nil
})
return data, err
}

4
go.mod
View file

@ -1,3 +1,7 @@
module github.com/leonelquinteros/gotext
// go: no requirements found in Gopkg.lock
require golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd
go 1.13

14
go.sum Normal file
View file

@ -0,0 +1,14 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs=
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=