diff options
Diffstat (limited to 'gopls/internal/lsp/template/parse.go')
-rw-r--r-- | gopls/internal/lsp/template/parse.go | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go new file mode 100644 index 000000000..a6befdcb9 --- /dev/null +++ b/gopls/internal/lsp/template/parse.go @@ -0,0 +1,508 @@ +// Copyright 2021 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 template contains code for dealing with templates +package template + +// template files are small enough that the code reprocesses them each time +// this may be a bad choice for projects with lots of template files. + +// This file contains the parsing code, some debugging printing, and +// implementations for Diagnose, Definition, Hover, References + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "regexp" + "runtime" + "sort" + "text/template" + "text/template/parse" + "unicode/utf8" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" +) + +var ( + Left = []byte("{{") + Right = []byte("}}") +) + +type Parsed struct { + buf []byte //contents + lines [][]byte // needed?, other than for debugging? + elided []int // offsets where Left was replaced by blanks + + // tokens are matched Left-Right pairs, computed before trying to parse + tokens []Token + + // result of parsing + named []*template.Template // the template and embedded templates + ParseErr error + symbols []symbol + stack []parse.Node // used while computing symbols + + // for mapping from offsets in buf to LSP coordinates + // See FromPosition() and LineCol() + nls []int // offset of newlines before each line (nls[0]==-1) + lastnl int // last line seen + check int // used to decide whether to use lastnl or search through nls + nonASCII bool // are there any non-ascii runes in buf? +} + +// Token is a single {{...}}. More precisely, Left...Right +type Token struct { + Start, End int // offset from start of template + Multiline bool +} + +// All contains the Parse of all the template files +type All struct { + files map[span.URI]*Parsed +} + +// New returns the Parses of the snapshot's tmpl files +// (maybe cache these, but then avoiding import cycles needs code rearrangements) +func New(tmpls map[span.URI]source.FileHandle) *All { + all := make(map[span.URI]*Parsed) + for k, v := range tmpls { + buf, err := v.Read() + if err != nil { // PJW: decide what to do with these errors + log.Printf("failed to read %s (%v)", v.URI().Filename(), err) + continue + } + all[k] = parseBuffer(buf) + } + return &All{files: all} +} + +func parseBuffer(buf []byte) *Parsed { + ans := &Parsed{ + buf: buf, + check: -1, + nls: []int{-1}, + } + if len(buf) == 0 { + return ans + } + // how to compute allAscii... + for _, b := range buf { + if b >= utf8.RuneSelf { + ans.nonASCII = true + break + } + } + if buf[len(buf)-1] != '\n' { + ans.buf = append(buf, '\n') + } + for i, p := range ans.buf { + if p == '\n' { + ans.nls = append(ans.nls, i) + } + } + ans.setTokens() // ans.buf may be a new []byte + ans.lines = bytes.Split(ans.buf, []byte{'\n'}) + t, err := template.New("").Parse(string(ans.buf)) + if err != nil { + funcs := make(template.FuncMap) + for t == nil && ans.ParseErr == nil { + // in 1.17 it may be possible to avoid getting this error + // template: :2: function "foo" not defined + matches := parseErrR.FindStringSubmatch(err.Error()) + if len(matches) == 2 { + // suppress the error by giving it a function with the right name + funcs[matches[1]] = func() interface{} { return nil } + t, err = template.New("").Funcs(funcs).Parse(string(ans.buf)) + continue + } + ans.ParseErr = err // unfixed error + return ans + } + } + ans.named = t.Templates() + // set the symbols + for _, t := range ans.named { + ans.stack = append(ans.stack, t.Root) + ans.findSymbols() + if t.Name() != "" { + // defining a template. The pos is just after {{define...}} (or {{block...}}?) + at, sz := ans.FindLiteralBefore(int(t.Root.Pos)) + s := symbol{start: at, length: sz, name: t.Name(), kind: protocol.Namespace, vardef: true} + ans.symbols = append(ans.symbols, s) + } + } + + sort.Slice(ans.symbols, func(i, j int) bool { + left, right := ans.symbols[i], ans.symbols[j] + if left.start != right.start { + return left.start < right.start + } + if left.vardef != right.vardef { + return left.vardef + } + return left.kind < right.kind + }) + return ans +} + +// FindLiteralBefore locates the first preceding string literal +// returning its position and length in buf +// or returns -1 if there is none. +// Assume double-quoted string rather than backquoted string for now. +func (p *Parsed) FindLiteralBefore(pos int) (int, int) { + left, right := -1, -1 + for i := pos - 1; i >= 0; i-- { + if p.buf[i] != '"' { + continue + } + if right == -1 { + right = i + continue + } + left = i + break + } + if left == -1 { + return -1, 0 + } + return left + 1, right - left - 1 +} + +var ( + parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`) +) + +func (p *Parsed) setTokens() { + const ( + // InRaw and InString only occur inside an action (SeenLeft) + Start = iota + InRaw + InString + SeenLeft + ) + state := Start + var left, oldState int + for n := 0; n < len(p.buf); n++ { + c := p.buf[n] + switch state { + case InRaw: + if c == '`' { + state = oldState + } + case InString: + if c == '"' && !isEscaped(p.buf[:n]) { + state = oldState + } + case SeenLeft: + if c == '`' { + oldState = state // it's SeenLeft, but a little clearer this way + state = InRaw + continue + } + if c == '"' { + oldState = state + state = InString + continue + } + if bytes.HasPrefix(p.buf[n:], Right) { + right := n + len(Right) + tok := Token{Start: left, + End: right, + Multiline: bytes.Contains(p.buf[left:right], []byte{'\n'}), + } + p.tokens = append(p.tokens, tok) + state = Start + } + // If we see (unquoted) Left then the original left is probably the user + // typing. Suppress the original left + if bytes.HasPrefix(p.buf[n:], Left) { + p.elideAt(left) + left = n + n += len(Left) - 1 // skip the rest + } + case Start: + if bytes.HasPrefix(p.buf[n:], Left) { + left = n + state = SeenLeft + n += len(Left) - 1 // skip the rest (avoids {{{ bug) + } + } + } + // this error occurs after typing {{ at the end of the file + if state != Start { + // Unclosed Left. remove the Left at left + p.elideAt(left) + } +} + +func (p *Parsed) elideAt(left int) { + if p.elided == nil { + // p.buf is the same buffer that v.Read() returns, so copy it. + // (otherwise the next time it's parsed, elided information is lost) + b := make([]byte, len(p.buf)) + copy(b, p.buf) + p.buf = b + } + for i := 0; i < len(Left); i++ { + p.buf[left+i] = ' ' + } + p.elided = append(p.elided, left) +} + +// isEscaped reports whether the byte after buf is escaped +func isEscaped(buf []byte) bool { + backSlashes := 0 + for j := len(buf) - 1; j >= 0 && buf[j] == '\\'; j-- { + backSlashes++ + } + return backSlashes%2 == 1 +} + +func (p *Parsed) Tokens() []Token { + return p.tokens +} + +// TODO(adonovan): the next 100 lines could perhaps replaced by use of protocol.Mapper. + +func (p *Parsed) utf16len(buf []byte) int { + cnt := 0 + if !p.nonASCII { + return len(buf) + } + // we need a utf16len(rune), but we don't have it + for _, r := range string(buf) { + cnt++ + if r >= 1<<16 { + cnt++ + } + } + return cnt +} + +func (p *Parsed) TokenSize(t Token) (int, error) { + if t.Multiline { + return -1, fmt.Errorf("TokenSize called with Multiline token %#v", t) + } + ans := p.utf16len(p.buf[t.Start:t.End]) + return ans, nil +} + +// RuneCount counts runes in line l, from col s to e +// (e==0 for end of line. called only for multiline tokens) +func (p *Parsed) RuneCount(l, s, e uint32) uint32 { + start := p.nls[l] + 1 + int(s) + end := p.nls[l] + 1 + int(e) + if e == 0 || end > p.nls[l+1] { + end = p.nls[l+1] + } + return uint32(utf8.RuneCount(p.buf[start:end])) +} + +// LineCol converts from a 0-based byte offset to 0-based line, col. col in runes +func (p *Parsed) LineCol(x int) (uint32, uint32) { + if x < p.check { + p.lastnl = 0 + } + p.check = x + for i := p.lastnl; i < len(p.nls); i++ { + if p.nls[i] <= x { + continue + } + p.lastnl = i + var count int + if i > 0 && x == p.nls[i-1] { // \n + count = 0 + } else { + count = p.utf16len(p.buf[p.nls[i-1]+1 : x]) + } + return uint32(i - 1), uint32(count) + } + if x == len(p.buf)-1 { // trailing \n + return uint32(len(p.nls) - 1), 0 + } + // shouldn't happen + for i := 1; i < 4; i++ { + _, f, l, ok := runtime.Caller(i) + if !ok { + break + } + log.Printf("%d: %s:%d", i, f, l) + } + + msg := fmt.Errorf("LineCol off the end, %d of %d, nls=%v, %q", x, len(p.buf), p.nls, p.buf[x:]) + event.Error(context.Background(), "internal error", msg) + return 0, 0 +} + +// Position produces a protocol.Position from an offset in the template +func (p *Parsed) Position(pos int) protocol.Position { + line, col := p.LineCol(pos) + return protocol.Position{Line: line, Character: col} +} + +func (p *Parsed) Range(x, length int) protocol.Range { + line, col := p.LineCol(x) + ans := protocol.Range{ + Start: protocol.Position{Line: line, Character: col}, + End: protocol.Position{Line: line, Character: col + uint32(length)}, + } + return ans +} + +// FromPosition translates a protocol.Position into an offset into the template +func (p *Parsed) FromPosition(x protocol.Position) int { + l, c := int(x.Line), int(x.Character) + if l >= len(p.nls) || p.nls[l]+1 >= len(p.buf) { + // paranoia to avoid panic. return the largest offset + return len(p.buf) + } + line := p.buf[p.nls[l]+1:] + cnt := 0 + for w := range string(line) { + if cnt >= c { + return w + p.nls[l] + 1 + } + cnt++ + } + // do we get here? NO + pos := int(x.Character) + p.nls[int(x.Line)] + 1 + event.Error(context.Background(), "internal error", fmt.Errorf("surprise %#v", x)) + return pos +} + +func symAtPosition(fh source.FileHandle, loc protocol.Position) (*symbol, *Parsed, error) { + buf, err := fh.Read() + if err != nil { + return nil, nil, err + } + p := parseBuffer(buf) + pos := p.FromPosition(loc) + syms := p.SymsAtPos(pos) + if len(syms) == 0 { + return nil, p, fmt.Errorf("no symbol found") + } + if len(syms) > 1 { + log.Printf("Hover: %d syms, not 1 %v", len(syms), syms) + } + sym := syms[0] + return &sym, p, nil +} + +func (p *Parsed) SymsAtPos(pos int) []symbol { + ans := []symbol{} + for _, s := range p.symbols { + if s.start <= pos && pos < s.start+s.length { + ans = append(ans, s) + } + } + return ans +} + +type wrNode struct { + p *Parsed + w io.Writer +} + +// WriteNode is for debugging +func (p *Parsed) WriteNode(w io.Writer, n parse.Node) { + wr := wrNode{p: p, w: w} + wr.writeNode(n, "") +} + +func (wr wrNode) writeNode(n parse.Node, indent string) { + if n == nil { + return + } + at := func(pos parse.Pos) string { + line, col := wr.p.LineCol(int(pos)) + return fmt.Sprintf("(%d)%v:%v", pos, line, col) + } + switch x := n.(type) { + case *parse.ActionNode: + fmt.Fprintf(wr.w, "%sActionNode at %s\n", indent, at(x.Pos)) + wr.writeNode(x.Pipe, indent+". ") + case *parse.BoolNode: + fmt.Fprintf(wr.w, "%sBoolNode at %s, %v\n", indent, at(x.Pos), x.True) + case *parse.BranchNode: + fmt.Fprintf(wr.w, "%sBranchNode at %s\n", indent, at(x.Pos)) + wr.writeNode(x.Pipe, indent+"Pipe. ") + wr.writeNode(x.List, indent+"List. ") + wr.writeNode(x.ElseList, indent+"Else. ") + case *parse.ChainNode: + fmt.Fprintf(wr.w, "%sChainNode at %s, %v\n", indent, at(x.Pos), x.Field) + case *parse.CommandNode: + fmt.Fprintf(wr.w, "%sCommandNode at %s, %d children\n", indent, at(x.Pos), len(x.Args)) + for _, a := range x.Args { + wr.writeNode(a, indent+". ") + } + //case *parse.CommentNode: // 1.16 + case *parse.DotNode: + fmt.Fprintf(wr.w, "%sDotNode at %s\n", indent, at(x.Pos)) + case *parse.FieldNode: + fmt.Fprintf(wr.w, "%sFieldNode at %s, %v\n", indent, at(x.Pos), x.Ident) + case *parse.IdentifierNode: + fmt.Fprintf(wr.w, "%sIdentifierNode at %s, %v\n", indent, at(x.Pos), x.Ident) + case *parse.IfNode: + fmt.Fprintf(wr.w, "%sIfNode at %s\n", indent, at(x.Pos)) + wr.writeNode(&x.BranchNode, indent+". ") + case *parse.ListNode: + if x == nil { + return // nil BranchNode.ElseList + } + fmt.Fprintf(wr.w, "%sListNode at %s, %d children\n", indent, at(x.Pos), len(x.Nodes)) + for _, n := range x.Nodes { + wr.writeNode(n, indent+". ") + } + case *parse.NilNode: + fmt.Fprintf(wr.w, "%sNilNode at %s\n", indent, at(x.Pos)) + case *parse.NumberNode: + fmt.Fprintf(wr.w, "%sNumberNode at %s, %s\n", indent, at(x.Pos), x.Text) + case *parse.PipeNode: + if x == nil { + return // {{template "xxx"}} + } + fmt.Fprintf(wr.w, "%sPipeNode at %s, %d vars, %d cmds, IsAssign:%v\n", + indent, at(x.Pos), len(x.Decl), len(x.Cmds), x.IsAssign) + for _, d := range x.Decl { + wr.writeNode(d, indent+"Decl. ") + } + for _, c := range x.Cmds { + wr.writeNode(c, indent+"Cmd. ") + } + case *parse.RangeNode: + fmt.Fprintf(wr.w, "%sRangeNode at %s\n", indent, at(x.Pos)) + wr.writeNode(&x.BranchNode, indent+". ") + case *parse.StringNode: + fmt.Fprintf(wr.w, "%sStringNode at %s, %s\n", indent, at(x.Pos), x.Quoted) + case *parse.TemplateNode: + fmt.Fprintf(wr.w, "%sTemplateNode at %s, %s\n", indent, at(x.Pos), x.Name) + wr.writeNode(x.Pipe, indent+". ") + case *parse.TextNode: + fmt.Fprintf(wr.w, "%sTextNode at %s, len %d\n", indent, at(x.Pos), len(x.Text)) + case *parse.VariableNode: + fmt.Fprintf(wr.w, "%sVariableNode at %s, %v\n", indent, at(x.Pos), x.Ident) + case *parse.WithNode: + fmt.Fprintf(wr.w, "%sWithNode at %s\n", indent, at(x.Pos)) + wr.writeNode(&x.BranchNode, indent+". ") + } +} + +var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property", + "Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String", + "Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event", + "Operator", "TypeParameter"} + +func kindStr(k protocol.SymbolKind) string { + n := int(k) + if n < 1 || n >= len(kindNames) { + return fmt.Sprintf("?SymbolKind %d?", n) + } + return kindNames[n] +} |