feat: release v0.26 portable i18n and presentation adapters
This commit is contained in:
122
i18n/i18ntool/extract.go
Normal file
122
i18n/i18ntool/extract.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user