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