From 3ef2d153d09e603f977ef92a96c9c7652c50a1ab Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Fri, 14 Apr 2023 23:24:49 +0200 Subject: [PATCH 01/12] fix: add isMethodAllowed checks --- internal/server/completion.go | 4 ++++ internal/server/definition.go | 4 ++++ internal/server/formatting.go | 4 ++++ internal/server/hover.go | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/internal/server/completion.go b/internal/server/completion.go index 314e06f..af2cb75 100644 --- a/internal/server/completion.go +++ b/internal/server/completion.go @@ -29,6 +29,10 @@ func (s *Server) Completion( ctx context.Context, params *protocol.CompletionParams, ) (*protocol.CompletionList, error) { + if err := s.isMethodAllowed("Completion"); err != nil { + return nil, err + } + start := time.Now() defer func() { log.Printf("Retrieving completion took %s\n", time.Since(start)) }() diff --git a/internal/server/definition.go b/internal/server/definition.go index e9e08ab..2a04ece 100644 --- a/internal/server/definition.go +++ b/internal/server/definition.go @@ -17,6 +17,10 @@ func (s *Server) Definition( ctx context.Context, params *protocol.DefinitionParams, ) ([]protocol.Location, error) { + if err := s.isMethodAllowed("Definition"); err != nil { + return nil, err + } + start := time.Now() defer func() { log.Printf("Retrieving definition took %s\n", time.Since(start)) }() diff --git a/internal/server/formatting.go b/internal/server/formatting.go index 7b20062..72d2ea2 100644 --- a/internal/server/formatting.go +++ b/internal/server/formatting.go @@ -14,6 +14,10 @@ func (s *Server) Formatting( ctx context.Context, params *protocol.DocumentFormattingParams, ) ([]protocol.TextEdit, error) { + if err := s.isMethodAllowed("Formatting"); err != nil { + return nil, err + } + if !s.phpcbf.HasExecutable() { return nil, nil } diff --git a/internal/server/hover.go b/internal/server/hover.go index 5e50268..4552113 100644 --- a/internal/server/hover.go +++ b/internal/server/hover.go @@ -10,6 +10,10 @@ import ( ) func (s *Server) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { + if err := s.isMethodAllowed("Hover"); err != nil { + return nil, err + } + start := time.Now() defer func() { log.Printf("Retrieving hover took %s\n", time.Since(start)) From a9903265dd4867ab989fabaee0e5b7fe6271c6da Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Sat, 15 Apr 2023 03:08:19 +0200 Subject: [PATCH 02/12] feat: start on references --- internal/fqner/fqner.go | 12 +- internal/project/definition.go | 5 +- internal/project/hover.go | 3 +- internal/server/definition.go | 7 +- internal/server/lifecycle.go | 3 + internal/server/references.go | 211 +++++++++++++++++++++++++++++++ internal/server/unimplemented.go | 7 - pkg/nodeident/nodeident.go | 1 - pkg/parsing/parsing.go | 1 + pkg/position/position.go | 16 +++ 10 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 internal/server/references.go diff --git a/internal/fqner/fqner.go b/internal/fqner/fqner.go index 3aca32b..271c74f 100644 --- a/internal/fqner/fqner.go +++ b/internal/fqner/fqner.go @@ -10,19 +10,13 @@ import ( "github.com/laytan/phpls/pkg/fqn" "github.com/laytan/phpls/pkg/functional" "github.com/laytan/phpls/pkg/nodeident" - "github.com/laytan/phpls/pkg/nodescopes" "github.com/laytan/phpls/pkg/position" ) func FullyQualifyName(root *ast.Root, name ast.Vertex) *fqn.FQN { - if !nodescopes.IsName(name.GetType()) { - panic( - "FullyQualifyName can only be called with *ast.Name, *ast.NameFullyQualified or *ast.NameRelative", - ) - } - - if strings.HasPrefix(nodeident.Get(name), `\`) { - return fqn.New(nodeident.Get(name)) + ident := nodeident.Get(name) + if strings.HasPrefix(ident, `\`) { + return fqn.New(ident) } t := fqn.NewTraverser() diff --git a/internal/project/definition.go b/internal/project/definition.go index f412bb1..bf45cbf 100644 --- a/internal/project/definition.go +++ b/internal/project/definition.go @@ -11,7 +11,6 @@ import ( "github.com/laytan/phpls/internal/project/definition" "github.com/laytan/phpls/internal/project/definition/providers" "github.com/laytan/phpls/internal/wrkspc" - "github.com/laytan/phpls/pkg/functional" "github.com/laytan/phpls/pkg/position" ) @@ -40,7 +39,7 @@ type DefinitionProvider interface { Define(ctx *context.Ctx) ([]*definition.Definition, error) } -func (p *Project) Definition(pos *position.Position) ([]*position.Position, error) { +func (p *Project) Definition(pos *position.Position) ([]*definition.Definition, error) { ctx, err := context.New(pos) if err != nil { return nil, fmt.Errorf("Could not create definition context: %w", err) @@ -59,7 +58,7 @@ func (p *Project) Definition(pos *position.Position) ([]*position.Position, erro return nil, ErrNoDefinitionFound } - return functional.MapFilter(defs, defPosition), nil + return defs, nil } } } diff --git a/internal/project/hover.go b/internal/project/hover.go index 3f00a35..6f36d35 100644 --- a/internal/project/hover.go +++ b/internal/project/hover.go @@ -114,7 +114,8 @@ func nodeToHover(p *Project, currpos *position.Position) ([]ast.Vertex, *ast.Roo } pos := poss[0] - return napper(pos) + content := wrkspc.Current.FContentOf(pos.Path) + return napper(position.FromIRPosition(pos.Path, content, pos.Position.StartPos)) } func NodeSignature(node ast.Vertex) string { diff --git a/internal/server/definition.go b/internal/server/definition.go index 2a04ece..16ae712 100644 --- a/internal/server/definition.go +++ b/internal/server/definition.go @@ -8,6 +8,8 @@ import ( "github.com/laytan/go-lsp-protocol/pkg/lsp/protocol" "github.com/laytan/phpls/internal/project" + "github.com/laytan/phpls/internal/project/definition" + "github.com/laytan/phpls/internal/wrkspc" "github.com/laytan/phpls/pkg/functional" "github.com/laytan/phpls/pkg/lsperrors" "github.com/laytan/phpls/pkg/position" @@ -36,7 +38,8 @@ func (s *Server) Definition( return nil, lsperrors.ErrRequestFailed(err.Error()) } - return functional.Map(definitions, func(def *position.Position) protocol.Location { - return def.ToLSPLocation() + return functional.Map(definitions, func(def *definition.Definition) protocol.Location { + content := wrkspc.Current.FContentOf(def.Path) + return position.FromIRPosition(def.Path, content, def.Position.StartPos).ToLSPLocation() }), nil } diff --git a/internal/server/lifecycle.go b/internal/server/lifecycle.go index 1ca81e3..c22b1f7 100644 --- a/internal/server/lifecycle.go +++ b/internal/server/lifecycle.go @@ -105,6 +105,9 @@ func (s *Server) Initialize( DocumentFormattingProvider: &protocol.Or_ServerCapabilities_documentFormattingProvider{ Value: true, }, + ReferencesProvider: &protocol.Or_ServerCapabilities_referencesProvider{ + Value: true, + }, }, ServerInfo: &protocol.PServerInfoMsg_initialize{ Name: config.Name, diff --git a/internal/server/references.go b/internal/server/references.go new file mode 100644 index 0000000..5ceeab0 --- /dev/null +++ b/internal/server/references.go @@ -0,0 +1,211 @@ +package server + +import ( + "context" + "fmt" + "log" + "sync" + "sync/atomic" + "time" + + "github.com/laytan/php-parser/pkg/ast" + "github.com/laytan/php-parser/pkg/visitor" + "github.com/laytan/php-parser/pkg/visitor/traverser" + pcontext "github.com/laytan/phpls/internal/context" + "github.com/laytan/phpls/internal/fqner" + "github.com/laytan/phpls/internal/wrkspc" + + "github.com/laytan/go-lsp-protocol/pkg/lsp/protocol" + "github.com/laytan/phpls/pkg/fqn" + "github.com/laytan/phpls/pkg/lsperrors" + "github.com/laytan/phpls/pkg/nodeident" + "github.com/laytan/phpls/pkg/parsing" + "github.com/laytan/phpls/pkg/phpversion" + "github.com/laytan/phpls/pkg/position" +) + +// TODO: add config to skip vendor directory. +func (s *Server) References( + ctx context.Context, + params *protocol.ReferenceParams, +) ([]protocol.Location, error) { + if err := s.isMethodAllowed("Initialize"); err != nil { + return nil, err + } + + start := time.Now() + defer func() { + log.Printf("Retrieving references took %s", time.Since(start)) + }() + + target := position.FromTextDocumentPositionParams(¶ms.Position, ¶ms.TextDocument) + definitions, err := s.project.Definition(target) + if err != nil { + log.Println(err) + return nil, lsperrors.ErrRequestFailed(err.Error()) + } + + if len(definitions) > 1 { + msg := "Multiple definitions found in references call, not supported" + log.Println(msg) + return nil, lsperrors.ErrRequestFailed(msg) + } + + d := definitions[0] + content := wrkspc.Current.FContentOf(d.Path) + dpos := position.FromIRPosition(d.Path, content, d.Position.StartPos) + + pctx, err := pcontext.New(dpos) + if err != nil { + log.Printf("Unable to recreate definition context: %v", err) + return nil, lsperrors.ErrRequestFailed(err.Error()) + } + // Advance to the node that matches the definition. + for ok := true; ok; ok = pctx.Advance() { + if nodeident.Get(pctx.Current()) == d.Identifier { + break + } + } + + switch pctx.Current().(type) { + case *ast.StmtClass: + hasErrors := false + + files := make(chan *wrkspc.ParsedFile) + references := make(chan protocol.Location) + + tvpool := sync.Pool{ + New: func() any { + return &classReferenceVisitor{references: references} + }, + } + + // TODO: parser should be like configured. + parser := parsing.New(phpversion.Latest()) + name := fqner.FullyQualifyName(pctx.Root(), pctx.Current()) + + log.Printf("[DEBUG]: finding references of %q", name) + + go func() { + total := &atomic.Uint64{} + totalDone := make(chan bool, 1) + if err := wrkspc.Current.Index(files, total, totalDone); err != nil { + log.Println( + fmt.Errorf( + "Could not index the file content of root %s: %w", + wrkspc.Current.Root(), + err, + ), + ) + hasErrors = true + } + }() + + go func() { + defer close(references) + wg := sync.WaitGroup{} + defer wg.Wait() + for file := range files { + file := file + wg.Add(1) + go func() { + defer func() { + wg.Done() + if r := recover(); r != nil { + log.Printf("ERROR: could not parse %q into an AST: %v", file.Path, r) + } + }() + + root, err := parser.Parse([]byte(file.Content)) + if err != nil { + log.Printf("ERROR: parsing %q: %v", file.Path, err) + hasErrors = true + return + } + + // TODO: sync.pool + v := tvpool.Get().(*classReferenceVisitor) + defer tvpool.Put(v) + v.Reset(file.Path, file.Content, name) + tv := traverser.NewTraverser(v) + root.Accept(tv) + }() + } + }() + + var accReferences []protocol.Location + for reference := range references { + log.Printf("[DEBUG]: found reference: %v", reference) + accReferences = append(accReferences, reference) + } + + if hasErrors { + log.Println( + "Parsing the project for references resulted in errors, check the logs for more details", + ) + } + + log.Printf("[DEBUG]: References: %v", accReferences) + + return accReferences, nil + default: + // TODO: others. + msg := fmt.Sprintf("Unsupported node type %T for references", pctx.Current()) + log.Println(msg) + return nil, lsperrors.ErrRequestFailed(msg) + } +} + +type classReferenceVisitor struct { + visitor.Null + references chan protocol.Location + names []ast.Vertex + fqnv *fqn.Traverser + fqn *fqn.FQN + path string + content string +} + +func (c *classReferenceVisitor) Reset(path string, content string, name *fqn.FQN) { + if len(c.names) > 0 { + c.names = c.names[:0] + } + + c.fqn = name + c.path = path + c.content = content + c.fqnv = fqn.NewTraverser() +} + +func (c *classReferenceVisitor) EnterNode(node ast.Vertex) bool { + c.fqnv.EnterNode(node) + return true +} + +func (c *classReferenceVisitor) StmtClass(node *ast.StmtClass) { + c.names = append(c.names, node.Name) +} + +func (c *classReferenceVisitor) NameName(node *ast.Name) { + c.names = append(c.names, node) +} + +func (c *classReferenceVisitor) NameFullyQualified(node *ast.NameFullyQualified) { + c.names = append(c.names, node) +} + +func (c *classReferenceVisitor) NameRelative(node *ast.NameRelative) { + c.names = append(c.names, node) +} + +func (c *classReferenceVisitor) LeaveNode(node ast.Vertex) { + if node.GetType() != ast.TypeRoot { + return + } + + for _, name := range c.names { + if c.fqnv.ResultFor(name).String() == c.fqn.String() { + c.references <- position.AstToLspLocation(c.path, name.GetPosition()) + } + } +} diff --git a/internal/server/unimplemented.go b/internal/server/unimplemented.go index 2fabaae..51d262e 100644 --- a/internal/server/unimplemented.go +++ b/internal/server/unimplemented.go @@ -281,13 +281,6 @@ func (s *Server) SignatureHelp( return nil, errorUnimplemented } -func (s *Server) References( - context.Context, - *protocol.ReferenceParams, -) ([]protocol.Location, error) { - return nil, errorUnimplemented -} - func (s *Server) DocumentHighlight( context.Context, *protocol.DocumentHighlightParams, diff --git a/pkg/nodeident/nodeident.go b/pkg/nodeident/nodeident.go index 2480604..dc8ee33 100644 --- a/pkg/nodeident/nodeident.go +++ b/pkg/nodeident/nodeident.go @@ -15,7 +15,6 @@ import ( // TODO: return bytes. func Get(n ast.Vertex) string { if n == nil { - log.Println("Warning: nodeident.Get with nil node") return "" } diff --git a/pkg/parsing/parsing.go b/pkg/parsing/parsing.go index 9557b86..da896ff 100644 --- a/pkg/parsing/parsing.go +++ b/pkg/parsing/parsing.go @@ -59,6 +59,7 @@ func New(phpv *phpversion.PHPVersion) Parser { } } +// TODO: recover panics func (p *parser) Parse(content []byte) (*ast.Root, error) { a, err := astParser.Parse(content, p.config) if err != nil || a == nil { diff --git a/pkg/position/position.go b/pkg/position/position.go index fc8fc45..3b8210f 100644 --- a/pkg/position/position.go +++ b/pkg/position/position.go @@ -131,3 +131,19 @@ func LocToPos(content string, row uint, col uint) uint { return 0 } + +func AstToLspLocation(path string, p *position.Position) protocol.Location { + return protocol.Location{ + URI: protocol.DocumentURI("file://" + path), + Range: protocol.Range{ + Start: protocol.Position{ + Line: uint32(p.StartLine) - 1, + Character: uint32(p.StartCol), + }, + End: protocol.Position{ + Line: uint32(p.EndLine) - 1, + Character: uint32(p.EndCol), + }, + }, + } +} From 6ab88f50d7d8d5e4a927f33b26925ab76a14ea08 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 00:14:13 +0200 Subject: [PATCH 03/12] fix: project tests fixes by updating php-parser and adapting to no longer returning positions --- go.mod | 2 +- go.sum | 2 ++ internal/project/project_test.go | 19 +++++++++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b0ddc4d..e1e8215 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.2 github.com/laytan/go-lsp-protocol v0.0.0-20230331135813-fccb4f5d33c5 - github.com/laytan/php-parser v0.9.1-0.20230411203829-6b3673ece43a + github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838 github.com/stretchr/testify v1.8.2 github.com/xeipuuv/gojsonschema v1.2.0 go.uber.org/goleak v1.2.1 diff --git a/go.sum b/go.sum index be32eac..8078b5c 100644 --- a/go.sum +++ b/go.sum @@ -291,6 +291,8 @@ github.com/laytan/go-lsp-protocol v0.0.0-20230331135813-fccb4f5d33c5 h1:NtTaaRSn github.com/laytan/go-lsp-protocol v0.0.0-20230331135813-fccb4f5d33c5/go.mod h1:D1njoCxBWyG+vZ1r9Xy11U02X5XB5R8PTKRj3kcer7I= github.com/laytan/php-parser v0.9.1-0.20230411203829-6b3673ece43a h1:V9fMfdfEYUsauFstzQmdS5Evu9I6N030IC4epJNLGVY= github.com/laytan/php-parser v0.9.1-0.20230411203829-6b3673ece43a/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= +github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838 h1:IOkVuzF0K25reG7JcRuKF3uKlodtM4C9RzGAOhsb550= +github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 20e197d..6d74610 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -9,8 +9,10 @@ import ( "github.com/laytan/phpls/internal/config" "github.com/laytan/phpls/internal/index" "github.com/laytan/phpls/internal/project" + "github.com/laytan/phpls/internal/project/definition" "github.com/laytan/phpls/internal/wrkspc" "github.com/laytan/phpls/pkg/annotated" + "github.com/laytan/phpls/pkg/functional" "github.com/laytan/phpls/pkg/pathutils" "github.com/laytan/phpls/pkg/phpversion" "github.com/laytan/phpls/pkg/position" @@ -174,7 +176,8 @@ func TestStdlibDefinitions(t *testing.T) { require.NoError(t, err) require.Len(t, out, 1) - if !reflect.DeepEqual(out[0], scenario.out) { + actPos := position.FromAst(out[0].Path, out[0].Position) + if !reflect.DeepEqual(actPos, scenario.out) { what.Is(out) what.Is(scenario.out) t.Errorf("definitions don't match, run with `-tags what` to debug") @@ -194,7 +197,7 @@ func TestParserPanicIsRecovered(t *testing.T) { ) err := project.ParseWithoutProgress() - require.NoError(t, err) + require.Error(t, err) } //nolint:paralleltest,tparallel // Causes data race (indexing while testing?) @@ -223,7 +226,7 @@ func TestAnnotatedDefinitions(t *testing.T) { } if scenario.IsDump { - root, err := wrkspc.Current.IROf(scenario.In.Path) + root, err := wrkspc.Current.Ast(scenario.In.Path) require.NoError(t, err) what.Is(root) return @@ -234,14 +237,22 @@ func TestAnnotatedDefinitions(t *testing.T) { } out, err := proj.Definition(&scenario.In) + what.Is(out) if scenario.IsNoDef { require.ErrorIs(t, err, project.ErrNoDefinitionFound) return } + outPosses := functional.Map( + out, + func(d *definition.Definition) *position.Position { + return position.FromAst(d.Path, d.Position) + }, + ) + require.NoError(t, err) - require.Equal(t, out, scenario.Out) + require.Equal(t, outPosses, scenario.Out) }) } }) From ad5d618145faed189f86741428fb3a8c95210c66 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 00:18:06 +0200 Subject: [PATCH 04/12] feat: polish references, add renaming of references, huge wrkspc and parsing refactor --- internal/context/context.go | 4 +- internal/diagnostics/phpcs.go | 2 +- internal/expr/starters.go | 12 +- internal/fqner/fqner.go | 2 +- internal/index/index.go | 12 +- internal/project/completion.go | 21 +- internal/project/definition.go | 6 - internal/project/definition/providers/name.go | 7 + internal/project/hover.go | 4 +- internal/project/parsing.go | 15 +- internal/server/completion.go | 2 +- internal/server/completion_test.go | 52 +-- internal/server/definition.go | 2 +- internal/server/lifecycle.go | 23 +- internal/server/references.go | 103 ++++-- internal/server/rename.go | 70 ++++ internal/server/sync.go | 10 +- internal/server/unimplemented.go | 4 - internal/symbol/class.go | 2 +- internal/symbol/symbol_test.go | 2 +- internal/throws/throws.go | 2 +- internal/throws/throws_test.go | 4 +- internal/wrkspc/rooter.go | 2 +- internal/wrkspc/wrkspc.go | 330 +++++++++--------- internal/wrkspc/wrkspc_test.go | 3 +- pkg/parsing/parsing.go | 55 ++- pkg/phpcs/phpcbf/phpcbf.go | 2 +- pkg/position/position.go | 8 + 28 files changed, 415 insertions(+), 346 deletions(-) create mode 100644 internal/server/rename.go diff --git a/internal/context/context.go b/internal/context/context.go index 1a9c097..9ea69b5 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -73,7 +73,7 @@ func (c *Ctx) InComment() (*token.Token, CommentPosition) { return nil, 0 } - content := wrkspc.Current.FContentOf(c.start.Path) + content := wrkspc.Current.ContentF(c.start.Path) cursorPos := position.LocToPos(content, c.start.Row, c.start.Col) return c.comment, CommentPosition(int(cursorPos) - c.comment.Position.StartPos) } @@ -150,7 +150,7 @@ func (c *Ctx) setScopes() { } func (c *Ctx) init() error { - content, root, err := wrkspc.Current.AllOf(c.start.Path) + content, root, err := wrkspc.Current.All(c.start.Path) if err != nil { return fmt.Errorf( "Unable to parse context of %s: %w", diff --git a/internal/diagnostics/phpcs.go b/internal/diagnostics/phpcs.go index 3cddf4b..7176e9c 100644 --- a/internal/diagnostics/phpcs.go +++ b/internal/diagnostics/phpcs.go @@ -49,7 +49,7 @@ func (p *PhpcsAnalyzer) AnalyzeSave( ctx context.Context, path string, ) ([]protocol.Diagnostic, error) { - return p.Analyze(ctx, path, []byte(wrkspc.Current.FContentOf(path))) + return p.Analyze(ctx, path, []byte(wrkspc.Current.ContentF(path))) } func (p *PhpcsAnalyzer) Name() string { diff --git a/internal/expr/starters.go b/internal/expr/starters.go index e045119..cafce59 100644 --- a/internal/expr/starters.go +++ b/internal/expr/starters.go @@ -58,7 +58,7 @@ func (p *variableResolver) Up( Position: scopes.Block.GetPosition(), Parts: nameParts(nodeident.Get(scopes.Class)), }); ok { - return &Resolved{Path: node.Path, Node: node.ToIRNode(wrk.FIROf(node.Path))}, + return &Resolved{Path: node.Path, Node: node.ToIRNode(wrk.AstF(node.Path))}, node.FQN, phprivacy.PrivacyPrivate, true @@ -69,7 +69,7 @@ func (p *variableResolver) Up( case "parent": node := parentOf(scopes) - return &Resolved{Path: node.Path, Node: node.ToIRNode(wrk.FIROf(node.Path))}, + return &Resolved{Path: node.Path, Node: node.ToIRNode(wrk.AstF(node.Path))}, node.FQN, phprivacy.PrivacyProtected, true @@ -200,7 +200,7 @@ func (p *nameResolver) Up( return nil, nil, 0, false } - return &Resolved{Path: res.Path, Node: res.ToIRNode(wrkspc.Current.FIROf(res.Path))}, + return &Resolved{Path: res.Path, Node: res.ToIRNode(wrkspc.Current.AstF(res.Path))}, qualified, privacy, true @@ -313,7 +313,7 @@ func (p *functionResolver) Up( Position: toResolve.Position, Parts: nameParts(toResolve.Identifier), }); ok { - n := def.ToIRNode(wrkspc.Current.FIROf(def.Path)) + n := def.ToIRNode(wrkspc.Current.AstF(def.Path)) return &Resolved{ Node: n, Path: def.Path, @@ -328,7 +328,7 @@ func (p *functionResolver) Up( return nil, nil, 0, false } - n := def.ToIRNode(wrkspc.Current.FIROf(def.Path)) + n := def.ToIRNode(wrkspc.Current.AstF(def.Path)) return &Resolved{ Node: n, Path: def.Path, @@ -378,7 +378,7 @@ func (newresolver *newResolver) Up( return &Resolved{ Path: def.Path, - Node: def.ToIRNode(wrkspc.Current.FIROf(def.Path)), + Node: def.ToIRNode(wrkspc.Current.AstF(def.Path)), }, qualified, phprivacy.PrivacyPublic, diff --git a/internal/fqner/fqner.go b/internal/fqner/fqner.go index 271c74f..0cce842 100644 --- a/internal/fqner/fqner.go +++ b/internal/fqner/fqner.go @@ -33,7 +33,7 @@ func FindFullyQualifiedName(root *ast.Root, name ast.Vertex) (*index.INode, bool // Returns whether the file at given pos needs a use statement for the given fqn. func NeedsUseStmtFor(pos *position.Position, name *fqn.FQN) bool { - content, root := wrkspc.Current.FAllOf(pos.Path) + content, root := wrkspc.Current.AllF(pos.Path) parts := strings.Split(name.String(), `\`) className := parts[len(parts)-1] diff --git a/internal/index/index.go b/internal/index/index.go index df130e9..58659c2 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -52,8 +52,8 @@ type Index interface { } type index struct { - normalParser parsing.Parser - stubParser parsing.Parser + normalParser *parsing.Parser + stubParser *parsing.Parser symbolTrie *symboltrie.Trie[*INode] symbolTraversers *sync.Pool @@ -81,12 +81,6 @@ func New(phpv *phpversion.PHPVersion) Index { } func (i *index) Index(path string, content string) error { - defer func() { - if r := recover(); r != nil { - log.Printf("Could not parse %s into an AST: %v", path, r) - } - }() - root, err := i.parser(path).Parse([]byte(content)) if err != nil { return fmt.Errorf(errParseFmt, path, err) @@ -205,7 +199,7 @@ func (i *index) Delete(path string) error { var stubsDir = filepath.Join(pathutils.Root(), "third_party", "phpstorm-stubs") -func (i *index) parser(path string) parsing.Parser { +func (i *index) parser(path string) *parsing.Parser { if strings.HasPrefix(path, config.Current.StubsPath) || strings.HasPrefix(path, stubsDir) { return i.stubParser } diff --git a/internal/project/completion.go b/internal/project/completion.go index 7bebef7..dfa72ab 100644 --- a/internal/project/completion.go +++ b/internal/project/completion.go @@ -84,7 +84,12 @@ func GetCompletionQuery(pos *position.Position) CompletionContext { // what.Is(v.Nodes) ctx := CompletionContext{} - lexer := wrkspc.Current.FLexerOf(pos.Path) + lexer, err := wrkspc.Current.Lexer(pos.Path) + if err != nil { + log.Printf("[ERROR]: creating lexer for completion context: %v", err) + return ctx + } + for tok := lexer.Lex(); tok != nil && tok.ID != 0; tok = lexer.Lex() { if tok.Position.EndLine > int(pos.Row) { break @@ -308,7 +313,7 @@ func Complete(pos *position.Position, comp CompletionContext) (list protocol.Com }) } case NameRelative: - root := wrkspc.Current.FIROf(pos.Path) + root := wrkspc.Current.AstF(pos.Path) v := traversers.NewNamespace(int(pos.Row)) tv := traverser.NewTraverser(v) root.Accept(tv) @@ -370,7 +375,7 @@ func Complete(pos *position.Position, comp CompletionContext) (list protocol.Com return nil } - if !wrkspc.Current.IsPhpFile(path) { + if !wrkspc.Current.IsPhp(path) { return nil } @@ -609,11 +614,11 @@ func PredictNamespace(pos *position.Position) string { continue } - if !wrkspc.Current.IsPhpFile(fn) { + if !wrkspc.Current.IsPhp(fn) { continue } - root, err := wrkspc.Current.IROf(fn) + root, err := wrkspc.Current.Ast(fn) if err != nil { log.Println(err) continue @@ -667,6 +672,10 @@ func AddScopeVars( ) { // TODO: if scope is arrow function, use the scope above that one (vars are inherited in arrow funcs). scope := ctx.Scope() + if scope == nil { + return + } + scopet := scope.GetType() if scopet != ast.TypeStmtFunction && scopet != ast.TypeStmtClassMethod && scopet != ast.TypeExprClosure && scopet != ast.TypeExprArrowFunction { @@ -700,7 +709,7 @@ func AddScopeVars( // Gets the current word ([a-zA-Z0-9]*) that the position is at. func (p *Project) getCompletionQuery(pos *position.Position) string { - content := wrkspc.Current.FContentOf(pos.Path) + content := wrkspc.Current.ContentF(pos.Path) scanner := bufio.NewScanner(strings.NewReader(content)) for i := 0; scanner.Scan(); i++ { // The target line: diff --git a/internal/project/definition.go b/internal/project/definition.go index bf45cbf..994b868 100644 --- a/internal/project/definition.go +++ b/internal/project/definition.go @@ -10,7 +10,6 @@ import ( "github.com/laytan/phpls/internal/context" "github.com/laytan/phpls/internal/project/definition" "github.com/laytan/phpls/internal/project/definition/providers" - "github.com/laytan/phpls/internal/wrkspc" "github.com/laytan/phpls/pkg/position" ) @@ -66,8 +65,3 @@ func (p *Project) Definition(pos *position.Position) ([]*definition.Definition, log.Println("no definition provider registered for the given position") return nil, ErrNoDefinitionFound } - -func defPosition(def *definition.Definition) *position.Position { - content := wrkspc.Current.FContentOf(def.Path) - return position.FromIRPosition(def.Path, content, def.Position.StartPos) -} diff --git a/internal/project/definition/providers/name.go b/internal/project/definition/providers/name.go index 6025417..1a986e9 100644 --- a/internal/project/definition/providers/name.go +++ b/internal/project/definition/providers/name.go @@ -17,6 +17,13 @@ func NewName() *NameProvider { } func (p *NameProvider) CanDefine(ctx *context.Ctx, kind ast.Type) bool { + // The class definition is an identifier, so if we get that & wrapped by a class, we can define it. + if kind == ast.TypeIdentifier { + return ctx.DirectlyWrappedBy(ast.TypeStmtClass) || + ctx.DirectlyWrappedBy(ast.TypeStmtInterface) || + ctx.DirectlyWrappedBy(ast.TypeStmtTrait) + } + if !nodescopes.IsName(kind) { return false } diff --git a/internal/project/hover.go b/internal/project/hover.go index 6f36d35..bdb6093 100644 --- a/internal/project/hover.go +++ b/internal/project/hover.go @@ -99,7 +99,7 @@ Nodes: func nodeToHover(p *Project, currpos *position.Position) ([]ast.Vertex, *ast.Root) { napper := func(pos *position.Position) ([]ast.Vertex, *ast.Root) { - content, root := wrkspc.Current.FAllOf(pos.Path) + content, root := wrkspc.Current.AllF(pos.Path) apos := position.LocToPos(content, pos.Row, pos.Col) nap := traversers.NewNodeAtPos(int(apos)) napt := traverser.NewTraverser(nap) @@ -114,7 +114,7 @@ func nodeToHover(p *Project, currpos *position.Position) ([]ast.Vertex, *ast.Roo } pos := poss[0] - content := wrkspc.Current.FContentOf(pos.Path) + content := wrkspc.Current.ContentF(pos.Path) return napper(position.FromIRPosition(pos.Path, content, pos.Position.StartPos)) } diff --git a/internal/project/parsing.go b/internal/project/parsing.go index 2083b51..6ab84e8 100644 --- a/internal/project/parsing.go +++ b/internal/project/parsing.go @@ -13,7 +13,7 @@ import ( // This should only be called once at the beginning of the connection with a // client. -func (p *Project) Parse(done *atomic.Uint64, total *atomic.Uint64, totalDone chan<- bool) error { +func (p *Project) Parse(done *atomic.Uint64, total *atomic.Uint64) error { // Parsing creates alot of garbage, after parsing, run a gc cycle manually // because we know there is a lot to clean up. defer func() { @@ -50,7 +50,7 @@ func (p *Project) Parse(done *atomic.Uint64, total *atomic.Uint64, totalDone cha }() w := wrkspc.Current - if err := w.Index(files, total, totalDone); err != nil { + if err := w.Walk(files, total, wrkspc.WalkAll); err != nil { log.Println( fmt.Errorf( "Could not index the file content of root %s: %w", @@ -76,19 +76,10 @@ func (p *Project) Parse(done *atomic.Uint64, total *atomic.Uint64, totalDone cha func (p *Project) ParseWithoutProgress() error { done := &atomic.Uint64{} total := &atomic.Uint64{} - totalDone := make(chan bool, 1) - - return p.Parse(done, total, totalDone) + return p.Parse(done, total) } func (p *Project) ParseFileUpdate(path string, content string) error { - w := wrkspc.Current - - // NOTE: order is important here. - if err := w.RefreshFrom(path, content); err != nil { - return fmt.Errorf("Could not refresh indexed content of %s: %w", path, err) - } - if err := index.Current.Refresh(path, content); err != nil { return fmt.Errorf("Could not refresh indexed symbols of %s: %w", path, err) } diff --git a/internal/server/completion.go b/internal/server/completion.go index af2cb75..7e0bcd1 100644 --- a/internal/server/completion.go +++ b/internal/server/completion.go @@ -103,7 +103,7 @@ func InsertUseStmt( return nil } - root := wrkspc.Current.FIROf(currPos.Path) + root := wrkspc.Current.AstF(currPos.Path) v := &useInserterVisitor{EndLine: int(currPos.Row)} tv := traverser.NewTraverser(v) root.Accept(tv) diff --git a/internal/server/completion_test.go b/internal/server/completion_test.go index 278c94f..f35f853 100644 --- a/internal/server/completion_test.go +++ b/internal/server/completion_test.go @@ -485,62 +485,64 @@ type mockWrkspc struct { t *testing.T } -var _ wrkspc.Wrkspc = &mockWrkspc{} +func (*mockWrkspc) All(path string) (content string, root *ast.Root, err error) { + panic("unimplemented") +} -func (*mockWrkspc) FLexerOf(path string) lexer.Lexer { +func (*mockWrkspc) Ast(path string) (*ast.Root, error) { panic("unimplemented") } -func (*mockWrkspc) IsPhpFile(path string) bool { +func (*mockWrkspc) Content(path string) (string, error) { panic("unimplemented") } -func (mockWrkspc) AllOf(path string) (string, *ast.Root, error) { +func (*mockWrkspc) ContentF(path string) string { panic("unimplemented") } -func (mockWrkspc) ContentOf(path string) (string, error) { +func (*mockWrkspc) DeleteOverlay(path string) { panic("unimplemented") } -func (w mockWrkspc) FAllOf(path string) (string, *ast.Root) { - if w.path != path { - w.t.Errorf("expected wrkspc.FAllOf to be called with %s, but called with %s", w.path, path) - } +func (*mockWrkspc) IsPhp(path string) bool { + panic("unimplemented") +} - return w.content, w.root +func (*mockWrkspc) Lexer(path string) (lexer.Lexer, error) { + panic("unimplemented") } -func (mockWrkspc) FContentOf(path string) string { +func (*mockWrkspc) Parse(path string, content []byte) (*ast.Root, error) { panic("unimplemented") } -func (w mockWrkspc) FIROf(path string) *ast.Root { - return w.root +func (*mockWrkspc) PutOverlay(path string, content string) { + panic("unimplemented") } -func (mockWrkspc) IROf(path string) (*ast.Root, error) { +func (*mockWrkspc) Root() string { panic("unimplemented") } -func (mockWrkspc) Index( +func (*mockWrkspc) Walk( files chan<- *wrkspc.ParsedFile, total *atomic.Uint64, - totalDone chan<- bool, + opts wrkspc.WalkOptions, ) error { panic("unimplemented") } -func (mockWrkspc) Refresh(path string) error { - panic("unimplemented") -} +var _ wrkspc.Wrkspc = &mockWrkspc{} -func (mockWrkspc) RefreshFrom(path string, content string) error { - panic("unimplemented") -} +func (w mockWrkspc) AllF(path string) (string, *ast.Root) { + if w.path != path { + w.t.Errorf("expected wrkspc.FAllOf to be called with %s, but called with %s", w.path, path) + } -func (mockWrkspc) Root() string { - panic("unimplemented") + return w.content, w.root } -var _ wrkspc.Wrkspc = &mockWrkspc{} +func (w mockWrkspc) AstF(path string) *ast.Root { + return w.root +} diff --git a/internal/server/definition.go b/internal/server/definition.go index 16ae712..0da001e 100644 --- a/internal/server/definition.go +++ b/internal/server/definition.go @@ -39,7 +39,7 @@ func (s *Server) Definition( } return functional.Map(definitions, func(def *definition.Definition) protocol.Location { - content := wrkspc.Current.FContentOf(def.Path) + content := wrkspc.Current.ContentF(def.Path) return position.FromIRPosition(def.Path, content, def.Position.StartPos).ToLSPLocation() }), nil } diff --git a/internal/server/lifecycle.go b/internal/server/lifecycle.go index c22b1f7..cbbdcdb 100644 --- a/internal/server/lifecycle.go +++ b/internal/server/lifecycle.go @@ -108,6 +108,7 @@ func (s *Server) Initialize( ReferencesProvider: &protocol.Or_ServerCapabilities_referencesProvider{ Value: true, }, + RenameProvider: true, }, ServerInfo: &protocol.PServerInfoMsg_initialize{ Name: config.Name, @@ -161,35 +162,19 @@ func (s *Server) index() { done := &atomic.Uint64{} total := &atomic.Uint64{} - var finalTotal float64 - - totalDoneChan := make(chan bool) - go func() { - <-totalDoneChan - finalTotal = float64(total.Load()) - }() - - getTotal := func() float64 { - if finalTotal != 0 { - return finalTotal - } - - return float64(total.Load()) - } - stop, err := s.progress.Track( ctx, func() float64 { return float64(done.Load()) }, - getTotal, + func() float64 { return float64(total.Load()) }, "indexing project", - time.Millisecond*100, + time.Millisecond*50, ) if err != nil { s.showAndLog(ctx, protocol.Error, err) return } - err = s.project.Parse(done, total, totalDoneChan) + err = s.project.Parse(done, total) if err != nil { if err := stop(err); err != nil { s.showAndLog(ctx, protocol.Error, fmt.Errorf("stopping progress: %w", err)) diff --git a/internal/server/references.go b/internal/server/references.go index 5ceeab0..78cb0ab 100644 --- a/internal/server/references.go +++ b/internal/server/references.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "sync" "sync/atomic" "time" @@ -11,6 +12,7 @@ import ( "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/php-parser/pkg/visitor" "github.com/laytan/php-parser/pkg/visitor/traverser" + "github.com/laytan/phpls/internal/config" pcontext "github.com/laytan/phpls/internal/context" "github.com/laytan/phpls/internal/fqner" "github.com/laytan/phpls/internal/wrkspc" @@ -19,12 +21,9 @@ import ( "github.com/laytan/phpls/pkg/fqn" "github.com/laytan/phpls/pkg/lsperrors" "github.com/laytan/phpls/pkg/nodeident" - "github.com/laytan/phpls/pkg/parsing" - "github.com/laytan/phpls/pkg/phpversion" "github.com/laytan/phpls/pkg/position" ) -// TODO: add config to skip vendor directory. func (s *Server) References( ctx context.Context, params *protocol.ReferenceParams, @@ -39,27 +38,40 @@ func (s *Server) References( }() target := position.FromTextDocumentPositionParams(¶ms.Position, ¶ms.TextDocument) - definitions, err := s.project.Definition(target) + references, err := s.references(ctx, target) if err != nil { - log.Println(err) + log.Printf("[ERROR]: finding references of %v: %v", target, err) return nil, lsperrors.ErrRequestFailed(err.Error()) } + return references, nil +} + +// TODO: add config to skip vendor directory. +func (s *Server) references( + ctx context.Context, + target *position.Position, +) ([]protocol.Location, error) { + // PERF: could probably skip this, and use context of target, + // but, definition is extremely fast, so no real pressure. + definitions, err := s.project.Definition(target) + if err != nil { + return nil, fmt.Errorf("finding definition of symbol to get references of: %w", err) + } + if len(definitions) > 1 { - msg := "Multiple definitions found in references call, not supported" - log.Println(msg) - return nil, lsperrors.ErrRequestFailed(msg) + return nil, fmt.Errorf("multiple definitions found in references call, not supported") } d := definitions[0] - content := wrkspc.Current.FContentOf(d.Path) + content := wrkspc.Current.ContentF(d.Path) dpos := position.FromIRPosition(d.Path, content, d.Position.StartPos) pctx, err := pcontext.New(dpos) if err != nil { - log.Printf("Unable to recreate definition context: %v", err) - return nil, lsperrors.ErrRequestFailed(err.Error()) + return nil, fmt.Errorf("Unable to recreate definition context: %w", err) } + // Advance to the node that matches the definition. for ok := true; ok; ok = pctx.Advance() { if nodeident.Get(pctx.Current()) == d.Identifier { @@ -80,23 +92,43 @@ func (s *Server) References( }, } - // TODO: parser should be like configured. - parser := parsing.New(phpversion.Latest()) + done := &atomic.Uint64{} + total := &atomic.Uint64{} + stop, err := s.progress.Track( + ctx, + func() float64 { return float64(done.Load()) }, + func() float64 { return float64(total.Load()) }, + "finding references", + time.Millisecond*50, + ) + if err != nil { + log.Printf("[ERROR]: starting references progress: %v", err) + } + defer func() { + if err := stop(nil); err != nil { + log.Printf("[ERROR]: stopping references progress: %v", err) + } + }() + name := fqner.FullyQualifyName(pctx.Root(), pctx.Current()) log.Printf("[DEBUG]: finding references of %q", name) go func() { - total := &atomic.Uint64{} - totalDone := make(chan bool, 1) - if err := wrkspc.Current.Index(files, total, totalDone); err != nil { - log.Println( - fmt.Errorf( - "Could not index the file content of root %s: %w", - wrkspc.Current.Root(), - err, - ), - ) + // If the definition is in stubs or in vendor, we need to check everywhere, + // But, if the definition is in the project files, it can not be used/referenced in vendor or stubs + // so, there is no need to walk those directories. + definitionInVendor := strings.Contains(d.Path, "/vendor/") + // Note: this does not work in tests. + definitionInStubs := strings.HasPrefix(d.Path, config.Current.StubsPath) + walkOpts := wrkspc.WalkOptions{ + DoStubs: definitionInVendor || definitionInStubs, + DoVendor: definitionInVendor || definitionInStubs, + } + log.Println(walkOpts) + + if err := wrkspc.Current.Walk(files, total, walkOpts); err != nil { + log.Printf("[WARN]: could not index the file content of root %s: %v", wrkspc.Current.Root(), err) hasErrors = true } }() @@ -109,21 +141,21 @@ func (s *Server) References( file := file wg.Add(1) go func() { - defer func() { - wg.Done() - if r := recover(); r != nil { - log.Printf("ERROR: could not parse %q into an AST: %v", file.Path, r) - } - }() - - root, err := parser.Parse([]byte(file.Content)) + defer done.Add(1) + defer wg.Done() + + // PERF: Don't parse when the class name is not in the file. + if !strings.Contains(file.Content, name.Name()) { + return + } + + root, err := wrkspc.Current.Parse(file.Path, []byte(file.Content)) if err != nil { log.Printf("ERROR: parsing %q: %v", file.Path, err) hasErrors = true return } - // TODO: sync.pool v := tvpool.Get().(*classReferenceVisitor) defer tvpool.Put(v) v.Reset(file.Path, file.Content, name) @@ -135,7 +167,6 @@ func (s *Server) References( var accReferences []protocol.Location for reference := range references { - log.Printf("[DEBUG]: found reference: %v", reference) accReferences = append(accReferences, reference) } @@ -145,8 +176,6 @@ func (s *Server) References( ) } - log.Printf("[DEBUG]: References: %v", accReferences) - return accReferences, nil default: // TODO: others. @@ -204,6 +233,10 @@ func (c *classReferenceVisitor) LeaveNode(node ast.Vertex) { } for _, name := range c.names { + if name == nil { + continue + } + if c.fqnv.ResultFor(name).String() == c.fqn.String() { c.references <- position.AstToLspLocation(c.path, name.GetPosition()) } diff --git a/internal/server/rename.go b/internal/server/rename.go new file mode 100644 index 0000000..f4b1f3a --- /dev/null +++ b/internal/server/rename.go @@ -0,0 +1,70 @@ +package server + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/laytan/go-lsp-protocol/pkg/lsp/protocol" + "github.com/laytan/phpls/pkg/functional" + "github.com/laytan/phpls/pkg/position" +) + +func (s *Server) Rename( + ctx context.Context, + params *protocol.RenameParams, +) (*protocol.WorkspaceEdit, error) { + if err := s.isMethodAllowed("Rename"); err != nil { + return nil, err + } + + start := time.Now() + defer func() { + log.Printf("[INFO]: Rename took %s", time.Since(start)) + }() + + // TODO: check if new name is a valid name for this symbol. + target := position.FromTextDocumentPositionParams(¶ms.Position, ¶ms.TextDocument) + references, err := s.references(ctx, target) + if err != nil { + return nil, fmt.Errorf("finding references to rename: %w", err) + } + + edit := protocol.WorkspaceEdit{} + + fileRefs := map[protocol.DocumentURI][]protocol.Location{} + for _, reference := range references { + if _, ok := fileRefs[reference.URI]; ok { + fileRefs[reference.URI] = append(fileRefs[reference.URI], reference) + continue + } + + fileRefs[reference.URI] = []protocol.Location{reference} + } + + for uri, references := range fileRefs { + edits := functional.Map( + references, + func(reference protocol.Location) protocol.TextEdit { + return protocol.TextEdit{ + Range: reference.Range, + NewText: params.NewName, + } + }, + ) + + dc := protocol.DocumentChanges{ + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: uri}, + }, + Edits: edits, + }, + } + + edit.DocumentChanges = append(edit.DocumentChanges, dc) + } + + return &edit, nil +} diff --git a/internal/server/sync.go b/internal/server/sync.go index 74a5526..47b3a38 100644 --- a/internal/server/sync.go +++ b/internal/server/sync.go @@ -20,7 +20,7 @@ func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocume path := strings.TrimPrefix(string(params.TextDocument.URI), "file://") if s.diag != nil && !inStubs(path) { - code := wrkspc.Current.FContentOf(path) + code := wrkspc.Current.ContentF(path) if err := s.diag.Run(ctx, int(params.TextDocument.Version), path, []byte(code)); err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { s.showAndLog(ctx, protocol.Error, err) @@ -54,7 +54,7 @@ func (s *Server) DidChange( path := strings.TrimPrefix(string(params.TextDocument.URI), "file://") newContent := params.ContentChanges[len(params.ContentChanges)-1].Text - prevContent := wrkspc.Current.FContentOf(path) + prevContent := wrkspc.Current.ContentF(path) if strutil.RemoveWhitespace(prevContent) == strutil.RemoveWhitespace(newContent) { return nil } @@ -62,6 +62,7 @@ func (s *Server) DidChange( // TODO: check if this file extension is in config file extensions, and return if not. go func() { + wrkspc.Current.PutOverlay(path, newContent) if err := s.project.ParseFileUpdate(path, newContent); err != nil { s.showAndLog(ctx, protocol.Warning, err) } @@ -85,8 +86,11 @@ func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocu return err } + path := strings.TrimPrefix(string(params.TextDocument.URI), "file://") + + wrkspc.Current.DeleteOverlay(path) + if s.diag != nil { - path := strings.TrimPrefix(string(params.TextDocument.URI), "file://") if err := s.diag.StopWatching(path); err != nil { s.showAndLog(ctx, protocol.Error, err) } diff --git a/internal/server/unimplemented.go b/internal/server/unimplemented.go index 51d262e..5583e77 100644 --- a/internal/server/unimplemented.go +++ b/internal/server/unimplemented.go @@ -363,10 +363,6 @@ func (s *Server) OnTypeFormatting( return nil, errorUnimplemented } -func (s *Server) Rename(context.Context, *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { - return nil, errorUnimplemented -} - func (s *Server) PrepareRename( context.Context, *protocol.PrepareRenameParams, diff --git a/internal/symbol/class.go b/internal/symbol/class.go index 2641cc5..a2fa24d 100644 --- a/internal/symbol/class.go +++ b/internal/symbol/class.go @@ -54,7 +54,7 @@ func NewClassLikeFromName(nameRoot *ast.Root, n ast.Vertex) (*ClassLike, error) return nil, fmt.Errorf("[symbol.NewClassLikeFromName]: can't find %v in index", n) } - root := wrkspc.Current.FIROf(iNode.Path) + root := wrkspc.Current.AstF(iNode.Path) ct := traversers.NewClassLike(nodeident.Get(n)) tv := traverser.NewTraverser(ct) root.Accept(tv) diff --git a/internal/symbol/symbol_test.go b/internal/symbol/symbol_test.go index a67e55b..6139a59 100644 --- a/internal/symbol/symbol_test.go +++ b/internal/symbol/symbol_test.go @@ -37,7 +37,7 @@ func TestClass(t *testing.T) { require.NoError(t, err) root, err := wrkspc.Current. - IROf(filepath.Join(pathutils.Root(), "internal", "symbol", "testdata", "test.php")) + Ast(filepath.Join(pathutils.Root(), "internal", "symbol", "testdata", "test.php")) require.NoError(t, err) n := root.Stmts[0].(*ast.StmtExpression).Expr.(*ast.ExprNew).Class.(*ast.Name) diff --git a/internal/throws/throws.go b/internal/throws/throws.go index 6d85b9c..60c4164 100644 --- a/internal/throws/throws.go +++ b/internal/throws/throws.go @@ -338,7 +338,7 @@ func (t *Throws) resolve(node ast.Vertex) (*ast.Root, *expr.Resolved, error) { ) } - resolvedRoot := wrkspc.Current.FIROf(resolvement.Path) + resolvedRoot := wrkspc.Current.AstF(resolvement.Path) return resolvedRoot, resolvement, nil } diff --git a/internal/throws/throws_test.go b/internal/throws/throws_test.go index 0c02786..5459008 100644 --- a/internal/throws/throws_test.go +++ b/internal/throws/throws_test.go @@ -58,7 +58,7 @@ func TestAnnotateThrows(t *testing.T) { } if scenario.IsDump { - root, err := wrkspc.Current.IROf(scenario.In.Path) + root, err := wrkspc.Current.Ast(scenario.In.Path) require.NoError(t, err) what.Is(root) return @@ -102,7 +102,7 @@ func TestAnnotateThrows(t *testing.T) { t.Errorf("out node is not a class like node %v", cls) } - root, err := wrkspc.Current.IROf(pos.Path) + root, err := wrkspc.Current.Ast(pos.Path) require.NoError(t, err) fqn := fqner.FullyQualifyName( diff --git a/internal/wrkspc/rooter.go b/internal/wrkspc/rooter.go index c3dbcf0..3e7ea67 100644 --- a/internal/wrkspc/rooter.go +++ b/internal/wrkspc/rooter.go @@ -36,7 +36,7 @@ func NewRooter(path string, root ...*ast.Root) *Rooter { func (r *Rooter) Path() string { return r.path } func (r *Rooter) Root() *ast.Root { if r.root == nil { - r.root = Current.FIROf(r.path) + r.root = Current.AstF(r.path) } return r.root diff --git a/internal/wrkspc/wrkspc.go b/internal/wrkspc/wrkspc.go index 60a408b..61ff9ae 100644 --- a/internal/wrkspc/wrkspc.go +++ b/internal/wrkspc/wrkspc.go @@ -9,9 +9,7 @@ import ( "strings" "sync" "sync/atomic" - "time" - "appliedgo.net/what" "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/php-parser/pkg/lexer" "github.com/laytan/phpls/internal/config" @@ -22,143 +20,136 @@ import ( "golang.org/x/sync/errgroup" ) -const ( - ErrParseFmt = "Error parsing file %s syntax nodes: %w" - ErrReadFmt = "Error reading file %s: %w" - ErrParseWalkFmt = "Error walking the workspace files: %w" - - irCacheCapacity = 25 - contentCacheCapacity = 50 -) +const astCacheCapacity = 25 var ( - ErrFileNotIndexed = errors.New("File is not indexed in the workspace") + ErrFileNotIndexed = errors.New("file is not indexed in the workspace") + ErrRead = errors.New("unable to read file") indexGoRoutinesLimit = 64 Current Wrkspc ) -type file struct { - content string -} - -func newFile(content string) *file { - return &file{ - content: content, - } -} - -type ParsedFile struct { - Path string - Content string -} - type Wrkspc interface { Root() string - Index(files chan<- *ParsedFile, total *atomic.Uint64, totalDone chan<- bool) error + PutOverlay(path, content string) + DeleteOverlay(path string) - // ContentOf returns the content of the file at the given path. - ContentOf(path string) (string, error) - // FContentOf returns the content of the file at the given path. - // If an error occurs, it logs it and returns an empty string. - // Use ContentOf for access to the error. - FContentOf(path string) string + Walk(files chan<- *ParsedFile, total *atomic.Uint64, opts WalkOptions) error - // IROf returns the parsed root node of the given path. - // If an error occurs, it returns an empty root node, this NEVER returns nil. - IROf(path string) (*ast.Root, error) - // FIROf returns the root of the given path, if an error occurs, it logs it - // and returns an empty root node. This never returns nil. - // Use IROf for access to the error. - FIROf(path string) *ast.Root + Content(path string) (string, error) + ContentF(path string) string - // AllOf returns the content & parsed root node of the given path. - // If an error occurs, it returns an empty root node, this NEVER returns nil. - AllOf(path string) (string, *ast.Root, error) - // FAllOf returns both the content and root node of the given path. - // If an error occurs, it returns an empty root node and string after logging the error. - FAllOf(path string) (string, *ast.Root) + Ast(path string) (*ast.Root, error) + AstF(path string) *ast.Root - Refresh(path string) error + Parse(path string, content []byte) (*ast.Root, error) - RefreshFrom(path string, content string) error + All(path string) (content string, root *ast.Root, err error) + AllF(path string) (content string, root *ast.Root) - FLexerOf(path string) lexer.Lexer + Lexer(path string) (lexer.Lexer, error) - IsPhpFile(path string) bool + IsPhp(path string) bool +} + +type ParsedFile struct { + Path string + Content string } // fileExtensions should all start with a period. func New(phpv *phpversion.PHPVersion, root string, stubs string) Wrkspc { - normalParser := parsing.New(phpv) - // TODO: not ideal, temporary - stubParser := parsing.New(phpversion.EightOne()) - - files := cache.New[string, *file](contentCacheCapacity) - irs := cache.New[string, *ast.Root](irCacheCapacity) - - t := time.NewTicker(time.Second * 60) - go func() { - for { - <-t.C - log.Printf("File cache: %s", files.String()) - log.Printf("IR cache: %s", irs.String()) - } - }() - return &wrkspc{ - normalParser: normalParser, - stubParser: stubParser, + normalParser: parsing.New(phpv), + stubParser: parsing.New(phpversion.Latest()), roots: []string{root, stubs}, fileExtensions: config.Current.Extensions, ignoredDirNames: config.Current.IgnoredDirectories, - files: files, - irs: irs, + overlays: map[string]string{}, + asts: cache.New[string, *ast.Root](astCacheCapacity), + totals: map[WalkOptions]uint64{}, } } type wrkspc struct { - normalParser parsing.Parser - stubParser parsing.Parser + normalParser *parsing.Parser + stubParser *parsing.Parser roots []string fileExtensions []string ignoredDirNames []string - files *cache.Cache[string, *file] - irs *cache.Cache[string, *ast.Root] + asts *cache.Cache[string, *ast.Root] + + // Overlays are open files/buffers, the contents might not be saved yet, + // So we can't rely on the file contents. + overlays map[string]string + overlaysMu sync.RWMutex + + // Cache the totals, so that after the first walk, the total will be pretty accurate from the start. + totals map[WalkOptions]uint64 } func (w *wrkspc) Root() string { return w.roots[0] } +func (w *wrkspc) PutOverlay(path string, content string) { + w.overlaysMu.Lock() + defer w.overlaysMu.Unlock() + w.overlays[path] = content +} + +func (w *wrkspc) DeleteOverlay(path string) { + w.overlaysMu.Lock() + defer w.overlaysMu.Unlock() + delete(w.overlays, path) +} + +type WalkOptions struct { + DoStubs bool + DoVendor bool +} + +var WalkAll = WalkOptions{ + DoStubs: true, + DoVendor: true, +} + // TODO: error handling. -func (w *wrkspc) Index( - files chan<- *ParsedFile, - total *atomic.Uint64, - totalDone chan<- bool, -) error { +func (w *wrkspc) Walk(files chan<- *ParsedFile, total *atomic.Uint64, opts WalkOptions) error { + w.overlaysMu.RLock() + defer w.overlaysMu.RUnlock() + defer close(files) - go func() { - if err := w.walk(func(_ string, d fs.DirEntry) error { - total.Add(1) - return nil - }); err != nil { - panic(err) - } + // Set total to the cached total so that it is "accurate" from the start. + var actTotal *atomic.Uint64 + if cachedTotal, ok := w.totals[opts]; ok { + total.Store(cachedTotal) + actTotal = &atomic.Uint64{} + } else { + actTotal = total + } - totalDone <- true - close(totalDone) - }() + for path, content := range w.overlays { + actTotal.Add(1) + files <- &ParsedFile{Path: path, Content: content} + } g := errgroup.Group{} g.SetLimit(indexGoRoutinesLimit) - if err := w.walk(func(path string, d fs.DirEntry) error { + if err := w.walk(opts, func(path string, d fs.DirEntry) error { + if _, ok := w.overlays[path]; ok { + return nil + } + + actTotal.Add(1) + g.Go(func() error { content, err := w.parser(path).Read(path) if err != nil { - return fmt.Errorf(ErrReadFmt, path, err) + return fmt.Errorf("%q: %w: %w", path, ErrRead, err) } files <- &ParsedFile{Path: path, Content: content} @@ -170,6 +161,11 @@ func (w *wrkspc) Index( panic(err) } + // Update the cache and set total to the actual total. + act := actTotal.Load() + total.Store(act) + w.totals[opts] = act + if err := g.Wait(); err != nil { panic(err) } @@ -177,32 +173,27 @@ func (w *wrkspc) Index( return nil } -// ContentOf returns the content of the file at the given path. -func (w *wrkspc) ContentOf(path string) (string, error) { - file := w.files.Cached(path, func() *file { - what.Happens("Getting fresh content of %s", path) - - content, err := w.parser(path).Read(path) - if err != nil { - log.Println(err) - return nil - } - - return newFile(content) - }) +// Content returns the content of the file at the given path. +func (w *wrkspc) Content(path string) (string, error) { + w.overlaysMu.RLock() + defer w.overlaysMu.RUnlock() + if overlay, ok := w.overlays[path]; ok { + return overlay, nil + } - if file == nil { - return "", ErrFileNotIndexed + content, err := w.parser(path).Read(path) + if err != nil { + return "", fmt.Errorf("%q: %w: %w", path, ErrRead, err) } - return file.content, nil + return content, nil } -// FContentOf returns the content of the file at the given path. +// ContentF returns the content of the file at the given path. // If an error occurs, it logs it and returns an empty string. -// Use ContentOf for access to the error. -func (w *wrkspc) FContentOf(path string) string { - content, err := w.ContentOf(path) +// Use Content for access to the error. +func (w *wrkspc) ContentF(path string) string { + content, err := w.Content(path) if err != nil { log.Println(err) } @@ -210,19 +201,17 @@ func (w *wrkspc) FContentOf(path string) string { return content } -// IROf returns the parsed root node of the given path. +// Ast returns the parsed root node of the given path. // If an error occurs, it returns an empty root node, this NEVER returns nil. -func (w *wrkspc) IROf(path string) (*ast.Root, error) { - root := w.irs.Cached(path, func() *ast.Root { - what.Happens("Getting fresh AST of %s", path) - - content, err := w.ContentOf(path) +func (w *wrkspc) Ast(path string) (*ast.Root, error) { + root := w.asts.Cached(path, func() *ast.Root { + content, err := w.Content(path) if err != nil { log.Println(err) return nil } - root, err := w.parser(path).Parse([]byte(content)) + root, err := w.Parse(path, []byte(content)) if err != nil { log.Println(err) return nil @@ -238,11 +227,11 @@ func (w *wrkspc) IROf(path string) (*ast.Root, error) { return root, nil } -// FIROf returns the root of the given path, if an error occurs, it logs it +// AstF returns the root of the given path, if an error occurs, it logs it // and returns an empty root node. This never returns nil. -// Use IROf for access to the error. -func (w *wrkspc) FIROf(path string) *ast.Root { - root, err := w.IROf(path) +// Use AstF for access to the error. +func (w *wrkspc) AstF(path string) *ast.Root { + root, err := w.Ast(path) if err != nil { log.Println(err) } @@ -250,11 +239,20 @@ func (w *wrkspc) FIROf(path string) *ast.Root { return root } -// AllOf returns the content & parsed root node of the given path. +func (w *wrkspc) Parse(path string, content []byte) (*ast.Root, error) { + root, err := w.parser(path).Parse(content) + if err != nil { + return nil, fmt.Errorf("parsing path %q with content %q: %w", path, string(content), err) + } + + return root, nil +} + +// All returns the content & parsed root node of the given path. // If an error occurs, it returns an empty root node, this NEVER returns nil. -func (w *wrkspc) AllOf(path string) (string, *ast.Root, error) { - content, cErr := w.ContentOf(path) - root, rErr := w.IROf(path) +func (w *wrkspc) All(path string) (string, *ast.Root, error) { + content, cErr := w.Content(path) + root, rErr := w.Ast(path) if cErr != nil { return content, root, cErr @@ -267,10 +265,10 @@ func (w *wrkspc) AllOf(path string) (string, *ast.Root, error) { return content, root, nil } -// FAllOf returns both the content and root node of the given path. +// AllF returns both the content and root node of the given path. // If an error occurs, it returns an empty root node and string after logging the error. -func (w *wrkspc) FAllOf(path string) (string, *ast.Root) { - content, root, err := w.AllOf(path) +func (w *wrkspc) AllF(path string) (string, *ast.Root) { + content, root, err := w.All(path) if err != nil { log.Println(err) } @@ -278,45 +276,35 @@ func (w *wrkspc) FAllOf(path string) (string, *ast.Root) { return content, root } -func (w *wrkspc) Refresh(path string) error { - content, err := w.parser(path).Read(path) +func (w *wrkspc) Lexer(path string) (lexer.Lexer, error) { + lexer, err := w.parser(path).Lexer([]byte(w.ContentF(path))) if err != nil { - return fmt.Errorf(ErrReadFmt, path, err) + return nil, fmt.Errorf("creating lexer: %w", err) } - return w.RefreshFrom(path, content) + return lexer, nil } -func (w *wrkspc) RefreshFrom(path string, content string) error { - root, err := w.parser(path).Parse([]byte(content)) - if err != nil { - return fmt.Errorf(ErrParseFmt, path, err) - } - - file := newFile(content) - - w.files.Put(path, file) - w.irs.Put(path, root) - - return nil -} - -func (w *wrkspc) FLexerOf(path string) lexer.Lexer { - lexer, err := w.parser(path).Lexer([]byte(w.FContentOf(path))) - if err != nil { - log.Println(err) +func (w *wrkspc) IsPhp(path string) bool { + for _, extension := range w.fileExtensions { + if strings.HasSuffix(path, extension) { + return true + } } - - return lexer + return false } -func (w *wrkspc) walk(walker func(path string, d fs.DirEntry) error) error { +func (w *wrkspc) walk(opts WalkOptions, walker func(path string, d fs.DirEntry) error) error { wg := sync.WaitGroup{} - wg.Add(len(w.roots)) var finalErr error for _, root := range w.roots { + if !opts.DoStubs && isStubs(root) { + continue + } + + wg.Add(1) go func(root string) { defer wg.Done() @@ -325,7 +313,7 @@ func (w *wrkspc) walk(walker func(path string, d fs.DirEntry) error) error { return err } - doParse, err := w.shouldParse(d) + doParse, err := w.shouldParse(opts.DoVendor, d) if err != nil { return err } @@ -336,7 +324,6 @@ func (w *wrkspc) walk(walker func(path string, d fs.DirEntry) error) error { return nil }); err != nil { - log.Println(fmt.Errorf(ErrParseWalkFmt, err)) finalErr = err } }(root) @@ -346,19 +333,14 @@ func (w *wrkspc) walk(walker func(path string, d fs.DirEntry) error) error { return finalErr } -func (w *wrkspc) IsPhpFile(path string) bool { - for _, extension := range w.fileExtensions { - if strings.HasSuffix(path, extension) { - return true - } - } - return false -} - -func (w *wrkspc) shouldParse(d fs.DirEntry) (bool, error) { +func (w *wrkspc) shouldParse(doVendor bool, d fs.DirEntry) (bool, error) { if d.IsDir() { // Skip ignored directories. n := d.Name() + if !doVendor && n == "vendor" { + return false, filepath.SkipDir + } + for _, ignored := range w.ignoredDirNames { if n == ignored { return false, filepath.SkipDir @@ -368,19 +350,23 @@ func (w *wrkspc) shouldParse(d fs.DirEntry) (bool, error) { return false, nil } - if w.IsPhpFile(d.Name()) { + if w.IsPhp(d.Name()) { return true, nil } return false, nil } -var stubsDir = filepath.Join(pathutils.Root(), "third_party", "phpstorm-stubs") - -func (w *wrkspc) parser(path string) parsing.Parser { - if strings.HasPrefix(path, config.Current.StubsPath) || strings.HasPrefix(path, stubsDir) { +func (w *wrkspc) parser(path string) *parsing.Parser { + if isStubs(path) { return w.stubParser } return w.normalParser } + +var stubsDir = filepath.Join(pathutils.Root(), "third_party", "phpstorm-stubs") + +func isStubs(path string) bool { + return strings.HasPrefix(path, config.Current.StubsPath) || strings.HasPrefix(path, stubsDir) +} diff --git a/internal/wrkspc/wrkspc_test.go b/internal/wrkspc/wrkspc_test.go index b096a28..98eef6d 100644 --- a/internal/wrkspc/wrkspc_test.go +++ b/internal/wrkspc/wrkspc_test.go @@ -26,9 +26,8 @@ func BenchmarkWalk(b *testing.B) { } }() - totalDone := make(chan bool, 1) total := &atomic.Uint64{} - err := wrkspc.Current.Index(filesChan, total, totalDone) + err := wrkspc.Current.Walk(filesChan, total, wrkspc.WalkAll) require.NoError(b, err) } } diff --git a/pkg/parsing/parsing.go b/pkg/parsing/parsing.go index da896ff..6be4d6a 100644 --- a/pkg/parsing/parsing.go +++ b/pkg/parsing/parsing.go @@ -17,35 +17,21 @@ import ( "github.com/laytan/phpls/pkg/phpversion" ) -const ( - ErrReadFmt = "Could not read file at %s: %w" - ErrASTFmt = "Error parsing file into AST: %w" - ErrWalkFmt = "Error arose during walk of root %s: %w" +var ( + ErrRead = errors.New("could not read file") + ErrAst = errors.New("could not parse content into AST") + ErrWalk = errors.New("error arose walking") + ErrNoContent = errors.New( + "no AST could be parsed, there were unrecoverable syntax errors or the file was empty", + ) ) -var ErrNoContent = errors.New( - "No AST could be parsed, there were unrecoverable syntax errors or the file was empty", -) - -// Parser is responsible for and a central place for -// file system access and parsing file content into -// IR for use everywhere else. -type Parser interface { - Parse(content []byte) (*ast.Root, error) - - Lexer(content []byte) (lexer.Lexer, error) - - Read(path string) (string, error) - - Walk(root string, walker func(path string, d fs.DirEntry, err error) error) error -} - -type parser struct { +type Parser struct { config conf.Config } -func New(phpv *phpversion.PHPVersion) Parser { - return &parser{ +func New(phpv *phpversion.PHPVersion) *Parser { + return &Parser{ config: conf.Config{ Version: &version.Version{ Major: uint64(phpv.Major), @@ -59,21 +45,26 @@ func New(phpv *phpversion.PHPVersion) Parser { } } -// TODO: recover panics -func (p *parser) Parse(content []byte) (*ast.Root, error) { +func (p *Parser) Parse(content []byte) (root *ast.Root, err error) { + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("%w: %v", ErrAst, e) + } + }() + a, err := astParser.Parse(content, p.config) if err != nil || a == nil { if err == nil { return nil, ErrNoContent } - return nil, fmt.Errorf(ErrASTFmt, err) + return nil, fmt.Errorf("%w: %w", ErrAst, err) } return a.(*ast.Root), nil } -func (p *parser) Lexer(content []byte) (lexer.Lexer, error) { +func (p *Parser) Lexer(content []byte) (lexer.Lexer, error) { res, err := lexer.New(content, p.config) if err != nil { return nil, fmt.Errorf("creating lexer: %w", err) @@ -82,23 +73,23 @@ func (p *parser) Lexer(content []byte) (lexer.Lexer, error) { return res, nil } -func (p *parser) Read(path string) (string, error) { +func (p *Parser) Read(path string) (string, error) { content, err := os.ReadFile(path) if err != nil { - return "", fmt.Errorf(ErrReadFmt, path, err) + return "", fmt.Errorf("reading %q: %w: %w", path, ErrRead, err) } return string(content), nil } -func (p *parser) Walk(root string, walker func(path string, d fs.DirEntry, err error) error) error { +func (p *Parser) Walk(root string, walker func(path string, d fs.DirEntry, err error) error) error { err := filepath.WalkDir(root, walker) if err != nil { if err == filepath.SkipDir { //nolint:errorlint // stdlib checks == so don't want wrapping. return err //nolint:wrapcheck // stdlib checks == so don't want wrapping. } - return fmt.Errorf(ErrWalkFmt, root, err) + return fmt.Errorf("walking %q: %w: %w", root, ErrWalk, err) } return nil diff --git a/pkg/phpcs/phpcbf/phpcbf.go b/pkg/phpcs/phpcbf/phpcbf.go index 080690e..c2ef87b 100644 --- a/pkg/phpcs/phpcbf/phpcbf.go +++ b/pkg/phpcs/phpcbf/phpcbf.go @@ -120,7 +120,7 @@ func (p *Instance) Format(code []byte) ([]byte, error) { // TODO: should probably not be in this package. func (p *Instance) FormatFileEdits(path string) ([]protocol.TextEdit, error) { - code := wrkspc.Current.FContentOf(path) + code := wrkspc.Current.ContentF(path) lines := len(strutil.Lines(code)) formatted, err := p.Format([]byte(code)) if err != nil { diff --git a/pkg/position/position.go b/pkg/position/position.go index 3b8210f..6a35916 100644 --- a/pkg/position/position.go +++ b/pkg/position/position.go @@ -147,3 +147,11 @@ func AstToLspLocation(path string, p *position.Position) protocol.Location { }, } } + +func FromAst(path string, p *position.Position) *Position { + return &Position{ + Path: path, + Row: uint(p.StartLine), + Col: uint(p.StartCol) + 1, + } +} From 677e172838aeae0cfcfe7b4294daac041d0836d0 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 01:22:52 +0200 Subject: [PATCH 05/12] fix: update php-parser to fix name parts start and end cols --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e1e8215..bdcf597 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.2 github.com/laytan/go-lsp-protocol v0.0.0-20230331135813-fccb4f5d33c5 - github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838 + github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b github.com/stretchr/testify v1.8.2 github.com/xeipuuv/gojsonschema v1.2.0 go.uber.org/goleak v1.2.1 diff --git a/go.sum b/go.sum index 8078b5c..747efa0 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,8 @@ github.com/laytan/php-parser v0.9.1-0.20230411203829-6b3673ece43a h1:V9fMfdfEYUs github.com/laytan/php-parser v0.9.1-0.20230411203829-6b3673ece43a/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838 h1:IOkVuzF0K25reG7JcRuKF3uKlodtM4C9RzGAOhsb550= github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= +github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b h1:bwyZHQZmVUMwO59auHDh/GDSBHfN2acMEuwNg6jmsno= +github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= From 0ee0864bf42beacd1ec85647c8838a0c2b1f9c01 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 01:23:39 +0200 Subject: [PATCH 06/12] feat: support trait and interface references and rename --- internal/server/references.go | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/internal/server/references.go b/internal/server/references.go index 78cb0ab..420977d 100644 --- a/internal/server/references.go +++ b/internal/server/references.go @@ -32,6 +32,8 @@ func (s *Server) References( return nil, err } + // TODO: support params.IncludeDeclaration. + start := time.Now() defer func() { log.Printf("Retrieving references took %s", time.Since(start)) @@ -80,7 +82,7 @@ func (s *Server) references( } switch pctx.Current().(type) { - case *ast.StmtClass: + case *ast.StmtClass, *ast.StmtInterface, *ast.StmtTrait: hasErrors := false files := make(chan *wrkspc.ParsedFile) @@ -137,6 +139,21 @@ func (s *Server) references( defer close(references) wg := sync.WaitGroup{} defer wg.Wait() + + // Definition is also a reference. + var dname ast.Vertex + switch tn := pctx.Current().(type) { + case *ast.StmtClass: + dname = tn.Name + case *ast.StmtInterface: + dname = tn.Name + case *ast.StmtTrait: + dname = tn.Name + default: + panic("unreachable") + } + references <- position.AstToLspLocation(d.Path, dname.GetPosition()) + for file := range files { file := file wg.Add(1) @@ -144,7 +161,7 @@ func (s *Server) references( defer done.Add(1) defer wg.Done() - // PERF: Don't parse when the class name is not in the file. + // PERF: We don't parse when the class name is not in the file. if !strings.Contains(file.Content, name.Name()) { return } @@ -211,10 +228,6 @@ func (c *classReferenceVisitor) EnterNode(node ast.Vertex) bool { return true } -func (c *classReferenceVisitor) StmtClass(node *ast.StmtClass) { - c.names = append(c.names, node.Name) -} - func (c *classReferenceVisitor) NameName(node *ast.Name) { c.names = append(c.names, node) } @@ -238,7 +251,16 @@ func (c *classReferenceVisitor) LeaveNode(node ast.Vertex) { } if c.fqnv.ResultFor(name).String() == c.fqn.String() { - c.references <- position.AstToLspLocation(c.path, name.GetPosition()) + var lastPart ast.Vertex + switch tn := name.(type) { + case *ast.Name: + lastPart = tn.Parts[len(tn.Parts)-1] + case *ast.NameFullyQualified: + lastPart = tn.Parts[len(tn.Parts)-1] + case *ast.NameRelative: + lastPart = tn.Parts[len(tn.Parts)-1] + } + c.references <- position.AstToLspLocation(c.path, lastPart.GetPosition()) } } } From f92246b4cc194516dd718b90cb874ee30fd585c5 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 01:24:03 +0200 Subject: [PATCH 07/12] fix: refresh the cache of overlays --- internal/wrkspc/wrkspc.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/wrkspc/wrkspc.go b/internal/wrkspc/wrkspc.go index 61ff9ae..b12d0b2 100644 --- a/internal/wrkspc/wrkspc.go +++ b/internal/wrkspc/wrkspc.go @@ -95,14 +95,24 @@ func (w *wrkspc) Root() string { func (w *wrkspc) PutOverlay(path string, content string) { w.overlaysMu.Lock() - defer w.overlaysMu.Unlock() w.overlays[path] = content + w.overlaysMu.Unlock() + + // Refresh/put updates in the cache. + root, err := w.Parse(path, []byte(content)) + if err != nil { + log.Printf("[WARN]: could not parse new overlay AST") + w.asts.Delete(path) + return + } + + w.asts.Put(path, root) } func (w *wrkspc) DeleteOverlay(path string) { w.overlaysMu.Lock() - defer w.overlaysMu.Unlock() delete(w.overlays, path) + w.overlaysMu.Unlock() } type WalkOptions struct { From da4749ab10936a1d05ce4c08e4b3a5b566c17103 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 17 Apr 2023 01:42:11 +0200 Subject: [PATCH 08/12] build(deps): update to php-parser 0.10 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index bdcf597..115db93 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.2 github.com/laytan/go-lsp-protocol v0.0.0-20230331135813-fccb4f5d33c5 - github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b + github.com/laytan/php-parser v0.10.0 github.com/stretchr/testify v1.8.2 github.com/xeipuuv/gojsonschema v1.2.0 go.uber.org/goleak v1.2.1 diff --git a/go.sum b/go.sum index 747efa0..fd7b73a 100644 --- a/go.sum +++ b/go.sum @@ -295,6 +295,8 @@ github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838 h1:IOkVuzF0K25 github.com/laytan/php-parser v0.9.1-0.20230416220624-307175c40838/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b h1:bwyZHQZmVUMwO59auHDh/GDSBHfN2acMEuwNg6jmsno= github.com/laytan/php-parser v0.9.1-0.20230416231421-9a4744a28d1b/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= +github.com/laytan/php-parser v0.10.0 h1:Vpf4AdrK5uIvBLtEKbh41mnHYzT06e6yu/Kd2mJN2q0= +github.com/laytan/php-parser v0.10.0/go.mod h1:crDMdef5HZkk8JsREDMA6wgTXdip/8mJa4asmjt3ogM= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= From 5a903e6c24782fa3a0e749fc478657c5c6e98e25 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Tue, 18 Apr 2023 23:25:17 +0200 Subject: [PATCH 09/12] feat: basic block scoped function references --- internal/expr/starters.go | 2 +- .../project/definition/providers/function.go | 12 +- internal/project/definition/providers/name.go | 7 +- internal/server/references.go | 297 +++++++++++------- pkg/position/position.go | 30 ++ pkg/traversers/function_call.go | 13 +- 6 files changed, 236 insertions(+), 125 deletions(-) diff --git a/internal/expr/starters.go b/internal/expr/starters.go index cafce59..464fc05 100644 --- a/internal/expr/starters.go +++ b/internal/expr/starters.go @@ -266,7 +266,7 @@ func (p *functionResolver) Up( return nil, nil, 0, false } - t := traversers.NewFunctionCall(toResolve.Identifier) + t := traversers.NewFunctionCall(toResolve.Identifier, false) tv := traverser.NewTraverser(t) scopes.Block.Accept(tv) if t.Result == nil { diff --git a/internal/project/definition/providers/function.go b/internal/project/definition/providers/function.go index c9c561e..89be68d 100644 --- a/internal/project/definition/providers/function.go +++ b/internal/project/definition/providers/function.go @@ -4,6 +4,7 @@ import ( "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/phpls/internal/context" "github.com/laytan/phpls/internal/project/definition" + "github.com/laytan/phpls/pkg/nodeident" ) // FunctionProvider resolves the definition of a function call. @@ -16,9 +17,18 @@ func NewFunction() *FunctionProvider { } func (p *FunctionProvider) CanDefine(ctx *context.Ctx, kind ast.Type) bool { - return kind == ast.TypeExprFunctionCall + return kind == ast.TypeExprFunctionCall || kind == ast.TypeStmtFunction } func (p *FunctionProvider) Define(ctx *context.Ctx) ([]*definition.Definition, error) { + // If stmt function, it is the current function, so just return that one. + if ctx.Current().GetType() == ast.TypeStmtFunction { + return []*definition.Definition{{ + Path: ctx.Path(), + Position: ctx.Current().GetPosition(), + Identifier: nodeident.Get(ctx.Current()), + }}, nil + } + return DefineExpr(ctx) } diff --git a/internal/project/definition/providers/name.go b/internal/project/definition/providers/name.go index 1a986e9..ee5bd02 100644 --- a/internal/project/definition/providers/name.go +++ b/internal/project/definition/providers/name.go @@ -17,11 +17,8 @@ func NewName() *NameProvider { } func (p *NameProvider) CanDefine(ctx *context.Ctx, kind ast.Type) bool { - // The class definition is an identifier, so if we get that & wrapped by a class, we can define it. - if kind == ast.TypeIdentifier { - return ctx.DirectlyWrappedBy(ast.TypeStmtClass) || - ctx.DirectlyWrappedBy(ast.TypeStmtInterface) || - ctx.DirectlyWrappedBy(ast.TypeStmtTrait) + if kind == ast.TypeStmtClass || kind == ast.TypeStmtInterface || kind == ast.TypeStmtTrait { + return true } if !nodescopes.IsName(kind) { diff --git a/internal/server/references.go b/internal/server/references.go index 420977d..cf81b0a 100644 --- a/internal/server/references.go +++ b/internal/server/references.go @@ -15,6 +15,7 @@ import ( "github.com/laytan/phpls/internal/config" pcontext "github.com/laytan/phpls/internal/context" "github.com/laytan/phpls/internal/fqner" + "github.com/laytan/phpls/internal/project/definition" "github.com/laytan/phpls/internal/wrkspc" "github.com/laytan/go-lsp-protocol/pkg/lsp/protocol" @@ -22,6 +23,7 @@ import ( "github.com/laytan/phpls/pkg/lsperrors" "github.com/laytan/phpls/pkg/nodeident" "github.com/laytan/phpls/pkg/position" + "github.com/laytan/phpls/pkg/traversers" ) func (s *Server) References( @@ -49,13 +51,10 @@ func (s *Server) References( return references, nil } -// TODO: add config to skip vendor directory. func (s *Server) references( ctx context.Context, target *position.Position, ) ([]protocol.Location, error) { - // PERF: could probably skip this, and use context of target, - // but, definition is extremely fast, so no real pressure. definitions, err := s.project.Definition(target) if err != nil { return nil, fmt.Errorf("finding definition of symbol to get references of: %w", err) @@ -69,137 +68,211 @@ func (s *Server) references( content := wrkspc.Current.ContentF(d.Path) dpos := position.FromIRPosition(d.Path, content, d.Position.StartPos) - pctx, err := pcontext.New(dpos) + dctx, err := pcontext.New(dpos) if err != nil { return nil, fmt.Errorf("Unable to recreate definition context: %w", err) } // Advance to the node that matches the definition. - for ok := true; ok; ok = pctx.Advance() { - if nodeident.Get(pctx.Current()) == d.Identifier { + for ok := true; ok; ok = dctx.Advance() { + if nodeident.Get(dctx.Current()) == d.Identifier { break } } - switch pctx.Current().(type) { + switch td := dctx.Current().(type) { case *ast.StmtClass, *ast.StmtInterface, *ast.StmtTrait: - hasErrors := false + return s.classReferences(ctx, dctx, d), nil + case *ast.StmtFunction: + // Cases: global scope (from index) + // block scope: check block for references + + // tctx, err := pcontext.New(target) + // if err != nil { + // return nil, fmt.Errorf("creating target context: %w", err) + // } + + // currScope := tctx.Scope() + // If the definition is inside the current scope. + // References will also only be in the current scope. + if dctx.Scope().GetType() != ast.TypeRoot { + if position.PosInAst(dctx.Scope().GetPosition(), target) { + v := traversers.NewFunctionCall(d.Identifier, true) + tv := traverser.NewTraverser(v) + dctx.Scope().Accept(tv) + + res := make([]protocol.Location, 0, len(v.Result)+1) + res = append(res, position.AstToLspLocation(d.Path, td.Name.GetPosition())) + for _, call := range v.Result { + res = append(res, position.AstToLspLocation(d.Path, call.Function.GetPosition())) + } + + return res, nil + } + } - files := make(chan *wrkspc.ParsedFile) - references := make(chan protocol.Location) + return nil, lsperrors.ErrRequestFailed("Unimplemented: non-local function references are not implemented") + + // // If have local scope, check it for the function. + // if scopes.Block.GetType() != ast.TypeRoot { + // ft := traversers.NewFunction(toResolve.Identifier) + // tv := traverser.NewTraverser(ft) + // scopes.Block.Accept(tv) + // + // if ft.Function != nil { + // return &Resolved{ + // Node: ft.Function, + // Path: scopes.Path, + // }, typeOfFunc(ft.Function), phprivacy.PrivacyPublic, true + // } + // } + // + // // Check for functions defined in the used namespaces. + // if def, ok := fqner.FindFullyQualifiedName(scopes.Root, &ast.Name{ + // Position: toResolve.Position, + // Parts: nameParts(toResolve.Identifier), + // }); ok { + // n := def.ToIRNode(wrkspc.Current.AstF(def.Path)) + // return &Resolved{ + // Node: n, + // Path: def.Path, + // }, typeOfFunc(n), phprivacy.PrivacyPublic, true + // } + // + // // Check for global functions. + // key := fqn.New(fqn.PartSeperator + toResolve.Identifier) + // def, ok := index.Current.Find(key) + // if !ok { + // log.Println(fmt.Errorf("[expr.functionResolver.Up]: unable to find %s in index", key)) + // return nil, nil, 0, false + // } + default: + msg := fmt.Sprintf("Unsupported node type %T for references", dctx.Current()) + log.Println(msg) + return nil, lsperrors.ErrRequestFailed(msg) + } +} - tvpool := sync.Pool{ - New: func() any { - return &classReferenceVisitor{references: references} - }, - } +func (s *Server) classReferences( + ctx context.Context, + pctx *pcontext.Ctx, + d *definition.Definition, +) []protocol.Location { + hasErrors := false + + files := make(chan *wrkspc.ParsedFile) + references := make(chan protocol.Location) + + tvpool := sync.Pool{ + New: func() any { + return &classReferenceVisitor{references: references} + }, + } - done := &atomic.Uint64{} - total := &atomic.Uint64{} - stop, err := s.progress.Track( - ctx, - func() float64 { return float64(done.Load()) }, - func() float64 { return float64(total.Load()) }, - "finding references", - time.Millisecond*50, - ) - if err != nil { - log.Printf("[ERROR]: starting references progress: %v", err) + done := &atomic.Uint64{} + total := &atomic.Uint64{} + stop, err := s.progress.Track( + ctx, + func() float64 { return float64(done.Load()) }, + func() float64 { return float64(total.Load()) }, + "finding references", + time.Millisecond*50, + ) + if err != nil { + log.Printf("[ERROR]: starting references progress: %v", err) + } + defer func() { + if err := stop(nil); err != nil { + log.Printf("[ERROR]: stopping references progress: %v", err) } - defer func() { - if err := stop(nil); err != nil { - log.Printf("[ERROR]: stopping references progress: %v", err) - } - }() - - name := fqner.FullyQualifyName(pctx.Root(), pctx.Current()) - - log.Printf("[DEBUG]: finding references of %q", name) - - go func() { - // If the definition is in stubs or in vendor, we need to check everywhere, - // But, if the definition is in the project files, it can not be used/referenced in vendor or stubs - // so, there is no need to walk those directories. - definitionInVendor := strings.Contains(d.Path, "/vendor/") - // Note: this does not work in tests. - definitionInStubs := strings.HasPrefix(d.Path, config.Current.StubsPath) - walkOpts := wrkspc.WalkOptions{ - DoStubs: definitionInVendor || definitionInStubs, - DoVendor: definitionInVendor || definitionInStubs, - } - log.Println(walkOpts) + }() - if err := wrkspc.Current.Walk(files, total, walkOpts); err != nil { - log.Printf("[WARN]: could not index the file content of root %s: %v", wrkspc.Current.Root(), err) - hasErrors = true - } - }() - - go func() { - defer close(references) - wg := sync.WaitGroup{} - defer wg.Wait() - - // Definition is also a reference. - var dname ast.Vertex - switch tn := pctx.Current().(type) { - case *ast.StmtClass: - dname = tn.Name - case *ast.StmtInterface: - dname = tn.Name - case *ast.StmtTrait: - dname = tn.Name - default: - panic("unreachable") - } - references <- position.AstToLspLocation(d.Path, dname.GetPosition()) - - for file := range files { - file := file - wg.Add(1) - go func() { - defer done.Add(1) - defer wg.Done() - - // PERF: We don't parse when the class name is not in the file. - if !strings.Contains(file.Content, name.Name()) { - return - } - - root, err := wrkspc.Current.Parse(file.Path, []byte(file.Content)) - if err != nil { - log.Printf("ERROR: parsing %q: %v", file.Path, err) - hasErrors = true - return - } - - v := tvpool.Get().(*classReferenceVisitor) - defer tvpool.Put(v) - v.Reset(file.Path, file.Content, name) - tv := traverser.NewTraverser(v) - root.Accept(tv) - }() - } - }() + name := fqner.FullyQualifyName(pctx.Root(), pctx.Current()) + + log.Printf("[DEBUG]: finding references of %q", name) - var accReferences []protocol.Location - for reference := range references { - accReferences = append(accReferences, reference) + go func() { + // If the definition is in stubs or in vendor, we need to check everywhere, + // But, if the definition is in the project files, it can not be used/referenced in vendor or stubs + // so, there is no need to walk those directories. + definitionInVendor := strings.Contains(d.Path, "/vendor/") + // Note: this does not work in tests. + definitionInStubs := strings.HasPrefix(d.Path, config.Current.StubsPath) + walkOpts := wrkspc.WalkOptions{ + DoStubs: definitionInVendor || definitionInStubs, + DoVendor: definitionInVendor || definitionInStubs, } + log.Println(walkOpts) - if hasErrors { - log.Println( - "Parsing the project for references resulted in errors, check the logs for more details", + if err := wrkspc.Current.Walk(files, total, walkOpts); err != nil { + log.Printf( + "[WARN]: could not index the file content of root %s: %v", + wrkspc.Current.Root(), + err, ) + hasErrors = true } + }() - return accReferences, nil - default: - // TODO: others. - msg := fmt.Sprintf("Unsupported node type %T for references", pctx.Current()) - log.Println(msg) - return nil, lsperrors.ErrRequestFailed(msg) + go func() { + defer close(references) + wg := sync.WaitGroup{} + defer wg.Wait() + + // Definition is also a reference. + var dname ast.Vertex + switch tn := pctx.Current().(type) { + case *ast.StmtClass: + dname = tn.Name + case *ast.StmtInterface: + dname = tn.Name + case *ast.StmtTrait: + dname = tn.Name + default: + panic("unreachable") + } + references <- position.AstToLspLocation(d.Path, dname.GetPosition()) + + for file := range files { + file := file + wg.Add(1) + go func() { + defer done.Add(1) + defer wg.Done() + + // PERF: We don't parse when the class name is not in the file. + if !strings.Contains(file.Content, name.Name()) { + return + } + + root, err := wrkspc.Current.Parse(file.Path, []byte(file.Content)) + if err != nil { + log.Printf("ERROR: parsing %q: %v", file.Path, err) + hasErrors = true + return + } + + v := tvpool.Get().(*classReferenceVisitor) + defer tvpool.Put(v) + v.Reset(file.Path, file.Content, name) + tv := traverser.NewTraverser(v) + root.Accept(tv) + }() + } + }() + + accReferences := []protocol.Location{} + for reference := range references { + accReferences = append(accReferences, reference) } + + if hasErrors { + log.Println( + "Parsing the project for references resulted in errors, check the logs for more details", + ) + } + + return accReferences } type classReferenceVisitor struct { diff --git a/pkg/position/position.go b/pkg/position/position.go index 6a35916..232d240 100644 --- a/pkg/position/position.go +++ b/pkg/position/position.go @@ -155,3 +155,33 @@ func FromAst(path string, p *position.Position) *Position { Col: uint(p.StartCol) + 1, } } + +func AstInAst(scope *position.Position, p *position.Position) bool { + if scope == nil || p == nil { + return false + } + + if scope.EndLine == 0 && scope.EndPos == 0 { + return p.StartLine >= scope.StartLine && p.StartPos >= scope.StartPos + } + + return p.StartLine >= scope.StartLine && p.EndLine <= scope.EndLine && + p.StartPos >= scope.StartPos && + p.EndPos <= scope.EndPos +} + +func PosInAst(scope *position.Position, p *Position) bool { + if scope == nil { + return false + } + + if p.Row == uint(scope.StartLine) { + return p.Col >= uint(scope.StartCol+1) + } + + if p.Row == uint(scope.EndLine) { + return p.Col <= uint(scope.EndCol+1) + } + + return p.Row >= uint(scope.StartLine) && p.Row <= uint(scope.EndLine) +} diff --git a/pkg/traversers/function_call.go b/pkg/traversers/function_call.go index a2bd76f..3efed95 100644 --- a/pkg/traversers/function_call.go +++ b/pkg/traversers/function_call.go @@ -8,20 +8,21 @@ import ( type FunctionCall struct { visitor.Null - name string - Result *ast.ExprFunctionCall + name string + Result []*ast.ExprFunctionCall + multiple bool } -func NewFunctionCall(name string) *FunctionCall { - return &FunctionCall{name: name} +func NewFunctionCall(name string, multiple bool) *FunctionCall { + return &FunctionCall{name: name, multiple: multiple} } func (v *FunctionCall) EnterNode(node ast.Vertex) bool { - return v.Result == nil + return v.multiple || v.Result == nil } func (v *FunctionCall) ExprFunctionCall(node *ast.ExprFunctionCall) { if v.name == nodeident.Get(node) { - v.Result = node + v.Result = append(v.Result, node) } } From 6dd31a80e900bb090117d433236a6278f15c682f Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Tue, 18 Apr 2023 23:37:56 +0200 Subject: [PATCH 10/12] fix: recursive function calls being early-returned --- pkg/traversers/function.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/traversers/function.go b/pkg/traversers/function.go index cb44857..d01ad90 100644 --- a/pkg/traversers/function.go +++ b/pkg/traversers/function.go @@ -28,16 +28,16 @@ func (f *Function) EnterNode(node ast.Vertex) bool { return false } + if fn, ok := node.(*ast.StmtFunction); ok && nodeident.Get(fn.Name) == f.name { + f.Function = fn + } + // If the scope of the traverser is a function, the first call will be a // function which we need to ignore. if f.currNodeIsRoot && node.GetType() == ast.TypeStmtFunction { return true } - if fn, ok := node.(*ast.StmtFunction); ok && nodeident.Get(fn.Name) == f.name { - f.Function = fn - } - if nodescopes.IsScope(node.GetType()) { return false } From 3d4a870c05e8566af38d2c32b83456d8cbad5065 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Wed, 19 Apr 2023 20:55:36 +0200 Subject: [PATCH 11/12] feat: references&rename of variables in scope --- internal/expr/starters.go | 6 +-- .../project/definition/providers/variable.go | 41 +++++++++++++- .../definitions/annotated/parameter.php | 4 +- internal/server/references.go | 13 +++++ pkg/traversers/assignment.go | 18 +++++++ pkg/traversers/variable.go | 54 ++++++++++++++++--- 6 files changed, 122 insertions(+), 14 deletions(-) diff --git a/internal/expr/starters.go b/internal/expr/starters.go index 464fc05..4d298eb 100644 --- a/internal/expr/starters.go +++ b/internal/expr/starters.go @@ -79,14 +79,14 @@ func (p *variableResolver) Up( return nil, nil, 0, false } - t := traversers.NewVariable(toResolve.Identifier) + t := traversers.NewVariable(toResolve.Identifier, false) tv := traverser.NewTraverser(t) scopes.Block.Accept(tv) - if t.Result == nil { + if len(t.Results) == 0 { return nil, nil, 0, false } - ta := traversers.NewAssignment(t.Result) + ta := traversers.NewAssignment(t.Results[0]) tav := traverser.NewTraverser(ta) scopes.Block.Accept(tav) if ta.Assignment == nil || ta.Scope == nil { diff --git a/internal/project/definition/providers/variable.go b/internal/project/definition/providers/variable.go index 5ccd4b1..78fa59e 100644 --- a/internal/project/definition/providers/variable.go +++ b/internal/project/definition/providers/variable.go @@ -1,6 +1,8 @@ package providers import ( + "log" + "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/php-parser/pkg/visitor/traverser" "github.com/laytan/phpls/internal/context" @@ -21,9 +23,25 @@ func (p *VariableProvider) CanDefine(ctx *context.Ctx, kind ast.Type) bool { return kind == ast.TypeExprVariable } -// TODO: use DefineExpr. func (p *VariableProvider) Define(ctx *context.Ctx) ([]*definition.Definition, error) { - t := traversers.NewAssignment(ctx.Current().(*ast.ExprVariable)) + varN := ctx.Current().(*ast.ExprVariable) + varName := nodeident.Get(ctx.Current()) + + // If scope is a closure, and that closure has a use statement with this variable. + // Check the next scope for the definition. + scope := ctx.Scope() + nextScope := func() { + ctx.Advance() + scope = ctx.Scope() + } + for ; scope.GetType() == ast.TypeExprClosure; nextScope() { + cs := scope.(*ast.ExprClosure) + if !hasUsageNamed(cs, varName) { + break + } + } + + t := traversers.NewAssignment(varN) tt := traverser.NewTraverser(t) ctx.Scope().Accept(tt) @@ -37,3 +55,22 @@ func (p *VariableProvider) Define(ctx *context.Ctx) ([]*definition.Definition, e Identifier: nodeident.Get(t.Assignment), }}, nil } + +func hasUsageNamed(node *ast.ExprClosure, name string) bool { + for _, u := range node.Uses { + switch tu := u.(type) { + case *ast.ExprClosureUse: + switch tv := tu.Var.(type) { + case *ast.ExprVariable: + if nodeident.Get(tv.Name) == name { + return true + } + default: + log.Panicf("unexpected uses variable %T", tu.Var) + } + default: + log.Panicf("unexpected uses node %T", u) + } + } + return false +} diff --git a/internal/project/testdata/definitions/annotated/parameter.php b/internal/project/testdata/definitions/annotated/parameter.php index a841fae..89c4c7b 100644 --- a/internal/project/testdata/definitions/annotated/parameter.php +++ b/internal/project/testdata/definitions/annotated/parameter.php @@ -6,7 +6,7 @@ function foobar($foo) // @t_out(param_untyped_func, 17) @t_out(param_function_ca return $foo; // @t_in(param_untyped_func, 13) } -function closures($foo) // @t_out(param_decoy_closures, 19) +function closures($foo) // @t_out(param_decoy_closures, 19) @t_out(param_reassign, 19) { $closure = function ($foo) { // @t_out(param_untyped_closure, 26) $foo; // @t_in(param_untyped_closure, 10) @@ -18,7 +18,7 @@ function closures($foo) // @t_out(param_decoy_closures, 19) $foo; // @t_in(param_decoy_closures, 6) - $foo = fn($foo) => '.' . $foo; // @t_in(param_arrow_func, 30) @t_out(param_arrow_func, 15) @t_out(param_reassign, 5) + $foo = fn($foo) => '.' . $foo; // @t_in(param_arrow_func, 30) @t_out(param_arrow_func, 15) echo $foo('test'); // @t_in(param_reassign, 11) } diff --git a/internal/server/references.go b/internal/server/references.go index cf81b0a..8eeb7ab 100644 --- a/internal/server/references.go +++ b/internal/server/references.go @@ -20,6 +20,7 @@ import ( "github.com/laytan/go-lsp-protocol/pkg/lsp/protocol" "github.com/laytan/phpls/pkg/fqn" + "github.com/laytan/phpls/pkg/functional" "github.com/laytan/phpls/pkg/lsperrors" "github.com/laytan/phpls/pkg/nodeident" "github.com/laytan/phpls/pkg/position" @@ -80,6 +81,18 @@ func (s *Server) references( } } + if id, ok := dctx.Current().(*ast.Identifier); ok && + dctx.DirectlyWrappedBy(ast.TypeExprVariable) { + v := traversers.NewVariable(nodeident.Get(id), true) + tv := traverser.NewTraverser(v) + dctx.Scope().Accept(tv) + return functional.Map(v.Results, func(n *ast.ExprVariable) protocol.Location { + loc := position.AstToLspLocation(d.Path, n.Name.GetPosition()) + loc.Range.Start.Character++ // The new name does not have the $, so add a col so it stays. + return loc + }), nil + } + switch td := dctx.Current().(type) { case *ast.StmtClass, *ast.StmtInterface, *ast.StmtTrait: return s.classReferences(ctx, dctx, d), nil diff --git a/pkg/traversers/assignment.go b/pkg/traversers/assignment.go index a2dff3f..206bc76 100644 --- a/pkg/traversers/assignment.go +++ b/pkg/traversers/assignment.go @@ -1,6 +1,8 @@ package traversers import ( + "log" + "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/php-parser/pkg/visitor" "github.com/laytan/phpls/pkg/nodeident" @@ -24,6 +26,10 @@ type Assignment struct { } func (a *Assignment) EnterNode(node ast.Vertex) bool { + if a.Assignment != nil { + return false + } + defer func() { a.isFirst = false }() // Only check the current scope. @@ -53,6 +59,18 @@ func (a *Assignment) EnterNode(node ast.Vertex) bool { return true } +func (a *Assignment) ExprClosureUse(node *ast.ExprClosureUse) { + switch tn := node.Var.(type) { + case *ast.ExprVariable: + if nodeident.Get(tn) == nodeident.Get(a.variable) { + a.Assignment = tn + a.Scope = node + } + default: + log.Panicf("unexpected use var type %T", node.Var) + } +} + func (a *Assignment) Parameter(param *ast.Parameter) { varName := nodeident.Get(a.variable) if varName == nodeident.Get(param.Var.(*ast.ExprVariable)) { diff --git a/pkg/traversers/variable.go b/pkg/traversers/variable.go index 45c361e..4d4df77 100644 --- a/pkg/traversers/variable.go +++ b/pkg/traversers/variable.go @@ -1,27 +1,67 @@ package traversers import ( + "log" + "github.com/laytan/php-parser/pkg/ast" "github.com/laytan/php-parser/pkg/visitor" "github.com/laytan/phpls/pkg/nodeident" + "github.com/laytan/phpls/pkg/nodescopes" ) type Variable struct { visitor.Null - name string - Result *ast.ExprVariable + target string + Results []*ast.ExprVariable + multiple bool + isFirst bool } -func NewVariable(name string) *Variable { - return &Variable{name: name} +func NewVariable(name string, multiple bool) *Variable { + return &Variable{isFirst: true, target: name, multiple: multiple} } func (v *Variable) EnterNode(node ast.Vertex) bool { - return v.Result == nil + if !v.multiple && len(v.Results) > 0 { + return false + } + + defer func() { v.isFirst = false }() + if v.isFirst { + return true + } + + switch tn := node.(type) { + case *ast.ExprClosure: + return hasUsageNamed(tn, v.target) + case *ast.ExprArrowFunction: + return true + default: + return !nodescopes.IsScope(node.GetType()) + } } func (v *Variable) ExprVariable(node *ast.ExprVariable) { - if v.name == nodeident.Get(node) { - v.Result = node + if nodeident.Get(node.Name) == v.target { + v.Results = append(v.Results, node) + } +} + +func hasUsageNamed(node *ast.ExprClosure, name string) bool { + for _, u := range node.Uses { + switch tu := u.(type) { + case *ast.ExprClosureUse: + switch tv := tu.Var.(type) { + case *ast.ExprVariable: + if nodeident.Get(tv.Name) == name { + return true + } + default: + log.Panicf("unexpected uses variable %T", tu.Var) + } + default: + log.Panicf("unexpected uses node %T", u) + } } + return false } From 4a6fd8158450b0f6bd8539e4692ae815e6d9ad72 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Sat, 11 Nov 2023 21:47:48 +0100 Subject: [PATCH 12/12] fix: automatic use statement completion --- internal/project/completion.go | 10 ++++++---- internal/server/completion.go | 2 +- pkg/fqn/traverser.go | 6 ++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/project/completion.go b/internal/project/completion.go index dfa72ab..5c1e4ac 100644 --- a/internal/project/completion.go +++ b/internal/project/completion.go @@ -414,14 +414,15 @@ func AddExprCompletions( } scopes := definition.ContextToScopes(ctx) - res, lastClass, left := expr.Resolve(subj, scopes) + _, lastClass, left := expr.Resolve(subj, scopes) if left != 0 { log.Println("[WARN]: could not resolve reconstructed expression to provide completion") return } - - log.Printf("[DEBUG]: res: %#v", res) - log.Printf("[DEBUG]: lastClass: %#v", lastClass) + if lastClass == nil { + log.Printf("[INFO]: resolved expression has no resulting class to create completions from") + return + } cls, err := symbol.NewClassLikeFromFQN(wrkspc.NewRooter(ctx.Path(), ctx.Root()), lastClass) if err != nil { @@ -656,6 +657,7 @@ func AddFromIndex( // Add completion data for automatic use statements. if nodescopes.IsClassLike(node.Kind) { item.Data = CompletionData(pos, node) + item.Detail = node.FQN.String() // Adding an additional text edit, so the client shows it in the UI. // The actual text edit is added in the Resolve method. item.AdditionalTextEdits = []protocol.TextEdit{{}} diff --git a/internal/server/completion.go b/internal/server/completion.go index 7e0bcd1..f3b15bf 100644 --- a/internal/server/completion.go +++ b/internal/server/completion.go @@ -87,7 +87,7 @@ func (s *Server) additionalTextEdits( item *protocol.CompletionItem, pos *position.Position, ) []protocol.TextEdit { - return InsertUseStmt(fqn.New(`\`+item.Detail), pos) + return InsertUseStmt(fqn.New(item.Detail), pos) } // This works but doesn't really look good, but that is probably a problem with the Hover method. diff --git a/pkg/fqn/traverser.go b/pkg/fqn/traverser.go index 4d7745b..c286f5a 100644 --- a/pkg/fqn/traverser.go +++ b/pkg/fqn/traverser.go @@ -1,6 +1,7 @@ package fqn import ( + "log" "strings" "github.com/laytan/php-parser/pkg/ast" @@ -40,6 +41,11 @@ func (f *Traverser) ResultFor2(position *position.Position, name string) *FQN { func (f *Traverser) ResultFor(name ast.Vertex) *FQN { nv := nodeident.Get(name) + if nv == "" { + log.Printf("[WARN]: fqn.ResultFor called with node of type %T without a resolved name", name) + return nil + } + // Handle self and static by returning the class the block is in. if f.block != nil && f.blockClass != nil { if nv == "self" || nv == "static" {