123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
// Package i18ntool provides conservative, syntax-only Go key extraction.
|
|
package i18ntool
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"strconv"
|
|
)
|
|
|
|
type Reference struct {
|
|
Namespace string `json:"namespace"`
|
|
Key string `json:"key"`
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
}
|
|
type Finding struct {
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
Problem string `json:"problem"`
|
|
}
|
|
type Extraction struct {
|
|
References []Reference `json:"references"`
|
|
Findings []Finding `json:"findings"`
|
|
}
|
|
|
|
// ExtractGo reads literal i18n.Ref expressions, including import aliases. It
|
|
// neither executes project code nor claims to resolve dynamic keys or wrappers.
|
|
// Render/RenderContext calls without a literal reference are reported for review.
|
|
func ExtractGo(filename string, source []byte) (Extraction, error) {
|
|
out := Extraction{References: []Reference{}, Findings: []Finding{}}
|
|
fs := token.NewFileSet()
|
|
file, err := parser.ParseFile(fs, filename, source, 0)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
aliases := map[string]bool{}
|
|
for _, imp := range file.Imports {
|
|
path, _ := strconv.Unquote(imp.Path.Value)
|
|
if path == "git.yuntuo.hk/k2safe/go-omni/i18n" {
|
|
name := "i18n"
|
|
if imp.Name != nil {
|
|
name = imp.Name.Name
|
|
}
|
|
if name == "." {
|
|
out.Findings = append(out.Findings, Finding{filename, fs.Position(imp.Pos()).Line, "dot import requires manual extraction"})
|
|
} else {
|
|
aliases[name] = true
|
|
}
|
|
}
|
|
}
|
|
if len(aliases) == 0 {
|
|
return out, nil
|
|
}
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := lit.Type.(*ast.SelectorExpr)
|
|
if !ok || sel.Sel.Name != "Ref" {
|
|
return true
|
|
}
|
|
pkg, ok := sel.X.(*ast.Ident)
|
|
if !ok || !aliases[pkg.Name] {
|
|
return true
|
|
}
|
|
values := map[string]string{}
|
|
valid := true
|
|
for i, el := range lit.Elts {
|
|
key := ""
|
|
value := el
|
|
if kv, ok := el.(*ast.KeyValueExpr); ok {
|
|
id, ok := kv.Key.(*ast.Ident)
|
|
if !ok {
|
|
valid = false
|
|
continue
|
|
}
|
|
key = id.Name
|
|
value = kv.Value
|
|
} else if i == 0 {
|
|
key = "Namespace"
|
|
} else if i == 1 {
|
|
key = "Key"
|
|
} else {
|
|
valid = false
|
|
}
|
|
s, ok := value.(*ast.BasicLit)
|
|
if !ok || s.Kind != token.STRING {
|
|
valid = false
|
|
continue
|
|
}
|
|
v, e := strconv.Unquote(s.Value)
|
|
if e != nil {
|
|
valid = false
|
|
}
|
|
values[key] = v
|
|
}
|
|
line := fs.Position(lit.Pos()).Line
|
|
if valid && values["Namespace"] != "" && values["Key"] != "" {
|
|
out.References = append(out.References, Reference{values["Namespace"], values["Key"], filename, line})
|
|
} else {
|
|
out.Findings = append(out.Findings, Finding{filename, line, "dynamic or incomplete message reference"})
|
|
}
|
|
return true
|
|
})
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || sel.Sel.Name != "Render" && sel.Sel.Name != "RenderContext" || len(call.Args) != 3 {
|
|
return true
|
|
}
|
|
if _, ok := call.Args[1].(*ast.CompositeLit); !ok {
|
|
out.Findings = append(out.Findings, Finding{filename, fs.Position(call.Pos()).Line, "nonliteral Render reference; verify manually (syntax-only extraction)"})
|
|
}
|
|
return true
|
|
})
|
|
return out, nil
|
|
}
|