// 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 }