extract.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. // Copyright 2016 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package pipeline
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "go/ast"
  10. "go/constant"
  11. "go/format"
  12. "go/token"
  13. "go/types"
  14. "path/filepath"
  15. "sort"
  16. "strings"
  17. "unicode"
  18. "unicode/utf8"
  19. fmtparser "golang.org/x/text/internal/format"
  20. "golang.org/x/tools/go/callgraph"
  21. "golang.org/x/tools/go/callgraph/cha"
  22. "golang.org/x/tools/go/loader"
  23. "golang.org/x/tools/go/ssa"
  24. "golang.org/x/tools/go/ssa/ssautil"
  25. )
  26. const debug = false
  27. // TODO:
  28. // - merge information into existing files
  29. // - handle different file formats (PO, XLIFF)
  30. // - handle features (gender, plural)
  31. // - message rewriting
  32. // - `msg:"etc"` tags
  33. // Extract extracts all strings form the package defined in Config.
  34. func Extract(c *Config) (*State, error) {
  35. x, err := newExtracter(c)
  36. if err != nil {
  37. return nil, wrap(err, "")
  38. }
  39. if err := x.seedEndpoints(); err != nil {
  40. return nil, err
  41. }
  42. x.extractMessages()
  43. return &State{
  44. Config: *c,
  45. program: x.iprog,
  46. Extracted: Messages{
  47. Language: c.SourceLanguage,
  48. Messages: x.messages,
  49. },
  50. }, nil
  51. }
  52. type extracter struct {
  53. conf loader.Config
  54. iprog *loader.Program
  55. prog *ssa.Program
  56. callGraph *callgraph.Graph
  57. // Calls and other expressions to collect.
  58. globals map[token.Pos]*constData
  59. funcs map[token.Pos]*callData
  60. messages []Message
  61. }
  62. func newExtracter(c *Config) (x *extracter, err error) {
  63. x = &extracter{
  64. conf: loader.Config{},
  65. globals: map[token.Pos]*constData{},
  66. funcs: map[token.Pos]*callData{},
  67. }
  68. x.iprog, err = loadPackages(&x.conf, c.Packages)
  69. if err != nil {
  70. return nil, wrap(err, "")
  71. }
  72. x.prog = ssautil.CreateProgram(x.iprog, ssa.GlobalDebug|ssa.BareInits)
  73. x.prog.Build()
  74. x.callGraph = cha.CallGraph(x.prog)
  75. return x, nil
  76. }
  77. func (x *extracter) globalData(pos token.Pos) *constData {
  78. cd := x.globals[pos]
  79. if cd == nil {
  80. cd = &constData{}
  81. x.globals[pos] = cd
  82. }
  83. return cd
  84. }
  85. func (x *extracter) seedEndpoints() error {
  86. pkgInfo := x.iprog.Package("golang.org/x/text/message")
  87. if pkgInfo == nil {
  88. return errors.New("pipeline: golang.org/x/text/message is not imported")
  89. }
  90. pkg := x.prog.Package(pkgInfo.Pkg)
  91. typ := types.NewPointer(pkg.Type("Printer").Type())
  92. x.processGlobalVars()
  93. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Printf"), &callData{
  94. formatPos: 1,
  95. argPos: 2,
  96. isMethod: true,
  97. })
  98. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Sprintf"), &callData{
  99. formatPos: 1,
  100. argPos: 2,
  101. isMethod: true,
  102. })
  103. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Fprintf"), &callData{
  104. formatPos: 2,
  105. argPos: 3,
  106. isMethod: true,
  107. })
  108. return nil
  109. }
  110. // processGlobalVars finds string constants that are assigned to global
  111. // variables.
  112. func (x *extracter) processGlobalVars() {
  113. for _, p := range x.prog.AllPackages() {
  114. m, ok := p.Members["init"]
  115. if !ok {
  116. continue
  117. }
  118. for _, b := range m.(*ssa.Function).Blocks {
  119. for _, i := range b.Instrs {
  120. s, ok := i.(*ssa.Store)
  121. if !ok {
  122. continue
  123. }
  124. a, ok := s.Addr.(*ssa.Global)
  125. if !ok {
  126. continue
  127. }
  128. t := a.Type()
  129. for {
  130. p, ok := t.(*types.Pointer)
  131. if !ok {
  132. break
  133. }
  134. t = p.Elem()
  135. }
  136. if b, ok := t.(*types.Basic); !ok || b.Kind() != types.String {
  137. continue
  138. }
  139. x.visitInit(a, s.Val)
  140. }
  141. }
  142. }
  143. }
  144. type constData struct {
  145. call *callData // to provide a signature for the constants
  146. values []constVal
  147. others []token.Pos // Assigned to other global data.
  148. }
  149. func (d *constData) visit(x *extracter, f func(c constant.Value)) {
  150. for _, v := range d.values {
  151. f(v.value)
  152. }
  153. for _, p := range d.others {
  154. if od, ok := x.globals[p]; ok {
  155. od.visit(x, f)
  156. }
  157. }
  158. }
  159. type constVal struct {
  160. value constant.Value
  161. pos token.Pos
  162. }
  163. type callData struct {
  164. call ssa.CallInstruction
  165. expr *ast.CallExpr
  166. formats []constant.Value
  167. callee *callData
  168. isMethod bool
  169. formatPos int
  170. argPos int // varargs at this position in the call
  171. argTypes []int // arguments extractable from this position
  172. }
  173. func (c *callData) callFormatPos() int {
  174. c = c.callee
  175. if c.isMethod {
  176. return c.formatPos - 1
  177. }
  178. return c.formatPos
  179. }
  180. func (c *callData) callArgsStart() int {
  181. c = c.callee
  182. if c.isMethod {
  183. return c.argPos - 1
  184. }
  185. return c.argPos
  186. }
  187. func (c *callData) Pos() token.Pos { return c.call.Pos() }
  188. func (c *callData) Pkg() *types.Package { return c.call.Parent().Pkg.Pkg }
  189. func (x *extracter) handleFunc(f *ssa.Function, fd *callData) {
  190. for _, e := range x.callGraph.Nodes[f].In {
  191. if e.Pos() == 0 {
  192. continue
  193. }
  194. call := e.Site
  195. caller := x.funcs[call.Pos()]
  196. if caller != nil {
  197. // TODO: theoretically a format string could be passed to multiple
  198. // arguments of a function. Support this eventually.
  199. continue
  200. }
  201. x.debug(call, "CALL", f.String())
  202. caller = &callData{
  203. call: call,
  204. callee: fd,
  205. formatPos: -1,
  206. argPos: -1,
  207. }
  208. // Offset by one if we are invoking an interface method.
  209. offset := 0
  210. if call.Common().IsInvoke() {
  211. offset = -1
  212. }
  213. x.funcs[call.Pos()] = caller
  214. if fd.argPos >= 0 {
  215. x.visitArgs(caller, call.Common().Args[fd.argPos+offset])
  216. }
  217. x.visitFormats(caller, call.Common().Args[fd.formatPos+offset])
  218. }
  219. }
  220. type posser interface {
  221. Pos() token.Pos
  222. Parent() *ssa.Function
  223. }
  224. func (x *extracter) debug(v posser, header string, args ...interface{}) {
  225. if debug {
  226. pos := ""
  227. if p := v.Parent(); p != nil {
  228. pos = posString(&x.conf, p.Package().Pkg, v.Pos())
  229. }
  230. if header != "CALL" && header != "INSERT" {
  231. header = " " + header
  232. }
  233. fmt.Printf("%-32s%-10s%-15T ", pos+fmt.Sprintf("@%d", v.Pos()), header, v)
  234. for _, a := range args {
  235. fmt.Printf(" %v", a)
  236. }
  237. fmt.Println()
  238. }
  239. }
  240. // visitInit evaluates and collects values assigned to global variables in an
  241. // init function.
  242. func (x *extracter) visitInit(global *ssa.Global, v ssa.Value) {
  243. if v == nil {
  244. return
  245. }
  246. x.debug(v, "GLOBAL", v)
  247. switch v := v.(type) {
  248. case *ssa.Phi:
  249. for _, e := range v.Edges {
  250. x.visitInit(global, e)
  251. }
  252. case *ssa.Const:
  253. // Only record strings with letters.
  254. if str := constant.StringVal(v.Value); isMsg(str) {
  255. cd := x.globalData(global.Pos())
  256. cd.values = append(cd.values, constVal{v.Value, v.Pos()})
  257. }
  258. // TODO: handle %m-directive.
  259. case *ssa.Global:
  260. cd := x.globalData(global.Pos())
  261. cd.others = append(cd.others, v.Pos())
  262. case *ssa.FieldAddr, *ssa.Field:
  263. // TODO: mark field index v.Field of v.X.Type() for extraction. extract
  264. // an example args as to give parameters for the translator.
  265. case *ssa.Slice:
  266. if v.Low == nil && v.High == nil && v.Max == nil {
  267. x.visitInit(global, v.X)
  268. }
  269. case *ssa.Alloc:
  270. if ref := v.Referrers(); ref == nil {
  271. for _, r := range *ref {
  272. values := []ssa.Value{}
  273. for _, o := range r.Operands(nil) {
  274. if o == nil || *o == v {
  275. continue
  276. }
  277. values = append(values, *o)
  278. }
  279. // TODO: return something different if we care about multiple
  280. // values as well.
  281. if len(values) == 1 {
  282. x.visitInit(global, values[0])
  283. }
  284. }
  285. }
  286. case ssa.Instruction:
  287. rands := v.Operands(nil)
  288. if len(rands) == 1 && rands[0] != nil {
  289. x.visitInit(global, *rands[0])
  290. }
  291. }
  292. return
  293. }
  294. // visitFormats finds the original source of the value. The returned index is
  295. // position of the argument if originated from a function argument or -1
  296. // otherwise.
  297. func (x *extracter) visitFormats(call *callData, v ssa.Value) {
  298. if v == nil {
  299. return
  300. }
  301. x.debug(v, "VALUE", v)
  302. switch v := v.(type) {
  303. case *ssa.Phi:
  304. for _, e := range v.Edges {
  305. x.visitFormats(call, e)
  306. }
  307. case *ssa.Const:
  308. // Only record strings with letters.
  309. if isMsg(constant.StringVal(v.Value)) {
  310. x.debug(call.call, "FORMAT", v.Value.ExactString())
  311. call.formats = append(call.formats, v.Value)
  312. }
  313. // TODO: handle %m-directive.
  314. case *ssa.Global:
  315. x.globalData(v.Pos()).call = call
  316. case *ssa.FieldAddr, *ssa.Field:
  317. // TODO: mark field index v.Field of v.X.Type() for extraction. extract
  318. // an example args as to give parameters for the translator.
  319. case *ssa.Slice:
  320. if v.Low == nil && v.High == nil && v.Max == nil {
  321. x.visitFormats(call, v.X)
  322. }
  323. case *ssa.Parameter:
  324. // TODO: handle the function for the index parameter.
  325. f := v.Parent()
  326. for i, p := range f.Params {
  327. if p == v {
  328. if call.formatPos < 0 {
  329. call.formatPos = i
  330. // TODO: is there a better way to detect this is calling
  331. // a method rather than a function?
  332. call.isMethod = len(f.Params) > f.Signature.Params().Len()
  333. x.handleFunc(v.Parent(), call)
  334. } else if debug && i != call.formatPos {
  335. // TODO: support this.
  336. fmt.Printf("WARNING:%s: format string passed to arg %d and %d\n",
  337. posString(&x.conf, call.Pkg(), call.Pos()),
  338. call.formatPos, i)
  339. }
  340. }
  341. }
  342. case *ssa.Alloc:
  343. if ref := v.Referrers(); ref == nil {
  344. for _, r := range *ref {
  345. values := []ssa.Value{}
  346. for _, o := range r.Operands(nil) {
  347. if o == nil || *o == v {
  348. continue
  349. }
  350. values = append(values, *o)
  351. }
  352. // TODO: return something different if we care about multiple
  353. // values as well.
  354. if len(values) == 1 {
  355. x.visitFormats(call, values[0])
  356. }
  357. }
  358. }
  359. // TODO:
  360. // case *ssa.Index:
  361. // // Get all values in the array if applicable
  362. // case *ssa.IndexAddr:
  363. // // Get all values in the slice or *array if applicable.
  364. // case *ssa.Lookup:
  365. // // Get all values in the map if applicable.
  366. case *ssa.FreeVar:
  367. // TODO: find the link between free variables and parameters:
  368. //
  369. // func freeVar(p *message.Printer, str string) {
  370. // fn := func(p *message.Printer) {
  371. // p.Printf(str)
  372. // }
  373. // fn(p)
  374. // }
  375. case *ssa.Call:
  376. case ssa.Instruction:
  377. rands := v.Operands(nil)
  378. if len(rands) == 1 && rands[0] != nil {
  379. x.visitFormats(call, *rands[0])
  380. }
  381. }
  382. }
  383. // Note: a function may have an argument marked as both format and passthrough.
  384. // visitArgs collects information on arguments. For wrapped functions it will
  385. // just determine the position of the variable args slice.
  386. func (x *extracter) visitArgs(fd *callData, v ssa.Value) {
  387. if v == nil {
  388. return
  389. }
  390. x.debug(v, "ARGV", v)
  391. switch v := v.(type) {
  392. case *ssa.Slice:
  393. if v.Low == nil && v.High == nil && v.Max == nil {
  394. x.visitArgs(fd, v.X)
  395. }
  396. case *ssa.Parameter:
  397. // TODO: handle the function for the index parameter.
  398. f := v.Parent()
  399. for i, p := range f.Params {
  400. if p == v {
  401. fd.argPos = i
  402. }
  403. }
  404. case *ssa.Alloc:
  405. if ref := v.Referrers(); ref == nil {
  406. for _, r := range *ref {
  407. values := []ssa.Value{}
  408. for _, o := range r.Operands(nil) {
  409. if o == nil || *o == v {
  410. continue
  411. }
  412. values = append(values, *o)
  413. }
  414. // TODO: return something different if we care about
  415. // multiple values as well.
  416. if len(values) == 1 {
  417. x.visitArgs(fd, values[0])
  418. }
  419. }
  420. }
  421. case ssa.Instruction:
  422. rands := v.Operands(nil)
  423. if len(rands) == 1 && rands[0] != nil {
  424. x.visitArgs(fd, *rands[0])
  425. }
  426. }
  427. }
  428. // print returns Go syntax for the specified node.
  429. func (x *extracter) print(n ast.Node) string {
  430. var buf bytes.Buffer
  431. format.Node(&buf, x.conf.Fset, n)
  432. return buf.String()
  433. }
  434. type packageExtracter struct {
  435. f *ast.File
  436. x *extracter
  437. info *loader.PackageInfo
  438. cmap ast.CommentMap
  439. }
  440. func (px packageExtracter) getComment(n ast.Node) string {
  441. cs := px.cmap.Filter(n).Comments()
  442. if len(cs) > 0 {
  443. return strings.TrimSpace(cs[0].Text())
  444. }
  445. return ""
  446. }
  447. func (x *extracter) extractMessages() {
  448. prog := x.iprog
  449. keys := make([]*types.Package, 0, len(x.iprog.AllPackages))
  450. for k := range x.iprog.AllPackages {
  451. keys = append(keys, k)
  452. }
  453. sort.Slice(keys, func(i, j int) bool { return keys[i].Path() < keys[j].Path() })
  454. files := []packageExtracter{}
  455. for _, k := range keys {
  456. info := x.iprog.AllPackages[k]
  457. for _, f := range info.Files {
  458. // Associate comments with nodes.
  459. px := packageExtracter{
  460. f, x, info,
  461. ast.NewCommentMap(prog.Fset, f, f.Comments),
  462. }
  463. files = append(files, px)
  464. }
  465. }
  466. for _, px := range files {
  467. ast.Inspect(px.f, func(n ast.Node) bool {
  468. switch v := n.(type) {
  469. case *ast.CallExpr:
  470. if d := x.funcs[v.Lparen]; d != nil {
  471. d.expr = v
  472. }
  473. }
  474. return true
  475. })
  476. }
  477. for _, px := range files {
  478. ast.Inspect(px.f, func(n ast.Node) bool {
  479. switch v := n.(type) {
  480. case *ast.CallExpr:
  481. return px.handleCall(v)
  482. case *ast.ValueSpec:
  483. return px.handleGlobal(v)
  484. }
  485. return true
  486. })
  487. }
  488. }
  489. func (px packageExtracter) handleGlobal(spec *ast.ValueSpec) bool {
  490. comment := px.getComment(spec)
  491. for _, ident := range spec.Names {
  492. data, ok := px.x.globals[ident.Pos()]
  493. if !ok {
  494. continue
  495. }
  496. name := ident.Name
  497. var arguments []argument
  498. if data.call != nil {
  499. arguments = px.getArguments(data.call)
  500. } else if !strings.HasPrefix(name, "msg") && !strings.HasPrefix(name, "Msg") {
  501. continue
  502. }
  503. data.visit(px.x, func(c constant.Value) {
  504. px.addMessage(spec.Pos(), []string{name}, c, comment, arguments)
  505. })
  506. }
  507. return true
  508. }
  509. func (px packageExtracter) handleCall(call *ast.CallExpr) bool {
  510. x := px.x
  511. data := x.funcs[call.Lparen]
  512. if data == nil || len(data.formats) == 0 {
  513. return true
  514. }
  515. if data.expr != call {
  516. panic("invariant `data.call != call` failed")
  517. }
  518. x.debug(data.call, "INSERT", data.formats)
  519. argn := data.callFormatPos()
  520. if argn >= len(call.Args) {
  521. return true
  522. }
  523. format := call.Args[argn]
  524. arguments := px.getArguments(data)
  525. comment := ""
  526. key := []string{}
  527. if ident, ok := format.(*ast.Ident); ok {
  528. key = append(key, ident.Name)
  529. if v, ok := ident.Obj.Decl.(*ast.ValueSpec); ok && v.Comment != nil {
  530. // TODO: get comment above ValueSpec as well
  531. comment = v.Comment.Text()
  532. }
  533. }
  534. if c := px.getComment(call.Args[0]); c != "" {
  535. comment = c
  536. }
  537. formats := data.formats
  538. for _, c := range formats {
  539. px.addMessage(call.Lparen, key, c, comment, arguments)
  540. }
  541. return true
  542. }
  543. func (px packageExtracter) getArguments(data *callData) []argument {
  544. arguments := []argument{}
  545. x := px.x
  546. info := px.info
  547. if data.callArgsStart() >= 0 {
  548. args := data.expr.Args[data.callArgsStart():]
  549. for i, arg := range args {
  550. expr := x.print(arg)
  551. val := ""
  552. if v := info.Types[arg].Value; v != nil {
  553. val = v.ExactString()
  554. switch arg.(type) {
  555. case *ast.BinaryExpr, *ast.UnaryExpr:
  556. expr = val
  557. }
  558. }
  559. arguments = append(arguments, argument{
  560. ArgNum: i + 1,
  561. Type: info.Types[arg].Type.String(),
  562. UnderlyingType: info.Types[arg].Type.Underlying().String(),
  563. Expr: expr,
  564. Value: val,
  565. Comment: px.getComment(arg),
  566. Position: posString(&x.conf, info.Pkg, arg.Pos()),
  567. // TODO report whether it implements
  568. // interfaces plural.Interface,
  569. // gender.Interface.
  570. })
  571. }
  572. }
  573. return arguments
  574. }
  575. func (px packageExtracter) addMessage(
  576. pos token.Pos,
  577. key []string,
  578. c constant.Value,
  579. comment string,
  580. arguments []argument) {
  581. x := px.x
  582. fmtMsg := constant.StringVal(c)
  583. ph := placeholders{index: map[string]string{}}
  584. trimmed, _, _ := trimWS(fmtMsg)
  585. p := fmtparser.Parser{}
  586. simArgs := make([]interface{}, len(arguments))
  587. for i, v := range arguments {
  588. simArgs[i] = v
  589. }
  590. msg := ""
  591. p.Reset(simArgs)
  592. for p.SetFormat(trimmed); p.Scan(); {
  593. name := ""
  594. var arg *argument
  595. switch p.Status {
  596. case fmtparser.StatusText:
  597. msg += p.Text()
  598. continue
  599. case fmtparser.StatusSubstitution,
  600. fmtparser.StatusBadWidthSubstitution,
  601. fmtparser.StatusBadPrecSubstitution:
  602. arguments[p.ArgNum-1].used = true
  603. arg = &arguments[p.ArgNum-1]
  604. name = getID(arg)
  605. case fmtparser.StatusBadArgNum, fmtparser.StatusMissingArg:
  606. arg = &argument{
  607. ArgNum: p.ArgNum,
  608. Position: posString(&x.conf, px.info.Pkg, pos),
  609. }
  610. name, arg.UnderlyingType = verbToPlaceholder(p.Text(), p.ArgNum)
  611. }
  612. sub := p.Text()
  613. if !p.HasIndex {
  614. r, sz := utf8.DecodeLastRuneInString(sub)
  615. sub = fmt.Sprintf("%s[%d]%c", sub[:len(sub)-sz], p.ArgNum, r)
  616. }
  617. msg += fmt.Sprintf("{%s}", ph.addArg(arg, name, sub))
  618. }
  619. key = append(key, msg)
  620. // Add additional Placeholders that can be used in translations
  621. // that are not present in the string.
  622. for _, arg := range arguments {
  623. if arg.used {
  624. continue
  625. }
  626. ph.addArg(&arg, getID(&arg), fmt.Sprintf("%%[%d]v", arg.ArgNum))
  627. }
  628. x.messages = append(x.messages, Message{
  629. ID: key,
  630. Key: fmtMsg,
  631. Message: Text{Msg: msg},
  632. // TODO(fix): this doesn't get the before comment.
  633. Comment: comment,
  634. Placeholders: ph.slice,
  635. Position: posString(&x.conf, px.info.Pkg, pos),
  636. })
  637. }
  638. func posString(conf *loader.Config, pkg *types.Package, pos token.Pos) string {
  639. p := conf.Fset.Position(pos)
  640. file := fmt.Sprintf("%s:%d:%d", filepath.Base(p.Filename), p.Line, p.Column)
  641. return filepath.Join(pkg.Path(), file)
  642. }
  643. func getID(arg *argument) string {
  644. s := getLastComponent(arg.Expr)
  645. s = strip(s)
  646. s = strings.Replace(s, " ", "", -1)
  647. // For small variable names, use user-defined types for more info.
  648. if len(s) <= 2 && arg.UnderlyingType != arg.Type {
  649. s = getLastComponent(arg.Type)
  650. }
  651. return strings.Title(s)
  652. }
  653. // strip is a dirty hack to convert function calls to placeholder IDs.
  654. func strip(s string) string {
  655. s = strings.Map(func(r rune) rune {
  656. if unicode.IsSpace(r) || r == '-' {
  657. return '_'
  658. }
  659. if !unicode.In(r, unicode.Letter, unicode.Mark, unicode.Number) {
  660. return -1
  661. }
  662. return r
  663. }, s)
  664. // Strip "Get" from getter functions.
  665. if strings.HasPrefix(s, "Get") || strings.HasPrefix(s, "get") {
  666. if len(s) > len("get") {
  667. r, _ := utf8.DecodeRuneInString(s)
  668. if !unicode.In(r, unicode.Ll, unicode.M) { // not lower or mark
  669. s = s[len("get"):]
  670. }
  671. }
  672. }
  673. return s
  674. }
  675. // verbToPlaceholder gives a name for a placeholder based on the substitution
  676. // verb. This is only to be used if there is otherwise no other type information
  677. // available.
  678. func verbToPlaceholder(sub string, pos int) (name, underlying string) {
  679. r, _ := utf8.DecodeLastRuneInString(sub)
  680. name = fmt.Sprintf("Arg_%d", pos)
  681. switch r {
  682. case 's', 'q':
  683. underlying = "string"
  684. case 'd':
  685. name = "Integer"
  686. underlying = "int"
  687. case 'e', 'f', 'g':
  688. name = "Number"
  689. underlying = "float64"
  690. case 'm':
  691. name = "Message"
  692. underlying = "string"
  693. default:
  694. underlying = "interface{}"
  695. }
  696. return name, underlying
  697. }
  698. type placeholders struct {
  699. index map[string]string
  700. slice []Placeholder
  701. }
  702. func (p *placeholders) addArg(arg *argument, name, sub string) (id string) {
  703. id = name
  704. alt, ok := p.index[id]
  705. for i := 1; ok && alt != sub; i++ {
  706. id = fmt.Sprintf("%s_%d", name, i)
  707. alt, ok = p.index[id]
  708. }
  709. p.index[id] = sub
  710. p.slice = append(p.slice, Placeholder{
  711. ID: id,
  712. String: sub,
  713. Type: arg.Type,
  714. UnderlyingType: arg.UnderlyingType,
  715. ArgNum: arg.ArgNum,
  716. Expr: arg.Expr,
  717. Comment: arg.Comment,
  718. })
  719. return id
  720. }
  721. func getLastComponent(s string) string {
  722. return s[1+strings.LastIndexByte(s, '.'):]
  723. }
  724. // isMsg returns whether s should be translated.
  725. func isMsg(s string) bool {
  726. // TODO: parse as format string and omit strings that contain letters
  727. // coming from format verbs.
  728. for _, r := range s {
  729. if unicode.In(r, unicode.L) {
  730. return true
  731. }
  732. }
  733. return false
  734. }