| name | port-hadolint-rule |
| description | Port a Hadolint rule from Haskell to Go implementation |
| argument-hint | rule-code (e.g. DL3022) |
| disable-model-invocation | true |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash(go *), Bash(git status), mcp__github__get_file_contents, mcp__github__search_code |
Port Hadolint Rule to Go
You are porting a Hadolint rule from Haskell to Go for the tally project.
Rule to Port: $ARGUMENTS
Step 1: Fetch Original Haskell Implementation
Use the GitHub MCP tools to fetch the original Haskell implementation:
-
First, try to get the rule implementation file directly:
- Use
mcp__github__get_file_contents with:
owner: "hadolint"
repo: "hadolint"
path: "src/Hadolint/Rule/$ARGUMENTS.hs"
branch: "master"
-
If that fails, use mcp__github__search_code to find the rule:
- Search in
repo:hadolint/hadolint for the rule code
-
Carefully analyze the Haskell implementation to understand:
- What Dockerfile instructions it checks (RUN, COPY, FROM, etc.)
- The exact conditions that trigger a violation
- The error message format
- Any edge cases handled
Step 2: Fetch ALL Original Test Cases
Use GitHub MCP tools to get the test specification:
-
Try to get the test file directly:
- Use
mcp__github__get_file_contents with:
owner: "hadolint"
repo: "hadolint"
path: "test/Hadolint/Rule/$ARGUMENTSSpec.hs"
branch: "master"
-
If that fails, search for the test file:
- Use
mcp__github__search_code in repo:hadolint/hadolint for "$ARGUMENTSSpec"
-
Extract ALL test cases from the spec file - both passing and failing cases
ruleCatches indicates the rule SHOULD trigger (expect violation)
ruleCatchesNot indicates the rule should NOT trigger (expect no violation)
Step 3: Analyze Existing Patterns
Before implementing, read these files to understand the patterns:
- Read
internal/rules/hadolint/dl3004.go - a standard rule implementation
- Read
internal/rules/hadolint/dl3012.go - a pointer file for semantic-based rules
- Read
internal/shell/shell.go - shell parsing utilities
- Read
internal/shell/packages.go - package manager parsing
- Read
internal/semantic/semantic.go - semantic model
- Read
internal/semantic/builder.go - semantic model builder
- Read
internal/rules/rule.go - Rule interface and LintInput
Step 4: Determine Implementation Location
Decide where to implement based on rule nature:
Option A: Standard Rule (internal/rules/hadolint/$ARGUMENTS.go)
- For rules checking specific instructions (RUN commands, COPY sources, etc.)
- For rules that can iterate through stages and commands
Option B: Semantic Model (internal/semantic/builder.go) + Pointer File
- For rules requiring cross-instruction analysis
- For rules checking duplicate instructions (like DL3012 for HEALTHCHECK)
- For rules checking stage references
- Create pointer file at
internal/rules/hadolint/$ARGUMENTS.go documenting the semantic implementation
Step 5: Implementation Requirements
CRITICAL: Shell Command Parsing
NEVER parse shell commands using regex or string operations.
Always use the internal/shell package:
import "github.com/wharflab/tally/internal/shell"
if shell.ContainsCommandWithVariant(cmdStr, "sudo", shellVariant) {
}
commands := shell.CommandNamesWithVariant(cmdStr, shellVariant)
installs := shell.ExtractPackageInstalls(cmdStr, shellVariant)
CRITICAL: Use Semantic Model
Always leverage the semantic model (internal/semantic/):
sem, ok := input.Semantic.(*semantic.Model)
if !ok {
sem = nil
}
if sem != nil {
if info := sem.StageInfo(stageIdx); info != nil {
shellVariant = info.ShellSetting.Variant
if shellVariant.IsNonPOSIX() {
continue
}
}
}
for info := range sem.ExternalImageStages() {
}
Enhance Semantic Model If Needed
If the rule requires semantic information not yet tracked:
- Add fields to
StageInfo in internal/semantic/stage_info.go
- Populate fields in
internal/semantic/builder.go
- Document the enhancement
Rule Structure Template
package hadolint
import (
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/wharflab/tally/internal/rules"
"github.com/wharflab/tally/internal/semantic"
"github.com/wharflab/tally/internal/shell"
)
type $ARGUMENTSRule struct{}
func New$ARGUMENTSRule() *$ARGUMENTSRule {
return &$ARGUMENTSRule{}
}
func (r *$ARGUMENTSRule) Metadata() rules.RuleMetadata {
return rules.RuleMetadata{
Code: rules.HadolintRulePrefix + "$ARGUMENTS",
Name: "...",
Description: "...",
DocURL: rules.HadolintDocURL("$ARGUMENTS"),
DefaultSeverity: rules.SeverityWarning,
Category: "...",
IsExperimental: false,
}
}
func (r *$ARGUMENTSRule) Check(input rules.LintInput) []rules.Violation {
var violations []rules.Violation
meta := r.Metadata()
sem, ok := input.Semantic.(*semantic.Model)
if !ok {
sem = nil
}
return violations
}
func init() {
rules.Register(New$ARGUMENTSRule())
}
Step 6: Write Tests
Create test file internal/rules/hadolint/$ARGUMENTS_test.go:
- Include ALL test cases from the original Hadolint spec
- Follow the pattern in
internal/rules/hadolint/dl3004_test.go
- Use
testutil.MakeLintInput to create test inputs
func Test$ARGUMENTSRule_Check(t *testing.T) {
tests := []struct {
name string
dockerfile string
wantCount int
}{
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := testutil.MakeLintInput(t, "Dockerfile", tt.dockerfile)
r := New$ARGUMENTSRule()
violations := r.Check(input)
if len(violations) != tt.wantCount {
t.Errorf("got %d violations, want %d", len(violations), tt.wantCount)
}
})
}
}
Step 7: Verify Implementation
Run the tests to verify:
go test ./internal/rules/hadolint/... -run $ARGUMENTS -v
Ensure ALL original Hadolint test cases pass.
Step 8: Add Integration Test
Add a directory fixture for the new rule:
- Create
internal/integration/fixtures/lint/$ARGUMENTS/Dockerfile.
- Add
internal/integration/fixtures/lint/$ARGUMENTS/.tally.toml selecting the Hadolint rule and any overlap rules needed by the case.
- If the port includes a fix, also create
internal/integration/fixtures/fix/$ARGUMENTS/Dockerfile and optional .tally.toml.
- Run
UPDATE_SNAPS=true go test ./internal/integration -run 'Test(Lint|Fix)Fixtures' -count=1.
- Re-run without
UPDATE_SNAPS.
The fixture harness writes go-snaps files next to the Dockerfile: result_1.snap.json for lint, fixed_1.snap.Dockerfile for fix output, and
optional report_1.snap.md for fix reports. Use explicit Go integration tests only for behavior that needs CLI/config/discovery/stdin/multi-file
coverage outside the directory harness.
Step 9: Consider Auto-Fix Support
Evaluate whether the rule can provide automatic fixes:
When to Add Auto-Fix
- Good candidates: Simple text replacements (apt → apt-get), command additions (--no-cache)
- Avoid auto-fix for: Rules requiring significant restructuring or user decisions
Sync Fixes (Immediate Edits)
For fixes that can be computed immediately:
func (r *MyRule) Check(input rules.LintInput) []rules.Violation {
return ScanRunCommandsWithPOSIXShell(input, func(run *instructions.RunCommand, shellVariant shell.Variant, file string) []rules.Violation {
fix := &rules.SuggestedFix{
Description: "Replace X with Y",
Safety: rules.FixSafe,
Edits: []rules.TextEdit{{
Location: rules.NewRangeLocation(file, startLine, startCol, endLine, endCol),
NewText: "replacement text",
}},
}
return []rules.Violation{
rules.NewViolation(loc, meta.Code, msg, meta.DefaultSeverity).
WithSuggestedFix(fix),
}
})
}
Async Fixes (External Data Required)
For fixes requiring network I/O (image digests, checksums):
fix := &rules.SuggestedFix{
Description: "Add image digest",
Safety: rules.FixSafe,
NeedsResolve: true,
ResolverID: "image-digest",
ResolverData: map[string]string{"image": "alpine", "tag": "3.18"},
}
Safety Levels
FixSafe: Always correct, won't change behavior
FixSuggestion: Likely correct but may need review
FixUnsafe: May change behavior significantly
Shell Position Tracking
For precise command replacement within RUN instructions, use shell.FindCommandOccurrences() to get exact byte offsets within shell scripts.
Step 10: Update Hadolint Status Tracking
After implementation is complete, update the tracking files:
-
Update hadolint-status.json:
Add an entry to internal/rules/hadolint-status.json:
"$ARGUMENTS": {
"status": "implemented",
"tally_rule": "hadolint/$ARGUMENTS"
}
Place it in alphabetical order among the other rules.
-
Regenerate documentation:
./scripts/generate-hadolint-table.sh --update
This updates the Hadolint compatibility table in the documentation.
-
Update integration test snapshots (if needed):
UPDATE_SNAPS=true go test ./internal/integration/...
Step 11: Create Rule Documentation
Create _docs/rules/hadolint/$ARGUMENTS.mdx following the pattern in _docs/rules/hadolint/DL3004.mdx:
- Title:
# hadolint/$ARGUMENTS
- Properties table (Severity, Category, Default, Auto-fix)
- Description from the Hadolint wiki
- Examples (Problematic / Correct code)
## Auto-fix section with before/after example (if auto-fix is supported)
## Reference section with named link: - [hadolint/$ARGUMENTS](https://github.com/hadolint/hadolint/wiki/$ARGUMENTS)
Add the rule to _docs/rules/hadolint/overview.mdx in the main table.
Checklist Before Completion