Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to the Docker Language Server will be documented in this fil
- convert links properly if a WSL URI with a dollar sign is used ([#378](https://github.com/docker/docker-language-server/issues/378))
- textDocument/inlineCompletion
- convert links properly if a WSL URI with a dollar sign is used ([#384](https://github.com/docker/docker-language-server/issues/384))
- textDocument/publishDiagnostics
- update the URI handling so that a WSL URI with a dollar sign can be scanned for errors ([#386](https://github.com/docker/docker-language-server/issues/386))

## [0.14.0] - 2025-07-16

Expand Down
54 changes: 22 additions & 32 deletions internal/bake/hcl/diagnosticsCollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ func (c *BakeHCLDiagnosticsCollector) SupportsLanguageIdentifier(languageIdentif

func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder string, doc document.Document, text string) []protocol.Diagnostic {
input := doc.Input()
_, err := bake.ParseFile(input, doc.URI().Filename())
dp, err := doc.DocumentPath()
if err != nil {
return nil
}

_, err = bake.ParseFile(input, dp.FileName)
diagnostics := []protocol.Diagnostic{}
if err != nil {
var sourceError *errdefs.SourceError
Expand Down Expand Up @@ -119,12 +124,14 @@ func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder
return diagnostics
}

targetDockerfiles := map[string]string{}
dockerfileContent := map[string][]*parser.Node{}
for _, b := range body.Blocks {
if b.Type == "target" && len(b.Labels) == 1 {
dockerfilePath, _ := bakeDoc.DockerfileForTarget(b)
targetDockerfiles[b.Labels[0]] = dockerfilePath
if _, ok := dockerfileContent[b.Labels[0]]; !ok {
targetDockerfileURI, targetDockerfilePath, _ := bakeDoc.DockerfileDocumentPathForTarget(b)
_, nodes := document.OpenDockerfile(context.Background(), c.docs, targetDockerfileURI, targetDockerfilePath)
dockerfileContent[b.Labels[0]] = nodes
}
}
}

Expand Down Expand Up @@ -217,26 +224,19 @@ func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder
}
}

dockerfilePath, err := bakeDoc.DockerfileForTarget(block)
_, dockerfilePath, err := bakeDoc.DockerfileDocumentPathForTarget(block)
if dockerfilePath == "" || err != nil {
continue
}

if attribute, ok := block.Body.Attributes["target"]; ok {
if expr, ok := attribute.Expr.(*hclsyntax.TemplateExpr); ok && len(expr.Parts) == 1 {
if literalValueExpr, ok := expr.Parts[0].(*hclsyntax.LiteralValueExpr); ok {
dockerfile := targetDockerfiles[block.Labels[0]]
if dockerfile == "" {
dockerfileContent[""] = nil
}
nodes, ok := dockerfileContent[dockerfile]
if !ok {
_, nodes = document.OpenDockerfile(context.Background(), c.docs, "", dockerfilePath)
dockerfileContent[block.Labels[0]] = nodes
}
diagnostic := c.checkTargetTarget(nodes, expr, literalValueExpr, source)
if diagnostic != nil {
diagnostics = append(diagnostics, *diagnostic)
if nodes, ok := dockerfileContent[block.Labels[0]]; ok {
diagnostic := c.checkTargetTarget(nodes, expr, literalValueExpr, source)
if diagnostic != nil {
diagnostics = append(diagnostics, *diagnostic)
}
}
}
}
Expand All @@ -249,27 +249,17 @@ func (c *BakeHCLDiagnosticsCollector) CollectDiagnostics(source, workspaceFolder
if b.Type == "target" && len(b.Labels) == 1 && b.Labels[0] != block.Labels[0] {
parents, _ := bakeDoc.ParentTargets(b.Labels[0])
if slices.Contains(parents, block.Labels[0]) {
dockerfile := targetDockerfiles[b.Labels[0]]
if dockerfile == "" {
dockerfileContent[""] = nil
}
nodes, ok := dockerfileContent[dockerfile]
if !ok {
_, nodes = document.OpenDockerfile(context.Background(), c.docs, "", dockerfile)
dockerfileContent[dockerfile] = nodes
if nodes, ok := dockerfileContent[b.Labels[0]]; ok {
c.collectARGs(nodes, args)
}
c.collectARGs(nodes, args)
}
}
}

nodes, ok := dockerfileContent[dockerfilePath]
if !ok {
_, nodes = document.OpenDockerfile(context.Background(), c.docs, "", dockerfilePath)
dockerfileContent[dockerfilePath] = nodes
if nodes, ok := dockerfileContent[block.Labels[0]]; ok {
argsDiagnostics := c.checkTargetArgs(nodes, input, expr, source, args)
diagnostics = append(diagnostics, argsDiagnostics...)
}
argsDiagnostics := c.checkTargetArgs(nodes, input, expr, source, args)
diagnostics = append(diagnostics, argsDiagnostics...)
}
}
}
Expand Down
187 changes: 160 additions & 27 deletions internal/bake/hcl/diagnosticsCollector_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hcl

import (
"context"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -52,7 +53,7 @@ func TestCollectDiagnostics(t *testing.T) {
},
},
{
name: "target block with network attribute empty string",
name: "target block with alpine:3.17.0 is flagged with vulnerabilities",
content: "target \"t1\" {\n tags = [ \"alpine:3.17.0\" ]\n}",
diagnostics: []protocol.Diagnostic{
{
Expand Down Expand Up @@ -126,10 +127,26 @@ func TestCollectDiagnostics(t *testing.T) {
},
},
{
name: "args resolution to a dockerfile that points to a variable",
content: "variable var { }\ntarget \"t1\" {\n dockerfile = var\nargs = {\n missing = \"value\"\n }\n}",
name: "args resolution to a dockerfile that points to a valid variable",
content: "variable var { default = \"./backend/Dockerfile\" }\ntarget \"t1\" {\n dockerfile = var\nargs = {\n BACKEND_VAR = \"newValue\"\n }\n}",
diagnostics: []protocol.Diagnostic{},
},
{
name: "args resolution to a dockerfile that points to a valid variable",
content: "variable var { default = \"./backend/Dockerfile\" }\ntarget \"t1\" {\n dockerfile = var\nargs = {\n missing = \"newValue\"\n }\n}",

diagnostics: []protocol.Diagnostic{
{
Message: "'missing' not defined as an ARG in your Dockerfile",
Source: types.CreateStringPointer("docker-language-server"),
Severity: types.CreateDiagnosticSeverityPointer(protocol.DiagnosticSeverityError),
Range: protocol.Range{
Start: protocol.Position{Line: 4, Character: 4},
End: protocol.Position{Line: 4, Character: 11},
},
},
},
},
{
name: "args resolution when inherting a parent that points to a var",
content: "variable var { }\ntarget \"t1\" {\n inherits = [var]\nargs = {\n missing = \"value\"\n }\n}",
Expand Down Expand Up @@ -215,30 +232,56 @@ target "lint2" {
diagnostics: []protocol.Diagnostic{},
},
{
name: "context has a variable",
name: "context has a variable and the referenced ARG is valid",
content: `
variable "GITHUB_WORKSPACE" {
default = "."
variable "VAR" {
default = "./backend"
}

target "build" {
context = "${GITHUB_WORKSPACE}/folder/subfolder"
context = "${VAR}"
dockerfile = "Dockerfile"
args = {
VAR = "value"
BACKEND_VAR = "newValue"
}
}`,
diagnostics: []protocol.Diagnostic{},
},
{
name: "context has a variable and the referenced ARG is invalid",
content: `
variable "VAR" {
default = "./backend"
}

target "build" {
context = "${VAR}"
dockerfile = "Dockerfile"
args = {
NON_EXISTENT_VAR = "newValue"
}
}`,
diagnostics: []protocol.Diagnostic{
{
Message: "'NON_EXISTENT_VAR' not defined as an ARG in your Dockerfile",
Source: types.CreateStringPointer("docker-language-server"),
Severity: types.CreateDiagnosticSeverityPointer(protocol.DiagnosticSeverityError),
Range: protocol.Range{
Start: protocol.Position{Line: 9, Character: 4},
End: protocol.Position{Line: 9, Character: 20},
},
},
},
},
{
name: "context has a variable and the target is inherited",
content: `
variable "GITHUB_WORKSPACE" {
variable "VAR" {
default = "."
}

target "common-base" {
context = "${GITHUB_WORKSPACE}/folder/subfolder"
context = "${VAR}/folder/subfolder"
dockerfile = "Dockerfile"
}

Expand All @@ -253,12 +296,12 @@ target "build" {
{
name: "parent target cannot be resolved but local target is resolvable",
content: `
variable "GITHUB_WORKSPACE" {
variable "VAR" {
default = "."
}

target "common-base" {
dockerfile = "${GITHUB_WORKSPACE}/folder/subfolder/Dockerfile"
dockerfile = "${VAR}/folder/subfolder/Dockerfile"
}

target "build" {
Expand Down Expand Up @@ -336,15 +379,84 @@ target "build" {
}
}

func TestCollectDiagnostics_InterFileDependency(t *testing.T) {
func TestCollectDiagnostics_WSL(t *testing.T) {
testCases := []struct {
name string
content string
diagnostics []protocol.Diagnostic
name string
content string
dockerfileContent string
diagnostics []protocol.Diagnostic
}{
{
name: "child target's Dockerfile defines the ARG",
content: `
name: "target found in Dockerfile",
content: "target \"t1\" {\n target = \"base\"\n}",
dockerfileContent: "FROM scratch AS base",
diagnostics: []protocol.Diagnostic{},
},
{
name: "target cannot be found in Dockerfile",
content: "target \"t1\" {\n target = \"nonexistent\"\n}",
dockerfileContent: "FROM scratch AS base",
diagnostics: []protocol.Diagnostic{
{
Message: "target could not be found in your Dockerfile",
Source: types.CreateStringPointer("docker-language-server"),
Severity: types.CreateDiagnosticSeverityPointer(protocol.DiagnosticSeverityError),
Range: protocol.Range{
Start: protocol.Position{Line: 1, Character: 11},
End: protocol.Position{Line: 1, Character: 24},
},
},
},
},
{
name: "args can be found in Dockerfile",
content: "target \"t1\" {\n args = {\n VAR = \"newValue\"\n }\n}",
dockerfileContent: "ARG VAR=value\nFROM scratch",
diagnostics: []protocol.Diagnostic{},
},
{
name: "args cannot be found in Dockerfile",
content: "target \"t1\" {\n args = {\n missing = \"newValue\"\n }\n}",
dockerfileContent: "ARG VAR=value\nFROM scratch",
diagnostics: []protocol.Diagnostic{
{
Message: "'missing' not defined as an ARG in your Dockerfile",
Source: types.CreateStringPointer("docker-language-server"),
Severity: types.CreateDiagnosticSeverityPointer(protocol.DiagnosticSeverityError),
Range: protocol.Range{
Start: protocol.Position{Line: 2, Character: 4},
End: protocol.Position{Line: 2, Character: 11},
},
},
},
},
}

dockerfileURI := "file://wsl%24/docker-desktop/tmp/Dockerfile"
bakeFileURI := uri.URI("file://wsl%24/docker-desktop/tmp/docker-bake.hcl")

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
changed, err := manager.Write(context.Background(), uri.URI(dockerfileURI), protocol.DockerfileLanguage, 1, []byte(tc.dockerfileContent))
require.NoError(t, err)
require.True(t, changed)
collector := &BakeHCLDiagnosticsCollector{docs: manager, scout: scout.NewService()}
doc := document.NewBakeHCLDocument(bakeFileURI, 1, []byte(tc.content))
diagnostics := collector.CollectDiagnostics("docker-language-server", "", doc, "")
require.Equal(t, tc.diagnostics, diagnostics)
})
}
}

var hierarchyTests = []struct {
name string
content string
diagnostics []protocol.Diagnostic
}{
{
name: "child target's Dockerfile defines the ARG",
content: `
target parent {
args = {
other = "value2"
Expand All @@ -355,11 +467,11 @@ target foo {
dockerfile = "Dockerfile2"
inherits = ["parent"]
}`,
diagnostics: []protocol.Diagnostic{},
},
{
name: "child target's Dockerfile defines the ARG but not another child",
content: `
diagnostics: []protocol.Diagnostic{},
},
{
name: "child target's Dockerfile defines the ARG but not another child",
content: `
target parent {
args = {
other = "value2"
Expand All @@ -374,20 +486,41 @@ target foo {
target foo2 {
inherits = ["parent"]
}`,
diagnostics: []protocol.Diagnostic{},
},
}
diagnostics: []protocol.Diagnostic{},
},
}

func TestCollectDiagnostics_Hierarchy(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(wd)))
diagnosticsTestFolderPath := filepath.Join(projectRoot, "testdata", "diagnostics")
bakeFilePath := filepath.Join(diagnosticsTestFolderPath, "docker-bake.hcl")
bakeFileURI := uri.URI(fmt.Sprintf("file:///%v", strings.TrimPrefix(filepath.ToSlash(bakeFilePath), "/")))

for _, tc := range testCases {
for _, tc := range hierarchyTests {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
bytes := []byte(tc.content)
collector := &BakeHCLDiagnosticsCollector{docs: manager, scout: scout.NewService()}
doc := document.NewBakeHCLDocument(bakeFileURI, 1, bytes)
diagnostics := collector.CollectDiagnostics("docker-language-server", "", doc, "")
require.Equal(t, tc.diagnostics, diagnostics)
})
}
}

func TestCollectDiagnostics_WSLHierarchy(t *testing.T) {
dockerfileContent := "ARG other=value\nFROM scratch AS build"
dockerfileURI := uri.URI("file://wsl%24/docker-desktop/tmp/Dockerfile2")
bakeFileURI := uri.URI("file://wsl%24/docker-desktop/tmp/docker-bake.hcl")

for _, tc := range hierarchyTests {
t.Run(tc.name, func(t *testing.T) {
manager := document.NewDocumentManager()
changed, err := manager.Write(context.Background(), dockerfileURI, protocol.DockerfileLanguage, 1, []byte(dockerfileContent))
require.NoError(t, err)
require.True(t, changed)
bytes := []byte(tc.content)
collector := &BakeHCLDiagnosticsCollector{docs: manager, scout: scout.NewService()}
doc := document.NewBakeHCLDocument(bakeFileURI, 1, bytes)
Expand Down
Loading
Loading