104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
// Copyright 2025 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package astutil
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// RangeInStringLiteral calculates the positional range within a string literal
|
|
// corresponding to the specified start and end byte offsets within the logical string.
|
|
func RangeInStringLiteral(lit *ast.BasicLit, start, end int) (Range, error) {
|
|
startPos, err := PosInStringLiteral(lit, start)
|
|
if err != nil {
|
|
return Range{}, fmt.Errorf("start: %v", err)
|
|
}
|
|
endPos, err := PosInStringLiteral(lit, end)
|
|
if err != nil {
|
|
return Range{}, fmt.Errorf("end: %v", err)
|
|
}
|
|
return Range{startPos, endPos}, nil
|
|
}
|
|
|
|
// PosInStringLiteral returns the position within a string literal
|
|
// corresponding to the specified byte offset within the logical
|
|
// string that it denotes.
|
|
func PosInStringLiteral(lit *ast.BasicLit, offset int) (token.Pos, error) {
|
|
raw := lit.Value
|
|
|
|
value, err := strconv.Unquote(raw)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !(0 <= offset && offset <= len(value)) {
|
|
return 0, fmt.Errorf("invalid offset")
|
|
}
|
|
|
|
pos, _ := walkStringLiteral(lit, lit.End(), offset)
|
|
return pos, nil
|
|
}
|
|
|
|
// OffsetInStringLiteral returns the byte offset within the logical (unquoted)
|
|
// string corresponding to the specified source position.
|
|
func OffsetInStringLiteral(lit *ast.BasicLit, pos token.Pos) (int, error) {
|
|
if !NodeContainsPos(lit, pos) {
|
|
return 0, fmt.Errorf("invalid position")
|
|
}
|
|
|
|
raw := lit.Value
|
|
|
|
value, err := strconv.Unquote(raw)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
_, offset := walkStringLiteral(lit, pos, len(value))
|
|
return offset, nil
|
|
}
|
|
|
|
// walkStringLiteral iterates through the raw string literal to map between
|
|
// a file position and a logical byte offset. It stops when it reaches
|
|
// either the targetPos or the targetOffset.
|
|
//
|
|
// TODO(hxjiang): consider making an iterator.
|
|
func walkStringLiteral(lit *ast.BasicLit, targetPos token.Pos, targetOffset int) (token.Pos, int) {
|
|
raw := lit.Value
|
|
norm := int(lit.End()-lit.Pos()) > len(lit.Value)
|
|
|
|
// remove quotes
|
|
quote := raw[0] // '"' or '`'
|
|
raw = raw[1 : len(raw)-1]
|
|
|
|
var (
|
|
i = 0 // byte index within logical value
|
|
pos = lit.Pos() + 1 // position within literal
|
|
)
|
|
|
|
for raw != "" {
|
|
r, _, rest, _ := strconv.UnquoteChar(raw, quote) // can't fail
|
|
sz := len(raw) - len(rest) // length of literal char in raw bytes
|
|
|
|
nextPos := pos + token.Pos(sz)
|
|
if norm && r == '\n' {
|
|
nextPos++
|
|
}
|
|
nextI := i + utf8.RuneLen(r) // length of logical char in "cooked" bytes
|
|
|
|
if nextPos > targetPos || nextI > targetOffset {
|
|
break
|
|
}
|
|
|
|
raw = raw[sz:]
|
|
i = nextI
|
|
pos = nextPos
|
|
}
|
|
|
|
return pos, i
|
|
}
|