From fd44eb2ead0dee27f45c85037305d781a8a12ad6 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Wed, 11 Jun 2025 12:42:03 -0400 Subject: [PATCH 1/2] Provide code actions to easily skip certain build checks Signed-off-by: Remy Suen --- e2e-tests/publishDiagnostics_test.go | 28 ++++++ internal/pkg/buildkit/service.go | 98 ++++++++++++++++----- internal/pkg/buildkit/service_test.go | 120 ++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 23 deletions(-) diff --git a/e2e-tests/publishDiagnostics_test.go b/e2e-tests/publishDiagnostics_test.go index 35b85fd..c52c481 100644 --- a/e2e-tests/publishDiagnostics_test.go +++ b/e2e-tests/publishDiagnostics_test.go @@ -128,6 +128,14 @@ func testPublishDiagnostics(t *testing.T, initializeParams protocol.InitializePa "edit": "LABEL org.opencontainers.image.authors=\"x\"", "title": "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", }, + map[string]any{ + "edit": "# check=skip=MaintainerDeprecated\n", + "title": "Ignore this type of error with check=skip=MaintainerDeprecated", + "range": map[string]any{ + "start": map[string]any{"line": float64(0), "character": float64(0)}, + "end": map[string]any{"line": float64(0), "character": float64(0)}, + }, + }, }, }, }, @@ -150,6 +158,16 @@ func testPublishDiagnostics(t *testing.T, initializeParams protocol.InitializePa Start: protocol.Position{Line: 1, Character: 0}, End: protocol.Position{Line: 1, Character: 6}, }, + Data: []any{ + map[string]any{ + "edit": "# check=skip=JSONArgsRecommended\n", + "title": "Ignore this type of error with check=skip=JSONArgsRecommended", + "range": map[string]any{ + "start": map[string]any{"line": float64(0), "character": float64(0)}, + "end": map[string]any{"line": float64(0), "character": float64(0)}, + }, + }, + }, }, { Message: "The image contains 1 critical and 3 high vulnerabilities", @@ -242,6 +260,16 @@ func testPublishDiagnostics(t *testing.T, initializeParams protocol.InitializePa Start: protocol.Position{Line: 0, Character: 0}, End: protocol.Position{Line: 0, Character: 35}, }, + Data: []any{ + map[string]any{ + "edit": "# check=skip=FromPlatformFlagConstDisallowed\n", + "title": "Ignore this type of error with check=skip=FromPlatformFlagConstDisallowed", + "range": map[string]any{ + "start": map[string]any{"line": float64(0), "character": float64(0)}, + "end": map[string]any{"line": float64(0), "character": float64(0)}, + }, + }, + }, }, }, }, params) diff --git a/internal/pkg/buildkit/service.go b/internal/pkg/buildkit/service.go index 6333e73..deb932c 100644 --- a/internal/pkg/buildkit/service.go +++ b/internal/pkg/buildkit/service.go @@ -93,39 +93,33 @@ func encloseWithQuotes(s string) string { return fmt.Sprintf(`"%v"`, s) } -func setDiagnosticData(diagnostic *protocol.Diagnostic, instruction *parser.Node, warning lint.Warning) { +func createResolutionEdit(instruction *parser.Node, warning lint.Warning) *types.NamedEdit { if instruction != nil && instruction.StartLine == int(warning.Location.Ranges[0].Start.Line) && instruction.Next != nil { if warning.RuleName == "MaintainerDeprecated" { - diagnostic.Data = []types.NamedEdit{ - { - Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", - Edit: fmt.Sprintf(`LABEL org.opencontainers.image.authors=%v`, encloseWithQuotes(instruction.Next.Value)), - }, + return &types.NamedEdit{ + Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", + Edit: fmt.Sprintf(`LABEL org.opencontainers.image.authors=%v`, encloseWithQuotes(instruction.Next.Value)), } } else if warning.RuleName == "StageNameCasing" { stageName := instruction.Next.Next.Next.Value lowercase := strings.ToLower(stageName) words := []string{instruction.Value, instruction.Next.Value, instruction.Next.Next.Value, lowercase} - diagnostic.Data = []types.NamedEdit{ - { - Title: fmt.Sprintf("Convert stage name (%v) to lowercase (%v)", stageName, lowercase), - Edit: strings.Join(words, " "), - }, + return &types.NamedEdit{ + Title: fmt.Sprintf("Convert stage name (%v) to lowercase (%v)", stageName, lowercase), + Edit: strings.Join(words, " "), } } else if warning.RuleName == "RedundantTargetPlatform" { words := getWords(instruction) for i := range words { if words[i] == "--platform=$TARGETPLATFORM" { words = slices.Delete(words, i, i+1) - diagnostic.Data = []types.NamedEdit{ - { - Title: "Remove unnecessary --platform flag", - Edit: strings.Join(words, " "), - }, - } break } } + return &types.NamedEdit{ + Title: "Remove unnecessary --platform flag", + Edit: strings.Join(words, " "), + } } else if warning.RuleName == "ConsistentInstructionCasing" { words := getWords(instruction) suggestion := strings.ToUpper(instruction.Value) @@ -134,14 +128,13 @@ func setDiagnosticData(diagnostic *protocol.Diagnostic, instruction *parser.Node suggestion = strings.ToLower(suggestion) } words[0] = suggestion - diagnostic.Data = []types.NamedEdit{ - { - Title: fmt.Sprintf("Convert to %v", caseSuggestion), - Edit: strings.Join(words, " "), - }, + return &types.NamedEdit{ + Title: fmt.Sprintf("Convert to %v", caseSuggestion), + Edit: strings.Join(words, " "), } } } + return nil } func convertToDiagnostics(source string, doc document.DockerfileDocument, lines []string, warnings []lint.Warning) []protocol.Diagnostic { @@ -168,12 +161,71 @@ func convertToDiagnostics(source string, doc document.DockerfileDocument, lines diagnostic.Tags = []protocol.DiagnosticTag{protocol.DiagnosticTagDeprecated} } instruction := doc.Instruction(protocol.Position{Line: uint32(warning.Location.Ranges[0].Start.Line) - 1}) - setDiagnosticData(diagnostic, instruction, warning) + ignoreEdit := createIgnoreEdit(warning.RuleName) + resolutionEdit := createResolutionEdit(instruction, warning) + if resolutionEdit == nil { + if ignoreEdit != nil { + diagnostic.Data = []types.NamedEdit{*ignoreEdit} + } + } else { + diagnostic.Data = []types.NamedEdit{*resolutionEdit, *ignoreEdit} + } diagnostics = append(diagnostics, *diagnostic) } return diagnostics } +func createIgnoreEdit(ruleName string) *types.NamedEdit { + switch ruleName { + case "ConsistentInstructionCasing": + fallthrough + case "CopyIgnoredFile": + fallthrough + case "DuplicateStageName": + fallthrough + case "FromAsCasing": + fallthrough + case "FromPlatformFlagConstDisallowed": + fallthrough + case "InvalidDefaultArgInFrom": + fallthrough + case "InvalidDefinitionDescription": + fallthrough + case "JSONArgsRecommended": + fallthrough + case "LegacyKeyValueFormat": + fallthrough + case "MaintainerDeprecated": + fallthrough + case "MultipleInstructionsDisallowed": + fallthrough + case "NoEmptyContinuation": + fallthrough + case "RedundantTargetPlatform": + fallthrough + case "ReservedStageName": + fallthrough + case "SecretsUsedInArgOrEnv": + fallthrough + case "StageNameCasing": + fallthrough + case "UndefinedArgInFrom": + fallthrough + case "UndefinedVar": + fallthrough + case "WorkdirRelativePath": + return &types.NamedEdit{ + Title: fmt.Sprintf("Ignore this type of error with check=skip=%v", ruleName), + Edit: fmt.Sprintf("# check=skip=%v\n", ruleName), + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + } + } + return nil +} + func lintWithBuildKitBinary(contextPath, source string, doc document.DockerfileDocument, content string) ([]protocol.Diagnostic, error) { var buf bytes.Buffer cmd := exec.Command("docker", "buildx", "build", "--call=check,format=json", "-f-", contextPath) diff --git a/internal/pkg/buildkit/service_test.go b/internal/pkg/buildkit/service_test.go index e90adc9..a7dcedb 100644 --- a/internal/pkg/buildkit/service_test.go +++ b/internal/pkg/buildkit/service_test.go @@ -117,6 +117,14 @@ func TestParse(t *testing.T) { Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", Edit: "LABEL org.opencontainers.image.authors=\"test123@docker.com\"", }, + { + Title: "Ignore this type of error with check=skip=MaintainerDeprecated", + Edit: "# check=skip=MaintainerDeprecated\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -144,6 +152,14 @@ func TestParse(t *testing.T) { Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", Edit: "LABEL org.opencontainers.image.authors=\"hello world\"", }, + { + Title: "Ignore this type of error with check=skip=MaintainerDeprecated", + Edit: "# check=skip=MaintainerDeprecated\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -171,6 +187,14 @@ func TestParse(t *testing.T) { Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", Edit: "LABEL org.opencontainers.image.authors=\"hello world\"", }, + { + Title: "Ignore this type of error with check=skip=MaintainerDeprecated", + Edit: "# check=skip=MaintainerDeprecated\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -198,6 +222,14 @@ func TestParse(t *testing.T) { Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", Edit: "LABEL org.opencontainers.image.authors=\"hello world\"", }, + { + Title: "Ignore this type of error with check=skip=MaintainerDeprecated", + Edit: "# check=skip=MaintainerDeprecated\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -225,6 +257,14 @@ func TestParse(t *testing.T) { Title: "Convert MAINTAINER to a org.opencontainers.image.authors LABEL", Edit: "LABEL org.opencontainers.image.authors=\"hello world\"", }, + { + Title: "Ignore this type of error with check=skip=MaintainerDeprecated", + Edit: "# check=skip=MaintainerDeprecated\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -251,6 +291,14 @@ func TestParse(t *testing.T) { Title: "Convert stage name (TEST) to lowercase (test)", Edit: "FROM scratch AS test", }, + { + Title: "Ignore this type of error with check=skip=StageNameCasing", + Edit: "# check=skip=StageNameCasing\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -277,6 +325,14 @@ func TestParse(t *testing.T) { Title: "Convert stage name (MixeD) to lowercase (mixed)", Edit: "FROM scratch AS mixed", }, + { + Title: "Ignore this type of error with check=skip=StageNameCasing", + Edit: "# check=skip=StageNameCasing\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -303,6 +359,14 @@ func TestParse(t *testing.T) { Title: "Remove unnecessary --platform flag", Edit: "FROM alpine AS builder", }, + { + Title: "Ignore this type of error with check=skip=RedundantTargetPlatform", + Edit: "# check=skip=RedundantTargetPlatform\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -329,6 +393,14 @@ func TestParse(t *testing.T) { Title: "Convert to uppercase", Edit: "COPY --link . .", }, + { + Title: "Ignore this type of error with check=skip=ConsistentInstructionCasing", + Edit: "# check=skip=ConsistentInstructionCasing\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -355,6 +427,14 @@ func TestParse(t *testing.T) { Title: "Convert to lowercase", Edit: "copy --link . .", }, + { + Title: "Ignore this type of error with check=skip=ConsistentInstructionCasing", + Edit: "# check=skip=ConsistentInstructionCasing\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, }, }, }, @@ -388,6 +468,16 @@ func TestParse(t *testing.T) { CodeDescription: &protocol.CodeDescription{ HRef: "https://docs.docker.com/go/dockerfile/rule/duplicate-stage-name/", }, + Data: []types.NamedEdit{ + { + Title: "Ignore this type of error with check=skip=DuplicateStageName", + Edit: "# check=skip=DuplicateStageName\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, + }, }, }, }, @@ -408,6 +498,16 @@ func TestParse(t *testing.T) { CodeDescription: &protocol.CodeDescription{ HRef: "https://docs.docker.com/go/dockerfile/rule/multiple-instructions-disallowed/", }, + Data: []types.NamedEdit{ + { + Title: "Ignore this type of error with check=skip=MultipleInstructionsDisallowed", + Edit: "# check=skip=MultipleInstructionsDisallowed\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, + }, }, }, }, @@ -428,6 +528,16 @@ func TestParse(t *testing.T) { CodeDescription: &protocol.CodeDescription{ HRef: "https://docs.docker.com/go/dockerfile/rule/no-empty-continuation/", }, + Data: []types.NamedEdit{ + { + Title: "Ignore this type of error with check=skip=NoEmptyContinuation", + Edit: "# check=skip=NoEmptyContinuation\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, + }, }, }, }, @@ -448,6 +558,16 @@ func TestParse(t *testing.T) { CodeDescription: &protocol.CodeDescription{ HRef: "https://docs.docker.com/go/dockerfile/rule/workdir-relative-path/", }, + Data: []types.NamedEdit{ + { + Title: "Ignore this type of error with check=skip=WorkdirRelativePath", + Edit: "# check=skip=WorkdirRelativePath\n", + Range: &protocol.Range{ + Start: protocol.Position{Line: 0, Character: 0}, + End: protocol.Position{Line: 0, Character: 0}, + }, + }, + }, }, }, }, From 84b62b2bc4f91710b22f3bd2577f114eb50875fa Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Wed, 11 Jun 2025 12:44:59 -0400 Subject: [PATCH 2/2] Include the fix in CHANGELOG.md Signed-off-by: Remy Suen --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f85472..d707e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to the Docker Language Server will be documented in this fil ### Added +- Dockerfile + - textDocument/publishDiagnostics + - provide code actions to easily ignore build checks ([#320](https://github.com/docker/docker-language-server/issues/320)) - Compose - textDocument/completion - add support for suggesting `include` properties ([#316](https://github.com/docker/docker-language-server/issues/316))