Files
gotext/cli/xgotext/parser/golang.go

272 lines
5.5 KiB
Go

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() == nil || t.Obj().Pkg().Path() != "git.deineagentur.com/DeineAgenturUG/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 != "git.deineagentur.com/DeineAgenturUG/gotext" {
return
}
} else {
// validate type of object
t := g.getType(e)
if t == nil || !g.checkType(t.Type()) {
return
}
}
// call to attribute
case *ast.SelectorExpr:
// validate type of object
t := g.getType(e.Sel)
if t == nil || !g.checkType(t.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, _ = strconv.Unquote(args[def.Domain].Value)
}
// only handle function calls with strings as ID
if args[def.Id] == nil || args[def.Id].Kind != token.STRING {
log.Printf("ERR: Unsupported call at %s (ID not a string)", pos)
return
}
trans := Translation{
MsgId: args[def.Id].Value,
SourceLocations: []string{pos},
}
if def.Plural > 0 {
// plural ID must be a string
if args[def.Plural] == nil || args[def.Plural].Kind != token.STRING {
log.Printf("ERR: Unsupported call at %s (Plural not a string)", pos)
return
}
trans.MsgIdPlural = args[def.Plural].Value
}
if def.Context > 0 {
// Context must be a string
if args[def.Context] == nil || args[def.Context].Kind != token.STRING {
log.Printf("ERR: Unsupported call at %s (Context not a string)", pos)
return
}
trans.Context = args[def.Context].Value
}
g.data.AddTranslation(domain, &trans)
}