Skip to content

Commit 79d4e32

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/lsp/cache/parsego: clamp positions when fixing statements
In golang/go#64488, we observe how a seemingly innocuous change to postfix completion tests led to significant flakiness in our integration tests, which started to encounter a new bug.Report for out-of-bounds positions on type checker errors. The root cause is that the new syntax of the test data triggered AST fixes (i.e. parsego.fixAST) that overflowed the original file. The failure was not deterministic because the postfix snippet tests do not wait for diagnostics: adding an env.AfterChange() to the end of the test body made the failure deterministic. Additionally, while investigating I encountered and fixed a clear bug that fixes (and therefore parsego.File.Fixed()) were not correctly counted. This was incidental, and did not contribute to the test failures. We don't actually use Fixed() very much, though we probably should consider it more. To fix the underlying bug, clamp positions to the token.File. This is definitely unsatisfactory, but after an hour of tracing through the fix logic, I am hesitant to commit to a more principled yet risky change. This logic is rather tricky and clearly contains a lot of embedded knowledge. It's probably best to leave it alone and redirect efforts to improved parser recovery. This is strong evidence that at some point we do need to carefully scrutinize the AST fixes (see also golang/go#64335). Updates golang/go#64335 Fixes golang/go#64488 Change-Id: I70a33c0c9aae66baae78e6474ee56cdaa25e45f4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/546655 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 58556d1 commit 79d4e32

File tree

4 files changed

+88
-24
lines changed

4 files changed

+88
-24
lines changed

Diff for: gopls/internal/lsp/cache/check.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -1806,7 +1806,12 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget
18061806
if !posn.IsValid() {
18071807
// All valid positions produced by the type checker should described by
18081808
// its fileset.
1809-
bug.Reportf("internal error: type checker error %v not outside its Fset", e)
1809+
//
1810+
// Note: in golang/go#64488, we observed an error that was positioned
1811+
// over fixed syntax, which overflowed its file. So it's definitely
1812+
// possible that we get here (it's hard to reason about fixing up the
1813+
// AST). Nevertheless, it's a bug.
1814+
bug.Reportf("internal error: type checker error %q outside its Fset", e)
18101815
continue
18111816
}
18121817
pgf, err := pkg.File(protocol.URIFromPath(posn.Filename))

Diff for: gopls/internal/lsp/cache/parsego/file.go

+8-11
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,27 @@ type File struct {
2424
// actual content of the file if we have fixed the AST.
2525
Src []byte
2626

27-
// FixedSrc and Fixed AST report on "fixing" that occurred during parsing of
27+
// fixedSrc and fixedAST report on "fixing" that occurred during parsing of
2828
// this file.
2929
//
30-
// If FixedSrc == true, the source contained in the Src field was modified
31-
// from the original source to improve parsing.
32-
//
33-
// If FixedAST == true, the ast was modified after parsing, and therefore
34-
// positions encoded in the AST may not accurately represent the content of
35-
// the Src field.
30+
// fixedSrc means Src holds file content that was modified to improve parsing.
31+
// fixedAST means File was modified after parsing, so AST positions may not
32+
// reflect the content of Src.
3633
//
3734
// TODO(rfindley): there are many places where we haphazardly use the Src or
3835
// positions without checking these fields. Audit these places and guard
3936
// accordingly. After doing so, we may find that we don't need to
40-
// differentiate FixedSrc and FixedAST.
41-
FixedSrc bool
42-
FixedAST bool
37+
// differentiate fixedSrc and fixedAST.
38+
fixedSrc bool
39+
fixedAST bool
4340
Mapper *protocol.Mapper // may map fixed Src, not file content
4441
ParseErr scanner.ErrorList
4542
}
4643

4744
// Fixed reports whether p was "Fixed", meaning that its source or positions
4845
// may not correlate with the original file.
4946
func (p File) Fixed() bool {
50-
return p.FixedSrc || p.FixedAST
47+
return p.fixedSrc || p.fixedAST
5148
}
5249

5350
// -- go/token domain convenience helpers --

Diff for: gopls/internal/lsp/cache/parsego/parse.go

+28-12
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
6767
if parseErr != nil {
6868
// Fix any badly parsed parts of the AST.
6969
astFixes := fixAST(file, tok, src)
70-
fixedAST = len(fixes) > 0
70+
fixedAST = len(astFixes) > 0
7171
if fixedAST {
7272
fixes = append(fixes, astFixes...)
7373
}
@@ -119,8 +119,8 @@ func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, s
119119
URI: uri,
120120
Mode: mode,
121121
Src: src,
122-
FixedSrc: fixedSrc,
123-
FixedAST: fixedAST,
122+
fixedSrc: fixedSrc,
123+
fixedAST: fixedAST,
124124
File: file,
125125
Tok: tok,
126126
Mapper: protocol.NewMapper(uri, src),
@@ -519,7 +519,7 @@ func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte)
519519
return false
520520
}
521521
stmtBytes := src[start : end+1]
522-
stmt, err := parseStmt(bad.Pos(), stmtBytes)
522+
stmt, err := parseStmt(tok, bad.Pos(), stmtBytes)
523523
if err != nil {
524524
return false
525525
}
@@ -621,7 +621,7 @@ func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte
621621
// literal to be parseable.
622622
exprBytes = append(exprBytes, '{', '}')
623623

624-
expr, err := parseExpr(from, exprBytes)
624+
expr, err := parseExpr(tok, from, exprBytes)
625625
if err != nil {
626626
return false
627627
}
@@ -786,7 +786,7 @@ FindTo:
786786
exprBytes = append(exprBytes, '_')
787787
}
788788

789-
expr, err := parseExpr(from, exprBytes)
789+
expr, err := parseExpr(tok, from, exprBytes)
790790
if err != nil {
791791
return false
792792
}
@@ -811,7 +811,10 @@ FindTo:
811811

812812
// parseStmt parses the statement in src and updates its position to
813813
// start at pos.
814-
func parseStmt(pos token.Pos, src []byte) (ast.Stmt, error) {
814+
//
815+
// tok is the original file containing pos. Used to ensure that all adjusted
816+
// positions are valid.
817+
func parseStmt(tok *token.File, pos token.Pos, src []byte) (ast.Stmt, error) {
815818
// Wrap our expression to make it a valid Go file we can pass to ParseFile.
816819
fileSrc := bytes.Join([][]byte{
817820
[]byte("package fake;func _(){"),
@@ -840,15 +843,15 @@ func parseStmt(pos token.Pos, src []byte) (ast.Stmt, error) {
840843

841844
// parser.ParseFile returns undefined positions.
842845
// Adjust them for the current file.
843-
offsetPositions(stmt, pos-1-(stmt.Pos()-1))
846+
offsetPositions(tok, stmt, pos-1-(stmt.Pos()-1))
844847

845848
return stmt, nil
846849
}
847850

848851
// parseExpr parses the expression in src and updates its position to
849852
// start at pos.
850-
func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) {
851-
stmt, err := parseStmt(pos, src)
853+
func parseExpr(tok *token.File, pos token.Pos, src []byte) (ast.Expr, error) {
854+
stmt, err := parseStmt(tok, pos, src)
852855
if err != nil {
853856
return nil, err
854857
}
@@ -864,7 +867,9 @@ func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) {
864867
var tokenPosType = reflect.TypeOf(token.NoPos)
865868

866869
// offsetPositions applies an offset to the positions in an ast.Node.
867-
func offsetPositions(n ast.Node, offset token.Pos) {
870+
func offsetPositions(tok *token.File, n ast.Node, offset token.Pos) {
871+
fileBase := int64(tok.Base())
872+
fileEnd := fileBase + int64(tok.Size())
868873
ast.Inspect(n, func(n ast.Node) bool {
869874
if n == nil {
870875
return false
@@ -889,7 +894,18 @@ func offsetPositions(n ast.Node, offset token.Pos) {
889894
continue
890895
}
891896

892-
f.SetInt(f.Int() + int64(offset))
897+
// Clamp value to valid range; see #64335.
898+
//
899+
// TODO(golang/go#64335): this is a hack, because our fixes should not
900+
// produce positions that overflow (but they do: golang/go#64488).
901+
pos := f.Int() + int64(offset)
902+
if pos < fileBase {
903+
pos = fileBase
904+
}
905+
if pos > fileEnd {
906+
pos = fileEnd
907+
}
908+
f.SetInt(pos)
893909
}
894910
}
895911

Diff for: gopls/internal/lsp/cache/parsego/parse_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package parsego_test
6+
7+
import (
8+
"context"
9+
"go/ast"
10+
"go/token"
11+
"testing"
12+
13+
"golang.org/x/tools/gopls/internal/lsp/cache/parsego"
14+
"golang.org/x/tools/gopls/internal/util/safetoken"
15+
"golang.org/x/tools/internal/tokeninternal"
16+
)
17+
18+
// TODO(golang/go#64335): we should have many more tests for fixed syntax.
19+
20+
func TestFixPosition_Issue64488(t *testing.T) {
21+
// This test reproduces the conditions of golang/go#64488, where a type error
22+
// on fixed syntax overflows the token.File.
23+
const src = `
24+
package foo
25+
26+
func _() {
27+
type myThing struct{}
28+
var foo []myThing
29+
for ${1:}, ${2:} := range foo {
30+
$0
31+
}
32+
}
33+
`
34+
35+
pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.ParseFull, false)
36+
fset := tokeninternal.FileSetFor(pgf.Tok)
37+
ast.Inspect(pgf.File, func(n ast.Node) bool {
38+
if n != nil {
39+
posn := safetoken.StartPosition(fset, n.Pos())
40+
if !posn.IsValid() {
41+
t.Fatalf("invalid position for %T (%v): %v not in [%d, %d]", n, n, n.Pos(), pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size())
42+
}
43+
}
44+
return true
45+
})
46+
}

0 commit comments

Comments
 (0)