You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

655 lines
20 KiB

  1. // Copyright 2014 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. // Stringer is a tool to automate the creation of methods that satisfy the fmt.Stringer
  5. // interface. Given the name of a (signed or unsigned) integer type T that has constants
  6. // defined, stringer will create a new self-contained Go source file implementing
  7. // func (t T) String() string
  8. // The file is created in the same package and directory as the package that defines T.
  9. // It has helpful defaults designed for use with go generate.
  10. //
  11. // Stringer works best with constants that are consecutive values such as created using iota,
  12. // but creates good code regardless. In the future it might also provide custom support for
  13. // constant sets that are bit patterns.
  14. //
  15. // For example, given this snippet,
  16. //
  17. // package painkiller
  18. //
  19. // type Pill int
  20. //
  21. // const (
  22. // Placebo Pill = iota
  23. // Aspirin
  24. // Ibuprofen
  25. // Paracetamol
  26. // Acetaminophen = Paracetamol
  27. // )
  28. //
  29. // running this command
  30. //
  31. // stringer -type=Pill
  32. //
  33. // in the same directory will create the file pill_string.go, in package painkiller,
  34. // containing a definition of
  35. //
  36. // func (Pill) String() string
  37. //
  38. // That method will translate the value of a Pill constant to the string representation
  39. // of the respective constant name, so that the call fmt.Print(painkiller.Aspirin) will
  40. // print the string "Aspirin".
  41. //
  42. // Typically this process would be run using go generate, like this:
  43. //
  44. // //go:generate stringer -type=Pill
  45. //
  46. // If multiple constants have the same value, the lexically first matching name will
  47. // be used (in the example, Acetaminophen will print as "Paracetamol").
  48. //
  49. // With no arguments, it processes the package in the current directory.
  50. // Otherwise, the arguments must name a single directory holding a Go package
  51. // or a set of Go source files that represent a single Go package.
  52. //
  53. // The -type flag accepts a comma-separated list of types so a single run can
  54. // generate methods for multiple types. The default output file is t_string.go,
  55. // where t is the lower-cased name of the first type listed. It can be overridden
  56. // with the -output flag.
  57. //
  58. package main // import "golang.org/x/tools/cmd/stringer"
  59. import (
  60. "bytes"
  61. "flag"
  62. "fmt"
  63. "go/ast"
  64. "go/build"
  65. exact "go/constant"
  66. "go/format"
  67. "go/parser"
  68. "go/token"
  69. "go/types"
  70. "io/ioutil"
  71. "log"
  72. "os"
  73. "path/filepath"
  74. "sort"
  75. "strings"
  76. )
  77. var (
  78. typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
  79. output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
  80. trimprefix = flag.String("trimprefix", "", "trim the `prefix` from the generated constant names")
  81. linecomment = flag.Bool("linecomment", false, "use line comment text as printed text when present")
  82. )
  83. // Usage is a replacement usage function for the flags package.
  84. func Usage() {
  85. fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
  86. fmt.Fprintf(os.Stderr, "\tstringer [flags] -type T [directory]\n")
  87. fmt.Fprintf(os.Stderr, "\tstringer [flags] -type T files... # Must be a single package\n")
  88. fmt.Fprintf(os.Stderr, "For more information, see:\n")
  89. fmt.Fprintf(os.Stderr, "\thttp://godoc.org/golang.org/x/tools/cmd/stringer\n")
  90. fmt.Fprintf(os.Stderr, "Flags:\n")
  91. flag.PrintDefaults()
  92. }
  93. func main() {
  94. log.SetFlags(0)
  95. log.SetPrefix("stringer: ")
  96. flag.Usage = Usage
  97. flag.Parse()
  98. if len(*typeNames) == 0 {
  99. flag.Usage()
  100. os.Exit(2)
  101. }
  102. types := strings.Split(*typeNames, ",")
  103. // We accept either one directory or a list of files. Which do we have?
  104. args := flag.Args()
  105. if len(args) == 0 {
  106. // Default: process whole package in current directory.
  107. args = []string{"."}
  108. }
  109. // Parse the package once.
  110. var dir string
  111. g := Generator{
  112. trimPrefix: *trimprefix,
  113. lineComment: *linecomment,
  114. }
  115. if len(args) == 1 && isDirectory(args[0]) {
  116. dir = args[0]
  117. g.parsePackageDir(args[0])
  118. } else {
  119. dir = filepath.Dir(args[0])
  120. g.parsePackageFiles(args)
  121. }
  122. // Print the header and package clause.
  123. g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
  124. g.Printf("\n")
  125. g.Printf("package %s", g.pkg.name)
  126. g.Printf("\n")
  127. g.Printf("import \"strconv\"\n") // Used by all methods.
  128. // Run generate for each type.
  129. for _, typeName := range types {
  130. g.generate(typeName)
  131. }
  132. // Format the output.
  133. src := g.format()
  134. // Write to file.
  135. outputName := *output
  136. if outputName == "" {
  137. baseName := fmt.Sprintf("%s_string.go", types[0])
  138. outputName = filepath.Join(dir, strings.ToLower(baseName))
  139. }
  140. err := ioutil.WriteFile(outputName, src, 0644)
  141. if err != nil {
  142. log.Fatalf("writing output: %s", err)
  143. }
  144. }
  145. // isDirectory reports whether the named file is a directory.
  146. func isDirectory(name string) bool {
  147. info, err := os.Stat(name)
  148. if err != nil {
  149. log.Fatal(err)
  150. }
  151. return info.IsDir()
  152. }
  153. // Generator holds the state of the analysis. Primarily used to buffer
  154. // the output for format.Source.
  155. type Generator struct {
  156. buf bytes.Buffer // Accumulated output.
  157. pkg *Package // Package we are scanning.
  158. trimPrefix string
  159. lineComment bool
  160. }
  161. func (g *Generator) Printf(format string, args ...interface{}) {
  162. fmt.Fprintf(&g.buf, format, args...)
  163. }
  164. // File holds a single parsed file and associated data.
  165. type File struct {
  166. pkg *Package // Package to which this file belongs.
  167. file *ast.File // Parsed AST.
  168. // These fields are reset for each type being generated.
  169. typeName string // Name of the constant type.
  170. values []Value // Accumulator for constant values of that type.
  171. trimPrefix string
  172. lineComment bool
  173. }
  174. type Package struct {
  175. dir string
  176. name string
  177. defs map[*ast.Ident]types.Object
  178. files []*File
  179. typesPkg *types.Package
  180. }
  181. // parsePackageDir parses the package residing in the directory.
  182. func (g *Generator) parsePackageDir(directory string) {
  183. pkg, err := build.Default.ImportDir(directory, 0)
  184. if err != nil {
  185. log.Fatalf("cannot process directory %s: %s", directory, err)
  186. }
  187. var names []string
  188. names = append(names, pkg.GoFiles...)
  189. names = append(names, pkg.CgoFiles...)
  190. // TODO: Need to think about constants in test files. Maybe write type_string_test.go
  191. // in a separate pass? For later.
  192. // names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
  193. names = append(names, pkg.SFiles...)
  194. names = prefixDirectory(directory, names)
  195. g.parsePackage(directory, names, nil)
  196. }
  197. // parsePackageFiles parses the package occupying the named files.
  198. func (g *Generator) parsePackageFiles(names []string) {
  199. g.parsePackage(".", names, nil)
  200. }
  201. // prefixDirectory places the directory name on the beginning of each name in the list.
  202. func prefixDirectory(directory string, names []string) []string {
  203. if directory == "." {
  204. return names
  205. }
  206. ret := make([]string, len(names))
  207. for i, name := range names {
  208. ret[i] = filepath.Join(directory, name)
  209. }
  210. return ret
  211. }
  212. // parsePackage analyzes the single package constructed from the named files.
  213. // If text is non-nil, it is a string to be used instead of the content of the file,
  214. // to be used for testing. parsePackage exits if there is an error.
  215. func (g *Generator) parsePackage(directory string, names []string, text interface{}) {
  216. var files []*File
  217. var astFiles []*ast.File
  218. g.pkg = new(Package)
  219. fs := token.NewFileSet()
  220. for _, name := range names {
  221. if !strings.HasSuffix(name, ".go") {
  222. continue
  223. }
  224. parsedFile, err := parser.ParseFile(fs, name, text, parser.ParseComments)
  225. if err != nil {
  226. log.Fatalf("parsing package: %s: %s", name, err)
  227. }
  228. astFiles = append(astFiles, parsedFile)
  229. files = append(files, &File{
  230. file: parsedFile,
  231. pkg: g.pkg,
  232. trimPrefix: g.trimPrefix,
  233. lineComment: g.lineComment,
  234. })
  235. }
  236. if len(astFiles) == 0 {
  237. log.Fatalf("%s: no buildable Go files", directory)
  238. }
  239. g.pkg.name = astFiles[0].Name.Name
  240. g.pkg.files = files
  241. g.pkg.dir = directory
  242. // Type check the package.
  243. g.pkg.check(fs, astFiles)
  244. }
  245. // check type-checks the package. The package must be OK to proceed.
  246. func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) {
  247. pkg.defs = make(map[*ast.Ident]types.Object)
  248. config := types.Config{Importer: defaultImporter(), FakeImportC: true}
  249. info := &types.Info{
  250. Defs: pkg.defs,
  251. }
  252. typesPkg, err := config.Check(pkg.dir, fs, astFiles, info)
  253. if err != nil {
  254. log.Fatalf("checking package: %s", err)
  255. }
  256. pkg.typesPkg = typesPkg
  257. }
  258. // generate produces the String method for the named type.
  259. func (g *Generator) generate(typeName string) {
  260. values := make([]Value, 0, 100)
  261. for _, file := range g.pkg.files {
  262. // Set the state for this run of the walker.
  263. file.typeName = typeName
  264. file.values = nil
  265. if file.file != nil {
  266. ast.Inspect(file.file, file.genDecl)
  267. values = append(values, file.values...)
  268. }
  269. }
  270. if len(values) == 0 {
  271. log.Fatalf("no values defined for type %s", typeName)
  272. }
  273. runs := splitIntoRuns(values)
  274. // The decision of which pattern to use depends on the number of
  275. // runs in the numbers. If there's only one, it's easy. For more than
  276. // one, there's a tradeoff between complexity and size of the data
  277. // and code vs. the simplicity of a map. A map takes more space,
  278. // but so does the code. The decision here (crossover at 10) is
  279. // arbitrary, but considers that for large numbers of runs the cost
  280. // of the linear scan in the switch might become important, and
  281. // rather than use yet another algorithm such as binary search,
  282. // we punt and use a map. In any case, the likelihood of a map
  283. // being necessary for any realistic example other than bitmasks
  284. // is very low. And bitmasks probably deserve their own analysis,
  285. // to be done some other day.
  286. switch {
  287. case len(runs) == 1:
  288. g.buildOneRun(runs, typeName)
  289. case len(runs) <= 10:
  290. g.buildMultipleRuns(runs, typeName)
  291. default:
  292. g.buildMap(runs, typeName)
  293. }
  294. }
  295. // splitIntoRuns breaks the values into runs of contiguous sequences.
  296. // For example, given 1,2,3,5,6,7 it returns {1,2,3},{5,6,7}.
  297. // The input slice is known to be non-empty.
  298. func splitIntoRuns(values []Value) [][]Value {
  299. // We use stable sort so the lexically first name is chosen for equal elements.
  300. sort.Stable(byValue(values))
  301. // Remove duplicates. Stable sort has put the one we want to print first,
  302. // so use that one. The String method won't care about which named constant
  303. // was the argument, so the first name for the given value is the only one to keep.
  304. // We need to do this because identical values would cause the switch or map
  305. // to fail to compile.
  306. j := 1
  307. for i := 1; i < len(values); i++ {
  308. if values[i].value != values[i-1].value {
  309. values[j] = values[i]
  310. j++
  311. }
  312. }
  313. values = values[:j]
  314. runs := make([][]Value, 0, 10)
  315. for len(values) > 0 {
  316. // One contiguous sequence per outer loop.
  317. i := 1
  318. for i < len(values) && values[i].value == values[i-1].value+1 {
  319. i++
  320. }
  321. runs = append(runs, values[:i])
  322. values = values[i:]
  323. }
  324. return runs
  325. }
  326. // format returns the gofmt-ed contents of the Generator's buffer.
  327. func (g *Generator) format() []byte {
  328. src, err := format.Source(g.buf.Bytes())
  329. if err != nil {
  330. // Should never happen, but can arise when developing this code.
  331. // The user can compile the output to see the error.
  332. log.Printf("warning: internal error: invalid Go generated: %s", err)
  333. log.Printf("warning: compile the package to analyze the error")
  334. return g.buf.Bytes()
  335. }
  336. return src
  337. }
  338. // Value represents a declared constant.
  339. type Value struct {
  340. name string // The name of the constant.
  341. // The value is stored as a bit pattern alone. The boolean tells us
  342. // whether to interpret it as an int64 or a uint64; the only place
  343. // this matters is when sorting.
  344. // Much of the time the str field is all we need; it is printed
  345. // by Value.String.
  346. value uint64 // Will be converted to int64 when needed.
  347. signed bool // Whether the constant is a signed type.
  348. str string // The string representation given by the "go/exact" package.
  349. }
  350. func (v *Value) String() string {
  351. return v.str
  352. }
  353. // byValue lets us sort the constants into increasing order.
  354. // We take care in the Less method to sort in signed or unsigned order,
  355. // as appropriate.
  356. type byValue []Value
  357. func (b byValue) Len() int { return len(b) }
  358. func (b byValue) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
  359. func (b byValue) Less(i, j int) bool {
  360. if b[i].signed {
  361. return int64(b[i].value) < int64(b[j].value)
  362. }
  363. return b[i].value < b[j].value
  364. }
  365. // genDecl processes one declaration clause.
  366. func (f *File) genDecl(node ast.Node) bool {
  367. decl, ok := node.(*ast.GenDecl)
  368. if !ok || decl.Tok != token.CONST {
  369. // We only care about const declarations.
  370. return true
  371. }
  372. // The name of the type of the constants we are declaring.
  373. // Can change if this is a multi-element declaration.
  374. typ := ""
  375. // Loop over the elements of the declaration. Each element is a ValueSpec:
  376. // a list of names possibly followed by a type, possibly followed by values.
  377. // If the type and value are both missing, we carry down the type (and value,
  378. // but the "go/types" package takes care of that).
  379. for _, spec := range decl.Specs {
  380. vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
  381. if vspec.Type == nil && len(vspec.Values) > 0 {
  382. // "X = 1". With no type but a value, the constant is untyped.
  383. // Skip this vspec and reset the remembered type.
  384. typ = ""
  385. continue
  386. }
  387. if vspec.Type != nil {
  388. // "X T". We have a type. Remember it.
  389. ident, ok := vspec.Type.(*ast.Ident)
  390. if !ok {
  391. continue
  392. }
  393. typ = ident.Name
  394. }
  395. if typ != f.typeName {
  396. // This is not the type we're looking for.
  397. continue
  398. }
  399. // We now have a list of names (from one line of source code) all being
  400. // declared with the desired type.
  401. // Grab their names and actual values and store them in f.values.
  402. for _, name := range vspec.Names {
  403. if name.Name == "_" {
  404. continue
  405. }
  406. // This dance lets the type checker find the values for us. It's a
  407. // bit tricky: look up the object declared by the name, find its
  408. // types.Const, and extract its value.
  409. obj, ok := f.pkg.defs[name]
  410. if !ok {
  411. log.Fatalf("no value for constant %s", name)
  412. }
  413. info := obj.Type().Underlying().(*types.Basic).Info()
  414. if info&types.IsInteger == 0 {
  415. log.Fatalf("can't handle non-integer constant type %s", typ)
  416. }
  417. value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
  418. if value.Kind() != exact.Int {
  419. log.Fatalf("can't happen: constant is not an integer %s", name)
  420. }
  421. i64, isInt := exact.Int64Val(value)
  422. u64, isUint := exact.Uint64Val(value)
  423. if !isInt && !isUint {
  424. log.Fatalf("internal error: value of %s is not an integer: %s", name, value.String())
  425. }
  426. if !isInt {
  427. u64 = uint64(i64)
  428. }
  429. v := Value{
  430. name: name.Name,
  431. value: u64,
  432. signed: info&types.IsUnsigned == 0,
  433. str: value.String(),
  434. }
  435. if c := vspec.Comment; f.lineComment && c != nil && len(c.List) == 1 {
  436. v.name = strings.TrimSpace(c.Text())
  437. }
  438. v.name = strings.TrimPrefix(v.name, f.trimPrefix)
  439. f.values = append(f.values, v)
  440. }
  441. }
  442. return false
  443. }
  444. // Helpers
  445. // usize returns the number of bits of the smallest unsigned integer
  446. // type that will hold n. Used to create the smallest possible slice of
  447. // integers to use as indexes into the concatenated strings.
  448. func usize(n int) int {
  449. switch {
  450. case n < 1<<8:
  451. return 8
  452. case n < 1<<16:
  453. return 16
  454. default:
  455. // 2^32 is enough constants for anyone.
  456. return 32
  457. }
  458. }
  459. // declareIndexAndNameVars declares the index slices and concatenated names
  460. // strings representing the runs of values.
  461. func (g *Generator) declareIndexAndNameVars(runs [][]Value, typeName string) {
  462. var indexes, names []string
  463. for i, run := range runs {
  464. index, name := g.createIndexAndNameDecl(run, typeName, fmt.Sprintf("_%d", i))
  465. if len(run) != 1 {
  466. indexes = append(indexes, index)
  467. }
  468. names = append(names, name)
  469. }
  470. g.Printf("const (\n")
  471. for _, name := range names {
  472. g.Printf("\t%s\n", name)
  473. }
  474. g.Printf(")\n\n")
  475. if len(indexes) > 0 {
  476. g.Printf("var (")
  477. for _, index := range indexes {
  478. g.Printf("\t%s\n", index)
  479. }
  480. g.Printf(")\n\n")
  481. }
  482. }
  483. // declareIndexAndNameVar is the single-run version of declareIndexAndNameVars
  484. func (g *Generator) declareIndexAndNameVar(run []Value, typeName string) {
  485. index, name := g.createIndexAndNameDecl(run, typeName, "")
  486. g.Printf("const %s\n", name)
  487. g.Printf("var %s\n", index)
  488. }
  489. // createIndexAndNameDecl returns the pair of declarations for the run. The caller will add "const" and "var".
  490. func (g *Generator) createIndexAndNameDecl(run []Value, typeName string, suffix string) (string, string) {
  491. b := new(bytes.Buffer)
  492. indexes := make([]int, len(run))
  493. for i := range run {
  494. b.WriteString(run[i].name)
  495. indexes[i] = b.Len()
  496. }
  497. nameConst := fmt.Sprintf("_%s_name%s = %q", typeName, suffix, b.String())
  498. nameLen := b.Len()
  499. b.Reset()
  500. fmt.Fprintf(b, "_%s_index%s = [...]uint%d{0, ", typeName, suffix, usize(nameLen))
  501. for i, v := range indexes {
  502. if i > 0 {
  503. fmt.Fprintf(b, ", ")
  504. }
  505. fmt.Fprintf(b, "%d", v)
  506. }
  507. fmt.Fprintf(b, "}")
  508. return b.String(), nameConst
  509. }
  510. // declareNameVars declares the concatenated names string representing all the values in the runs.
  511. func (g *Generator) declareNameVars(runs [][]Value, typeName string, suffix string) {
  512. g.Printf("const _%s_name%s = \"", typeName, suffix)
  513. for _, run := range runs {
  514. for i := range run {
  515. g.Printf("%s", run[i].name)
  516. }
  517. }
  518. g.Printf("\"\n")
  519. }
  520. // buildOneRun generates the variables and String method for a single run of contiguous values.
  521. func (g *Generator) buildOneRun(runs [][]Value, typeName string) {
  522. values := runs[0]
  523. g.Printf("\n")
  524. g.declareIndexAndNameVar(values, typeName)
  525. // The generated code is simple enough to write as a Printf format.
  526. lessThanZero := ""
  527. if values[0].signed {
  528. lessThanZero = "i < 0 || "
  529. }
  530. if values[0].value == 0 { // Signed or unsigned, 0 is still 0.
  531. g.Printf(stringOneRun, typeName, usize(len(values)), lessThanZero)
  532. } else {
  533. g.Printf(stringOneRunWithOffset, typeName, values[0].String(), usize(len(values)), lessThanZero)
  534. }
  535. }
  536. // Arguments to format are:
  537. // [1]: type name
  538. // [2]: size of index element (8 for uint8 etc.)
  539. // [3]: less than zero check (for signed types)
  540. const stringOneRun = `func (i %[1]s) String() string {
  541. if %[3]si >= %[1]s(len(_%[1]s_index)-1) {
  542. return "%[1]s(" + strconv.FormatInt(int64(i), 10) + ")"
  543. }
  544. return _%[1]s_name[_%[1]s_index[i]:_%[1]s_index[i+1]]
  545. }
  546. `
  547. // Arguments to format are:
  548. // [1]: type name
  549. // [2]: lowest defined value for type, as a string
  550. // [3]: size of index element (8 for uint8 etc.)
  551. // [4]: less than zero check (for signed types)
  552. /*
  553. */
  554. const stringOneRunWithOffset = `func (i %[1]s) String() string {
  555. i -= %[2]s
  556. if %[4]si >= %[1]s(len(_%[1]s_index)-1) {
  557. return "%[1]s(" + strconv.FormatInt(int64(i + %[2]s), 10) + ")"
  558. }
  559. return _%[1]s_name[_%[1]s_index[i] : _%[1]s_index[i+1]]
  560. }
  561. `
  562. // buildMultipleRuns generates the variables and String method for multiple runs of contiguous values.
  563. // For this pattern, a single Printf format won't do.
  564. func (g *Generator) buildMultipleRuns(runs [][]Value, typeName string) {
  565. g.Printf("\n")
  566. g.declareIndexAndNameVars(runs, typeName)
  567. g.Printf("func (i %s) String() string {\n", typeName)
  568. g.Printf("\tswitch {\n")
  569. for i, values := range runs {
  570. if len(values) == 1 {
  571. g.Printf("\tcase i == %s:\n", &values[0])
  572. g.Printf("\t\treturn _%s_name_%d\n", typeName, i)
  573. continue
  574. }
  575. g.Printf("\tcase %s <= i && i <= %s:\n", &values[0], &values[len(values)-1])
  576. if values[0].value != 0 {
  577. g.Printf("\t\ti -= %s\n", &values[0])
  578. }
  579. g.Printf("\t\treturn _%s_name_%d[_%s_index_%d[i]:_%s_index_%d[i+1]]\n",
  580. typeName, i, typeName, i, typeName, i)
  581. }
  582. g.Printf("\tdefault:\n")
  583. g.Printf("\t\treturn \"%s(\" + strconv.FormatInt(int64(i), 10) + \")\"\n", typeName)
  584. g.Printf("\t}\n")
  585. g.Printf("}\n")
  586. }
  587. // buildMap handles the case where the space is so sparse a map is a reasonable fallback.
  588. // It's a rare situation but has simple code.
  589. func (g *Generator) buildMap(runs [][]Value, typeName string) {
  590. g.Printf("\n")
  591. g.declareNameVars(runs, typeName, "")
  592. g.Printf("\nvar _%s_map = map[%s]string{\n", typeName, typeName)
  593. n := 0
  594. for _, values := range runs {
  595. for _, value := range values {
  596. g.Printf("\t%s: _%s_name[%d:%d],\n", &value, typeName, n, n+len(value.name))
  597. n += len(value.name)
  598. }
  599. }
  600. g.Printf("}\n\n")
  601. g.Printf(stringMap, typeName)
  602. }
  603. // Argument to format is the type name.
  604. const stringMap = `func (i %[1]s) String() string {
  605. if str, ok := _%[1]s_map[i]; ok {
  606. return str
  607. }
  608. return "%[1]s(" + strconv.FormatInt(int64(i), 10) + ")"
  609. }
  610. `