diff --git a/gopls/internal/goxls/imports/imports.go b/gopls/internal/goxls/imports/imports.go index a2e144bbb60..3778fd01378 100644 --- a/gopls/internal/goxls/imports/imports.go +++ b/gopls/internal/goxls/imports/imports.go @@ -171,145 +171,8 @@ func parse(fset *token.FileSet, filename string, src []byte, opt *Options) (*ast parserMode |= parser.AllErrors } - // Try as whole source file. file, err := parser.ParseFile(fset, filename, src, parserMode) - if err == nil { - return file, nil, nil - } - // If the error is that the source file didn't begin with a - // package line and we accept fragmented input, fall through to - // try as a source fragment. Stop and return on any other error. - if !opt.Fragment || !strings.Contains(err.Error(), "expected 'package'") { - return nil, nil, err - } - - // If this is a declaration list, make it a source file - // by inserting a package clause. - // Insert using a ;, not a newline, so that parse errors are on - // the correct line. - const prefix = "package main;" - psrc := append([]byte(prefix), src...) - file, err = parser.ParseFile(fset, filename, psrc, parserMode) - if err == nil { - // Gofmt will turn the ; into a \n. - // Do that ourselves now and update the file contents, - // so that positions and line numbers are correct going forward. - psrc[len(prefix)-1] = '\n' - fset.File(file.Package).SetLinesForContent(psrc) - - // If a main function exists, we will assume this is a main - // package and leave the file. - if containsMainFunc(file) { - return file, nil, nil - } - - adjust := func(orig, src []byte) []byte { - // Remove the package clause. - src = src[len(prefix):] - return matchSpace(orig, src) - } - return file, adjust, nil - } - // If the error is that the source file didn't begin with a - // declaration, fall through to try as a statement list. - // Stop and return on any other error. - if !strings.Contains(err.Error(), "expected declaration") { - return nil, nil, err - } - - // If this is a statement list, make it a source file - // by inserting a package clause and turning the list - // into a function body. This handles expressions too. - // Insert using a ;, not a newline, so that the line numbers - // in fsrc match the ones in src. - fsrc := append(append([]byte("package p; func _() {"), src...), '}') - file, err = parser.ParseFile(fset, filename, fsrc, parserMode) - if err == nil { - adjust := func(orig, src []byte) []byte { - // Remove the wrapping. - // Gofmt has turned the ; into a \n\n. - src = src[len("package p\n\nfunc _() {"):] - src = src[:len(src)-len("}\n")] - // Gofmt has also indented the function body one level. - // Remove that indent. - src = bytes.Replace(src, []byte("\n\t"), []byte("\n"), -1) - return matchSpace(orig, src) - } - return file, adjust, nil - } - - // Failed, and out of options. - return nil, nil, err -} - -// containsMainFunc checks if a file contains a function declaration with the -// function signature 'func main()' -func containsMainFunc(file *ast.File) bool { - for _, decl := range file.Decls { - if f, ok := decl.(*ast.FuncDecl); ok { - if f.Name.Name != "main" { - continue - } - - if len(f.Type.Params.List) != 0 { - continue - } - - if f.Type.Results != nil && len(f.Type.Results.List) != 0 { - continue - } - - return true - } - } - - return false -} - -func cutSpace(b []byte) (before, middle, after []byte) { - i := 0 - for i < len(b) && (b[i] == ' ' || b[i] == '\t' || b[i] == '\n') { - i++ - } - j := len(b) - for j > 0 && (b[j-1] == ' ' || b[j-1] == '\t' || b[j-1] == '\n') { - j-- - } - if i <= j { - return b[:i], b[i:j], b[j:] - } - return nil, nil, b[j:] -} - -// matchSpace reformats src to use the same space context as orig. -// 1. If orig begins with blank lines, matchSpace inserts them at the beginning of src. -// 2. matchSpace copies the indentation of the first non-blank line in orig -// to every non-blank line in src. -// 3. matchSpace copies the trailing space from orig and uses it in place -// of src's trailing space. -func matchSpace(orig []byte, src []byte) []byte { - before, _, after := cutSpace(orig) - i := bytes.LastIndex(before, []byte{'\n'}) - before, indent := before[:i+1], before[i+1:] - - _, src, _ = cutSpace(src) - - var b bytes.Buffer - b.Write(before) - for len(src) > 0 { - line := src - if i := bytes.IndexByte(line, '\n'); i >= 0 { - line, src = line[:i+1], line[i+1:] - } else { - src = nil - } - if len(line) > 0 && line[0] != '\n' { // not blank - b.Write(indent) - } - b.Write(line) - } - b.Write(after) - return b.Bytes() + return file, nil, err } var impLine = regexp.MustCompile(`^\s+(?:[\w\.]+\s+)?"(.+?)"`) diff --git a/gopls/internal/goxls/imports/mod.go b/gopls/internal/goxls/imports/mod.go index e3733e39c49..42d9dac08a4 100644 --- a/gopls/internal/goxls/imports/mod.go +++ b/gopls/internal/goxls/imports/mod.go @@ -296,10 +296,6 @@ func (r *ModuleResolver) cacheStore(info directoryPackageInfo) { } } -func (r *ModuleResolver) cacheKeys() []string { - return append(r.moduleCacheCache.Keys(), r.otherCache.Keys()...) -} - // cachePackageName caches the package name for a dir already in the cache. func (r *ModuleResolver) cachePackageName(info directoryPackageInfo) (string, error) { if info.rootType == gopathwalk.RootModuleCache { diff --git a/gopls/internal/goxls/typesutil/exprstring.go b/gopls/internal/goxls/typesutil/exprstring.go new file mode 100644 index 00000000000..dc485994751 --- /dev/null +++ b/gopls/internal/goxls/typesutil/exprstring.go @@ -0,0 +1,237 @@ +// Copyright 2023 The GoPlus Authors (goplus.org). All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package typesutil + +import ( + "bytes" + "fmt" + + "github.com/goplus/gop/ast" + "golang.org/x/tools/gopls/internal/goxls/typesutil/typeparams" +) + +// ExprString returns the (possibly shortened) string representation for x. +// Shortened representations are suitable for user interfaces but may not +// necessarily follow Go syntax. +func ExprString(x ast.Expr) string { + var buf bytes.Buffer + WriteExpr(&buf, x) + return buf.String() +} + +// WriteExpr writes the (possibly shortened) string representation for x to buf. +// Shortened representations are suitable for user interfaces but may not +// necessarily follow Go syntax. +func WriteExpr(buf *bytes.Buffer, x ast.Expr) { + // The AST preserves source-level parentheses so there is + // no need to introduce them here to correct for different + // operator precedences. (This assumes that the AST was + // generated by a Go parser.) + + switch x := x.(type) { + default: + fmt.Fprintf(buf, "(ast: %T)", x) // nil, ast.BadExpr, ast.KeyValueExpr + + case *ast.Ident: + buf.WriteString(x.Name) + + case *ast.Ellipsis: + buf.WriteString("...") + if x.Elt != nil { + WriteExpr(buf, x.Elt) + } + + case *ast.BasicLit: + buf.WriteString(x.Value) + + case *ast.FuncLit: + buf.WriteByte('(') + WriteExpr(buf, x.Type) + buf.WriteString(" literal)") // shortened + + case *ast.CompositeLit: + WriteExpr(buf, x.Type) + buf.WriteByte('{') + if len(x.Elts) > 0 { + buf.WriteString("…") + } + buf.WriteByte('}') + + case *ast.ParenExpr: + buf.WriteByte('(') + WriteExpr(buf, x.X) + buf.WriteByte(')') + + case *ast.SelectorExpr: + WriteExpr(buf, x.X) + buf.WriteByte('.') + buf.WriteString(x.Sel.Name) + + case *ast.IndexExpr, *ast.IndexListExpr: + ix := typeparams.UnpackIndexExpr(x) + WriteExpr(buf, ix.X) + buf.WriteByte('[') + writeExprList(buf, ix.Indices) + buf.WriteByte(']') + + case *ast.SliceExpr: + WriteExpr(buf, x.X) + buf.WriteByte('[') + if x.Low != nil { + WriteExpr(buf, x.Low) + } + buf.WriteByte(':') + if x.High != nil { + WriteExpr(buf, x.High) + } + if x.Slice3 { + buf.WriteByte(':') + if x.Max != nil { + WriteExpr(buf, x.Max) + } + } + buf.WriteByte(']') + + case *ast.TypeAssertExpr: + WriteExpr(buf, x.X) + buf.WriteString(".(") + WriteExpr(buf, x.Type) + buf.WriteByte(')') + + case *ast.CallExpr: + WriteExpr(buf, x.Fun) + buf.WriteByte('(') + writeExprList(buf, x.Args) + if x.Ellipsis.IsValid() { + buf.WriteString("...") + } + buf.WriteByte(')') + + case *ast.StarExpr: + buf.WriteByte('*') + WriteExpr(buf, x.X) + + case *ast.UnaryExpr: + buf.WriteString(x.Op.String()) + WriteExpr(buf, x.X) + + case *ast.BinaryExpr: + WriteExpr(buf, x.X) + buf.WriteByte(' ') + buf.WriteString(x.Op.String()) + buf.WriteByte(' ') + WriteExpr(buf, x.Y) + + case *ast.ArrayType: + buf.WriteByte('[') + if x.Len != nil { + WriteExpr(buf, x.Len) + } + buf.WriteByte(']') + WriteExpr(buf, x.Elt) + + case *ast.StructType: + buf.WriteString("struct{") + writeFieldList(buf, x.Fields.List, "; ", false) + buf.WriteByte('}') + + case *ast.FuncType: + buf.WriteString("func") + writeSigExpr(buf, x) + + case *ast.InterfaceType: + buf.WriteString("interface{") + writeFieldList(buf, x.Methods.List, "; ", true) + buf.WriteByte('}') + + case *ast.MapType: + buf.WriteString("map[") + WriteExpr(buf, x.Key) + buf.WriteByte(']') + WriteExpr(buf, x.Value) + + case *ast.ChanType: + var s string + switch x.Dir { + case ast.SEND: + s = "chan<- " + case ast.RECV: + s = "<-chan " + default: + s = "chan " + } + buf.WriteString(s) + WriteExpr(buf, x.Value) + } +} + +func writeSigExpr(buf *bytes.Buffer, sig *ast.FuncType) { + buf.WriteByte('(') + writeFieldList(buf, sig.Params.List, ", ", false) + buf.WriteByte(')') + + res := sig.Results + n := res.NumFields() + if n == 0 { + // no result + return + } + + buf.WriteByte(' ') + if n == 1 && len(res.List[0].Names) == 0 { + // single unnamed result + WriteExpr(buf, res.List[0].Type) + return + } + + // multiple or named result(s) + buf.WriteByte('(') + writeFieldList(buf, res.List, ", ", false) + buf.WriteByte(')') +} + +func writeFieldList(buf *bytes.Buffer, list []*ast.Field, sep string, iface bool) { + for i, f := range list { + if i > 0 { + buf.WriteString(sep) + } + + // field list names + writeIdentList(buf, f.Names) + + // types of interface methods consist of signatures only + if sig, _ := f.Type.(*ast.FuncType); sig != nil && iface { + writeSigExpr(buf, sig) + continue + } + + // named fields are separated with a blank from the field type + if len(f.Names) > 0 { + buf.WriteByte(' ') + } + + WriteExpr(buf, f.Type) + + // ignore tag + } +} + +func writeIdentList(buf *bytes.Buffer, list []*ast.Ident) { + for i, x := range list { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(x.Name) + } +} + +func writeExprList(buf *bytes.Buffer, list []ast.Expr) { + for i, x := range list { + if i > 0 { + buf.WriteString(", ") + } + WriteExpr(buf, x) + } +} diff --git a/gopls/internal/goxls/typesutil/typeparams/typeparams.go b/gopls/internal/goxls/typesutil/typeparams/typeparams.go new file mode 100644 index 00000000000..89c7d06cb54 --- /dev/null +++ b/gopls/internal/goxls/typesutil/typeparams/typeparams.go @@ -0,0 +1,54 @@ +// 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 typeparams + +import ( + "github.com/goplus/gop/ast" + "github.com/goplus/gop/token" +) + +func PackIndexExpr(x ast.Expr, lbrack token.Pos, exprs []ast.Expr, rbrack token.Pos) ast.Expr { + switch len(exprs) { + case 0: + panic("internal error: PackIndexExpr with empty expr slice") + case 1: + return &ast.IndexExpr{ + X: x, + Lbrack: lbrack, + Index: exprs[0], + Rbrack: rbrack, + } + default: + return &ast.IndexListExpr{ + X: x, + Lbrack: lbrack, + Indices: exprs, + Rbrack: rbrack, + } + } +} + +// IndexExpr wraps an ast.IndexExpr or ast.IndexListExpr. +// +// Orig holds the original ast.Expr from which this IndexExpr was derived. +type IndexExpr struct { + Orig ast.Expr // the wrapped expr, which may be distinct from the IndexListExpr below. + *ast.IndexListExpr +} + +func UnpackIndexExpr(n ast.Node) *IndexExpr { + switch e := n.(type) { + case *ast.IndexExpr: + return &IndexExpr{e, &ast.IndexListExpr{ + X: e.X, + Lbrack: e.Lbrack, + Indices: []ast.Expr{e.Index}, + Rbrack: e.Rbrack, + }} + case *ast.IndexListExpr: + return &IndexExpr{e, e} + } + return nil +} diff --git a/gopls/internal/lsp/source/format_gox.go b/gopls/internal/lsp/source/format_gox.go index b849c48b462..770c65e720e 100644 --- a/gopls/internal/lsp/source/format_gox.go +++ b/gopls/internal/lsp/source/format_gox.go @@ -184,7 +184,9 @@ func gopComputeFixEdits(snapshot Snapshot, pgf *ParsedGopFile, options *imports. if err != nil { return nil, err } - extra := !strings.Contains(left, "\n") // one line may have more than imports + // goxls: left maybe empty in Go+ + // extra := !strings.Contains(left, "\n") // one line may have more than imports + extra := (left != "") && !strings.Contains(left, "\n") if extra { left = string(pgf.Src) } @@ -264,8 +266,10 @@ func gopImportPrefix(src []byte) (string, error) { return offset } if importEnd == 0 { - pkgEnd := f.Name.End() - importEnd = maybeAdjustToLineEnd(pkgEnd, false) + if f.Package != token.NoPos { // goxls: Go+ may no `package xxx` + pkgEnd := f.Name.End() + importEnd = maybeAdjustToLineEnd(pkgEnd, false) + } } for _, cgroup := range f.Comments { for _, c := range cgroup.List { diff --git a/gopls/internal/lsp/source/symbols_gox.go b/gopls/internal/lsp/source/symbols_gox.go new file mode 100644 index 00000000000..dcb6dbe605f --- /dev/null +++ b/gopls/internal/lsp/source/symbols_gox.go @@ -0,0 +1,228 @@ +// Copyright 2023 The GoPlus Authors (goplus.org). All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + + "github.com/goplus/gop/ast" + "github.com/goplus/gop/token" + "golang.org/x/tools/gopls/internal/goxls/parserutil" + "golang.org/x/tools/gopls/internal/goxls/typesutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" +) + +func GopDocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { + ctx, done := event.Start(ctx, "source.GopDocumentSymbols") + defer done() + + pgf, err := snapshot.ParseGop(ctx, fh, parserutil.ParseFull) + if err != nil { + return nil, fmt.Errorf("getting file for GopDocumentSymbols: %w", err) + } + + // Build symbols for file declarations. When encountering a declaration with + // errors (typically because positions are invalid), we skip the declaration + // entirely. VS Code fails to show any symbols if one of the top-level + // symbols is missing position information. + var symbols []protocol.DocumentSymbol + for _, decl := range pgf.File.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + if decl.Name.Name == "_" { + continue + } + fs, err := gopFuncSymbol(pgf.Mapper, pgf.Tok, decl) + if err == nil { + // If function is a method, prepend the type of the method. + if decl.Recv != nil && len(decl.Recv.List) > 0 { + fs.Name = fmt.Sprintf("(%s).%s", typesutil.ExprString(decl.Recv.List[0].Type), fs.Name) + } + symbols = append(symbols, fs) + } + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if spec.Name.Name == "_" { + continue + } + ts, err := gopTypeSymbol(pgf.Mapper, pgf.Tok, spec) + if err == nil { + symbols = append(symbols, ts) + } + case *ast.ValueSpec: + for _, name := range spec.Names { + if name.Name == "_" { + continue + } + vs, err := gopVarSymbol(pgf.Mapper, pgf.Tok, spec, name, decl.Tok == token.CONST) + if err == nil { + symbols = append(symbols, vs) + } + } + } + } + } + } + return symbols, nil +} + +func gopFuncSymbol(m *protocol.Mapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: decl.Name.Name, + Kind: protocol.Function, + } + if decl.Recv != nil { + s.Kind = protocol.Method + } + var err error + s.Range, err = m.NodeRange(tf, decl) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.NodeRange(tf, decl.Name) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.Detail = typesutil.ExprString(decl.Type) + return s, nil +} + +func gopTypeSymbol(m *protocol.Mapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: spec.Name.Name, + } + var err error + s.Range, err = m.NodeRange(tf, spec) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.NodeRange(tf, spec.Name) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.Kind, s.Detail, s.Children = gopTypeDetails(m, tf, spec.Type) + return s, nil +} + +func gopTypeDetails(m *protocol.Mapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { + switch typExpr := typExpr.(type) { + case *ast.StructType: + kind = protocol.Struct + children = gopFieldListSymbols(m, tf, typExpr.Fields, protocol.Field) + if len(children) > 0 { + detail = "struct{...}" + } else { + detail = "struct{}" + } + + // Find interface methods and embedded types. + case *ast.InterfaceType: + kind = protocol.Interface + children = gopFieldListSymbols(m, tf, typExpr.Methods, protocol.Method) + if len(children) > 0 { + detail = "interface{...}" + } else { + detail = "interface{}" + } + + case *ast.FuncType: + kind = protocol.Function + detail = typesutil.ExprString(typExpr) + + default: + kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically + detail = typesutil.ExprString(typExpr) + } + return +} + +func gopFieldListSymbols(m *protocol.Mapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { + if fields == nil { + return nil + } + + var symbols []protocol.DocumentSymbol + for _, field := range fields.List { + detail, children := "", []protocol.DocumentSymbol(nil) + if field.Type != nil { + _, detail, children = gopTypeDetails(m, tf, field.Type) + } + if len(field.Names) == 0 { // embedded interface or struct field + // By default, use the formatted type details as the name of this field. + // This handles potentially invalid syntax, as well as type embeddings in + // interfaces. + child := protocol.DocumentSymbol{ + Name: detail, + Kind: protocol.Field, // consider all embeddings to be fields + Children: children, + } + + // If the field is a valid embedding, promote the type name to field + // name. + selection := field.Type + if id := gopEmbeddedIdent(field.Type); id != nil { + child.Name = id.Name + child.Detail = detail + selection = id + } + + if rng, err := m.NodeRange(tf, field.Type); err == nil { + child.Range = rng + } + if rng, err := m.NodeRange(tf, selection); err == nil { + child.SelectionRange = rng + } + + symbols = append(symbols, child) + } else { + for _, name := range field.Names { + child := protocol.DocumentSymbol{ + Name: name.Name, + Kind: fieldKind, + Detail: detail, + Children: children, + } + + if rng, err := m.NodeRange(tf, field); err == nil { + child.Range = rng + } + if rng, err := m.NodeRange(tf, name); err == nil { + child.SelectionRange = rng + } + + symbols = append(symbols, child) + } + } + + } + return symbols +} + +func gopVarSymbol(m *protocol.Mapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: name.Name, + Kind: protocol.Variable, + } + if isConst { + s.Kind = protocol.Constant + } + var err error + s.Range, err = m.NodeRange(tf, spec) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.NodeRange(tf, name) + if err != nil { + return protocol.DocumentSymbol{}, err + } + if spec.Type != nil { // type may be missing from the syntax + _, s.Detail, s.Children = gopTypeDetails(m, tf, spec.Type) + } + return s, nil +} diff --git a/gopls/internal/lsp/source/util_gox.go b/gopls/internal/lsp/source/util_gox.go index 175232dc0f5..4e2977c641f 100644 --- a/gopls/internal/lsp/source/util_gox.go +++ b/gopls/internal/lsp/source/util_gox.go @@ -7,6 +7,8 @@ package source import ( "context" + "github.com/goplus/gop/ast" + "golang.org/x/tools/gopls/internal/goxls/typeparams" "golang.org/x/tools/gopls/internal/span" ) @@ -186,3 +188,29 @@ func UnquoteGopImportPath(s *ast.ImportSpec) ImportPath { func IsGopGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } + +// gopEmbeddedIdent returns the type name identifier for an embedding x, if x in a +// valid embedding. Otherwise, it returns nil. +// +// Spec: An embedded field must be specified as a type name T or as a pointer +// to a non-interface type name *T +func gopEmbeddedIdent(x ast.Expr) *ast.Ident { + if star, ok := x.(*ast.StarExpr); ok { + x = star.X + } + switch ix := x.(type) { // check for instantiated receivers + case *ast.IndexExpr: + x = ix.X + case *typeparams.IndexListExpr: + x = ix.X + } + switch x := x.(type) { + case *ast.Ident: + return x + case *ast.SelectorExpr: + if _, ok := x.X.(*ast.Ident); ok { + return x.Sel + } + } + return nil +} diff --git a/gopls/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go index b31d980484c..1ce7c5fdff6 100644 --- a/gopls/internal/lsp/symbols.go +++ b/gopls/internal/lsp/symbols.go @@ -29,6 +29,8 @@ func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSy docSymbols, err = template.DocumentSymbols(snapshot, fh) case source.Go: docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh) + case source.Gop: + docSymbols, err = source.GopDocumentSymbols(ctx, snapshot, fh) default: return []interface{}{}, nil } diff --git a/gopls/internal/lsp/symbols_gox.go b/gopls/internal/lsp/symbols_gox.go new file mode 100644 index 00000000000..e598d3b34fc --- /dev/null +++ b/gopls/internal/lsp/symbols_gox.go @@ -0,0 +1,5 @@ +// Copyright 2023 The GoPlus Authors (goplus.org). All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp