Created
December 11, 2023 00:56
-
-
Save konifar/65d7642b221e67ce1ab095b31b3654d1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"fmt" | |
"go/ast" | |
"go/parser" | |
"go/token" | |
"log" | |
"os" | |
"path/filepath" | |
"regexp" | |
"runtime" | |
"strconv" | |
"strings" | |
"sync/atomic" | |
"github.com/BurntSushi/toml" | |
) | |
var ( | |
localeFileDir string | |
targetDir string | |
) | |
func init() { | |
flag.StringVar(&localeFileDir, "localeFileDir", ".", "Localeファイルのあるディレクトリ") | |
flag.StringVar(&targetDir, "targetDir", ".", "チェック対象のディレクトリ") | |
} | |
type ErrorInfo struct { | |
Key string | |
FileName string | |
LineNo int | |
Message string // エラーメッセージ | |
} | |
type Result struct { | |
errorInfos []ErrorInfo | |
err error | |
} | |
// Localeファイルからキーと値の一覧を取得 | |
func readLocaleKeys(filename string) (map[string]string, error) { | |
keys := make(map[string]string) | |
_, err := toml.DecodeFile(filename, &keys) | |
if err != nil { | |
return nil, err | |
} | |
return keys, nil | |
} | |
// テンプレート文字列 {{}} のキー一覧を抽出する | |
func extractTemplateKeys(s string) []string { | |
re := regexp.MustCompile(`{{\.(.*?)}}`) | |
matches := re.FindAllStringSubmatch(s, -1) | |
keys := make([]string, len(matches)) | |
for i, match := range matches { | |
if len(match) > 1 { | |
keys[i] = strings.TrimSpace(match[1]) | |
} | |
} | |
return keys | |
} | |
// Goのコードを解析し、i18n.MustLocalize関数の第二引数がLocaleファイルのキーに存在するかをチェック | |
func checkLocalKeyWorker(filenames <-chan string, keys map[string]string, results chan<- Result) { | |
for filename := range filenames { | |
result := Result{} | |
var errorInfos []ErrorInfo | |
fileSet := token.NewFileSet() | |
node, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) | |
if err != nil { | |
result.err = err | |
results <- result | |
} | |
// NOTE: 以下の条件で検査します | |
// 1. i18n.MustLocalize~関数を呼び出している(MustLocalize, MustLocalizeByLanguageCodeなど) | |
// 2. 第 2 引数が文字列で、Localeファイルに存在する | |
// 3. 第 3 引数がマップで、キーがLocaleファイルのテンプレート文字列のキーと一致する | |
ast.Inspect(node, func(n ast.Node) bool { | |
expr, _ := n.(*ast.CallExpr) | |
// 引数の数が3つより小さければスキップ | |
if expr == nil || len(expr.Args) < 3 { | |
return true | |
} | |
// 関数が MustLocalize~ でなければスキップ | |
indent, ok := expr.Fun.(*ast.SelectorExpr) | |
if !ok || !strings.HasPrefix(indent.Sel.Name, "MustLocalize") { | |
return true | |
} | |
// 第2引数が文字列でなければスキップ | |
secondArg, ok := expr.Args[1].(*ast.BasicLit) | |
if !ok || secondArg.Kind != token.STRING { | |
return true | |
} | |
// 第2引数の値がLocaleファイルに登録されているキーでなければエラーとしてerrorInfos に追加 | |
key, _ := strconv.Unquote(secondArg.Value) // #nosec G104 | |
position := fileSet.Position(expr.Pos()) | |
message, ok := keys[key] | |
if !ok { | |
errorInfo := ErrorInfo{ | |
Key: key, | |
FileName: position.Filename, | |
LineNo: position.Line, | |
Message: "キーが存在しません", | |
} | |
errorInfos = append(errorInfos, errorInfo) | |
return true | |
} | |
thirdArg := expr.Args[2] | |
templateKeys := extractTemplateKeys(message) | |
// テンプレート文字列のキーがない時 | |
if len(templateKeys) == 0 { | |
// 第3引数がnilではなければエラーとして errorInfos に追加 | |
paramsMap, ok := thirdArg.(*ast.CompositeLit) | |
if !ok && paramsMap != nil { | |
errorInfo := ErrorInfo{ | |
Key: key, | |
FileName: position.Filename, | |
LineNo: position.Line, | |
Message: "第3引数はnilでなければなりません", | |
} | |
errorInfos = append(errorInfos, errorInfo) | |
return true | |
} | |
} else { | |
// テンプレート文字列のキーがある時 | |
// 第3引数がmapではなければエラーとして errorInfos に追加 | |
compositeLit, ok := thirdArg.(*ast.CompositeLit) | |
if !ok { | |
errorInfo := ErrorInfo{ | |
Key: key, | |
FileName: position.Filename, | |
LineNo: position.Line, | |
Message: "第3引数はmapでなければなりません", | |
} | |
errorInfos = append(errorInfos, errorInfo) | |
return true | |
} | |
// テンプレート文字列のキーが3引数がmapのキーになければエラーとして errorInfos に追加 | |
for _, elt := range compositeLit.Elts { | |
kvExpr, _ := elt.(*ast.KeyValueExpr) // #nosec G104 | |
keyIdent, _ := kvExpr.Key.(*ast.BasicLit) // #nosec G104 | |
paramKey, _ := strconv.Unquote(keyIdent.Value) // #nosec G104 | |
if !contains(templateKeys, paramKey) { | |
errorInfo := ErrorInfo{ | |
Key: key, | |
FileName: position.Filename, | |
LineNo: position.Line, | |
Message: fmt.Sprintf("第3引数のmapのキーがテンプレート文字列のキーと違います, paramsKey: %s, templateKeys: %v", paramKey, templateKeys), | |
} | |
errorInfos = append(errorInfos, errorInfo) | |
return true | |
} | |
} | |
} | |
log.Printf("[pass] key: %s, file: %s:%d\n", key, position.Filename, position.Line) | |
return true | |
}) | |
result.errorInfos = errorInfos | |
results <- result | |
} | |
} | |
func contains(arr []string, str string) bool { | |
for _, a := range arr { | |
if a == str { | |
return true | |
} | |
} | |
return false | |
} | |
// go run scripts/i18n_key_checker/main.go -localeFileDir=helpers/i18n -targetDir=./ | |
func main() { | |
flag.Parse() | |
log.Println("Start checking...") | |
log.Println("------------------------") | |
// Localeファイルからキーの一覧を読み込む | |
keys, err := readLocaleKeys(filepath.Join(localeFileDir, "ja.toml")) | |
if err != nil { | |
log.Fatal(err) | |
} | |
filePaths := make(chan string) | |
results := make(chan Result) | |
// NOTE: 検査チェッカーをつかえる CPU だけ起動 | |
for i := 0; i < runtime.NumCPU(); i++ { | |
go checkLocalKeyWorker(filePaths, keys, results) | |
} | |
// 検査対象のファイルをワーカーに渡し終えたらチャネルを閉じる | |
inputDone := make(chan struct{}) | |
var remainedCount int64 | |
// NOTE: .go ファイルを検査対象として、ワーカーにチャネルで渡す | |
go func() { | |
// #nosec G104 | |
_ = filepath.Walk(targetDir, func(path string, info os.FileInfo, err error) error { | |
if err != nil { | |
return err | |
} | |
// .goファイルの場合のみ | |
if !info.IsDir() && filepath.Ext(path) == ".go" { | |
atomic.AddInt64(&remainedCount, 1) | |
filePaths <- path | |
} | |
return nil | |
}) | |
close(inputDone) | |
close(filePaths) | |
}() | |
var errorInfos []ErrorInfo | |
for { | |
select { | |
case result := <-results: | |
if result.err != nil { | |
log.Fatal(result.err) | |
} | |
if len(result.errorInfos) > 0 { | |
errorInfos = append(errorInfos, result.errorInfos...) | |
} | |
atomic.AddInt64(&remainedCount, -1) | |
case <-inputDone: | |
if remainedCount == 0 { | |
log.Println("Finish checking...") | |
log.Println("------------------------") | |
if len(errorInfos) > 0 { | |
log.Printf("%d invalid keys/values are detected!\n", len(errorInfos)) | |
for _, info := range errorInfos { | |
log.Printf("[fail] %s, key: %s, file: %s:%d\n", info.Message, info.Key, info.FileName, info.LineNo) | |
} | |
os.Exit(1) | |
} | |
log.Println("All keys are valid!") | |
return | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment