Skip to content

Commit 1b086d5

Browse files
authored
Merge pull request #59 from kitagry/complete-declaration
Complete declaration
2 parents 7326a9a + 0c863ec commit 1b086d5

File tree

6 files changed

+200
-4
lines changed

6 files changed

+200
-4
lines changed

langserver/internal/source/completion/completion.go

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func (c *completor) Complete(ctx context.Context, parsedFile file.ParsedFile, po
3535

3636
result = append(result, c.completeColumns(ctx, parsedFile, position)...)
3737
result = append(result, c.completeBuiltinFunction(ctx, parsedFile, position)...)
38+
result = append(result, c.completeDeclaration(ctx, parsedFile, position)...)
3839
return result, nil
3940
}
4041

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package completion
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/goccy/go-zetasql"
9+
"github.com/goccy/go-zetasql/ast"
10+
"github.com/kitagry/bqls/langserver/internal/lsp"
11+
"github.com/kitagry/bqls/langserver/internal/source/file"
12+
)
13+
14+
func (c *completor) completeDeclaration(ctx context.Context, parsedFile file.ParsedFile, position lsp.Position) []CompletionItem {
15+
termOffset := parsedFile.TermOffset(position)
16+
17+
// When the cursor is in the middle of the table path, do not suggest the built-in functions.
18+
tablePathNode, ok := file.SearchAstNode[*ast.TablePathExpressionNode](parsedFile.Node, parsedFile.TermOffset(position))
19+
if ok && tablePathNode.ParseLocationRange().End().ByteOffset() != termOffset {
20+
return []CompletionItem{}
21+
}
22+
23+
incompleteColumnName := parsedFile.FindIncompleteColumnName(position)
24+
25+
declarations := file.ListAstNode[*ast.VariableDeclarationNode](parsedFile.Node)
26+
27+
result := make([]CompletionItem, 0, len(declarations))
28+
for _, d := range declarations {
29+
iList := d.VariableList().IdentifierList()
30+
for _, l := range iList {
31+
if !strings.HasPrefix(l.Name(), incompleteColumnName) {
32+
continue
33+
}
34+
result = append(result, CompletionItem{
35+
Kind: lsp.CIKVariable,
36+
NewText: l.Name(),
37+
Documentation: lsp.MarkupContent{
38+
Kind: lsp.MKMarkdown,
39+
Value: fmt.Sprintf("```sql\n%s```", zetasql.Unparse(d)),
40+
},
41+
})
42+
}
43+
}
44+
return result
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package completion
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
8+
bq "cloud.google.com/go/bigquery"
9+
"github.com/golang/mock/gomock"
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/kitagry/bqls/langserver/internal/bigquery/mock_bigquery"
12+
"github.com/kitagry/bqls/langserver/internal/lsp"
13+
"github.com/kitagry/bqls/langserver/internal/source/file"
14+
"github.com/kitagry/bqls/langserver/internal/source/helper"
15+
"github.com/sirupsen/logrus"
16+
)
17+
18+
func TestProject_CompleteDeclaration(t *testing.T) {
19+
tests := map[string]struct {
20+
files map[string]string
21+
bqTableMetadataMap map[string]*bq.TableMetadata
22+
23+
expectCompletionItems []CompletionItem
24+
}{
25+
"Complete variable declaration": {
26+
files: map[string]string{
27+
"file1.sql": "DECLARE a INT64;\n" + "SELECT | FROM `project.dataset.table`",
28+
},
29+
bqTableMetadataMap: map[string]*bq.TableMetadata{
30+
"project.dataset.table": {
31+
Schema: bq.Schema{
32+
{
33+
Name: "id",
34+
Type: bq.IntegerFieldType,
35+
Description: "id description",
36+
},
37+
{
38+
Name: "name",
39+
Type: bq.StringFieldType,
40+
},
41+
},
42+
},
43+
},
44+
expectCompletionItems: []CompletionItem{
45+
{
46+
Kind: lsp.CIKVariable,
47+
NewText: "a",
48+
Documentation: lsp.MarkupContent{
49+
Kind: lsp.MKMarkdown,
50+
Value: "```sql\nDECLARE a INT64\n```",
51+
},
52+
},
53+
},
54+
},
55+
}
56+
57+
for n, tt := range tests {
58+
t.Run(n, func(t *testing.T) {
59+
ctrl := gomock.NewController(t)
60+
bqClient := mock_bigquery.NewMockClient(ctrl)
61+
for tablePath, schema := range tt.bqTableMetadataMap {
62+
tablePathSplitted := strings.Split(tablePath, ".")
63+
if len(tablePathSplitted) != 3 {
64+
t.Fatalf("table path length should be 3, got %s", tablePath)
65+
}
66+
bqClient.EXPECT().GetTableMetadata(gomock.Any(), tablePathSplitted[0], tablePathSplitted[1], tablePathSplitted[2]).Return(schema, nil).MinTimes(0)
67+
}
68+
bqClient.EXPECT().ListTables(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).MinTimes(0)
69+
logger := logrus.New()
70+
logger.SetLevel(logrus.DebugLevel)
71+
72+
analyzer := file.NewAnalyzer(logger, bqClient)
73+
completor := New(logger, analyzer, bqClient)
74+
75+
files, path, position, err := helper.GetLspPosition(tt.files)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
80+
parsedFile := analyzer.ParseFile(path, files[path])
81+
82+
got := completor.completeDeclaration(context.Background(), parsedFile, position)
83+
if diff := cmp.Diff(got, tt.expectCompletionItems); diff != "" {
84+
t.Errorf("(-got, +want)\n%s", diff)
85+
}
86+
})
87+
}
88+
}

langserver/internal/source/file/analyze.go

+36-3
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,47 @@ func getDummyValueForDeclarationNode(node *ast.VariableDeclarationNode) (string,
246246
case *ast.ArrayTypeNode:
247247
return "[]", nil
248248
case *ast.SimpleTypeNode:
249-
return getDummyValueForDefaultValueNode(node.DefaultValue())
249+
if node.DefaultValue() != nil {
250+
return getDummyValueForDefaultValueNode(node.DefaultValue())
251+
}
252+
if pen, ok := n.Child(0).(*ast.PathExpressionNode); ok {
253+
if in, ok := pen.Child(0).(*ast.IdentifierNode); ok {
254+
return getDummyValueForDeclarationIdentifierName(in.Name())
255+
}
256+
}
257+
return "", fmt.Errorf("failed to load default value")
250258
default:
251259
return "", fmt.Errorf("not implemented: %s", n.Kind())
252260
}
253261
}
254262

263+
func getDummyValueForDeclarationIdentifierName(name string) (string, error) {
264+
switch name {
265+
case "BOOL":
266+
return "TRUE", nil
267+
case "INT64":
268+
return "1", nil
269+
case "FLOAT64":
270+
return "1.0", nil
271+
case "STRING":
272+
return "''", nil
273+
case "BYTES":
274+
return "b''", nil
275+
case "DATE":
276+
return "DATE('1970-01-01')", nil
277+
case "DATETIME":
278+
return "DATETIME('1970-01-01 00:00:00')", nil
279+
case "TIME":
280+
return "TIME('00:00:00')", nil
281+
case "TIMESTAMP":
282+
return "TIMESTAMP('1970-01-01 00:00:00')", nil
283+
default:
284+
return "", fmt.Errorf("not implemented: %s", name)
285+
}
286+
}
287+
255288
func getDummyValueForDefaultValueNode(node ast.ExpressionNode) (string, error) {
256-
switch n := node.(type) {
289+
switch node.(type) {
257290
case *ast.NullLiteralNode:
258291
return "NULL", nil
259292
case *ast.BooleanLiteralNode:
@@ -267,6 +300,6 @@ func getDummyValueForDefaultValueNode(node ast.ExpressionNode) (string, error) {
267300
case *ast.DateOrTimeLiteralNode:
268301
return "DATE('1970-01-01')", nil
269302
default:
270-
return "", fmt.Errorf("not implemented: %s", n.Kind())
303+
return "", fmt.Errorf("not implemented: %T", node)
271304
}
272305
}

langserver/internal/source/file/file_test.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ func TestAnalyzer_ParseFileWithDeclareStatement(t *testing.T) {
646646

647647
expectedErrs []file.Error
648648
}{
649-
"Parse with incomplete table name": {
649+
"Parse with default value": {
650650
file: "DECLARE target_id INT64 DEFAULT 10;\n" +
651651
"SELECT * FROM `project.dataset.table` WHERE id = target_id",
652652
bqTableMetadataMap: map[string]*bq.TableMetadata{
@@ -661,6 +661,21 @@ func TestAnalyzer_ParseFileWithDeclareStatement(t *testing.T) {
661661
},
662662
expectedErrs: []file.Error{},
663663
},
664+
"Parse without default value": {
665+
file: "DECLARE target_id INT64;\n" +
666+
"SELECT * FROM `project.dataset.table` WHERE id = target_id",
667+
bqTableMetadataMap: map[string]*bq.TableMetadata{
668+
"project.dataset.table": {
669+
Schema: bq.Schema{
670+
{
671+
Name: "id",
672+
Type: bq.IntegerFieldType,
673+
},
674+
},
675+
},
676+
},
677+
expectedErrs: []file.Error{},
678+
},
664679
}
665680

666681
for n, tt := range tests {

langserver/internal/source/file/node.go

+14
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,17 @@ func LookupNode[T astNode](n ast.Node) (T, bool) {
9191

9292
return LookupNode[T](n.Parent())
9393
}
94+
95+
func ListAstNode[T locationRangeNode](n ast.ScriptNode) []T {
96+
result := make([]T, 0)
97+
ast.Walk(n, func(n ast.Node) error {
98+
node, ok := n.(T)
99+
if !ok {
100+
return nil
101+
}
102+
result = append(result, node)
103+
return nil
104+
})
105+
106+
return result
107+
}

0 commit comments

Comments
 (0)