1// Copyright 2019 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package protobuf
16
17import (
18 "bytes"
19 "fmt"
20 "os"
21 "path"
22 "path/filepath"
23 "slices"
24 "strconv"
25 "strings"
26 "text/scanner"
27 "unicode"
28
29 "github.com/emicklei/proto"
30
31 "cuelang.org/go/cue/ast"
32 "cuelang.org/go/cue/ast/astutil"
33 "cuelang.org/go/cue/errors"
34 "cuelang.org/go/cue/literal"
35 "cuelang.org/go/cue/parser"
36 "cuelang.org/go/cue/token"
37 "cuelang.org/go/internal/source"
38)
39
40func (s *Extractor) parse(filename string, src interface{}) (p *protoConverter, err error) {
41 if filename == "" {
42 return nil, errors.Newf(token.NoPos, "empty filename")
43 }
44 if r, ok := s.fileCache[filename]; ok {
45 return r.p, r.err
46 }
47 defer func() {
48 s.fileCache[filename] = result{p, err}
49 }()
50
51 b, err := source.ReadAll(filename, src)
52 if err != nil {
53 return nil, err
54 }
55
56 parser := proto.NewParser(bytes.NewReader(b))
57 if filename != "" {
58 parser.Filename(filename)
59 }
60 d, err := parser.Parse()
61 if err != nil {
62 return nil, errors.Newf(token.NoPos, "protobuf: %v", err)
63 }
64
65 tfile := token.NewFile(filename, -1, len(b))
66 tfile.SetLinesForContent(b)
67
68 p = &protoConverter{
69 id: filename,
70 state: s,
71 tfile: tfile,
72 imported: map[string]bool{},
73 symbols: map[string]bool{},
74 }
75
76 defer func() {
77 switch x := recover().(type) {
78 case nil:
79 case protoError:
80 err = &protobufError{
81 path: p.path,
82 pos: p.toCUEPos(x.pos),
83 err: x.error,
84 }
85 default:
86 panic(x)
87 }
88 }()
89
90 p.file = &ast.File{Filename: filename}
91
92 p.addNames(d.Elements)
93
94 // Parse package definitions.
95 for _, e := range d.Elements {
96 switch x := e.(type) {
97 case *proto.Package:
98 p.protoPkg = x.Name
99 case *proto.Option:
100 if x.Name == "go_package" {
101 str, err := strconv.Unquote(x.Constant.SourceRepresentation())
102 if err != nil {
103 failf(x.Position, "unquoting package filed: %v", err)
104 }
105 split := strings.Split(str, ";")
106 switch {
107 case strings.Contains(split[0], "."):
108 p.cuePkgPath = split[0]
109 switch len(split) {
110 case 1:
111 p.shortPkgName = path.Base(str)
112 case 2:
113 p.shortPkgName = split[1]
114 default:
115 failf(x.Position, "unexpected ';' in %q", str)
116 }
117
118 case len(split) == 1:
119 p.shortPkgName = split[0]
120
121 default:
122 failf(x.Position, "malformed go_package clause %s", str)
123 }
124 // name.AddComment(comment(x.Comment, true))
125 // name.AddComment(comment(x.InlineComment, false))
126 }
127 }
128 }
129
130 if name := p.shortName(); name != "" {
131 p.file.Decls = append(p.file.Decls, &ast.Package{Name: ast.NewIdent(name)})
132 }
133
134 for _, e := range d.Elements {
135 switch x := e.(type) {
136 case *proto.Import:
137 if err := p.doImport(x); err != nil {
138 return nil, err
139 }
140 }
141 }
142
143 for _, e := range d.Elements {
144 p.topElement(e)
145 }
146
147 err = astutil.Sanitize(p.file)
148
149 return p, err
150}
151
152// A protoConverter converts a proto definition to CUE. Proto files map to
153// CUE files one to one.
154type protoConverter struct {
155 state *Extractor
156 tfile *token.File
157
158 proto3 bool
159
160 id string
161 protoPkg string
162 shortPkgName string
163 cuePkgPath string
164
165 file *ast.File
166 current *ast.StructLit
167
168 imported map[string]bool
169
170 path []string
171 scope []map[string]mapping // for symbols resolution within package.
172 symbols map[string]bool // symbols provided by package
173}
174
175type mapping struct {
176 cue func() ast.Expr // needs to be a new copy as position changes
177 pkg *protoConverter
178}
179
180func (p *protoConverter) qualifiedImportPath() string {
181 s := p.importPath()
182 if short := p.shortPkgName; short != "" && short != path.Base(s) {
183 s += ":" + short
184 }
185 return s
186}
187
188func (p *protoConverter) importPath() string {
189 if p.cuePkgPath == "" && p.protoPkg != "" {
190 dir := strings.Replace(p.protoPkg, ".", "/", -1)
191 p.cuePkgPath = path.Join("googleapis.com", dir)
192 }
193 return p.cuePkgPath
194}
195
196func (p *protoConverter) shortName() string {
197 if p.state.pkgName != "" {
198 return p.state.pkgName
199 }
200 if p.shortPkgName == "" && p.protoPkg != "" {
201 split := strings.Split(p.protoPkg, ".")
202 p.shortPkgName = split[len(split)-1]
203 }
204 return p.shortPkgName
205}
206
207func (p *protoConverter) toCUEPos(pos scanner.Position) token.Pos {
208 return p.tfile.Pos(pos.Offset, 0)
209}
210
211func (p *protoConverter) addRef(pos scanner.Position, name string, cue func() ast.Expr) {
212 top := p.scope[len(p.scope)-1]
213 if _, ok := top[name]; ok {
214 failf(pos, "entity %q already defined", name)
215 }
216 top[name] = mapping{cue: cue}
217}
218
219func (p *protoConverter) addNames(elems []proto.Visitee) {
220 p.scope = append(p.scope, map[string]mapping{})
221 for _, e := range elems {
222 var pos scanner.Position
223 var name string
224 switch x := e.(type) {
225 case *proto.Message:
226 if x.IsExtend {
227 continue
228 }
229 name = x.Name
230 pos = x.Position
231 case *proto.Enum:
232 name = x.Name
233 pos = x.Position
234 case *proto.NormalField:
235 name = x.Name
236 pos = x.Position
237 case *proto.MapField:
238 name = x.Name
239 pos = x.Position
240 case *proto.Oneof:
241 name = x.Name
242 pos = x.Position
243 default:
244 continue
245 }
246 sym := strings.Join(append(p.path, name), ".")
247 p.symbols[sym] = true
248 p.addRef(pos, name, func() ast.Expr { return ast.NewIdent("#" + name) })
249 }
250}
251
252func (p *protoConverter) popNames() {
253 p.scope = p.scope[:len(p.scope)-1]
254}
255
256func (p *protoConverter) resolve(pos scanner.Position, name string, options []*proto.Option) ast.Expr {
257 if expr := protoToCUE(name, options); expr != nil {
258 ast.SetPos(expr, p.toCUEPos(pos))
259 return expr
260 }
261 if strings.HasPrefix(name, ".") {
262 return p.resolveTopScope(pos, name[1:], options)
263 }
264 for _, scope := range slices.Backward(p.scope) {
265 if m, ok := scope[name]; ok {
266 return m.cue()
267 }
268 }
269 expr := p.resolveTopScope(pos, name, options)
270 return expr
271}
272
273func (p *protoConverter) resolveTopScope(pos scanner.Position, name string, options []*proto.Option) ast.Expr {
274 for i := 0; i < len(name); i++ {
275 k := strings.IndexByte(name[i:], '.')
276 i += k
277 if k == -1 {
278 i = len(name)
279 }
280 curName := name[:i]
281 if local, ok := strings.CutPrefix(curName, p.protoPkg+"."); ok {
282 curName = local
283 }
284 if m, ok := p.scope[0][curName]; ok {
285 if m.pkg != nil {
286 p.imported[m.pkg.qualifiedImportPath()] = true
287 }
288 expr := m.cue()
289 for i < len(name) {
290 name = name[i+1:]
291 if i = strings.IndexByte(name, '.'); i == -1 {
292 i = len(name)
293 }
294 expr = ast.NewSel(expr, "#"+name[:i])
295 }
296 ast.SetPos(expr, p.toCUEPos(pos))
297 return expr
298 }
299 }
300 failf(pos, "name %q not found", name)
301 return nil
302}
303
304func (p *protoConverter) doImport(v *proto.Import) error {
305 if p.mapBuiltinPackage(v.Filename) {
306 return nil
307 }
308
309 filename := ""
310 for _, p := range p.state.paths {
311 name := filepath.Join(p, v.Filename)
312 _, err := os.Stat(name)
313 if err != nil {
314 continue
315 }
316 filename = name
317 break
318 }
319
320 if filename == "" {
321 err := errors.Newf(p.toCUEPos(v.Position), "could not find import %q", v.Filename)
322 p.state.addErr(err)
323 return err
324 }
325
326 imp, err := p.state.parse(filename, nil)
327 if err != nil {
328 fail(v.Position, err)
329 }
330
331 pkgNamespace := strings.Split(imp.protoPkg, ".")
332 curNamespace := strings.Split(p.protoPkg, ".")
333 for {
334 for k := range imp.symbols {
335 ref := k
336 if len(pkgNamespace) > 0 {
337 ref = strings.Join(append(pkgNamespace, k), ".")
338 }
339 if _, ok := p.scope[0][ref]; !ok {
340 pkg := imp
341 a := toCue(k)
342
343 var f func() ast.Expr
344
345 if imp.qualifiedImportPath() == p.qualifiedImportPath() {
346 pkg = nil
347 f = func() ast.Expr { return ast.NewIdent(a[0]) }
348 } else {
349 f = func() ast.Expr {
350 ident := &ast.Ident{
351 Name: imp.shortName(),
352 Node: ast.NewImport(nil, imp.qualifiedImportPath()),
353 }
354 return ast.NewSel(ident, a[0])
355 }
356 }
357 p.scope[0][ref] = mapping{f, pkg}
358 }
359 }
360 if len(pkgNamespace) == 0 {
361 break
362 }
363 if len(curNamespace) == 0 || pkgNamespace[0] != curNamespace[0] {
364 break
365 }
366 pkgNamespace = pkgNamespace[1:]
367 curNamespace = curNamespace[1:]
368 }
369 return nil
370}
371
372// TODO: this doesn't work. Do something more principled.
373func toCue(name string) []string {
374 a := strings.Split(name, ".")
375 for i, s := range a {
376 a[i] = "#" + s
377 }
378 return a
379}
380
381func (p *protoConverter) stringLit(pos scanner.Position, s string) *ast.BasicLit {
382 return &ast.BasicLit{
383 ValuePos: p.toCUEPos(pos),
384 Kind: token.STRING,
385 Value: literal.String.Quote(s)}
386}
387
388func (p *protoConverter) ident(pos scanner.Position, name string) *ast.Ident {
389 return &ast.Ident{NamePos: p.toCUEPos(pos), Name: labelName(name)}
390}
391
392func (p *protoConverter) ref(pos scanner.Position) *ast.Ident {
393 name := "#" + p.path[len(p.path)-1]
394 return &ast.Ident{NamePos: p.toCUEPos(pos), Name: name}
395}
396
397func (p *protoConverter) subref(pos scanner.Position, name string) *ast.Ident {
398 return &ast.Ident{
399 NamePos: p.toCUEPos(pos),
400 Name: "#" + name,
401 }
402}
403
404func (p *protoConverter) addTag(f *ast.Field, body string) {
405 tag := "@protobuf(" + body + ")"
406 f.Attrs = append(f.Attrs, &ast.Attribute{Text: tag})
407}
408
409func (p *protoConverter) topElement(v proto.Visitee) {
410 switch x := v.(type) {
411 case *proto.Syntax:
412 p.proto3 = x.Value == "proto3"
413
414 case *proto.Comment:
415 addComments(p.file, 0, x, nil)
416
417 case *proto.Enum:
418 p.enum(x)
419
420 case *proto.Package:
421 if doc := x.Doc(); doc != nil {
422 addComments(p.file, 0, doc, nil)
423 }
424
425 case *proto.Message:
426 p.message(x)
427
428 case *proto.Option:
429 case *proto.Import:
430 // already handled.
431
432 case *proto.Service:
433 // TODO: handle services.
434
435 case *proto.Extensions, *proto.Reserved:
436 // no need to handle
437
438 default:
439 failf(scanner.Position{}, "unsupported type %T", x)
440 }
441}
442
443func (p *protoConverter) message(v *proto.Message) {
444 if v.IsExtend {
445 // TODO: we are not handling extensions as for now.
446 return
447 }
448
449 defer func(saved []string) { p.path = saved }(p.path)
450 p.path = append(p.path, v.Name)
451
452 p.addNames(v.Elements)
453 defer p.popNames()
454
455 // TODO: handle IsExtend/ proto2
456
457 s := &ast.StructLit{
458 Lbrace: p.toCUEPos(v.Position),
459 // TODO: set proto file position.
460 Rbrace: token.Newline.Pos(),
461 }
462
463 ref := p.ref(v.Position)
464 if v.Comment == nil {
465 ref.NamePos = newSection
466 }
467 f := &ast.Field{Label: ref, Value: s}
468 addComments(f, 1, v.Comment, nil)
469
470 p.addDecl(f)
471 defer func(current *ast.StructLit) {
472 p.current = current
473 }(p.current)
474 p.current = s
475
476 for i, e := range v.Elements {
477 p.messageField(s, i, e)
478 }
479}
480
481func (p *protoConverter) addDecl(d ast.Decl) {
482 if p.current == nil {
483 p.file.Decls = append(p.file.Decls, d)
484 } else {
485 p.current.Elts = append(p.current.Elts, d)
486 }
487}
488
489func (p *protoConverter) messageField(s *ast.StructLit, i int, v proto.Visitee) {
490 switch x := v.(type) {
491 case *proto.Comment:
492 s.Elts = append(s.Elts, comment(x, true))
493
494 case *proto.NormalField:
495 f := p.parseField(s, i, x.Field)
496
497 if x.Repeated {
498 f.Value = &ast.ListLit{
499 Lbrack: p.toCUEPos(x.Position),
500 Elts: []ast.Expr{&ast.Ellipsis{Type: f.Value}},
501 }
502 }
503
504 case *proto.MapField:
505 defer func(saved []string) { p.path = saved }(p.path)
506 p.path = append(p.path, x.Name)
507
508 f := &ast.Field{}
509
510 // All keys are converted to strings.
511 // TODO: support integer keys.
512 f.Label = ast.NewList(ast.NewIdent("string"))
513 f.Value = p.resolve(x.Position, x.Type, x.Options)
514
515 name := p.ident(x.Position, x.Name)
516 f = &ast.Field{
517 Label: name,
518 Value: ast.NewStruct(f),
519 }
520 addComments(f, i, x.Comment, x.InlineComment)
521
522 o := optionParser{message: s, field: f}
523 o.tags = fmt.Sprintf(`%d,map[%s]%s`, x.Sequence, x.KeyType, x.Type)
524 if x.Name != name.Name {
525 o.tags += "," + x.Name
526 }
527 s.Elts = append(s.Elts, f)
528 o.parse(x.Options)
529 p.addTag(f, o.tags)
530
531 if !o.required {
532 f.Constraint = token.OPTION
533 }
534
535 case *proto.Enum:
536 p.enum(x)
537
538 case *proto.Message:
539 p.message(x)
540
541 case *proto.Oneof:
542 p.oneOf(x)
543
544 case *proto.Extensions, *proto.Reserved:
545 // no need to handle
546
547 case *proto.Option:
548 opt := fmt.Sprintf("@protobuf(option %s=%s)", x.Name, x.Constant.Source)
549 attr := &ast.Attribute{
550 At: p.toCUEPos(x.Position),
551 Text: opt,
552 }
553 addComments(attr, i, x.Doc(), x.InlineComment)
554 s.Elts = append(s.Elts, attr)
555
556 default:
557 failf(scanner.Position{}, "unsupported field type %T", v)
558 }
559}
560
561// enum converts a proto enum definition to CUE.
562//
563// An enum will generate two top-level definitions:
564//
565// Enum:
566// "Value1" |
567// "Value2" |
568// "Value3"
569//
570// and
571//
572// Enum_value: {
573// "Value1": 0
574// "Value2": 1
575// }
576//
577// Enums are always defined at the top level. The name of a nested enum
578// will be prefixed with the name of its parent and an underscore.
579func (p *protoConverter) enum(x *proto.Enum) {
580
581 if len(x.Elements) == 0 {
582 failf(x.Position, "empty enum")
583 }
584
585 name := p.subref(x.Position, x.Name)
586
587 defer func(saved []string) { p.path = saved }(p.path)
588 p.path = append(p.path, x.Name)
589
590 p.addNames(x.Elements)
591
592 if len(p.path) == 0 {
593 defer func() { p.path = p.path[:0] }()
594 p.path = append(p.path, x.Name)
595 }
596
597 // Top-level enum entry.
598 enum := &ast.Field{Label: name}
599 addComments(enum, 1, x.Comment, nil)
600 if p.current != nil && len(p.current.Elts) > 0 {
601 ast.SetRelPos(enum, token.NewSection)
602 }
603
604 // Top-level enum values entry.
605 valueName := ast.NewIdent(name.Name + "_value")
606 valueName.NamePos = newSection
607 valueMap := &ast.StructLit{}
608 d := &ast.Field{Label: valueName, Value: valueMap}
609 // addComments(valueMap, 1, x.Comment, nil)
610
611 if strings.Contains(name.Name, "google") {
612 panic(name.Name)
613 }
614 p.addDecl(enum)
615
616 numEnums := 0
617 for _, v := range x.Elements {
618 if _, ok := v.(*proto.EnumField); ok {
619 numEnums++
620 }
621 }
622
623 lastSingle := false
624
625 firstSpace := token.NewSection
626
627 // The line comments for an enum field need to attach after the '|', which
628 // is only known at the next iteration.
629 var lastComment *proto.Comment
630 for i, v := range x.Elements {
631 switch y := v.(type) {
632 case *proto.EnumField:
633 // Add enum value to map
634 intValue := ast.NewLit(token.INT, strconv.Itoa(y.Integer))
635 f := &ast.Field{
636 Label: p.stringLit(y.Position, y.Name),
637 Value: intValue,
638 }
639 valueMap.Elts = append(valueMap.Elts, f)
640
641 var e ast.Expr
642 switch p.state.enumMode {
643 case "int":
644 e = ast.NewIdent("#" + y.Name)
645 ast.SetRelPos(e, token.Newline)
646
647 f := &ast.Field{
648 Label: ast.NewIdent("#" + y.Name),
649 Value: intValue,
650 }
651 ast.SetRelPos(f, firstSpace)
652 firstSpace = token.Newline
653 addComments(f, 0, y.Comment, y.InlineComment)
654 p.addDecl(f)
655
656 case "", "json":
657 // add to enum disjunction
658 value := p.stringLit(y.Position, y.Name)
659 embed := &ast.EmbedDecl{Expr: value}
660 ast.SetRelPos(embed, token.Blank)
661 field := &ast.Field{Label: ast.NewIdent("#enumValue"), Value: intValue}
662 st := &ast.StructLit{
663 Lbrace: token.Blank.Pos(),
664 Elts: []ast.Decl{embed, field},
665 }
666
667 addComments(embed, 0, y.Comment, y.InlineComment)
668 if y.Comment == nil && y.InlineComment == nil {
669 ast.SetRelPos(field, token.Blank)
670 ast.SetRelPos(field.Label, token.Blank)
671 st.Rbrace = token.Blank.Pos()
672 if i > 0 && lastSingle {
673 st.Lbrace = token.Newline.Pos()
674 }
675 lastSingle = true
676 } else {
677 lastSingle = false
678 }
679 e = st
680
681 default:
682 p.state.errs = errors.Append(p.state.errs,
683 errors.Newf(token.NoPos, "unknown enum mode %q", p.state.enumMode))
684 return
685 }
686
687 if enum.Value != nil {
688 e = &ast.BinaryExpr{X: enum.Value, Op: token.OR, Y: e}
689 }
690 enum.Value = e
691
692 // a := fmt.Sprintf("@protobuf(enum,name=%s)", y.Name)
693 // f.Attrs = append(f.Attrs, &ast.Attribute{Text: a})
694 }
695 }
696 p.addDecl(d)
697 addComments(enum.Value, 1, nil, lastComment)
698}
699
700// oneOf converts a Proto OneOf field to CUE. Note that Protobuf defines
701// a oneOf to be at most one of the fields. Rather than making each field
702// optional, we define oneOfs as all required fields, but add one more
703// disjunction allowing no fields. This makes it easier to constrain the
704// result to include at least one of the values.
705func (p *protoConverter) oneOf(x *proto.Oneof) {
706 s := ast.NewStruct()
707 ast.SetRelPos(s, token.Newline)
708 embed := &ast.EmbedDecl{Expr: s}
709 ast.AddComment(embed, comment(x.Comment, true))
710
711 p.addDecl(embed)
712
713 newStruct := func() {
714 s = &ast.StructLit{
715 // TODO: make this the default in the formatter.
716 Rbrace: token.Newline.Pos(),
717 }
718 embed.Expr = ast.NewBinExpr(token.OR, embed.Expr, s)
719 }
720 for _, v := range x.Elements {
721 switch x := v.(type) {
722 case *proto.OneOfField:
723 newStruct()
724 oneOf := p.parseField(s, 0, x.Field)
725 oneOf.Constraint = token.ILLEGAL
726
727 case *proto.Comment:
728 cg := comment(x, false)
729 ast.SetRelPos(cg, token.NewSection)
730 s.Elts = append(s.Elts, cg)
731
732 default:
733 newStruct()
734 p.messageField(s, 1, v)
735 }
736
737 }
738}
739
740func (p *protoConverter) parseField(s *ast.StructLit, i int, x *proto.Field) *ast.Field {
741 defer func(saved []string) { p.path = saved }(p.path)
742 p.path = append(p.path, x.Name)
743
744 f := &ast.Field{}
745 addComments(f, i, x.Comment, x.InlineComment)
746
747 name := p.ident(x.Position, x.Name)
748 f.Label = name
749 typ := p.resolve(x.Position, x.Type, x.Options)
750 f.Value = typ
751 s.Elts = append(s.Elts, f)
752
753 o := optionParser{message: s, field: f}
754
755 // body of @protobuf tag: sequence,type[,name=<name>][,...]
756 o.tags += fmt.Sprintf("%v,%s", x.Sequence, x.Type)
757 if x.Name != name.Name {
758 o.tags += ",name=" + x.Name
759 }
760 o.parse(x.Options)
761 p.addTag(f, o.tags)
762
763 if !o.required {
764 f.Constraint = token.OPTION
765 }
766 return f
767}
768
769type optionParser struct {
770 message *ast.StructLit
771 field *ast.Field
772 required bool
773 tags string
774}
775
776func (p *optionParser) parse(options []*proto.Option) {
777
778 // TODO: handle options
779 // - translate options to tags
780 // - interpret CUE options.
781 for _, o := range options {
782 switch o.Name {
783 case "(cue.opt).required":
784 p.required = true
785 // TODO: Dropping comments. Maybe add a dummy tag?
786
787 case "(cue.val)":
788 // TODO: set filename and base offset.
789 expr, err := parser.ParseExpr("", o.Constant.Source)
790 if err != nil {
791 failf(o.Position, "invalid cue.val value: %v", err)
792 }
793 // Any further checks will be done at the end.
794 constraint := &ast.Field{Label: p.field.Label, Value: expr}
795 addComments(constraint, 1, o.Comment, o.InlineComment)
796 p.message.Elts = append(p.message.Elts, constraint)
797 if !p.required {
798 constraint.Constraint = token.OPTION
799 }
800 case "(google.api.field_behavior)":
801 if o.Constant.Source == "REQUIRED" {
802 p.required = true
803 }
804 default:
805 // TODO: dropping comments. Maybe add dummy tag?
806
807 // TODO: should CUE support nested attributes?
808 source := o.Constant.SourceRepresentation()
809 p.tags += ","
810 switch source {
811 case "true":
812 p.tags += quoteOption(o.Name)
813 default:
814 p.tags += quoteOption(o.Name + "=" + source)
815 }
816 }
817 }
818}
819
820func quoteOption(s string) string {
821 needQuote := false
822 for _, r := range s {
823 if !unicode.In(r, unicode.L, unicode.N) {
824 needQuote = true
825 break
826 }
827 }
828 if !needQuote {
829 return s
830 }
831 if !strings.ContainsAny(s, `"\`) {
832 return literal.String.Quote(s)
833 }
834 esc := `\#`
835 for strings.Contains(s, esc) {
836 esc += "#"
837 }
838 return esc[1:] + `"` + s + `"` + esc[1:]
839}