// 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) (token.Pos, token.Pos, error) { startPos, err := PosInStringLiteral(lit, start) if err != nil { return 0, 0, fmt.Errorf("start: %v", err) } endPos, err := PosInStringLiteral(lit, end) if err != nil { return 0, 0, fmt.Errorf("end: %v", err) } return 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") } // remove quotes quote := raw[0] // '"' or '`' raw = raw[1 : len(raw)-1] var ( i = 0 // byte index within logical value pos = lit.ValuePos + 1 // position within literal ) for raw != "" && i < offset { r, _, rest, _ := strconv.UnquoteChar(raw, quote) // can't fail sz := len(raw) - len(rest) // length of literal char in raw bytes pos += token.Pos(sz) raw = raw[sz:] i += utf8.RuneLen(r) } return pos, nil } // PreorderStack traverses the tree rooted at root, // calling f before visiting each node. // // Each call to f provides the current node and traversal stack, // consisting of the original value of stack appended with all nodes // from root to n, excluding n itself. (This design allows calls // to PreorderStack to be nested without double counting.) // // If f returns false, the traversal skips over that subtree. Unlike // [ast.Inspect], no second call to f is made after visiting node n. // In practice, the second call is nearly always used only to pop the // stack, and it is surprisingly tricky to do this correctly; see // https://go.dev/issue/73319. // // TODO(adonovan): replace with [ast.PreorderStack] when go1.25 is assured. func PreorderStack(root ast.Node, stack []ast.Node, f func(n ast.Node, stack []ast.Node) bool) { before := len(stack) ast.Inspect(root, func(n ast.Node) bool { if n != nil { if !f(n, stack) { // Do not push, as there will be no corresponding pop. return false } stack = append(stack, n) // push } else { stack = stack[:len(stack)-1] // pop } return true }) if len(stack) != before { panic("push/pop mismatch") } }