this repo has no description
at master 839 lines 20 kB view raw
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}