// 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 }