mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	feat(build): improve lint-locale-usage further (#8736)
Print out a list of all unused msgids
Handle Go files that make calls to translation.
Handle `models/unit/unit.go`, which stores msgids in `$Unit.NameKey`
Handle .locale.Tr in templates
Handle simple dynamically constructed `Tr("msgid-prefix." + SomeFunctionCall())`.
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8736
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Ellen Εμιλία Άννα Zscheile <fogti+devel@ytrizja.de>
Co-committed-by: Ellen Εμιλία Άννα Zscheile <fogti+devel@ytrizja.de>
	
	
This commit is contained in:
		
					parent
					
						
							
								e101a8e2dd
							
						
					
				
			
			
				commit
				
					
						f447661345
					
				
			
		
					 18 changed files with 720 additions and 320 deletions
				
			
		
							
								
								
									
										217
									
								
								build/lint-locale-usage/handle-go.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								build/lint-locale-usage/handle-go.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,217 @@ | |||
| // Copyright 2023 The Gitea Authors. All rights reserved. | ||||
| // Copyright 2025 The Forgejo Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"go/ast" | ||||
| 	goParser "go/parser" | ||||
| 	"go/token" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| func (handler Handler) handleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit, prefix string) { | ||||
| 	if argLit.Kind == token.STRING { | ||||
| 		// extract string content | ||||
| 		arg, err := strconv.Unquote(argLit.Value) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		// found interesting strings | ||||
| 		arg = prefix + arg | ||||
| 		if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") { | ||||
| 			prep, trunc := PrepareMsgidPrefix(arg) | ||||
| 			if trunc { | ||||
| 				handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg)) | ||||
| 			} | ||||
| 			handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc) | ||||
| 		} else { | ||||
| 			handler.OnMsgid(fset, argLit.ValuePos, arg) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) { | ||||
| 	if argLit, ok := n.(*ast.BasicLit); ok { | ||||
| 		handler.handleGoTrBasicLit(fset, argLit, prefix) | ||||
| 	} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok { | ||||
| 		if argBinExpr.Op != token.ADD { | ||||
| 			// pass | ||||
| 		} else if argLit, ok := argBinExpr.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING { | ||||
| 			// extract string content | ||||
| 			arg, err := strconv.Unquote(argLit.Value) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 			// found interesting strings | ||||
| 			arg = prefix + arg | ||||
| 			prep, trunc := PrepareMsgidPrefix(arg) | ||||
| 			if trunc { | ||||
| 				handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg)) | ||||
| 			} | ||||
| 			handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (handler Handler) handleGoCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, commentPrefix string) *string { | ||||
| 	if cg == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	var matches []token.Pos | ||||
| 	matchInsPrefix := "" | ||||
| 	commentPrefix = "//" + commentPrefix | ||||
| 	for _, comment := range cg.List { | ||||
| 		ctxt := strings.TrimSpace(comment.Text) | ||||
| 		if ctxt == commentPrefix { | ||||
| 			matches = append(matches, comment.Slash) | ||||
| 		} else if after, found := strings.CutPrefix(ctxt, commentPrefix+"Suffix "); found { | ||||
| 			matches = append(matches, comment.Slash) | ||||
| 			matchInsPrefix = strings.TrimSpace(after) | ||||
| 		} | ||||
| 	} | ||||
| 	switch len(matches) { | ||||
| 	case 0: | ||||
| 		return nil | ||||
| 	case 1: | ||||
| 		return &matchInsPrefix | ||||
| 	default: | ||||
| 		handler.OnWarning( | ||||
| 			fset, | ||||
| 			matches[0], | ||||
| 			fmt.Sprintf("encountered multiple %s... directives, ignoring", strings.TrimSpace(commentPrefix)), | ||||
| 		) | ||||
| 		return &matchInsPrefix | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // the `Handle*File` functions follow the following calling convention: | ||||
| // * `fname` is the name of the input file | ||||
| // * `src` is either `nil` (then the function invokes `ReadFile` to read the file) | ||||
| //   or the contents of the file as {`[]byte`, or a `string`} | ||||
| 
 | ||||
| func (handler Handler) HandleGoFile(fname string, src any) error { | ||||
| 	fset := token.NewFileSet() | ||||
| 	node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments) | ||||
| 	if err != nil { | ||||
| 		return LocatedError{ | ||||
| 			Location: fname, | ||||
| 			Kind:     "Go parser", | ||||
| 			Err:      err, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	ast.Inspect(node, func(n ast.Node) bool { | ||||
| 		// search for function calls of the form `anything.Tr(any-string-lit, ...)` | ||||
| 
 | ||||
| 		switch n2 := n.(type) { | ||||
| 		case *ast.CallExpr: | ||||
| 			if len(n2.Args) == 0 { | ||||
| 				return true | ||||
| 			} | ||||
| 			funSel, ok := n2.Fun.(*ast.SelectorExpr) | ||||
| 			if !ok { | ||||
| 				return true | ||||
| 			} | ||||
| 
 | ||||
| 			ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name] | ||||
| 			if !ok { | ||||
| 				return true | ||||
| 			} | ||||
| 
 | ||||
| 			var gotUnexpectedInvoke *int | ||||
| 
 | ||||
| 			for _, argNum := range ltf { | ||||
| 				if len(n2.Args) <= int(argNum) { | ||||
| 					argc := len(n2.Args) | ||||
| 					gotUnexpectedInvoke = &argc | ||||
| 				} else { | ||||
| 					handler.handleGoTrArgument(fset, n2.Args[int(argNum)], "") | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if gotUnexpectedInvoke != nil { | ||||
| 				handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke) | ||||
| 			} | ||||
| 		case *ast.CompositeLit: | ||||
| 			ident, ok := n2.Type.(*ast.Ident) | ||||
| 			if !ok { | ||||
| 				return true | ||||
| 			} | ||||
| 
 | ||||
| 			// special case: models/unit/unit.go | ||||
| 			if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" { | ||||
| 				if len(n2.Elts) != 6 { | ||||
| 					handler.OnWarning(fset, n2.Pos(), "unexpected initialization of 'Unit' (unexpected number of arguments)") | ||||
| 				} | ||||
| 				// NameKey has index 2 | ||||
| 				//   invoked like '{{ctx.Locale.Tr $unit.NameKey}}' | ||||
| 				nameKey, ok := n2.Elts[2].(*ast.BasicLit) | ||||
| 				if !ok || nameKey.Kind != token.STRING { | ||||
| 					handler.OnWarning(fset, n2.Elts[2].Pos(), "unexpected initialization of 'Unit' (expected string literal as NameKey)") | ||||
| 					return true | ||||
| 				} | ||||
| 
 | ||||
| 				// extract string content | ||||
| 				arg, err := strconv.Unquote(nameKey.Value) | ||||
| 				if err == nil { | ||||
| 					// found interesting strings | ||||
| 					handler.OnMsgid(fset, nameKey.ValuePos, arg) | ||||
| 				} | ||||
| 			} | ||||
| 		case *ast.FuncDecl: | ||||
| 			matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey") | ||||
| 			if matchInsPrefix == nil { | ||||
| 				return true | ||||
| 			} | ||||
| 			results := n2.Type.Results.List | ||||
| 			if len(results) != 1 { | ||||
| 				handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name)) | ||||
| 				return true | ||||
| 			} | ||||
| 
 | ||||
| 			ast.Inspect(n2.Body, func(n ast.Node) bool { | ||||
| 				// search for return stmts | ||||
| 				// TODO: what about nested functions? | ||||
| 				if ret, ok := n.(*ast.ReturnStmt); ok { | ||||
| 					for _, res := range ret.Results { | ||||
| 						ast.Inspect(res, func(n ast.Node) bool { | ||||
| 							if expr, ok := n.(ast.Expr); ok { | ||||
| 								handler.handleGoTrArgument(fset, expr, *matchInsPrefix) | ||||
| 							} | ||||
| 							return true | ||||
| 						}) | ||||
| 					} | ||||
| 					return false | ||||
| 				} | ||||
| 				return true | ||||
| 			}) | ||||
| 			return true | ||||
| 		case *ast.GenDecl: | ||||
| 			if !(n2.Tok == token.CONST || n2.Tok == token.VAR) { | ||||
| 				return true | ||||
| 			} | ||||
| 			matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, " llu:TrKeys") | ||||
| 			if matchInsPrefix == nil { | ||||
| 				return true | ||||
| 			} | ||||
| 			for _, spec := range n2.Specs { | ||||
| 				// interpret all contained strings as message IDs | ||||
| 				ast.Inspect(spec, func(n ast.Node) bool { | ||||
| 					if argLit, ok := n.(*ast.BasicLit); ok { | ||||
| 						handler.handleGoTrBasicLit(fset, argLit, *matchInsPrefix) | ||||
| 						return false | ||||
| 					} | ||||
| 					return true | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return true | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue