stringlit.go (2769B)
1 // Copyright 2025 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 5 package astutil 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/token" 11 "strconv" 12 "unicode/utf8" 13 ) 14 15 // RangeInStringLiteral calculates the positional range within a string literal 16 // corresponding to the specified start and end byte offsets within the logical string. 17 func RangeInStringLiteral(lit *ast.BasicLit, start, end int) (Range, error) { 18 startPos, err := PosInStringLiteral(lit, start) 19 if err != nil { 20 return Range{}, fmt.Errorf("start: %v", err) 21 } 22 endPos, err := PosInStringLiteral(lit, end) 23 if err != nil { 24 return Range{}, fmt.Errorf("end: %v", err) 25 } 26 return Range{startPos, endPos}, nil 27 } 28 29 // PosInStringLiteral returns the position within a string literal 30 // corresponding to the specified byte offset within the logical 31 // string that it denotes. 32 func PosInStringLiteral(lit *ast.BasicLit, offset int) (token.Pos, error) { 33 raw := lit.Value 34 35 value, err := strconv.Unquote(raw) 36 if err != nil { 37 return 0, err 38 } 39 if !(0 <= offset && offset <= len(value)) { 40 return 0, fmt.Errorf("invalid offset") 41 } 42 43 pos, _ := walkStringLiteral(lit, lit.End(), offset) 44 return pos, nil 45 } 46 47 // OffsetInStringLiteral returns the byte offset within the logical (unquoted) 48 // string corresponding to the specified source position. 49 func OffsetInStringLiteral(lit *ast.BasicLit, pos token.Pos) (int, error) { 50 if !NodeContainsPos(lit, pos) { 51 return 0, fmt.Errorf("invalid position") 52 } 53 54 raw := lit.Value 55 56 value, err := strconv.Unquote(raw) 57 if err != nil { 58 return 0, err 59 } 60 61 _, offset := walkStringLiteral(lit, pos, len(value)) 62 return offset, nil 63 } 64 65 // walkStringLiteral iterates through the raw string literal to map between 66 // a file position and a logical byte offset. It stops when it reaches 67 // either the targetPos or the targetOffset. 68 // 69 // TODO(hxjiang): consider making an iterator. 70 func walkStringLiteral(lit *ast.BasicLit, targetPos token.Pos, targetOffset int) (token.Pos, int) { 71 raw := lit.Value 72 norm := int(lit.End()-lit.Pos()) > len(lit.Value) 73 74 // remove quotes 75 quote := raw[0] // '"' or '`' 76 raw = raw[1 : len(raw)-1] 77 78 var ( 79 i = 0 // byte index within logical value 80 pos = lit.Pos() + 1 // position within literal 81 ) 82 83 for raw != "" { 84 r, _, rest, _ := strconv.UnquoteChar(raw, quote) // can't fail 85 sz := len(raw) - len(rest) // length of literal char in raw bytes 86 87 nextPos := pos + token.Pos(sz) 88 if norm && r == '\n' { 89 nextPos++ 90 } 91 nextI := i + utf8.RuneLen(r) // length of logical char in "cooked" bytes 92 93 if nextPos > targetPos || nextI > targetOffset { 94 break 95 } 96 97 raw = raw[sz:] 98 i = nextI 99 pos = nextPos 100 } 101 102 return pos, i 103 }