From e1c7ccf5565f063fd8d2e58e56ae45da5f9f6254 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Thu, 10 Jul 2025 13:45:38 -0400 Subject: [PATCH] Return document links for the extends object's file attribute Signed-off-by: Remy Suen --- CHANGELOG.md | 1 + internal/compose/documentLink.go | 5 + internal/compose/documentLink_test.go | 150 ++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5060f0..3964f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the Docker Language Server will be documented in this fil - Compose - textDocument/documentLink - add anchor resolution for all supported document links ([#348](https://github.com/docker/docker-language-server/issues/348)) + - return document links for the `file` attribute of a service object's `extends` attribute object ([#172](https://github.com/docker/docker-language-server/issues/172)) ### Fixed diff --git a/internal/compose/documentLink.go b/internal/compose/documentLink.go index a1bda52..eaed46c 100644 --- a/internal/compose/documentLink.go +++ b/internal/compose/documentLink.go @@ -166,6 +166,11 @@ func scanForLinks(u *url.URL, n *ast.MappingValueNode) []protocol.DocumentLink { if link != nil { links = append(links, *link) } + + link = createdNestedLink(u, serviceAttribute, "extends", "file") + if link != nil { + links = append(links, *link) + } } } } diff --git a/internal/compose/documentLink_test.go b/internal/compose/documentLink_test.go index bd891eb..eeec5f6 100644 --- a/internal/compose/documentLink_test.go +++ b/internal/compose/documentLink_test.go @@ -1117,6 +1117,156 @@ services: }) } } + +func TestDocumentLink_ServiceExtendsFileLinks(t *testing.T) { + testsFolder := filepath.Join(os.TempDir(), t.Name()) + composeStringURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(testsFolder, "compose.yaml")), "/")) + + testCases := []struct { + name string + content string + path string + linkRange protocol.Range + }{ + { + name: "no anchors", + content: ` +services: + test2: + extends: + service: test + file: ./compose.other.yaml`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 5, Character: 12}, + End: protocol.Position{Line: 5, Character: 32}, + }, + }, + { + name: "anchors and aliases to nothing", + content: ` +services: + test: + extends: + file: &file + test2: + extends: + file: *file`, + }, + { + name: "anchor has string content", + content: ` +services: + test: + extends: + file: &file ./compose.other.yaml + test2: + extends: + file: *file`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 18}, + End: protocol.Position{Line: 4, Character: 38}, + }, + }, + { + name: "anchor on the services object", + content: ` +services: &anchor + test: + extends: + file: ./compose.other.yaml`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 12}, + End: protocol.Position{Line: 4, Character: 32}, + }, + }, + { + name: "anchor on the service JSON object", + content: ` +services: + test: &anchor { extends: { file: ./compose.other.yaml } }`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 35}, + End: protocol.Position{Line: 2, Character: 55}, + }, + }, + { + name: "anchor on the service object", + content: ` +services: + test: &anchor + extends: + file: ./compose.other.yaml`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 12}, + End: protocol.Position{Line: 4, Character: 32}, + }, + }, + { + name: "anchor on the build attribute inside a JSON object", + content: ` +services: + backend: { + &anchor extends: { file: ./compose.other.yaml } + }`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 3, Character: 29}, + End: protocol.Position{Line: 3, Character: 49}, + }, + }, + { + name: "anchor on the extends object", + content: ` +services: + test: + extends: &anchor + file: ./compose.other.yaml`, + path: filepath.Join(testsFolder, "compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 12}, + End: protocol.Position{Line: 4, Character: 32}, + }, + }, + { + name: "anchor on the file attribute", + content: ` +services: + test: + extends: + &anchor file: ./compose.other.yaml`, + path: filepath.Join(testsFolder, "./compose.other.yaml"), + linkRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 20}, + End: protocol.Position{Line: 4, Character: 40}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mgr := document.NewDocumentManager() + doc := document.NewComposeDocument(mgr, "compose.yaml", 1, []byte(tc.content)) + links, err := DocumentLink(context.Background(), composeStringURI, doc) + require.NoError(t, err) + if tc.path == "" { + require.Equal(t, []protocol.DocumentLink{}, links) + } else { + link := protocol.DocumentLink{ + Range: tc.linkRange, + Target: types.CreateStringPointer(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(tc.path), "/"))), + Tooltip: types.CreateStringPointer(tc.path), + } + require.Equal(t, []protocol.DocumentLink{link}, links) + } + }) + } +} + func TestDocumentLink_ConfigFileLinks(t *testing.T) { testsFolder := filepath.Join(os.TempDir(), t.Name()) composeStringURI := fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(filepath.Join(testsFolder, "compose.yaml")), "/"))