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)