From 77195fbbb1d33ec07b4165c8305f122a07761ac8 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 4 Apr 2024 14:39:09 -0400 Subject: [PATCH 1/4] Fix rune start positions for left recursive expressions --- pkg/schemadsl/parser/parser_impl.go | 6 +-- pkg/schemadsl/parser/parser_test.go | 1 + pkg/schemadsl/parser/tests/arrow.zed.expected | 2 +- .../parser/tests/associativity.zed.expected | 6 +-- .../parser/tests/multidef.zed.expected | 2 +- .../parser/tests/multiparen.zed.expected | 4 +- pkg/schemadsl/parser/tests/nil.zed.expected | 6 +-- pkg/schemadsl/parser/tests/unionpos.zed | 5 ++ .../parser/tests/unionpos.zed.expected | 49 +++++++++++++++++++ 9 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 pkg/schemadsl/parser/tests/unionpos.zed create mode 100644 pkg/schemadsl/parser/tests/unionpos.zed.expected diff --git a/pkg/schemadsl/parser/parser_impl.go b/pkg/schemadsl/parser/parser_impl.go index 90d7672e08..76f2bf3839 100644 --- a/pkg/schemadsl/parser/parser_impl.go +++ b/pkg/schemadsl/parser/parser_impl.go @@ -245,8 +245,7 @@ func (p *sourceParser) tryConsumeWithComments(types ...lexer.TokenType) (comment // properly handles decoration of the nodes with their proper start and end run locations and // comments. func (p *sourceParser) performLeftRecursiveParsing(subTryExprFn tryParserFn, rightNodeBuilder rightNodeConstructor, rightTokenTester lookaheadParserFn, operatorTokens ...lexer.TokenType) (AstNode, bool) { - var currentLeftToken commentedLexeme - currentLeftToken = p.currentToken + leftMostToken := p.currentToken // Consume the left side of the expression. leftNode, ok := subTryExprFn() @@ -290,11 +289,10 @@ func (p *sourceParser) performLeftRecursiveParsing(subTryExprFn tryParserFn, rig return currentLeftNode, true } - p.decorateStartRuneAndComments(exprNode, currentLeftToken) + p.decorateStartRuneAndComments(exprNode, leftMostToken) p.decorateEndRune(exprNode, p.previousToken) currentLeftNode = exprNode - currentLeftToken = operatorToken } return currentLeftNode, true diff --git a/pkg/schemadsl/parser/parser_test.go b/pkg/schemadsl/parser/parser_test.go index 40a461538b..af64f26014 100644 --- a/pkg/schemadsl/parser/parser_test.go +++ b/pkg/schemadsl/parser/parser_test.go @@ -117,6 +117,7 @@ func TestParser(t *testing.T) { {"associativity test", "associativity"}, {"super large test", "superlarge"}, {"invalid permission name test", "invalid_perm_name"}, + {"union positions test", "unionpos"}, } for _, test := range parserTests { diff --git a/pkg/schemadsl/parser/tests/arrow.zed.expected b/pkg/schemadsl/parser/tests/arrow.zed.expected index f8d4e49dd1..576f3bbad8 100644 --- a/pkg/schemadsl/parser/tests/arrow.zed.expected +++ b/pkg/schemadsl/parser/tests/arrow.zed.expected @@ -29,7 +29,7 @@ NodeTypeFile NodeTypeArrowExpression end-rune = 66 input-source = arrow test - start-rune = 57 + start-rune = 54 left-expr => NodeTypeArrowExpression end-rune = 61 diff --git a/pkg/schemadsl/parser/tests/associativity.zed.expected b/pkg/schemadsl/parser/tests/associativity.zed.expected index 0656636e76..608d02fb5f 100644 --- a/pkg/schemadsl/parser/tests/associativity.zed.expected +++ b/pkg/schemadsl/parser/tests/associativity.zed.expected @@ -18,7 +18,7 @@ NodeTypeFile NodeTypeUnionExpression end-rune = 53 input-source = associativity test - start-rune = 47 + start-rune = 45 left-expr => NodeTypeUnionExpression end-rune = 49 @@ -51,7 +51,7 @@ NodeTypeFile NodeTypeExclusionExpression end-rune = 90 input-source = associativity test - start-rune = 84 + start-rune = 82 left-expr => NodeTypeExclusionExpression end-rune = 86 @@ -84,7 +84,7 @@ NodeTypeFile NodeTypeIntersectExpression end-rune = 130 input-source = associativity test - start-rune = 124 + start-rune = 122 left-expr => NodeTypeIntersectExpression end-rune = 126 diff --git a/pkg/schemadsl/parser/tests/multidef.zed.expected b/pkg/schemadsl/parser/tests/multidef.zed.expected index 9e190f6093..4a7b374749 100644 --- a/pkg/schemadsl/parser/tests/multidef.zed.expected +++ b/pkg/schemadsl/parser/tests/multidef.zed.expected @@ -105,7 +105,7 @@ NodeTypeFile NodeTypeUnionExpression end-rune = 281 input-source = multiple definition test - start-rune = 255 + start-rune = 248 left-expr => NodeTypeUnionExpression end-rune = 262 diff --git a/pkg/schemadsl/parser/tests/multiparen.zed.expected b/pkg/schemadsl/parser/tests/multiparen.zed.expected index 956f421678..5bf967dd30 100644 --- a/pkg/schemadsl/parser/tests/multiparen.zed.expected +++ b/pkg/schemadsl/parser/tests/multiparen.zed.expected @@ -18,12 +18,12 @@ NodeTypeFile NodeTypeUnionExpression end-rune = 76 input-source = multiple parens test - start-rune = 48 + start-rune = 38 left-expr => NodeTypeUnionExpression end-rune = 62 input-source = multiple parens test - start-rune = 42 + start-rune = 38 left-expr => NodeTypeUnionExpression end-rune = 46 diff --git a/pkg/schemadsl/parser/tests/nil.zed.expected b/pkg/schemadsl/parser/tests/nil.zed.expected index 68d6adacc8..90bc5578b8 100644 --- a/pkg/schemadsl/parser/tests/nil.zed.expected +++ b/pkg/schemadsl/parser/tests/nil.zed.expected @@ -28,7 +28,7 @@ NodeTypeFile NodeTypeUnionExpression end-rune = 92 input-source = nil test - start-rune = 82 + start-rune = 78 left-expr => NodeTypeUnionExpression end-rune = 86 @@ -60,7 +60,7 @@ NodeTypeFile NodeTypeExclusionExpression end-rune = 139 input-source = nil test - start-rune = 131 + start-rune = 117 left-expr => NodeTypeExclusionExpression end-rune = 133 @@ -70,7 +70,7 @@ NodeTypeFile NodeTypeUnionExpression end-rune = 128 input-source = nil test - start-rune = 120 + start-rune = 118 left-expr => NodeTypeUnionExpression end-rune = 122 diff --git a/pkg/schemadsl/parser/tests/unionpos.zed b/pkg/schemadsl/parser/tests/unionpos.zed new file mode 100644 index 0000000000..bdb37b7114 --- /dev/null +++ b/pkg/schemadsl/parser/tests/unionpos.zed @@ -0,0 +1,5 @@ +definition user {} + +definition document { + permission view = third + editor + another +} \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/unionpos.zed.expected b/pkg/schemadsl/parser/tests/unionpos.zed.expected new file mode 100644 index 0000000000..1fc3353a86 --- /dev/null +++ b/pkg/schemadsl/parser/tests/unionpos.zed.expected @@ -0,0 +1,49 @@ +NodeTypeFile + end-rune = 86 + input-source = union positions test + start-rune = 0 + child-node => + NodeTypeDefinition + definition-name = user + end-rune = 17 + input-source = union positions test + start-rune = 0 + NodeTypeDefinition + definition-name = document + end-rune = 86 + input-source = union positions test + start-rune = 20 + child-node => + NodeTypePermission + end-rune = 84 + input-source = union positions test + relation-name = view + start-rune = 43 + compute-expression => + NodeTypeUnionExpression + end-rune = 84 + input-source = union positions test + start-rune = 61 + left-expr => + NodeTypeUnionExpression + end-rune = 74 + input-source = union positions test + start-rune = 61 + left-expr => + NodeTypeIdentifier + end-rune = 65 + identifier-value = third + input-source = union positions test + start-rune = 61 + right-expr => + NodeTypeIdentifier + end-rune = 74 + identifier-value = editor + input-source = union positions test + start-rune = 69 + right-expr => + NodeTypeIdentifier + end-rune = 84 + identifier-value = another + input-source = union positions test + start-rune = 78 \ No newline at end of file From 228f9b71a54349d71fda648dedb423f6dbfd5c44 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 4 Apr 2024 14:42:37 -0400 Subject: [PATCH 2/4] Add additional resolver test --- pkg/development/resolver_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/development/resolver_test.go b/pkg/development/resolver_test.go index 376c696639..11b084f994 100644 --- a/pkg/development/resolver_test.go +++ b/pkg/development/resolver_test.go @@ -168,6 +168,29 @@ func TestResolver(t *testing.T) { TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, }, }, + { + name: "longer test", + schema: `definition user {} + +definition document { + relation viewer: user + relation editor: user + relation third: user + permission another = viewer + permission view = third + editor + another +}`, + line: 7, + column: 19, + expectedReference: &SchemaReference{ + Source: input.Source("test"), + Position: input.Position{LineNumber: 7, ColumnPosition: 19}, + Text: "third", + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: "relation third", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 5, ColumnPosition: 1}, + }, + }, } for _, tc := range tcs { From caf69d8bdfed12b717d817e535789ee28f78e0e3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 4 Apr 2024 15:30:59 -0400 Subject: [PATCH 3/4] Add hover support to the LSP implementation --- internal/lsp/handlers.go | 127 ++++++++++++++--- internal/lsp/lsp.go | 6 +- internal/lsp/lsp_test.go | 35 ++++- internal/lsp/lspdefs.go | 16 ++- pkg/development/resolver.go | 161 +++++++++++++++------- pkg/development/resolver_test.go | 188 +++++++++++++++++++------- pkg/schemadsl/compiler/development.go | 10 +- pkg/schemadsl/generator/generator.go | 17 +++ 8 files changed, 436 insertions(+), 124 deletions(-) diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index f11dd1b52c..ebb7c20f25 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -13,7 +13,9 @@ import ( log "github.com/authzed/spicedb/internal/logging" "github.com/authzed/spicedb/pkg/development" developerv1 "github.com/authzed/spicedb/pkg/proto/developer/v1" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" "github.com/authzed/spicedb/pkg/schemadsl/generator" + "github.com/authzed/spicedb/pkg/schemadsl/input" ) func (s *Server) textDocDiagnostic(ctx context.Context, r *jsonrpc2.Request) (FullDocumentDiagnosticReport, error) { @@ -45,7 +47,7 @@ func (s *Server) textDocDiagnostic(ctx context.Context, r *jsonrpc2.Request) (Fu func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([]lsp.Diagnostic, error) { diagnostics := make([]lsp.Diagnostic, 0) // Important: must not be nil for the consumer on the client side - if err := s.withFiles(func(files *persistent.Map[lsp.DocumentURI, string]) error { + if err := s.withFiles(func(files *persistent.Map[lsp.DocumentURI, trackedFile]) error { file, ok := files.Get(uri) if !ok { log.Warn(). @@ -56,7 +58,7 @@ func (s *Server) computeDiagnostics(ctx context.Context, uri lsp.DocumentURI) ([ } _, devErrs, err := development.NewDevContext(ctx, &developerv1.RequestContext{ - Schema: file, + Schema: file.contents, Relationships: nil, }) if err != nil { @@ -88,7 +90,7 @@ func (s *Server) textDocDidChange(ctx context.Context, r *jsonrpc2.Request, conn return nil, err } - s.files.Set(params.TextDocument.URI, params.ContentChanges[0].Text, nil) + s.files.Set(params.TextDocument.URI, trackedFile{params.ContentChanges[0].Text, nil}, nil) if err := s.publishDiagnosticsIfNecessary(ctx, conn, params.TextDocument.URI); err != nil { return nil, err @@ -115,7 +117,7 @@ func (s *Server) textDocDidOpen(ctx context.Context, r *jsonrpc2.Request, conn * uri := params.TextDocument.URI contents := params.TextDocument.Text - s.files.Set(uri, contents, nil) + s.files.Set(uri, trackedFile{contents, nil}, nil) if err := s.publishDiagnosticsIfNecessary(ctx, conn, uri); err != nil { return nil, err @@ -150,36 +152,113 @@ func (s *Server) publishDiagnosticsIfNecessary(ctx context.Context, conn *jsonrp }) } -func (s *Server) textDocFormat(ctx context.Context, r *jsonrpc2.Request) ([]lsp.TextEdit, error) { - params, err := unmarshalParams[lsp.DocumentFormattingParams](r) +func (s *Server) getCompiledContents(path lsp.DocumentURI, files *persistent.Map[lsp.DocumentURI, trackedFile]) (*compiler.CompiledSchema, error) { + file, ok := files.Get(path) + if !ok { + return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInternalError, Message: "file not found"} + } + + compiled := file.parsed + if compiled != nil { + return compiled, nil + } + + justCompiled, derr, err := development.CompileSchema(file.contents) + if err != nil || derr != nil { + return nil, err + } + + files.Set(path, trackedFile{file.contents, justCompiled}, nil) + return justCompiled, nil +} + +func (s *Server) textDocHover(_ context.Context, r *jsonrpc2.Request) (*Hover, error) { + params, err := unmarshalParams[lsp.TextDocumentPositionParams](r) if err != nil { return nil, err } - var formatted string - err = s.withFiles(func(files *persistent.Map[lsp.DocumentURI, string]) error { - file, ok := files.Get(params.TextDocument.URI) - if !ok { - log.Warn(). - Str("uri", string(params.TextDocument.URI)). - Msg("file not found for formatting") + var hoverContents *Hover + err = s.withFiles(func(files *persistent.Map[lsp.DocumentURI, trackedFile]) error { + compiled, err := s.getCompiledContents(params.TextDocument.URI, files) + if err != nil { + return err + } - return &jsonrpc2.Error{Code: jsonrpc2.CodeInternalError, Message: "file not found"} + resolver, err := development.NewResolver(compiled) + if err != nil { + return err } - dctx, devErrs, err := development.NewDevContext(ctx, &developerv1.RequestContext{ - Schema: file, - Relationships: nil, - }) + position := input.Position{ + LineNumber: params.Position.Line, + ColumnPosition: params.Position.Character, + } + + resolved, err := resolver.ReferenceAtPosition(input.Source("schema"), position) if err != nil { return err } - if len(devErrs.GetInputErrors()) > 0 { + if resolved == nil { return nil } - formattedSchema, _, err := generator.GenerateSchema(dctx.CompiledSchema.OrderedDefinitions) + var lspRange *lsp.Range + if resolved.TargetPosition != nil { + lspRange = &lsp.Range{ + Start: lsp.Position{ + Line: resolved.TargetPosition.LineNumber, + Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset, + }, + End: lsp.Position{ + Line: resolved.TargetPosition.LineNumber, + Character: resolved.TargetPosition.ColumnPosition + resolved.TargetNamePositionOffset + len(resolved.Text), + }, + } + } + + if resolved.TargetSourceCode != "" { + hoverContents = &Hover{ + Contents: MarkupContent{ + Language: "spicedb", + Value: resolved.TargetSourceCode, + }, + Range: lspRange, + } + } else { + hoverContents = &Hover{ + Contents: MarkupContent{ + Kind: "markdown", + Value: resolved.ReferenceMarkdown, + }, + Range: lspRange, + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return hoverContents, nil +} + +func (s *Server) textDocFormat(_ context.Context, r *jsonrpc2.Request) ([]lsp.TextEdit, error) { + params, err := unmarshalParams[lsp.DocumentFormattingParams](r) + if err != nil { + return nil, err + } + + var formatted string + err = s.withFiles(func(files *persistent.Map[lsp.DocumentURI, trackedFile]) error { + compiled, err := s.getCompiledContents(params.TextDocument.URI, files) + if err != nil { + return err + } + + formattedSchema, _, err := generator.GenerateSchema(compiled.OrderedDefinitions) if err != nil { return err } @@ -236,6 +315,7 @@ func (s *Server) initialize(_ context.Context, r *jsonrpc2.Request) (any, error) CompletionProvider: &lsp.CompletionOptions{TriggerCharacters: []string{"."}}, DocumentFormattingProvider: true, DiagnosticProvider: &DiagnosticOptions{Identifier: "spicedb", InterFileDependencies: false, WorkspaceDiagnostics: false}, + HoverProvider: true, }, }, nil } @@ -247,7 +327,12 @@ func (s *Server) shutdown() error { return nil } -func (s *Server) withFiles(fn func(*persistent.Map[lsp.DocumentURI, string]) error) error { +type trackedFile struct { + contents string + parsed *compiler.CompiledSchema +} + +func (s *Server) withFiles(fn func(*persistent.Map[lsp.DocumentURI, trackedFile]) error) error { clone := s.files.Clone() defer clone.Destroy() return fn(clone) diff --git a/internal/lsp/lsp.go b/internal/lsp/lsp.go index cf248b700a..4d05e89250 100644 --- a/internal/lsp/lsp.go +++ b/internal/lsp/lsp.go @@ -25,7 +25,7 @@ const ( // Server is a Language Server Protocol server for SpiceDB schema development. type Server struct { - files *persistent.Map[lsp.DocumentURI, string] + files *persistent.Map[lsp.DocumentURI, trackedFile] state serverState requestsDiagnostics bool @@ -35,7 +35,7 @@ type Server struct { func NewServer() *Server { return &Server{ state: serverStateNotInitialized, - files: persistent.NewMap[lsp.DocumentURI, string](func(x, y lsp.DocumentURI) bool { + files: persistent.NewMap[lsp.DocumentURI, trackedFile](func(x, y lsp.DocumentURI) bool { return string(x) < string(y) }), } @@ -86,6 +86,8 @@ func (s *Server) handle(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Re result, err = s.textDocDiagnostic(ctx, r) case "textDocument/formatting": result, err = s.textDocFormat(ctx, r) + case "textDocument/hover": + result, err = s.textDocHover(ctx, r) default: log.Ctx(ctx).Warn(). Str("method", r.Method). diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index dffc981e42..08ff2b1916 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -28,13 +28,13 @@ func TestDocumentChange(t *testing.T) { contents, ok := tester.server.files.Get("file:///test") require.True(t, ok) - require.Equal(t, "test", contents) + require.Equal(t, "test", contents.contents) tester.setFileContents("file:///test", "test2") contents, ok = tester.server.files.Get("file:///test") require.True(t, ok) - require.Equal(t, "test2", contents) + require.Equal(t, "test2", contents.contents) } func TestDocumentNoDiagnostics(t *testing.T) { @@ -138,7 +138,7 @@ func TestDocumentOpenedClosed(t *testing.T) { contents, ok := tester.server.files.Get(lsp.DocumentURI("file:///test")) require.True(t, ok) - require.Equal(t, "definition user{}", contents) + require.Equal(t, "definition user{}", contents.contents) sendAndReceive[any](tester, "textDocument/didClose", lsp.DidCloseTextDocumentParams{ TextDocument: lsp.TextDocumentIdentifier{ @@ -149,3 +149,32 @@ func TestDocumentOpenedClosed(t *testing.T) { _, ok = tester.server.files.Get(lsp.DocumentURI("file:///test")) require.False(t, ok) } + +func TestDocumentHover(t *testing.T) { + tester := newLSPTester(t) + tester.initialize() + + sendAndReceive[any](tester, "textDocument/didOpen", lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: lsp.DocumentURI("file:///test"), + LanguageID: "test", + Version: 1, + Text: `definition user {} + +definition resource { + relation viewer: user +} +`, + }, + }) + + resp, _ := sendAndReceive[Hover](tester, "textDocument/hover", lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{ + URI: lsp.DocumentURI("file:///test"), + }, + Position: lsp.Position{Line: 3, Character: 18}, + }) + + require.Equal(t, "definition user {}", resp.Contents.Value) + require.Equal(t, "spicedb", resp.Contents.Language) +} diff --git a/internal/lsp/lspdefs.go b/internal/lsp/lspdefs.go index a6e015701d..0e3dd50466 100644 --- a/internal/lsp/lspdefs.go +++ b/internal/lsp/lspdefs.go @@ -1,6 +1,8 @@ package lsp -import baselsp "github.com/sourcegraph/go-lsp" +import ( + baselsp "github.com/sourcegraph/go-lsp" +) type InitializeResult struct { Capabilities ServerCapabilities `json:"capabilities,omitempty"` @@ -11,6 +13,7 @@ type ServerCapabilities struct { CompletionProvider *baselsp.CompletionOptions `json:"completionProvider,omitempty"` DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"` DiagnosticProvider *DiagnosticOptions `json:"diagnosticProvider,omitempty"` + HoverProvider bool `json:"hoverProvider,omitempty"` } type DiagnosticOptions struct { @@ -58,3 +61,14 @@ type DiagnosticWorkspaceClientCapabilities struct { // `textDocument/diagnostic` request. RefreshSupport bool `json:"refreshSupport,omitempty"` } + +type Hover struct { + Contents MarkupContent `json:"contents"` + Range *baselsp.Range `json:"range,omitempty"` +} + +type MarkupContent struct { + Kind string `json:"kind,omitempty"` + Language string `json:"language,omitempty"` + Value string `json:"value"` +} diff --git a/pkg/development/resolver.go b/pkg/development/resolver.go index 3f4af972e9..1203a2f021 100644 --- a/pkg/development/resolver.go +++ b/pkg/development/resolver.go @@ -8,6 +8,7 @@ import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/schemadsl/compiler" "github.com/authzed/spicedb/pkg/schemadsl/dslshape" + "github.com/authzed/spicedb/pkg/schemadsl/generator" "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/typesystem" ) @@ -46,6 +47,13 @@ type SchemaReference struct { // TargetPosition is the position of the target node, if any. TargetPosition *input.Position + + // TargetSourceCode is the source code representation of the target, if any. + TargetSourceCode string + + // TargetNamePositionOffset is the offset from the target position from where the + // *name* of the target is found. + TargetNamePositionOffset int } // Resolver resolves references to schema nodes from source positions. @@ -81,14 +89,65 @@ func (r *Resolver) ReferenceAtPosition(source input.Source, position input.Posit return nil, nil } + relationReference := func(relation *core.Relation, ts *typesystem.TypeSystem) (*SchemaReference, error) { + relationPosition := input.Position{ + LineNumber: int(relation.SourcePosition.ZeroIndexedLineNumber), + ColumnPosition: int(relation.SourcePosition.ZeroIndexedColumnPosition), + } + + targetSourceCode, err := generator.GenerateRelationSource(relation) + if err != nil { + return nil, err + } + + if ts.IsPermission(relation.Name) { + return &SchemaReference{ + Source: source, + Position: position, + Text: relation.Name, + + ReferenceType: ReferenceTypePermission, + ReferenceMarkdown: fmt.Sprintf("permission %s", relation.Name), + + TargetSource: &source, + TargetPosition: &relationPosition, + TargetSourceCode: targetSourceCode, + TargetNamePositionOffset: len("permission "), + }, nil + } + + return &SchemaReference{ + Source: source, + Position: position, + Text: relation.Name, + + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: fmt.Sprintf("relation %s", relation.Name), + + TargetSource: &source, + TargetPosition: &relationPosition, + TargetSourceCode: targetSourceCode, + TargetNamePositionOffset: len("relation "), + }, nil + } + // Type reference. - if ts, ok := r.typeReferenceChain(nodeChain); ok { + if ts, relation, ok := r.typeReferenceChain(nodeChain); ok { + if relation != nil { + return relationReference(relation, ts) + } + def := ts.Namespace() defPosition := input.Position{ LineNumber: int(def.SourcePosition.ZeroIndexedLineNumber), ColumnPosition: int(def.SourcePosition.ZeroIndexedColumnPosition), } + targetSourceCode := fmt.Sprintf("definition %s {\n\t// ...\n}", def.Name) + if len(def.Relation) == 0 { + targetSourceCode = fmt.Sprintf("definition %s {}", def.Name) + } + return &SchemaReference{ Source: source, Position: position, @@ -97,8 +156,10 @@ func (r *Resolver) ReferenceAtPosition(source input.Source, position input.Posit ReferenceType: ReferenceTypeDefinition, ReferenceMarkdown: fmt.Sprintf("definition %s", def.Name), - TargetSource: &source, - TargetPosition: &defPosition, + TargetSource: &source, + TargetPosition: &defPosition, + TargetSourceCode: targetSourceCode, + TargetNamePositionOffset: len("definition "), }, nil } @@ -109,6 +170,19 @@ func (r *Resolver) ReferenceAtPosition(source input.Source, position input.Posit ColumnPosition: int(caveatDef.SourcePosition.ZeroIndexedColumnPosition), } + var caveatSourceCode strings.Builder + caveatSourceCode.WriteString(fmt.Sprintf("caveat %s(", caveatDef.Name)) + index := 0 + for paramName, paramType := range caveatDef.ParameterTypes { + if index > 0 { + caveatSourceCode.WriteString(", ") + } + + caveatSourceCode.WriteString(fmt.Sprintf("%s %s", paramName, caveats.ParameterTypeString(paramType))) + index++ + } + caveatSourceCode.WriteString(") {\n\t// ...\n}") + return &SchemaReference{ Source: source, Position: position, @@ -117,51 +191,21 @@ func (r *Resolver) ReferenceAtPosition(source input.Source, position input.Posit ReferenceType: ReferenceTypeCaveat, ReferenceMarkdown: fmt.Sprintf("caveat %s", caveatDef.Name), - TargetSource: &source, - TargetPosition: &defPosition, + TargetSource: &source, + TargetPosition: &defPosition, + TargetSourceCode: caveatSourceCode.String(), + TargetNamePositionOffset: len("caveat "), }, nil } // Relation reference. if relation, ts, ok := r.relationReferenceChain(nodeChain); ok { - relationPosition := input.Position{ - LineNumber: int(relation.SourcePosition.ZeroIndexedLineNumber), - ColumnPosition: int(relation.SourcePosition.ZeroIndexedColumnPosition), - } - - if ts.IsPermission(relation.Name) { - return &SchemaReference{ - Source: source, - Position: position, - Text: relation.Name, - - ReferenceType: ReferenceTypePermission, - ReferenceMarkdown: fmt.Sprintf("permission %s", relation.Name), - - TargetSource: &source, - TargetPosition: &relationPosition, - }, nil - } - - return &SchemaReference{ - Source: source, - Position: position, - Text: relation.Name, - - ReferenceType: ReferenceTypeRelation, - ReferenceMarkdown: fmt.Sprintf("relation %s", relation.Name), - - TargetSource: &source, - TargetPosition: &relationPosition, - }, nil + return relationReference(relation, ts) } // Caveat parameter used in expression. if caveatParamName, caveatDef, ok := r.caveatParamChain(nodeChain, source, position); ok { - caveatPosition := input.Position{ - LineNumber: int(caveatDef.SourcePosition.ZeroIndexedLineNumber), - ColumnPosition: int(caveatDef.SourcePosition.ZeroIndexedColumnPosition), - } + targetSourceCode := fmt.Sprintf("%s %s", caveatParamName, caveats.ParameterTypeString(caveatDef.ParameterTypes[caveatParamName])) return &SchemaReference{ Source: source, @@ -169,10 +213,10 @@ func (r *Resolver) ReferenceAtPosition(source input.Source, position input.Posit Text: caveatParamName, ReferenceType: ReferenceTypeCaveatParameter, - ReferenceMarkdown: fmt.Sprintf("%s %s", caveatParamName, caveats.ParameterTypeString(caveatDef.ParameterTypes[caveatParamName])), + ReferenceMarkdown: targetSourceCode, - TargetSource: &source, - TargetPosition: &caveatPosition, + TargetSource: &source, + TargetSourceCode: targetSourceCode, }, nil } @@ -280,17 +324,42 @@ func (r *Resolver) caveatTypeReferenceChain(nodeChain *compiler.NodeChain) (*cor return r.lookupCaveat(caveatName) } -func (r *Resolver) typeReferenceChain(nodeChain *compiler.NodeChain) (*typesystem.TypeSystem, bool) { +func (r *Resolver) typeReferenceChain(nodeChain *compiler.NodeChain) (*typesystem.TypeSystem, *core.Relation, bool) { if !nodeChain.HasHeadType(dslshape.NodeTypeSpecificTypeReference) { - return nil, false + return nil, nil, false } defName, err := nodeChain.Head().GetString(dslshape.NodeSpecificReferencePredicateType) if err != nil { - return nil, false + return nil, nil, false + } + + def, ok := r.lookupDefinition(defName) + if !ok { + return nil, nil, false + } + + relationName, err := nodeChain.Head().GetString(dslshape.NodeSpecificReferencePredicateRelation) + if err != nil { + return def, nil, true + } + + startingRune, err := nodeChain.Head().GetInt(dslshape.NodePredicateStartRune) + if err != nil { + return def, nil, true + } + + // If hover over the definition name, return the definition. + if nodeChain.ForRunePosition() < startingRune+len(defName) { + return def, nil, true + } + + relation, ok := def.GetRelation(relationName) + if !ok { + return nil, nil, false } - return r.lookupDefinition(defName) + return def, relation, true } func (r *Resolver) relationReferenceChain(nodeChain *compiler.NodeChain) (*core.Relation, *typesystem.TypeSystem, bool) { diff --git a/pkg/development/resolver_test.go b/pkg/development/resolver_test.go index 11b084f994..ae3c2a053f 100644 --- a/pkg/development/resolver_test.go +++ b/pkg/development/resolver_test.go @@ -31,13 +31,15 @@ func TestResolver(t *testing.T) { line: 4, column: 24, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 4, ColumnPosition: 24}, - Text: "viewer", - ReferenceType: ReferenceTypeRelation, - ReferenceMarkdown: "relation viewer", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 3, ColumnPosition: 4}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 4, ColumnPosition: 24}, + Text: "viewer", + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: "relation viewer", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 3, ColumnPosition: 4}, + TargetSourceCode: "relation viewer: user\n", + TargetNamePositionOffset: 9, }, }, { @@ -54,13 +56,15 @@ func TestResolver(t *testing.T) { line: 6, column: 33, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 6, ColumnPosition: 33}, - Text: "edit", - ReferenceType: ReferenceTypePermission, - ReferenceMarkdown: "permission edit", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 5, ColumnPosition: 4}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 6, ColumnPosition: 33}, + Text: "edit", + ReferenceType: ReferenceTypePermission, + ReferenceMarkdown: "permission edit", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 5, ColumnPosition: 4}, + TargetSourceCode: "permission edit = editor\n", + TargetNamePositionOffset: 11, }, }, { @@ -75,13 +79,92 @@ func TestResolver(t *testing.T) { line: 3, column: 24, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 3, ColumnPosition: 24}, - Text: "user", - ReferenceType: ReferenceTypeDefinition, - ReferenceMarkdown: "definition user", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 0, ColumnPosition: 0}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 3, ColumnPosition: 24}, + Text: "user", + ReferenceType: ReferenceTypeDefinition, + ReferenceMarkdown: "definition user", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 0, ColumnPosition: 0}, + TargetSourceCode: "definition user {}", + TargetNamePositionOffset: 11, + }, + }, + { + name: "subject relation type", + schema: `definition user {} + + definition group { + relation member: user + } + + definition resource { + relation viewer: group#member + permission view = viewer + } + `, + line: 7, + column: 24, + expectedReference: &SchemaReference{ + Source: input.Source("test"), + Position: input.Position{LineNumber: 7, ColumnPosition: 24}, + Text: "group", + ReferenceType: ReferenceTypeDefinition, + ReferenceMarkdown: "definition group", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, + TargetSourceCode: "definition group {\n\t// ...\n}", + TargetNamePositionOffset: 11, + }, + }, + { + name: "subject relation relation", + schema: `definition user {} + + definition group { + relation member: user + } + + definition resource { + relation viewer: group#member + permission view = viewer + } + `, + line: 7, + column: 32, + expectedReference: &SchemaReference{ + Source: input.Source("test"), + Position: input.Position{LineNumber: 7, ColumnPosition: 32}, + Text: "member", + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: "relation member", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 3, ColumnPosition: 4}, + TargetSourceCode: "relation member: user\n", + TargetNamePositionOffset: 9, + }, + }, + { + name: "filled in type", + schema: `definition user {} + + definition resource { + relation viewer: user | resource + permission view = viewer + } + `, + line: 3, + column: 29, + expectedReference: &SchemaReference{ + Source: input.Source("test"), + Position: input.Position{LineNumber: 3, ColumnPosition: 29}, + Text: "resource", + ReferenceType: ReferenceTypeDefinition, + ReferenceMarkdown: "definition resource", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, + TargetSourceCode: "definition resource {\n\t// ...\n}", + TargetNamePositionOffset: 11, }, }, { @@ -100,13 +183,15 @@ func TestResolver(t *testing.T) { line: 7, column: 35, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 7, ColumnPosition: 35}, - Text: "somecaveat", - ReferenceType: ReferenceTypeCaveat, - ReferenceMarkdown: "caveat somecaveat", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 7, ColumnPosition: 35}, + Text: "somecaveat", + ReferenceType: ReferenceTypeCaveat, + ReferenceMarkdown: "caveat somecaveat", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, + TargetSourceCode: "caveat somecaveat(someparam int) {\n\t// ...\n}", + TargetNamePositionOffset: 7, }, }, { @@ -121,13 +206,15 @@ func TestResolver(t *testing.T) { line: 4, column: 23, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 4, ColumnPosition: 23}, - Text: "viewer", - ReferenceType: ReferenceTypeRelation, - ReferenceMarkdown: "relation viewer", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 3, ColumnPosition: 4}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 4, ColumnPosition: 23}, + Text: "viewer", + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: "relation viewer", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 3, ColumnPosition: 4}, + TargetSourceCode: "relation viewer: user\n", + TargetNamePositionOffset: 9, }, }, { @@ -159,13 +246,14 @@ func TestResolver(t *testing.T) { line: 3, column: 6, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 3, ColumnPosition: 6}, - Text: "someparam", - ReferenceType: ReferenceTypeCaveatParameter, - ReferenceMarkdown: "someparam int", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 2, ColumnPosition: 3}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 3, ColumnPosition: 6}, + Text: "someparam", + ReferenceType: ReferenceTypeCaveatParameter, + ReferenceMarkdown: "someparam int", + TargetSource: &testSource, + TargetSourceCode: "someparam int", + TargetNamePositionOffset: 0, }, }, { @@ -182,13 +270,15 @@ definition document { line: 7, column: 19, expectedReference: &SchemaReference{ - Source: input.Source("test"), - Position: input.Position{LineNumber: 7, ColumnPosition: 19}, - Text: "third", - ReferenceType: ReferenceTypeRelation, - ReferenceMarkdown: "relation third", - TargetSource: &testSource, - TargetPosition: &input.Position{LineNumber: 5, ColumnPosition: 1}, + Source: input.Source("test"), + Position: input.Position{LineNumber: 7, ColumnPosition: 19}, + Text: "third", + ReferenceType: ReferenceTypeRelation, + ReferenceMarkdown: "relation third", + TargetSource: &testSource, + TargetPosition: &input.Position{LineNumber: 5, ColumnPosition: 1}, + TargetSourceCode: "relation third: user\n", + TargetNamePositionOffset: 9, }, }, } diff --git a/pkg/schemadsl/compiler/development.go b/pkg/schemadsl/compiler/development.go index 4f5340e6d8..f554239999 100644 --- a/pkg/schemadsl/compiler/development.go +++ b/pkg/schemadsl/compiler/development.go @@ -15,7 +15,8 @@ type DSLNode interface { // NodeChain is a chain of nodes in the DSL AST. type NodeChain struct { - nodes []DSLNode + nodes []DSLNode + runePosition int } // Head returns the head node of the chain. @@ -28,6 +29,11 @@ func (nc *NodeChain) HasHeadType(nodeType dslshape.NodeType) bool { return nc.nodes[0].GetType() == nodeType } +// ForRunePosition returns the rune position of the chain. +func (nc *NodeChain) ForRunePosition() int { + return nc.runePosition +} + // FindNodeOfType returns the first node of the given type in the chain, if any. func (nc *NodeChain) FindNodeOfType(nodeType dslshape.NodeType) DSLNode { for _, node := range nc.nodes { @@ -74,7 +80,7 @@ func PositionToAstNodeChain(schema *CompiledSchema, source input.Source, positio return nil, nil } - return &NodeChain{nodes: found}, nil + return &NodeChain{nodes: found, runePosition: runePosition}, nil } func runePositionToAstNodeChain(node *dslNode, runePosition int) ([]DSLNode, error) { diff --git a/pkg/schemadsl/generator/generator.go b/pkg/schemadsl/generator/generator.go index 0c16380d69..6fbc95b53c 100644 --- a/pkg/schemadsl/generator/generator.go +++ b/pkg/schemadsl/generator/generator.go @@ -89,6 +89,23 @@ func GenerateSource(namespace *core.NamespaceDefinition) (string, bool, error) { return generator.buf.String(), !generator.hasIssue, nil } +// GenerateRelationSource generates a DSL view of the given relation definition. +func GenerateRelationSource(relation *core.Relation) (string, error) { + generator := &sourceGenerator{ + indentationLevel: 0, + hasNewline: true, + hasBlankline: true, + hasNewScope: true, + } + + err := generator.emitRelation(relation) + if err != nil { + return "", err + } + + return generator.buf.String(), nil +} + func (sg *sourceGenerator) emitCaveat(caveat *core.CaveatDefinition) error { sg.emitComments(caveat.Metadata) sg.append("caveat ") From adb641e9a11e47fd08deec3822fdc5742bc6ee81 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 4 Apr 2024 16:14:36 -0400 Subject: [PATCH 4/4] Fix logging in LSP --- pkg/cmd/lsp.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/cmd/lsp.go b/pkg/cmd/lsp.go index e93ec6d1d4..a3ed139692 100644 --- a/pkg/cmd/lsp.go +++ b/pkg/cmd/lsp.go @@ -4,10 +4,16 @@ import ( "context" "time" + "github.com/go-logr/zerologr" + "github.com/jzelinskie/cobrautil/v2" + "github.com/jzelinskie/cobrautil/v2/cobrazerolog" + "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/authzed/spicedb/internal/logging" "github.com/authzed/spicedb/internal/lsp" "github.com/authzed/spicedb/pkg/cmd/termination" + "github.com/authzed/spicedb/pkg/releases" ) // LSPConfig is the configuration for the LSP command. @@ -33,6 +39,15 @@ func NewLSPCommand(programName string, config *LSPConfig) *cobra.Command { return &cobra.Command{ Use: "lsp", Short: "serve language server protocol", + PreRunE: cobrautil.CommandStack( + cobrautil.SyncViperDotEnvPreRunE(programName, "spicedb.env", zerologr.New(&logging.Logger)), + cobrazerolog.New( + cobrazerolog.WithTarget(func(logger zerolog.Logger) { + logging.SetGlobalLogger(logger) + }), + ).RunE(), + releases.CheckAndLogRunE(), + ), RunE: termination.PublishError(func(cmd *cobra.Command, args []string) error { srv, err := config.Complete(cmd.Context()) if err != nil {