blob: 56aab520c08ca6fe959b06fdb14ecd4b4dff0d90 [file]
// Copyright 2026 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_test
import (
"go/ast"
"go/parser"
"go/token"
"strconv"
"strings"
"testing"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/testenv"
)
func TestPosInStringLiteral(t *testing.T) {
// Each string is Go source for a string literal with ^ marker at expected Pos.
tests := []string{
`"^abc"`,
`"a^bc"`,
`"ab^c"`,
`"abc^"`,
`"a\n^b"`,
`"\n^"`,
`"a\000^"`,
`"\x61^"`,
`"\u0061^"`,
`"\U00000061^"`,
`"€^"`,
`"a€^b"`,
"`abc^`",
"`a\n^b`",
// normalization of \r carriage returns:
"`a\r\n^b`",
"`a\r\nb\r\nc\r\n^d`",
}
for _, test := range tests {
// The workaround for \r requires the go1.26 fix for https://go.dev/issue/76031.
if strings.Contains(test, "\r") && testenv.Go1Point() < 26 {
continue
}
t.Logf("input: %#q", test)
// Parse.
const prefix = "package p; const _ = "
src := prefix + test
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "p.go", src, 0)
if err != nil {
t.Errorf("Parse: %v", err)
continue
}
// Find literal.
var lit *ast.BasicLit
ast.Inspect(f, func(n ast.Node) bool {
if b, ok := n.(*ast.BasicLit); ok {
lit = b
return false
}
return true
})
if lit == nil {
t.Errorf("No literal")
continue
}
// Find index of marker within logical value.
value, err := strconv.Unquote(lit.Value)
if err != nil {
t.Errorf("Unquote: %v", err)
continue
}
index := strings.Index(value, "^")
if index < 0 {
t.Errorf("Value %q contains no marker", value)
continue
}
// Convert logical index to file position.
pos, err := astutil.PosInStringLiteral(lit, index)
if err != nil {
t.Errorf("PosInStringLiteral(%d): %v", index, err)
continue
}
// Check that cut offset in original src file is before marker.
offset := fset.Position(pos).Offset
before, after := src[:offset], src[offset:]
t.Logf("\t%q :: %q", before, after)
if !strings.HasPrefix(after, "^") {
t.Errorf("no marker at cut point")
continue
}
}
}
func TestOffsetInStringLiteral(t *testing.T) {
// Each string is Go source for a string literal with | marker at expected
// physical pos, and $ marker at the expected logical unquoted index.
tests := []string{
`"$|abc"`,
`"a$|bc"`,
`"ab$|c"`,
`"abc$|"`,
`"a\n$|b"`,
`"\n$|"`,
`"a\000$|"`,
`"\x61$|"`,
`"\u0061$|"`,
`"\U00000061$|"`,
`"$\U00|000061"`,
`"€$|"`,
`"a€$|b"`,
"`abc$|`",
"`a\n$|b`",
// normalization of \r carriage returns:
"`a\r\n$|b`",
"`a\r\nb\r\nc\r\n$|d`",
}
for _, test := range tests {
// The workaround for \r requires the go1.26 fix for https://go.dev/issue/76031.
if strings.Contains(test, "\r") && testenv.Go1Point() < 26 {
continue
}
t.Logf("input: %#q", test)
const prefix = "package p; const _ = "
src := prefix + test
// Remove the physical marker to produce valid Go source.
offset := strings.Index(src, "|")
if offset < 0 {
t.Errorf("Source %q contains no marker", src)
continue
}
src = src[:offset] + src[offset+1:]
// Parse.
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "p.go", src, 0)
if err != nil {
t.Errorf("Parse: %v", err)
continue
}
// Find literal.
var lit *ast.BasicLit
ast.Inspect(f, func(n ast.Node) bool {
if b, ok := n.(*ast.BasicLit); ok {
lit = b
return false
}
return true
})
if lit == nil {
t.Errorf("No literal")
continue
}
tfile := fset.File(f.Pos())
pos := tfile.Pos(offset)
// Convert file position to logical index.
index, err := astutil.OffsetInStringLiteral(lit, pos)
if err != nil {
t.Errorf("OffsetInStringLiteral(%d): %v", pos, err)
continue
}
value, err := strconv.Unquote(lit.Value)
if err != nil {
t.Errorf("Unquote: %v", err)
continue
}
if index < 0 || index > len(value) {
t.Errorf("Returned index %d is out of bounds for logical string of length %d", index, len(value))
continue
}
// Check that cut offset in value is before marker.
before, after := value[:index], value[index:]
t.Logf("\t%q :: %q", before, after)
if !strings.HasSuffix(before, "$") {
t.Errorf("no marker at logical cut point, got before=%q", before)
continue
}
}
}