blob: 0de081c8578aad0b941fd7efc85da09847c7488e [file]
// Copyright 2023 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 parsego
import (
"go/ast"
"go/token"
"reflect"
"testing"
)
// oldOffsetPositions is the old clamping logic from before the fix
// to the clamping bug in golang/go#64960
func oldOffsetPositions(tok *token.File, n ast.Node, offset token.Pos) {
fileBase := token.Pos(tok.Base())
fileEnd := fileBase + token.Pos(tok.Size())
ast.Inspect(n, func(n ast.Node) bool {
if n == nil {
return false
}
v := reflect.ValueOf(n).Elem()
switch v.Kind() {
case reflect.Struct:
for _, f := range v.Fields() {
if f.Type() != tokenPosType {
continue
}
if !f.CanSet() {
continue
}
pos := token.Pos(f.Int())
if !pos.IsValid() {
continue
}
// Clamp value to valid range; see #64335.
//
// TODO(golang/go#64335): this is a hack, because our fixes should not
// produce positions that overflow (but they do; see golang/go#64488,
// #73438, #66790, #66683, #67704).
pos = min(max(pos+offset, fileBase), fileEnd)
f.SetInt(int64(pos))
}
}
return true
})
}
func TestOffsetPositionsClamping(t *testing.T) {
// Create a fake token.File of size 10.
// Base is 1. File spans [1, 11].
fset := token.NewFileSet()
tf := fset.AddFile("test.go", 1, 10)
fileBase := token.Pos(tf.Base()) // 1
fileEnd := fileBase + token.Pos(tf.Size()) // 11
// Map a composite literal "variance{}" to the end of the file.
// In the file, "variance" is at offset 0 (pos 1).
// Lbrace is at pos 9, Rbrace is at pos 10.
// Offset is fileEnd (11).
t.Run("old_clamping_fails", func(t *testing.T) {
cl := &ast.CompositeLit{
Type: &ast.Ident{
Name: "variance",
NamePos: 1,
},
Lbrace: 9,
Rbrace: 10,
}
// Using the old clamping logic.
oldOffsetPositions(tf, cl, 11)
// The old logic clamps the start positions of all tokens to fileEnd (11).
// This means the identifier "variance" (size 8) starts at 11, so it ends at 19.
// 19 is > fileEnd + 1 (12). This is the bug!
if int(cl.Type.End()) > tf.Base()+tf.Size()+1 {
t.Logf("PASS (demonstrates bug): old clamping results in Type.End() %d > fileEnd+1 %d", cl.Type.End(), tf.Base()+tf.Size()+1)
} else {
t.Errorf("expected old clamping to fail (Type.End() > fileEnd+1), but got Type.End() = %d", cl.Type.End())
}
})
t.Run("new_clamping_passes", func(t *testing.T) {
cl := &ast.CompositeLit{
Type: &ast.Ident{
Name: "variance",
NamePos: 1,
},
Lbrace: 9,
Rbrace: 10,
}
// Using the new clamping logic.
offsetPositions(tf, cl, 11)
// The new logic clamps NamePos to fileEnd + 1 - len("variance") = 11 + 1 - 8 = 4.
expectedPos := fileEnd + 1 - token.Pos(len("variance"))
ident := cl.Type.(*ast.Ident)
if ident.NamePos != expectedPos {
t.Errorf("expected NamePos to be clamped to %d, got %d", expectedPos, ident.NamePos)
}
// The End() of the identifier is NamePos + 8 = 4 + 8 = 12.
// 12 is exactly fileEnd + 1.
if int(cl.Type.End()) <= tf.Base()+tf.Size()+1 {
t.Logf("PASS: new clamping results in valid Type.End() %d <= fileEnd+1 %d", cl.Type.End(), tf.Base()+tf.Size()+1)
} else {
t.Errorf("expected new clamping to pass (Type.End() <= fileEnd+1), but got Type.End() = %d", cl.Type.End())
}
// Check that the order Lbrace <= Rbrace is preserved.
if cl.Lbrace > cl.Rbrace {
t.Errorf("Lbrace (%d) should not be greater than Rbrace (%d)", cl.Lbrace, cl.Rbrace)
}
})
}
// exercise all the cases
func TestPosSize(t *testing.T) {
tests := []struct {
node ast.Node
field string
want int
}{
{node: &ast.AssignStmt{Tok: token.DEFINE}, field: "TokPos", want: 2},
{node: &ast.AssignStmt{Tok: token.ASSIGN}, field: "TokPos", want: 1},
// Bad nodes for completeness
{node: &ast.BadDecl{From: 10, To: 15}, field: "From", want: 5},
{node: &ast.BadDecl{From: 10, To: 15}, field: "To", want: 0},
{node: &ast.BadExpr{From: 10, To: 15}, field: "From", want: 5},
{node: &ast.BadExpr{From: 10, To: 15}, field: "To", want: 0},
{node: &ast.BadStmt{From: 10, To: 15}, field: "From", want: 5},
{node: &ast.BadStmt{From: 10, To: 15}, field: "To", want: 0},
{node: &ast.BasicLit{Value: `"hello"`, ValuePos: 1}, field: "ValuePos", want: 7},
{node: &ast.BasicLit{Value: `"hello"`, ValuePos: 1}, field: "ValueEnd", want: 0},
{node: &ast.BinaryExpr{Op: token.ADD}, field: "OpPos", want: 1},
{node: &ast.BinaryExpr{Op: token.NEQ}, field: "OpPos", want: 2},
{node: &ast.BlockStmt{}, field: "Lbrace", want: 1},
{node: &ast.BlockStmt{}, field: "Rbrace", want: 1},
{node: &ast.BranchStmt{Tok: token.BREAK}, field: "TokPos", want: 5},
{node: &ast.BranchStmt{Tok: token.FALLTHROUGH}, field: "TokPos", want: 11},
{node: &ast.CallExpr{}, field: "Lparen", want: 1},
{node: &ast.CallExpr{}, field: "Rparen", want: 1},
{node: &ast.CallExpr{}, field: "Ellipsis", want: 3},
{node: &ast.CaseClause{List: []ast.Expr{&ast.Ident{}}}, field: "Case", want: 4},
{node: &ast.CaseClause{List: nil}, field: "Case", want: 7},
{node: &ast.CaseClause{}, field: "Colon", want: 1},
{node: &ast.ChanType{}, field: "Begin", want: 4},
{node: &ast.ChanType{}, field: "Arrow", want: 2},
{node: &ast.CommClause{Comm: &ast.ExprStmt{}}, field: "Case", want: 4},
{node: &ast.CommClause{Comm: nil}, field: "Case", want: 7},
{node: &ast.CommClause{}, field: "Colon", want: 1},
{node: &ast.Comment{Text: "// comment"}, field: "Slash", want: 10},
{node: &ast.CompositeLit{}, field: "Lbrace", want: 1},
{node: &ast.CompositeLit{}, field: "Rbrace", want: 1},
{node: &ast.DeferStmt{}, field: "Defer", want: 5},
{node: &ast.Ellipsis{}, field: "Ellipsis", want: 3},
{node: &ast.EmptyStmt{}, field: "Semicolon", want: 1},
{node: &ast.File{}, field: "Package", want: 7},
{node: &ast.File{}, field: "FileStart", want: 0},
{node: &ast.File{}, field: "FileEnd", want: 0},
{node: &ast.ForStmt{}, field: "For", want: 3},
{node: &ast.FuncType{}, field: "Func", want: 4},
{node: &ast.GenDecl{Tok: token.IMPORT}, field: "TokPos", want: 6},
{node: &ast.GenDecl{}, field: "Lparen", want: 1},
{node: &ast.GenDecl{}, field: "Rparen", want: 1},
{node: &ast.GoStmt{}, field: "Go", want: 2},
{node: &ast.Ident{Name: "foo"}, field: "NamePos", want: 3},
{node: &ast.IfStmt{}, field: "If", want: 2},
{node: &ast.ImportSpec{}, field: "EndPos", want: 0},
{node: &ast.IncDecStmt{Tok: token.INC}, field: "TokPos", want: 2},
{node: &ast.InterfaceType{}, field: "Interface", want: 9},
{node: &ast.KeyValueExpr{}, field: "Colon", want: 1},
{node: &ast.LabeledStmt{}, field: "Colon", want: 1},
{node: &ast.MapType{}, field: "Map", want: 3},
{node: &ast.RangeStmt{}, field: "For", want: 3},
{node: &ast.RangeStmt{Tok: token.DEFINE}, field: "TokPos", want: 2},
// These keyword tests are here for completeess
{node: &ast.RangeStmt{}, field: "Range", want: 5},
{node: &ast.ReturnStmt{}, field: "Return", want: 6},
{node: &ast.SelectStmt{}, field: "Select", want: 6},
{node: &ast.SendStmt{}, field: "Arrow", want: 2},
{node: &ast.StructType{}, field: "Struct", want: 6},
{node: &ast.SwitchStmt{}, field: "Switch", want: 6},
{node: &ast.TypeSpec{}, field: "Assign", want: 1},
{node: &ast.TypeSwitchStmt{}, field: "Switch", want: 6},
{node: &ast.UnaryExpr{Op: token.AND}, field: "OpPos", want: 1},
// Fallbacks
{node: &ast.Ident{}, field: "UnknownField", want: 1},
{node: &ast.Field{}, field: "Doc", want: 1},
}
for _, tc := range tests {
got := posSize(tc.node, tc.field)
if got != tc.want {
t.Errorf("posSize(%T, %q) = %d; want %d", tc.node, tc.field, got, tc.want)
}
}
}