From 69f6b68c8f5e0e298c824e63c5ae1b4adacc09d7 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Tue, 1 Apr 2025 13:46:53 -0400 Subject: [PATCH] Prefer to return LocationLinks if the client supports it Originally, we returned LocationLinks only in select cases. However, this can mean that the client may not provide the correct UI affordance if hyphens are involved so pivoting to always returning LocationLinks will help ensure the client presents the user with the accurate UX. Signed-off-by: Remy Suen --- CHANGELOG.md | 6 + internal/bake/hcl/definition.go | 214 ++++++---- internal/bake/hcl/definition_test.go | 591 +++++++++++++++++++++++---- 3 files changed, 670 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2624b..3cae407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ All notable changes to the Docker Language Server will be documented in this fil - textDocument/publishDiagnostics - introduce a setting to ignore certain diagnostics to not duplicate the ones from the Dockerfile Language Server +### Fixed + +- Docker Bake + - textDocument/definition + - always return LocationLinks to help disambiguate word boundaries for clients ([#31](https://github.com/docker/docker-language-server/issues/31)) + ## 0.1.0 - 2025-03-31 ### Added diff --git a/internal/bake/hcl/definition.go b/internal/bake/hcl/definition.go index f2958bb..1913955 100644 --- a/internal/bake/hcl/definition.go +++ b/internal/bake/hcl/definition.go @@ -89,21 +89,30 @@ func Definition(ctx context.Context, definitionLinkSupport bool, manager *docume for _, attribute := range body.Attributes { if isInsideRange(attribute.NameRange, position) { - return []protocol.Location{ - { - Range: protocol.Range{ - Start: protocol.Position{ - Line: uint32(attribute.NameRange.Start.Line) - 1, - Character: uint32(attribute.NameRange.Start.Column) - 1, - }, - End: protocol.Position{ - Line: uint32(attribute.NameRange.End.Line) - 1, - Character: uint32(attribute.NameRange.End.Column) - 1, - }, + return createDefinitionResult( + definitionLinkSupport, + protocol.Range{ + Start: protocol.Position{ + Line: uint32(attribute.NameRange.Start.Line) - 1, + Character: uint32(attribute.NameRange.Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(attribute.NameRange.End.Line) - 1, + Character: uint32(attribute.NameRange.End.Column) - 1, + }, + }, + &protocol.Range{ + Start: protocol.Position{ + Line: uint32(attribute.NameRange.Start.Line) - 1, + Character: uint32(attribute.NameRange.Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(attribute.NameRange.End.Line) - 1, + Character: uint32(attribute.NameRange.End.Column) - 1, }, - URI: string(documentURI), }, - }, nil + string(documentURI), + ), nil } if isInsideRange(attribute.SrcRange, position) { @@ -126,7 +135,18 @@ func ResolveAttributeValue(ctx context.Context, definitionLinkSupport bool, mana (sourceBlock.Type == "group" && attribute.Name == "targets") { value, _ := templateExpr.Value(&hcl.EvalContext{}) target := value.AsString() - return CalculateBlockLocation(input, body, documentURI, "target", target, false) + templateExprRange := templateExpr.Range() + sourceRange := hcl.Range{ + Start: hcl.Pos{ + Line: templateExprRange.Start.Line, + Column: templateExprRange.Start.Column + 1, + }, + End: hcl.Pos{ + Line: templateExprRange.End.Line, + Column: templateExprRange.End.Column - 1, + }, + } + return CalculateBlockLocation(definitionLinkSupport, input, body, documentURI, sourceRange, "target", target, false) } } } @@ -158,32 +178,30 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager for _, child := range nodes { if strings.EqualFold(child.Value, "FROM") { if child.Next != nil && child.Next.Next != nil && strings.EqualFold(child.Next.Next.Value, "AS") && child.Next.Next.Next != nil && child.Next.Next.Next.Value == target { - targetRange := protocol.Range{ - Start: protocol.Position{Line: uint32(child.StartLine) - 1, Character: 0}, - End: protocol.Position{Line: uint32(child.EndLine) - 1, Character: uint32(len(lines[child.EndLine-1]))}, - } - - linkURI := protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(dockerfilePath), "/"))) - if !definitionLinkSupport { - return []protocol.Location{ - { - Range: targetRange, - URI: linkURI, + return createDefinitionResult( + definitionLinkSupport, + protocol.Range{ + Start: protocol.Position{ + Line: uint32(child.StartLine) - 1, + Character: 0, }, - } - } - - return []protocol.LocationLink{ - { - OriginSelectionRange: &protocol.Range{ - Start: protocol.Position{Line: uint32(literalValueExpr.Range().Start.Line) - 1, Character: uint32(literalValueExpr.Range().Start.Column) - 1}, - End: protocol.Position{Line: uint32(literalValueExpr.Range().End.Line) - 1, Character: uint32(uint32(literalValueExpr.Range().End.Column) - 1)}, + End: protocol.Position{ + Line: uint32(child.EndLine) - 1, + Character: uint32(len(lines[child.EndLine-1])), }, - TargetRange: targetRange, - TargetSelectionRange: targetRange, - TargetURI: linkURI, }, - } + &protocol.Range{ + Start: protocol.Position{ + Line: uint32(literalValueExpr.Range().Start.Line) - 1, + Character: uint32(literalValueExpr.Range().Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(literalValueExpr.Range().End.Line) - 1, + Character: uint32(uint32(literalValueExpr.Range().End.Column) - 1), + }, + }, + protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(dockerfilePath), "/"))), + ) } } } @@ -225,15 +243,29 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager } if value == arg { - return []protocol.Location{ - { - Range: protocol.Range{ - Start: protocol.Position{Line: uint32(node.StartLine) - 1, Character: 0}, - End: protocol.Position{Line: uint32(node.EndLine) - 1, Character: uint32(len(lines[node.EndLine-1]))}, - }, - URI: protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(dockerfilePath), "/"))), + originSelectionRange := protocol.Range{ + Start: protocol.Position{ + Line: uint32(item.KeyExpr.Range().Start.Line) - 1, + Character: uint32(item.KeyExpr.Range().Start.Column) - 1, }, + End: protocol.Position{ + Line: uint32(item.KeyExpr.Range().End.Line) - 1, + Character: uint32(item.KeyExpr.Range().End.Column) - 1, + }, + } + if LiteralValue(item.KeyExpr) { + originSelectionRange.Start.Character = originSelectionRange.Start.Character + 1 + originSelectionRange.End.Character = originSelectionRange.End.Character - 1 } + return createDefinitionResult( + definitionLinkSupport, + protocol.Range{ + Start: protocol.Position{Line: uint32(node.StartLine) - 1, Character: 0}, + End: protocol.Position{Line: uint32(node.EndLine) - 1, Character: uint32(len(lines[node.EndLine-1]))}, + }, + &originSelectionRange, + protocol.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(dockerfilePath), "/"))), + ) } child = child.Next } @@ -278,7 +310,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager if _, ok := expression.(*hclsyntax.ScopeTraversalExpr); ok { name := string(input[expression.Range().Start.Byte:expression.Range().End.Byte]) - return CalculateBlockLocation(input, body, documentURI, "variable", name, true) + return CalculateBlockLocation(definitionLinkSupport, input, body, documentURI, expression.Range(), "variable", name, true) } if templateWrapExpr, ok := expression.(*hclsyntax.TemplateWrapExpr); ok { @@ -287,7 +319,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager if functionCallExpr, ok := expression.(*hclsyntax.FunctionCallExpr); ok { if isInsideRange(functionCallExpr.NameRange, position) { - return CalculateBlockLocation(input, body, documentURI, "function", functionCallExpr.Name, true) + return CalculateBlockLocation(definitionLinkSupport, input, body, documentURI, functionCallExpr.NameRange, "function", functionCallExpr.Name, true) } for _, arg := range functionCallExpr.Args { @@ -303,7 +335,7 @@ func ResolveExpression(ctx context.Context, definitionLinkSupport bool, manager // returns it. If variable is true then it will also look at the // top-level attributes of the HCL file and resolve to those if the // names match. -func CalculateBlockLocation(input []byte, body *hclsyntax.Body, documentURI uri.URI, blockName, name string, variable bool) any { +func CalculateBlockLocation(definitionLinkSupport bool, input []byte, body *hclsyntax.Body, documentURI uri.URI, sourceRange hcl.Range, blockName, name string, variable bool) any { for _, b := range body.Blocks { if b.Type == blockName && b.Labels[0] == name { startCharacter := uint32(b.LabelRanges[0].Start.Column) @@ -315,42 +347,80 @@ func CalculateBlockLocation(input []byte, body *hclsyntax.Body, documentURI uri. startCharacter-- endCharacter-- } - return []protocol.Location{ - { - Range: protocol.Range{ - Start: protocol.Position{ - Line: uint32(b.LabelRanges[0].Start.Line) - 1, - Character: startCharacter, - }, - End: protocol.Position{ - Line: uint32(b.LabelRanges[0].End.Line) - 1, - Character: endCharacter, - }, + return createDefinitionResult( + definitionLinkSupport, + protocol.Range{ + Start: protocol.Position{ + Line: uint32(b.LabelRanges[0].Start.Line) - 1, + Character: startCharacter, + }, + End: protocol.Position{ + Line: uint32(b.LabelRanges[0].End.Line) - 1, + Character: endCharacter, }, - URI: string(documentURI), }, - } + &protocol.Range{ + Start: protocol.Position{ + Line: uint32(sourceRange.Start.Line) - 1, + Character: uint32(sourceRange.Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(sourceRange.End.Line) - 1, + Character: uint32(sourceRange.End.Column) - 1, + }, + }, + string(documentURI), + ) } } if attribute, ok := body.Attributes[name]; ok && variable { + return createDefinitionResult( + definitionLinkSupport, + protocol.Range{ + Start: protocol.Position{ + Line: uint32(attribute.NameRange.Start.Line) - 1, + Character: uint32(attribute.NameRange.Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(attribute.NameRange.End.Line) - 1, + Character: uint32(attribute.NameRange.End.Column) - 1, + }, + }, + &protocol.Range{ + Start: protocol.Position{ + Line: uint32(sourceRange.Start.Line) - 1, + Character: uint32(sourceRange.Start.Column) - 1, + }, + End: protocol.Position{ + Line: uint32(sourceRange.End.Line) - 1, + Character: uint32(sourceRange.End.Column) - 1, + }, + }, + string(documentURI), + ) + } + return nil +} + +func createDefinitionResult(definitionLinkSupport bool, targetRange protocol.Range, originSelectionRange *protocol.Range, linkURI protocol.URI) any { + if !definitionLinkSupport { return []protocol.Location{ { - Range: protocol.Range{ - Start: protocol.Position{ - Line: uint32(attribute.NameRange.Start.Line) - 1, - Character: uint32(attribute.NameRange.Start.Column) - 1, - }, - End: protocol.Position{ - Line: uint32(attribute.NameRange.End.Line) - 1, - Character: uint32(attribute.NameRange.End.Column) - 1, - }, - }, - URI: string(documentURI), + Range: targetRange, + URI: linkURI, }, } } - return nil + + return []protocol.LocationLink{ + { + OriginSelectionRange: originSelectionRange, + TargetRange: targetRange, + TargetSelectionRange: targetRange, + TargetURI: linkURI, + }, + } } func ParseDockerfile(dockerfilePath string) ([]byte, *parser.Result, error) { diff --git a/internal/bake/hcl/definition_test.go b/internal/bake/hcl/definition_test.go index d9e741d..dc6f55b 100644 --- a/internal/bake/hcl/definition_test.go +++ b/internal/bake/hcl/definition_test.go @@ -62,6 +62,7 @@ func TestDefinition(t *testing.T) { content string line uint32 character uint32 + locations any links any }{ { @@ -69,6 +70,7 @@ func TestDefinition(t *testing.T) { content: "target \"default\" {\ndockerfile = \"Dockerfile\"\ntarget = \"stage\" }", line: 2, character: 0, // point to the attribute's name instead of value + locations: nil, links: nil, }, { @@ -76,6 +78,7 @@ func TestDefinition(t *testing.T) { content: "target \"default\" {\ndockerfile = \"Dockerfile\"\ntarget = \"stage\" }", line: 1, // point to the dockerfile attribute instead of the target attribute character: 13, + locations: nil, links: nil, }, { @@ -83,6 +86,7 @@ func TestDefinition(t *testing.T) { content: "target \"default\" {\n network = \"stage\"\n}", line: 1, character: 17, + locations: nil, links: nil, }, { @@ -90,6 +94,7 @@ func TestDefinition(t *testing.T) { content: "variable \"var\" {\n target = \"stage\"\n}", line: 1, character: 17, + locations: nil, links: nil, }, { @@ -97,7 +102,7 @@ func TestDefinition(t *testing.T) { content: "variable \"var\" {\n default = \"stageName\"\n}\ntarget \"default\" {\n context = var\n}", line: 4, character: 13, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -106,13 +111,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 12}, + End: protocol.Position{Line: 4, Character: 15}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "target attribute points to a stage defined by a declared variable", content: "variable \"var\" {\n default = \"stageName\"\n}\ntarget \"default\" {\n target = var\n}", line: 4, character: 13, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -121,13 +143,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 11}, + End: protocol.Position{Line: 4, Character: 14}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "target attribute points to a stage defined by a declared variable without quotes", content: "variable var {\n default = \"stageName\"\n}\ntarget \"default\" {\n target = var\n}", line: 4, character: 13, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -136,13 +175,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 11}, + End: protocol.Position{Line: 4, Character: 14}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + }, + }, }, { name: "target attribute points to a stage defined by ${var} with quotes", content: "variable var {\n default = \"stageName\"\n}\ntarget \"default\" {\n target = \"${var}\"\n}", line: 4, character: 15, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -151,12 +207,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 4, Character: 14}, + End: protocol.Position{Line: 4, Character: 17}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + }, + }, }, { name: "target attribute points to a stage defined by an undeclared variable", content: "target \"default\" {\n target = undefinedVariable\n}", line: 1, character: 20, + locations: nil, links: nil, }, { @@ -164,7 +238,7 @@ func TestDefinition(t *testing.T) { content: "stageName = \"abc\"\ntarget \"default\" {\n target = stageName\n}", line: 2, character: 16, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -173,13 +247,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 11}, + End: protocol.Position{Line: 2, Character: 20}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 9}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 9}, + }, + }, + }, }, { name: "inherits attribute points to a valid target", content: "target \"source\" {}\ntarget \"default\" {\n inherits = [ \"source\" ]\n}", line: 2, character: 20, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -188,13 +279,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 16}, + End: protocol.Position{Line: 2, Character: 22}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 8}, + End: protocol.Position{Line: 0, Character: 14}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 8}, + End: protocol.Position{Line: 0, Character: 14}, + }, + }, + }, }, { name: "group block's targets attribute points to a valid target", content: "target \"t1\" {}\ngroup \"g1\" {\n targets = [ \"t1\" ]\n}", line: 2, character: 16, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -203,13 +311,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 15}, + End: protocol.Position{Line: 2, Character: 17}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 8}, + End: protocol.Position{Line: 0, Character: 10}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 8}, + End: protocol.Position{Line: 0, Character: 10}, + }, + }, + }, }, { name: "inherits attribute points to an unquoted variable", content: "variable \"var\" {}\ntarget \"default\" {\n inherits = [ var ]\n}", line: 2, character: 17, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -218,13 +343,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 15}, + End: protocol.Position{Line: 2, Character: 18}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "inherits attribute points to a quoted variable", content: "variable \"var\" {}\ntarget \"default\" {\n inherits = [ \"${var}\" ]\n}", line: 2, character: 20, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -233,13 +375,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 18}, + End: protocol.Position{Line: 2, Character: 21}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "inherits attribute points to the second variable that is in quotes", content: "variable \"var\" {}\nvariable \"var2\" {}\ntarget \"default\" {\n inherits = [ var, \"${var2}\" ]\n}", line: 3, character: 24, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -248,13 +407,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 3, Character: 23}, + End: protocol.Position{Line: 3, Character: 27}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 14}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 14}, + }, + }, + }, }, { name: "inherits attribute points to the a quoted variable as the second item", content: "variable \"var\" {}\ntarget \"default\" {\n inherits = [ \"\", \"${var}\" ]\n}", line: 2, character: 24, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -263,12 +439,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 22}, + End: protocol.Position{Line: 2, Character: 25}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "inherits attribute pointing to a variable inside a quoted string should not work", content: "variable \"source\" {}\ntarget \"default\" {\n inherits = [ \"source\" ]\n}", line: 2, character: 20, + locations: nil, links: nil, }, { @@ -276,7 +470,7 @@ func TestDefinition(t *testing.T) { content: "variable \"source\" {}\ntarget \"default\" {\n entitlements = [ source ]\n}", line: 2, character: 22, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -285,13 +479,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 19}, + End: protocol.Position{Line: 2, Character: 25}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 16}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 16}, + }, + }, + }, }, { name: "inherits attribute pointing to a variable", content: "variable \"source\" {}\ntarget \"default\" {\n inherits = [ source ]\n}", line: 2, character: 18, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -300,13 +511,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 15}, + End: protocol.Position{Line: 2, Character: 21}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 16}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 16}, + }, + }, + }, }, { name: "formula referencing variable and top-level attribute with the location at the boolean check", content: "default_network = \"none\"\nvariable \"networkType\" {\n default = \"default\"\n}\ntarget \"default\" {\n network = networkType == \"host\" ? networkType : default_network\n}", line: 5, character: 19, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -315,13 +543,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 12}, + End: protocol.Position{Line: 5, Character: 23}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 21}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 21}, + }, + }, + }, }, { name: "formula referencing variable and top-level attribute with the location at the true result", content: "default_network = \"none\"\nvariable \"networkType\" {\n default = \"default\"\n}\ntarget \"default\" {\n network = networkType == \"host\" ? networkType : default_network\n}", line: 5, character: 43, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -330,13 +575,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 36}, + End: protocol.Position{Line: 5, Character: 47}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 21}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 10}, + End: protocol.Position{Line: 1, Character: 21}, + }, + }, + }, }, { name: "formula referencing variable and top-level attribute with the location at the false result", content: "default_network = \"none\"\nvariable \"networkType\" {\n default = \"default\"\n}\ntarget \"default\" {\n network = networkType == \"host\" ? networkType : default_network\n}", line: 5, character: 56, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -345,12 +607,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 50}, + End: protocol.Position{Line: 5, Character: 65}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 15}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 15}, + }, + }, + }, }, { name: "location in whitespace of a BinaryOpExpr", content: "default_network = \"none\"\nnetworkType2 = \"none\"\ntarget \"default\" {\n network = networkType == networkType2 ? networkType : default_network\n}", line: 3, character: 24, + locations: nil, links: nil, }, { @@ -358,6 +638,7 @@ func TestDefinition(t *testing.T) { content: "default_network = \"none\"\ntarget \"default\" {\n network = networkType == \"host\" ? networkType : default_network\n}", line: 2, character: 50, + locations: nil, links: nil, }, { @@ -365,7 +646,7 @@ func TestDefinition(t *testing.T) { content: "var = \"value\"\ntarget \"default\" {\n args = {\n arg = var\n }\n}", line: 3, character: 12, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -374,13 +655,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 3, Character: 10}, + End: protocol.Position{Line: 3, Character: 13}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 3}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 3}, + }, + }, + }, }, { name: "variable inside a function", content: "variable \"TAG\" {}\ntarget \"default\" {\n tags = [ notequal(\"\", TAG) ? \"image:${TAG}\" : \"image:latest\"\n}", line: 2, character: 26, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -389,13 +687,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 24}, + End: protocol.Position{Line: 2, Character: 27}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "${variable} inside a function", content: "variable \"TAG\" {}\ntarget \"default\" {\n tags = [ notequal(\"\", TAG) ? \"image:${TAG}\" : \"image:latest\"\n}", line: 2, character: 42, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -404,13 +719,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 40}, + End: protocol.Position{Line: 2, Character: 43}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "referenced function name", content: "function \"tag\" {\n params = [param]\n result = [\"${param}\"]\n}\ntarget \"default\" {\n tags = tag(\"v1\")\n}", line: 5, character: 10, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -419,13 +751,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 9}, + End: protocol.Position{Line: 5, Character: 12}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "referenced function name inside ${}", content: "function \"tag\" {\n params = [param]\n result = [\"${param}\"]\n}\ntarget \"default\" {\n tags = \"${tag(\"v1\")}\"\n}", line: 5, - character: 15, - links: []protocol.Location{ + character: 14, + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -434,12 +783,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 5, Character: 12}, + End: protocol.Position{Line: 5, Character: 15}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 10}, + End: protocol.Position{Line: 0, Character: 13}, + }, + }, + }, }, { name: "attribute string value", content: "a1 = \"value\"\n", line: 0, character: 9, + locations: nil, links: nil, }, { @@ -447,7 +814,7 @@ func TestDefinition(t *testing.T) { content: "a1 = \"value\"\na2 = a1", line: 1, character: 6, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -456,13 +823,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 1, Character: 5}, + End: protocol.Position{Line: 1, Character: 7}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 2}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 2}, + }, + }, + }, }, { name: "attribute should point at itself", content: "a1 = \"value\"\na2 = a1", line: 1, character: 1, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -471,13 +855,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 2}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 2}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 2}, + }, + }, + }, }, { name: "variable referenced in for loop conditional", content: "variable num { default = 3 }\nvariable varList { default = [\"tag\"] }\ntarget default {\n tags = [for var in varList : upper(var) if num > 2]\n}", line: 3, character: 46, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -486,13 +887,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 3, Character: 45}, + End: protocol.Position{Line: 3, Character: 48}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 12}, + }, + }, + }, }, { name: "variable inside a for loop", content: "variable varList { default = [\"tag\"] }\ntarget default {\n tags = [for var in varList : upper(var)]\n}", line: 2, character: 24, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: bakeFileURI, Range: protocol.Range{ @@ -501,13 +919,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 21}, + End: protocol.Position{Line: 2, Character: 28}, + }, + TargetURI: bakeFileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 16}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 0, Character: 9}, + End: protocol.Position{Line: 0, Character: 16}, + }, + }, + }, }, { name: "args key references Dockerfile ARG variable (unquoted key, no default value set)", content: "target default {\n args = {\n var = \"value\"\n }\n}", line: 2, character: 6, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: dockerfileURI, Range: protocol.Range{ @@ -516,13 +951,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 4}, + End: protocol.Position{Line: 2, Character: 7}, + }, + TargetURI: dockerfileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 7}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 7}, + }, + }, + }, }, { name: "args key references Dockerfile ARG variable (unquoted key, default value set)", content: "target default {\n args = {\n defined = \"value\"\n }\n}", line: 2, character: 8, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: dockerfileURI, Range: protocol.Range{ @@ -531,13 +983,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 4}, + End: protocol.Position{Line: 2, Character: 11}, + }, + TargetURI: dockerfileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 0}, + End: protocol.Position{Line: 2, Character: 19}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 0}, + End: protocol.Position{Line: 2, Character: 19}, + }, + }, + }, }, { name: "args key references Dockerfile ARG variable (quoted key, no default value set)", content: "target default {\n args = {\n \"var\" = \"value\"\n }\n}", line: 2, character: 7, - links: []protocol.Location{ + locations: []protocol.Location{ { URI: dockerfileURI, Range: protocol.Range{ @@ -546,12 +1015,30 @@ func TestDefinition(t *testing.T) { }, }, }, + links: []protocol.LocationLink{ + { + OriginSelectionRange: &protocol.Range{ + Start: protocol.Position{Line: 2, Character: 5}, + End: protocol.Position{Line: 2, Character: 8}, + }, + TargetURI: dockerfileURI, + TargetRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 7}, + }, + TargetSelectionRange: protocol.Range{ + Start: protocol.Position{Line: 1, Character: 0}, + End: protocol.Position{Line: 1, Character: 7}, + }, + }, + }, }, { name: "group block with an invalid inherits attribute should not return a result", content: "target t1 {}\ngroup g1 { inherits = [\"t1\"] }", line: 1, character: 25, + locations: nil, links: nil, }, { @@ -559,6 +1046,7 @@ func TestDefinition(t *testing.T) { content: "target t1 {}\nvariable v1 { inherits = [\"t1\"] }", line: 1, character: 28, + locations: nil, links: nil, }, { @@ -566,6 +1054,7 @@ func TestDefinition(t *testing.T) { content: "group g1 {\n args = {\n var = \"value\"\n }\n}", line: 2, character: 6, + locations: nil, links: nil, }, { @@ -573,44 +1062,9 @@ func TestDefinition(t *testing.T) { content: "variable var {\n args = {\n var = \"value\"\n }\n}", line: 2, character: 6, + locations: nil, links: nil, }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - manager := document.NewDocumentManager() - doc := document.NewBakeHCLDocument(uri.URI(bakeFileURI), 1, []byte(tc.content)) - links, err := Definition(context.Background(), true, manager, uri.URI(bakeFileURI), doc, protocol.Position{Line: tc.line, Character: tc.character}) - require.NoError(t, err) - require.Equal(t, tc.links, links) - }) - } -} - -func TestDefinitionVariedResults(t *testing.T) { - wd, err := os.Getwd() - require.NoError(t, err) - projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(wd))) - definitionTestFolderPath := filepath.Join(projectRoot, "testdata", "definition") - - dockerfilePath := filepath.Join(definitionTestFolderPath, "Dockerfile") - bakeFilePath := filepath.Join(definitionTestFolderPath, "docker-bake.hcl") - - dockerfilePath = filepath.ToSlash(dockerfilePath) - bakeFilePath = filepath.ToSlash(bakeFilePath) - - dockerfileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(dockerfilePath, "/")) - bakeFileURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(bakeFilePath, "/")) - - testCases := []struct { - name string - content string - line uint32 - character uint32 - locations any - links any - }{ { name: "reference valid stage (target block, target attribute)", content: "target \"default\" { target = \"stage\" }", @@ -774,17 +1228,16 @@ func TestDefinitionVariedResults(t *testing.T) { } for _, tc := range testCases { + manager := document.NewDocumentManager() + doc := document.NewBakeHCLDocument(uri.URI(bakeFileURI), 1, []byte(tc.content)) + t.Run(fmt.Sprintf("%v (Location)", tc.name), func(t *testing.T) { - manager := document.NewDocumentManager() - doc := document.NewBakeHCLDocument(uri.URI(bakeFileURI), 1, []byte(tc.content)) locations, err := Definition(context.Background(), false, manager, uri.URI(bakeFileURI), doc, protocol.Position{Line: tc.line, Character: tc.character}) require.NoError(t, err) require.Equal(t, tc.locations, locations) }) t.Run(fmt.Sprintf("%v (LocationLink)", tc.name), func(t *testing.T) { - manager := document.NewDocumentManager() - doc := document.NewBakeHCLDocument(uri.URI(bakeFileURI), 1, []byte(tc.content)) links, err := Definition(context.Background(), true, manager, uri.URI(bakeFileURI), doc, protocol.Position{Line: tc.line, Character: tc.character}) require.NoError(t, err) require.Equal(t, tc.links, links)