ast.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. // Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
  2. // Copyright © 2017 - William Edwards
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License
  15. package gdnative
  16. import (
  17. "bytes"
  18. "fmt"
  19. "go/ast"
  20. "go/printer"
  21. "go/token"
  22. "os"
  23. "reflect"
  24. "strings"
  25. )
  26. const (
  27. godotRegister string = "godot::register"
  28. godotConstructor string = "godot::constructor"
  29. godotDestructor string = "godot::destructor"
  30. godotExport string = "godot::export"
  31. )
  32. // LookupRegistrableTypeDeclarations parses the given package AST and adds any
  33. // relevant data to the registry so we can generate boilerplate registration code
  34. func LookupRegistrableTypeDeclarations(pkg *ast.Package) map[string]Registrable {
  35. var classes = make(map[string]Registrable)
  36. // make a first iteration to capture all registrable classes and their properties
  37. for _, file := range pkg.Files {
  38. for _, node := range file.Decls {
  39. gd, ok := node.(*ast.GenDecl)
  40. if !ok {
  41. continue
  42. }
  43. for _, d := range gd.Specs {
  44. tp, ok := d.(*ast.TypeSpec)
  45. if !ok {
  46. continue
  47. }
  48. sp, ok := tp.Type.(*ast.StructType)
  49. if !ok {
  50. continue
  51. }
  52. if gd.Doc != nil {
  53. for _, line := range gd.Doc.List {
  54. original := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
  55. docstring := strings.ToLower(original)
  56. if strings.HasPrefix(docstring, godotRegister) {
  57. className := getClassName(tp)
  58. class := registryClass{
  59. base: getBaseClassName(sp),
  60. }
  61. classes[className] = &class
  62. // set alias if defined
  63. if strings.Contains(docstring, " as ") {
  64. class.alias = strings.TrimSpace(strings.Split(strings.Split(original, " as ")[1], " ")[0])
  65. }
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }
  72. // make a second iteration to look for class methods and signals
  73. for className := range classes {
  74. class := classes[className]
  75. for _, file := range pkg.Files {
  76. class.SetConstructor(lookupInstanceCreateFunc(className, file))
  77. class.SetDestructor(lookupInstanceDestroyFunc(className, file))
  78. class.AddMethods(lookupMethods(className, file))
  79. class.AddSignals(lookupSignals(className, file))
  80. class.AddProperties(lookupProperties(className, file))
  81. }
  82. }
  83. return classes
  84. }
  85. // getClassName extracts and build the right class name for the registry
  86. func getClassName(tp *ast.TypeSpec) string {
  87. className := tp.Name.String()
  88. return className
  89. }
  90. // getBaseClassName extracts the base class name for the registry
  91. func getBaseClassName(sp *ast.StructType) string {
  92. // TODO: need to make this way smarter to look for parent types of this type
  93. var baseClassName string
  94. for i := 0; i < sp.Fields.NumFields()-1; i++ {
  95. expr, ok := sp.Fields.List[i].Type.(*ast.SelectorExpr)
  96. if !ok {
  97. continue
  98. }
  99. ident, ok := expr.X.(*ast.Ident)
  100. if !ok {
  101. continue
  102. }
  103. if ident.Name != "godot" {
  104. continue
  105. }
  106. baseClassName = fmt.Sprintf("godot.%s", expr.Sel.Name)
  107. break
  108. }
  109. return baseClassName
  110. }
  111. // lookupInstanceCreateFunc extract the "constructor" for the given type or create a default one
  112. func lookupInstanceCreateFunc(className string, file *ast.File) *registryConstructor {
  113. for _, node := range file.Decls {
  114. fd, ok := node.(*ast.FuncDecl)
  115. if !ok || fd.Doc == nil {
  116. continue
  117. }
  118. for _, line := range fd.Doc.List {
  119. docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
  120. if strings.HasPrefix(strings.ToLower(docstring), godotConstructor) {
  121. structName := strings.TrimSpace(docstring[len(godotConstructor):])
  122. // get rid of parenthesis
  123. if strings.HasPrefix(structName, "(") && strings.HasSuffix(structName, ")") {
  124. // make sure this is the only parenthesis structName
  125. if strings.Count(structName, "(") > 1 || strings.Count(structName, ")") > 1 {
  126. // this is a syntax error
  127. fmt.Printf("could not parse constructor comment %s, many parenthesis", docstring)
  128. }
  129. structName = structName[1 : len(structName)-1]
  130. if structName != className {
  131. // this constructor doesn't match with our class, skip it
  132. continue
  133. }
  134. constructor, err := validateConstructor(structName, fd)
  135. if err != nil {
  136. panic(err)
  137. }
  138. return constructor
  139. }
  140. }
  141. }
  142. }
  143. // if we are here it means the user didn't specified a custom constructor
  144. return nil
  145. }
  146. // validateConstructor returns an error if the given constructor is not valid, it returns nil otherwise
  147. func validateConstructor(structName string, fd *ast.FuncDecl) (*registryConstructor, error) {
  148. funcName := fd.Name.String()
  149. if fd.Recv != nil {
  150. value := "UnknownType"
  151. switch t := fd.Recv.List[0].Type.(type) {
  152. case *ast.StarExpr:
  153. value = t.X.(*ast.Ident).Name
  154. case *ast.Ident:
  155. value = t.Name
  156. }
  157. return nil, fmt.Errorf("%s is a method of %s type it can not be used as constructor", funcName, value)
  158. }
  159. if fd.Type.Params.List != nil {
  160. return nil, fmt.Errorf("constructors of %s values take no params but %s takes %d", structName, funcName, fd.Type.Params.NumFields())
  161. }
  162. if fd.Type.Results == nil || fd.Type.Results.List == nil {
  163. return nil, fmt.Errorf("constructors of %s values have to return a pointer to *%s but %s return nothing", structName, structName, funcName)
  164. }
  165. if fd.Type.Results.NumFields() > 1 {
  166. return nil, fmt.Errorf("constructors of %s values must return exactly one value but %s returns %d", structName, funcName, fd.Type.Results.NumFields())
  167. }
  168. switch t := fd.Type.Results.List[0].Type.(type) {
  169. case *ast.StarExpr:
  170. if t.X.(*ast.Ident).Name != structName {
  171. return nil, fmt.Errorf(
  172. "constructors of %s values must return a pointer to *%s but %s returns a pointer to %s instead",
  173. structName, structName, funcName, t.X.(*ast.Ident).Name,
  174. )
  175. }
  176. default:
  177. return nil, fmt.Errorf("constructors of %s values must return a pointer to *%s but %s returns %v", structName, structName, funcName, t)
  178. }
  179. constructor := registryConstructor{
  180. class: structName,
  181. customFunc: fd.Name.String(),
  182. }
  183. return &constructor, nil
  184. }
  185. // lookupInstanceDestroyFunc
  186. func lookupInstanceDestroyFunc(className string, file *ast.File) *registryDestructor {
  187. for _, node := range file.Decls {
  188. fd, ok := node.(*ast.FuncDecl)
  189. if !ok || fd.Doc == nil {
  190. continue
  191. }
  192. for _, line := range fd.Doc.List {
  193. docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
  194. if strings.HasPrefix(strings.ToLower(docstring), godotDestructor) {
  195. structName := strings.TrimSpace(docstring[len(godotDestructor):])
  196. // get rid of parenthesis
  197. if strings.HasPrefix(structName, "(") && strings.HasSuffix(structName, ")") {
  198. // make sure this is the only parenthesis structName
  199. if strings.Count(structName, "(") > 1 || strings.Count(structName, ")") > 1 {
  200. // this is a syntax error
  201. fmt.Printf("could not parse destructor comment %s, many parenthesis", docstring)
  202. os.Exit(1)
  203. }
  204. structName = structName[1 : len(structName)-1]
  205. if structName != className {
  206. // this destructor doesn't match with our class, skip it
  207. continue
  208. }
  209. destructor, err := validateDestructor(structName, fd)
  210. if err != nil {
  211. fmt.Printf("could not validate destructor: %s", err)
  212. os.Exit(1)
  213. }
  214. return destructor
  215. }
  216. }
  217. }
  218. }
  219. // if we are here that means the user didn't provide a custom destructor function
  220. return nil
  221. }
  222. // validateDestructor returns an error if the given constructor is not valid, it returns nil otherwise
  223. func validateDestructor(structName string, fd *ast.FuncDecl) (*registryDestructor, error) {
  224. funcName := fd.Name.String()
  225. if fd.Recv != nil {
  226. value := "UnknownType"
  227. switch t := fd.Recv.List[0].Type.(type) {
  228. case *ast.StarExpr:
  229. value = t.X.(*ast.Ident).Name
  230. case *ast.Ident:
  231. value = t.Name
  232. }
  233. return nil, fmt.Errorf("%s is a method of %s type it can not be used as destructor", funcName, value)
  234. }
  235. if fd.Type.Params.List != nil {
  236. return nil, fmt.Errorf("destructors of %s values take no params but %s takes %d", structName, funcName, fd.Type.Params.NumFields())
  237. }
  238. if fd.Type.Results != nil || fd.Type.Results.List != nil || len(fd.Type.Results.List) > 0 {
  239. return nil, fmt.Errorf("destructors of %s values have to return a pointer to *%s but %s return nothing", structName, structName, funcName)
  240. }
  241. destructor := registryDestructor{
  242. class: structName,
  243. customFunc: funcName,
  244. }
  245. return &destructor, nil
  246. }
  247. // lookupMethods look up for every exported method that is owned by the type
  248. // and fill a registration data structure with it
  249. func lookupMethods(className string, file *ast.File) []*registryMethod {
  250. methods := []*registryMethod{}
  251. for _, node := range file.Decls {
  252. fd, ok := node.(*ast.FuncDecl)
  253. if !ok {
  254. continue
  255. }
  256. // check for init function, if present fail and complain
  257. if fd.Name.String() == "init" {
  258. fmt.Printf(
  259. "init function present on files, you can not provide your own init function while autoregistering classes",
  260. )
  261. os.Exit(1)
  262. }
  263. // ignore non methods
  264. if fd.Recv == nil {
  265. continue
  266. }
  267. alias, exported := extractExportedAndAliasFromDoc(fd.Doc)
  268. // ignore non exported methods
  269. if !fd.Name.IsExported() && !exported {
  270. continue
  271. }
  272. // ignore methods from other types
  273. switch t := fd.Recv.List[0].Type.(type) {
  274. case *ast.StarExpr:
  275. if t.X.(*ast.Ident).Name != className {
  276. continue
  277. }
  278. case *ast.Ident:
  279. if t.Name != className {
  280. continue
  281. }
  282. default:
  283. // this should not be possible but just in case
  284. continue
  285. }
  286. funcName := fd.Name.String()
  287. method := registryMethod{
  288. name: funcName,
  289. alias: alias,
  290. class: className,
  291. params: lookupParams(fd.Type.Params),
  292. returnValues: lookupReturnValues(fd),
  293. }
  294. methods = append(methods, &method)
  295. }
  296. return methods
  297. }
  298. // lookupSignals look up for every signal that is owned by the type and fill
  299. // a registration data structure with it
  300. func lookupSignals(className string, file ast.Node) []*registrySignal {
  301. signals := []*registrySignal{}
  302. ast.Inspect(file, func(node ast.Node) (cont bool) {
  303. cont = true
  304. _, ok := node.(*ast.StructType)
  305. if ok {
  306. return
  307. }
  308. cl, ok := node.(*ast.CompositeLit)
  309. if !ok {
  310. return
  311. }
  312. st, ok := cl.Type.(*ast.SelectorExpr)
  313. if !ok {
  314. return
  315. }
  316. if st.X.(*ast.Ident).Name == "gdnative" && st.Sel.Name == "Signal" {
  317. signal := registrySignal{}
  318. for i := range cl.Elts {
  319. kv, ok := cl.Elts[i].(*ast.KeyValueExpr)
  320. if !ok {
  321. continue
  322. }
  323. key := kv.Key.(*ast.Ident).Name
  324. switch key {
  325. case "Name":
  326. signal.name = kv.Value.(*ast.BasicLit).Value
  327. case "Args":
  328. arguments, ok := kv.Value.(*ast.CompositeLit)
  329. if !ok {
  330. fmt.Printf(
  331. "WARNING: arguments on signal %s has the wrong type, it should be *ast.CompositeLit, arguments will be ignored\n",
  332. signal.name,
  333. )
  334. signal.args = "[]*gdnative.SignalArgument{}"
  335. continue
  336. }
  337. signal.args = parseSignalArgs(arguments)
  338. case "DefaultArgs":
  339. defaultArguments, ok := kv.Value.(*ast.CompositeLit)
  340. if !ok {
  341. fmt.Printf(
  342. "WARNING: default arguments on signal %s has the wrong type, it should be *ast.CompositeLit, arguments will be ignored\n",
  343. signal.name,
  344. )
  345. signal.defaults = "[]*gdnative.Variant{}"
  346. continue
  347. }
  348. signal.defaults = parseSignalArgs(defaultArguments)
  349. }
  350. }
  351. signals = append(signals, &signal)
  352. }
  353. return
  354. })
  355. return signals
  356. }
  357. func lookupParams(fields *ast.FieldList) []*registryMethodParam {
  358. params := []*registryMethodParam{}
  359. if fields.NumFields() > 0 {
  360. for _, field := range fields.List {
  361. kind := parseDefault(field.Type, "")
  362. switch kind {
  363. case "":
  364. // if we don't have a type skip iteration
  365. continue
  366. }
  367. for _, name := range field.Names {
  368. params = append(params, &registryMethodParam{
  369. name: name.String(),
  370. kind: kind,
  371. })
  372. }
  373. }
  374. }
  375. return params
  376. }
  377. func lookupReturnValues(fd *ast.FuncDecl) []*registryMethodReturnValue {
  378. returnValues := []*registryMethodReturnValue{}
  379. if fd.Type.Results.NumFields() > 0 {
  380. for _, result := range fd.Type.Results.List {
  381. value := registryMethodReturnValue{
  382. kind: parseDefault(result.Type, "gdnative.Variant"),
  383. }
  384. returnValues = append(returnValues, &value)
  385. }
  386. }
  387. return returnValues
  388. }
  389. func lookupProperties(className string, file *ast.File) []*registryProperty {
  390. properties := []*registryProperty{}
  391. for _, node := range file.Decls {
  392. gd, ok := node.(*ast.GenDecl)
  393. if !ok {
  394. continue
  395. }
  396. for _, d := range gd.Specs {
  397. tp, ok := d.(*ast.TypeSpec)
  398. if !ok {
  399. continue
  400. }
  401. sp, ok := tp.Type.(*ast.StructType)
  402. if !ok {
  403. continue
  404. }
  405. if getClassName(tp) != className {
  406. continue
  407. }
  408. if sp.Fields.NumFields() > 0 {
  409. for _, field := range sp.Fields.List {
  410. if field.Names == nil {
  411. // this field is a selector expression, skip it
  412. continue
  413. }
  414. gdnativeKind := ""
  415. kind := parseDefault(field.Type, "")
  416. switch kind {
  417. case "":
  418. // we don't have a type skip iteration
  419. continue
  420. case "gdnative.Signal":
  421. // this is a signal skip it
  422. continue
  423. default:
  424. gdnativeKind = kind
  425. typeFormat := fmt.Sprintf("VariantType%s", strings.ReplaceAll(kind, "gdnative.", ""))
  426. _, ok := VariantTypeLookupMap[typeFormat]
  427. if ok {
  428. kind = fmt.Sprintf("gdnative.%s", typeFormat)
  429. break
  430. }
  431. kind = "gdnative.VariantTypeObject"
  432. }
  433. alias, exported := extractExportedAndAliasFromDoc(field.Doc)
  434. tmpProperties := []*registryProperty{}
  435. for _, name := range field.Names {
  436. if !name.IsExported() && !exported {
  437. continue
  438. }
  439. tmpProperties = append(tmpProperties, &registryProperty{
  440. name: name.String(),
  441. alias: alias,
  442. kind: kind,
  443. gdnativeKind: gdnativeKind,
  444. })
  445. }
  446. if field.Tag != nil {
  447. // check if this tag is the ignore tag
  448. if field.Tag.Value == "`-`" || field.Tag.Value == "`_`" || field.Tag.Value == "`omit`" {
  449. continue
  450. }
  451. // create a fake reflect.StructTag to lookup our keys
  452. fakeTag := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", ""))
  453. rset, rsetOk := fakeTag.Lookup("rset_type")
  454. usage, usageOk := fakeTag.Lookup("usage")
  455. hint, hintOk := fakeTag.Lookup("hint")
  456. hintString, hintStringOk := fakeTag.Lookup("hint_string")
  457. if !hintStringOk {
  458. hintString = ""
  459. }
  460. if !hintOk {
  461. hint = "None"
  462. }
  463. if !rsetOk {
  464. rset = "Disabled"
  465. }
  466. if !usageOk {
  467. usage = "Default"
  468. }
  469. for i := range tmpProperties {
  470. mustSetPropertyTagHint(tmpProperties[i], hint)
  471. mustSetPropertyTagRset(tmpProperties[i], rset)
  472. mustSetPropertyTagUsage(tmpProperties[i], usage)
  473. tmpProperties[i].hintString = hintString
  474. }
  475. properties = append(properties, tmpProperties...)
  476. }
  477. }
  478. }
  479. }
  480. }
  481. return properties
  482. }
  483. func mustSetPropertyTagRset(property *registryProperty, value string) {
  484. rpcMode := fmt.Sprintf("MethodRpcMode%s", strings.Title(value))
  485. _, ok := MethodRpcModeLookupMap[rpcMode]
  486. if !ok {
  487. valid := []string{}
  488. for key := range MethodRpcModeLookupMap {
  489. valid = append(valid, strings.ToLower(key[13:]))
  490. }
  491. fmt.Printf(
  492. "on property %s: unknown rset_type %s, it must be one of:\n\t%s\n",
  493. property.name, value, strings.Join(valid, "\n\t"),
  494. )
  495. os.Exit(1)
  496. }
  497. property.rset = strings.Title(value)
  498. }
  499. func mustSetPropertyTagHint(property *registryProperty, value string) {
  500. hint := fmt.Sprintf("PropertyHint%s", strings.Title(value))
  501. _, ok := PropertyHintLookupMap[hint]
  502. if !ok {
  503. valid := []string{}
  504. for key := range PropertyHintLookupMap {
  505. valid = append(valid, strings.ToLower(key[12:]))
  506. }
  507. fmt.Printf("on property %s: unknown hint %s, it must be one of %s", property.name, value, strings.Join(valid, ", "))
  508. os.Exit(1)
  509. }
  510. property.hint = strings.Title(value)
  511. }
  512. func mustSetPropertyTagUsage(property *registryProperty, value string) {
  513. usage := fmt.Sprintf("PropertyUsage%s", strings.Title(value))
  514. _, ok := PropertyUsageFlagsLookupMap[usage]
  515. if !ok {
  516. valid := []string{}
  517. for key := range PropertyUsageFlagsLookupMap {
  518. valid = append(valid, strings.ToLower(key[13:]))
  519. }
  520. fmt.Printf("on property %s: unknown usage %s, it must be one of %s", property.name, value, strings.Join(valid, ", "))
  521. os.Exit(1)
  522. }
  523. property.usage = strings.Title(value)
  524. }
  525. func extractExportedAndAliasFromDoc(doc *ast.CommentGroup) (string, bool) {
  526. var alias string
  527. var exported bool
  528. if doc != nil {
  529. for _, line := range doc.List {
  530. docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
  531. if strings.HasPrefix(docstring, godotExport) {
  532. exported = true
  533. if strings.Contains(docstring, " as ") {
  534. alias = strings.TrimSpace(strings.Split(strings.Split(docstring, " as ")[1], " ")[0])
  535. }
  536. break
  537. }
  538. }
  539. }
  540. return alias, exported
  541. }
  542. func parseDefault(expr ast.Expr, def string) string {
  543. kind := def
  544. switch t := expr.(type) {
  545. case *ast.Ident:
  546. kind = t.Name
  547. case *ast.StarExpr:
  548. kind = parseStarExpr(t)
  549. case *ast.ParenExpr:
  550. kind = parseParenExpr(t)
  551. case *ast.ArrayType:
  552. kind = parseArray(t)
  553. case *ast.MapType:
  554. kind = parseMap(t)
  555. case *ast.SelectorExpr:
  556. kind = fmt.Sprintf("%s.%s", parseDefault(t.X, def), t.Sel.String())
  557. }
  558. return kind
  559. }
  560. func parseStarExpr(expr *ast.StarExpr) string {
  561. kind := "*%s"
  562. return fmt.Sprintf(kind, parseDefault(expr.X, "gdnatve.Pointer"))
  563. }
  564. func parseParenExpr(field *ast.ParenExpr) string {
  565. return parseDefault(field.X, "gdnative.Pointer")
  566. }
  567. func parseArray(field *ast.ArrayType) string {
  568. result := "ArrayType[%s]"
  569. return fmt.Sprintf(result, parseDefault(field.Elt, "ArrayType[]"))
  570. }
  571. func parseMap(field *ast.MapType) string {
  572. result := "MapType[%s]%s"
  573. key := parseDefault(field.Key, "gdnative.Pointer")
  574. value := parseDefault(field.Value, "gdnative.Pointer")
  575. return fmt.Sprintf(result, key, value)
  576. }
  577. func parseKeyValueExpr(expr *ast.KeyValueExpr) (string, string) { //nolint:unused
  578. var value string
  579. key := expr.Key.(*ast.Ident).Name
  580. switch t := expr.Value.(type) {
  581. case *ast.BasicLit:
  582. value = t.Value
  583. case *ast.CompositeLit:
  584. switch t2 := t.Type.(type) {
  585. case *ast.KeyValueExpr:
  586. _, value = parseKeyValueExpr(t2)
  587. default:
  588. value = parseDefault(t2, "gdnative.Pointer")
  589. }
  590. }
  591. return key, value
  592. }
  593. func parseSignalArgs(composite *ast.CompositeLit) string {
  594. buffer := []byte{}
  595. buf := bytes.NewBuffer(buffer)
  596. fileSet := token.NewFileSet()
  597. printer.Fprint(buf, fileSet, composite)
  598. return buf.String()
  599. }