rules

package
v0.18.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 26, 2026 License: GPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package rules provides the core rule system for the Dockerfile linter.

Index

Constants

View Source
const BuildKitRulePrefix = "buildkit/"

BuildKitRulePrefix is the namespace prefix for rules from BuildKit's linter.

View Source
const EpilogueOrderResolverID = "epilogue-order"

EpilogueOrderResolverID is the unique identifier for the epilogue-order fix resolver.

View Source
const HadolintRulePrefix = "hadolint/"

HadolintRulePrefix is the namespace prefix for Hadolint-compatible rules.

View Source
const HeredocDefaultMinCommands = 3

HeredocDefaultMinCommands is the default minimum number of commands required to suggest heredoc conversion. Heredocs add 2 lines overhead (<<EOF and EOF), so converting 2 commands saves no lines.

View Source
const HeredocResolverID = "prefer-run-heredoc"

HeredocResolverID is the unique identifier for the heredoc fix resolver.

View Source
const HeredocRuleCode = TallyRulePrefix + "prefer-run-heredoc"

HeredocRuleCode is the full rule code for the prefer-run-heredoc rule. Used by other rules (like DL3003) to check if heredoc conversion is enabled.

View Source
const NewlineResolverID = "newline-between-instructions"

NewlineResolverID is the unique identifier for the newline-between-instructions fix resolver.

View Source
const PipefailRuleCode = HadolintRulePrefix + "DL4006"

PipefailRuleCode is the full rule code for the DL4006 pipefail rule. Used by the heredoc formatter to determine whether to add set -o pipefail.

View Source
const ShellcheckRulePrefix = "shellcheck/"

ShellcheckRulePrefix is the namespace prefix for ShellCheck-derived rules.

View Source
const TallyRulePrefix = "tally/"

TallyRulePrefix is the namespace prefix for tally's own rules.

Variables

View Source
var EpilogueOrderRank = map[string]int{
	command.StopSignal:  0,
	command.Healthcheck: 1,
	command.Entrypoint:  2,
	command.Cmd:         3,
}

EpilogueOrderRank maps lowercase epilogue instruction names to their canonical position. This is the single source of truth for epilogue ordering, shared by the rule and resolver.

Functions

func BuildKitDocURL added in v0.13.0

func BuildKitDocURL(ruleName string) string

BuildKitDocURL returns the documentation URL for a BuildKit rule. The ruleName should be the PascalCase name without prefix (e.g. "StageNameCasing").

func CheckEpilogueOrder added in v0.11.0

func CheckEpilogueOrder(commands []instructions.Command) bool

CheckEpilogueOrder reports whether epilogue instructions in the given commands are correctly positioned at the end and in canonical order. ONBUILD commands are ignored.

func HadolintDocURL added in v0.13.0

func HadolintDocURL(ruleCode string) string

HadolintDocURL returns the documentation URL for a Hadolint rule. The ruleCode should be the DL/SC code without prefix (e.g. "DL3001").

func HasDuplicateEpilogueNames added in v0.11.0

func HasDuplicateEpilogueNames(names []string) bool

HasDuplicateEpilogueNames reports whether any name in the slice appears more than once.

func IsEpilogueInstruction added in v0.11.0

func IsEpilogueInstruction(name string) bool

IsEpilogueInstruction reports whether the lowercase instruction name is an epilogue instruction.

func Register

func Register(rule Rule)

Register adds a rule to the default registry.

func ShellcheckDocURL added in v0.17.0

func ShellcheckDocURL(ruleCode string) string

ShellcheckDocURL returns the documentation URL for a ShellCheck diagnostic code. The ruleCode should be the SC code without prefix (e.g. "SC2086").

We link to the upstream ShellCheck wiki, which has stable per-code pages.

func TallyDocURL added in v0.9.6

func TallyDocURL(ruleCode string) string

TallyDocURL returns the documentation URL for a tally rule code. The ruleCode should include the "tally/" prefix (e.g. "tally/max-lines").

Types

type AsyncRule

type AsyncRule interface {
	Rule
	PlanAsync(input LintInput) []async.CheckRequest
}

AsyncRule is an optional interface for rules that require slow I/O (registry, network, filesystem). Rules implementing this interface participate in the async checks pipeline.

PlanAsync is called during the planning phase (no I/O). It returns check requests that the async runtime will execute under budget control.

The handler's OnSuccess returns []any where each element is a rules.Violation. This avoids an import cycle between the async and rules packages.

type BuildContext

type BuildContext interface {
	// IsIgnored checks if a path would be ignored by .dockerignore.
	IsIgnored(path string) (bool, error)

	// FileExists checks if a file exists in the build context.
	FileExists(path string) bool

	// IsHeredocFile checks if a path is a virtual heredoc file.
	IsHeredocFile(path string) bool

	// HasIgnoreFile returns true if a .dockerignore file exists.
	HasIgnoreFile() bool
}

BuildContext is an interface for build-time context awareness. Rules can type-assert this to *context.BuildContext if they need context-aware features like .dockerignore checking.

type ConfigurableRule

type ConfigurableRule interface {
	Rule

	// Schema returns the JSON Schema for this rule's configuration options.
	// This follows ESLint's meta.schema pattern where rules define their own schema.
	// The schema is used for:
	//   - Config validation
	//   - Documentation generation
	//   - IDE autocompletion
	//
	// Example return value:
	//
	//	map[string]any{
	//	    "type": "object",
	//	    "properties": map[string]any{
	//	        "max": map[string]any{"type": "integer", "minimum": 0, "default": 50},
	//	    },
	//	    "additionalProperties": false,
	//	}
	Schema() map[string]any

	// DefaultConfig returns the default configuration for this rule.
	// Used when no user config is provided.
	DefaultConfig() any

	// ValidateConfig checks if a configuration is valid for this rule.
	ValidateConfig(config any) error
}

ConfigurableRule is an optional interface for rules that accept configuration.

type EpilogueOrderResolveData added in v0.11.0

type EpilogueOrderResolveData struct {
	// StageIndex is the 0-based index of the stage that triggered the violation.
	StageIndex int
}

EpilogueOrderResolveData carries resolver context. The resolver is self-contained (re-parses and re-analyzes the file), so StageIndex is informational — it identifies the stage that triggered the violation. The resolver processes all applicable stages in one pass.

type FixSafety

type FixSafety int

FixSafety categorizes how reliable a fix is.

const (
	// FixSafe means the fix is always correct and won't change behavior.
	// These fixes can be applied automatically without review.
	FixSafe FixSafety = iota

	// FixSuggestion means the fix is likely correct but may need review.
	// Examples: apt search → apt-cache search (different output format).
	FixSuggestion

	// FixUnsafe means the fix might change behavior significantly.
	// These require explicit --fix-unsafe flag to apply.
	FixUnsafe
)

func (FixSafety) String

func (s FixSafety) String() string

String returns the string representation of FixSafety.

type HeredocFixType

type HeredocFixType int

HeredocFixType indicates the type of heredoc fix.

const (
	// HeredocFixConsecutive is for consecutive RUN instructions.
	HeredocFixConsecutive HeredocFixType = iota
	// HeredocFixChained is for a single RUN with chained commands.
	HeredocFixChained
)

type HeredocResolveData

type HeredocResolveData struct {
	// Type indicates whether this is for consecutive RUNs or chained commands.
	Type HeredocFixType

	// StageIndex is the 0-based index of the stage containing the RUN(s).
	StageIndex int

	// ShellVariant is the shell variant for parsing.
	ShellVariant shell.Variant

	// MinCommands is the minimum number of commands to trigger heredoc conversion.
	MinCommands int

	// PipefailEnabled indicates whether DL4006 (pipefail) is enabled.
	// When true, the heredoc formatter will add "set -o pipefail" inside the
	// heredoc body if any command contains a pipe, avoiding the need for a
	// separate SHELL instruction.
	PipefailEnabled bool
}

HeredocResolveData contains the data needed to resolve a heredoc fix. This is stored in SuggestedFix.ResolverData.

The resolver uses re-parsing to find fixes rather than fingerprint matching. This approach is more robust because content may have changed due to sync fixes (apt → apt-get, cd → WORKDIR, etc.) applied before the heredoc resolver runs.

type LintInput

type LintInput struct {
	// File is the path to the Dockerfile being linted.
	File string

	// AST is the parsed Dockerfile AST from BuildKit (guaranteed non-nil).
	// Use AST nodes for line information, not raw source counting.
	AST *parser.Result

	// Stages contains the parsed build stages with typed instructions.
	// This is populated by BuildKit's instructions.Parse().
	Stages []instructions.Stage

	// MetaArgs contains ARG instructions that appear before the first FROM.
	// These are global build arguments that affect base image selection.
	MetaArgs []instructions.ArgCommand

	// Source is the raw source content of the Dockerfile.
	// Used for snippet extraction and directive parsing.
	Source []byte

	// Context is optional build context for context-aware rules.
	// Rules should check for nil before using.
	Context BuildContext

	// Semantic is the semantic model for cross-instruction analysis.
	// Provides stage resolution, variable scoping, and COPY --from validation.
	// May be nil for backward compatibility with rules that don't need it.
	// Type is *semantic.Model but declared as any to avoid import cycle.
	Semantic any

	// Config is the rule-specific configuration (type depends on rule).
	Config any

	// EnabledRules contains the codes of all rules that are enabled in this run.
	// Rules can check this to coordinate behavior, e.g., DL3003 can skip its fix
	// if prefer-run-heredoc is enabled and would handle the command better.
	// May be nil if not computed (for backward compatibility in tests).
	EnabledRules []string

	// HeredocMinCommands is the configured min-commands for the prefer-run-heredoc rule.
	// Rules that coordinate with heredoc (like DL3003) should use this value.
	// Zero means use the default (HeredocDefaultMinCommands).
	HeredocMinCommands int
}

LintInput contains all the information a rule needs to check a Dockerfile. Rules should work with the AST and typed instructions, not raw source text.

The linter guarantees that AST and Source are always valid (non-nil) when Check is called. If parsing fails, the linter reports parse errors and exits without invoking any rules (following ESLint's approach).

IMPORTANT: LintInput is read-only. Rules must not mutate any fields (File, AST, Stages, MetaArgs, Source, Context, Config). If a rule needs to modify data, it must copy it first. This prevents hidden coupling between rules.

func (LintInput) GetHeredocMinCommands

func (input LintInput) GetHeredocMinCommands() int

GetHeredocMinCommands returns the configured min-commands for heredoc conversion. Returns the configured value if set, otherwise returns HeredocDefaultMinCommands. Used by rules coordinating with prefer-run-heredoc (e.g., DL3003).

func (LintInput) IsRuleEnabled

func (input LintInput) IsRuleEnabled(ruleCode string) bool

IsRuleEnabled checks if a specific rule is enabled in the current run. Returns false if EnabledRules is nil (for backward compatibility in tests).

func (LintInput) Snippet

func (input LintInput) Snippet(startLine, endLine int) string

Snippet extracts a range of lines from the source (0-based, inclusive). This is a convenience wrapper around SourceMap().Snippet().

func (LintInput) SnippetForLocation

func (input LintInput) SnippetForLocation(loc Location) string

SnippetForLocation extracts the source code at a location. If the location is file-level (no specific line), returns empty string. If the location is a point, returns just that line. If the location is a range, returns all lines in the range.

Note: Location uses 1-based line numbers, SourceMap uses 0-based.

func (LintInput) SourceMap

func (input LintInput) SourceMap() *sourcemap.SourceMap

SourceMap creates a SourceMap for snippet extraction and line-based operations. The SourceMap is computed on demand from Source. Results are not cached; call once and reuse if needed multiple times.

type Location

type Location struct {
	// File is the path to the source file (not in parser.Range).
	File string `json:"file"`
	// Start is the starting position (inclusive, 1-based line numbers).
	Start Position `json:"start"`
	// End is the ending position (exclusive, LSP semantics).
	// Points to the first position after the range.
	// A point location has End.Line < 0 (unset) or End equals Start.
	End Position `json:"end"`
}

Location represents a range in a source file. This extends parser.Range by adding the File path. BuildKit's Range only contains Start/End positions without file context.

Following LSP conventions, Start is inclusive and End is exclusive. This means End points to the first position AFTER the covered text.

See: github.com/moby/buildkit/frontend/dockerfile/parser.Range

func NewFileLocation

func NewFileLocation(file string) Location

NewFileLocation creates a location for file-level issues (no specific line). Uses -1 as sentinel since 0 would be invalid (lines are 1-based).

func NewLineLocation

func NewLineLocation(file string, line int) Location

NewLineLocation creates a location for a specific line (1-based). Creates a point location (no range) at the start of the line.

func NewLocationFromRange

func NewLocationFromRange(file string, r parser.Range) Location

NewLocationFromRange converts a BuildKit parser.Range to our Location type. This bridges BuildKit's internal types with our output schema. BuildKit uses 1-based line numbers. The mapping is direct—no adjustment needed.

func NewLocationFromRanges

func NewLocationFromRanges(file string, ranges []parser.Range) Location

NewLocationFromRanges creates a Location from a slice of BuildKit Ranges. Uses the first range if multiple exist, or returns file-level if empty.

func NewRangeLocation

func NewRangeLocation(file string, startLine, startCol, endLine, endCol int) Location

NewRangeLocation creates a location spanning multiple lines/columns. Lines are 1-based, columns are 0-based.

func (Location) IsFileLevel

func (l Location) IsFileLevel() bool

IsFileLevel returns true if this is a file-level location (no specific line).

func (Location) IsPointLocation

func (l Location) IsPointLocation() bool

IsPointLocation returns true if this is a single-point location (no range). A point location has End.Line < 0 (unset) or End equals Start.

type NewlineResolveData added in v0.11.0

type NewlineResolveData struct {
	Mode string
}

NewlineResolveData carries the configuration mode for the resolver. This is stored in SuggestedFix.ResolverData.

type Position

type Position struct {
	// Line is the 1-based line number (first line is 1, same as BuildKit).
	Line int `json:"line"`
	// Column is the 0-based column number (same as BuildKit's Character field).
	Column int `json:"column"`
}

Position represents a single point in a source file. This is our JSON-serializable equivalent of parser.Position, which uses "Character" instead of "Column" and lacks JSON tags.

We use 1-based line numbers to align with BuildKit's internal representation. Note: BuildKit's AST uses 1-based lines (first line is 1), not 0-based.

See: github.com/moby/buildkit/frontend/dockerfile/parser.Position

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages rule registration and lookup.

func DefaultRegistry

func DefaultRegistry() *Registry

DefaultRegistry returns the global default registry.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new empty registry.

func (*Registry) All

func (r *Registry) All() []Rule

All returns all registered rules sorted by code.

func (*Registry) ByCategory

func (r *Registry) ByCategory(category string) []Rule

ByCategory returns rules filtered by category.

func (*Registry) BySeverity

func (r *Registry) BySeverity(severity Severity) []Rule

BySeverity returns rules filtered by default severity.

func (*Registry) Codes

func (r *Registry) Codes() []string

Codes returns all registered rule codes sorted alphabetically.

func (*Registry) Experimental

func (r *Registry) Experimental() []Rule

Experimental returns rules marked as experimental.

func (*Registry) Get

func (r *Registry) Get(code string) Rule

Get retrieves a rule by its code. Returns nil if no rule is found.

func (*Registry) Has

func (r *Registry) Has(code string) bool

Has returns true if a rule with the given code is registered.

func (*Registry) Register

func (r *Registry) Register(rule Rule)

Register adds a rule to the registry. Panics if a rule with the same code is already registered.

type Rule

type Rule interface {
	// Metadata returns static information about the rule.
	Metadata() RuleMetadata

	// Check runs the rule against the given input and returns any violations.
	// The AST and Source fields are guaranteed non-nil. The Context field
	// may be nil in v1.0 (context-aware linting is optional).
	Check(input LintInput) []Violation
}

Rule is the interface that all linting rules must implement.

func All

func All() []Rule

All returns all rules from the default registry.

type RuleMetadata

type RuleMetadata struct {
	// Code is the unique identifier (e.g., "DL3006", "max-lines").
	Code string

	// Name is the human-readable rule name.
	Name string

	// Description explains what the rule checks.
	Description string

	// DocURL links to detailed documentation.
	DocURL string

	// DefaultSeverity is the severity when not overridden.
	DefaultSeverity Severity

	// Category groups related rules (e.g., "security", "performance", "style").
	Category string

	// IsExperimental marks rules that may change or be removed.
	IsExperimental bool

	// FixPriority determines the order in which fixes are applied.
	// Lower values = earlier application (content fixes like DL3027: apt → apt-get).
	// Higher values = later application (structural transforms like prefer-run-heredoc).
	// Default 0 is for content fixes. Use 100+ for structural transformations.
	FixPriority int
}

RuleMetadata contains static information about a rule.

type Severity

type Severity int

Severity represents the severity level of a rule violation.

const (
	// SeverityError indicates a critical issue that should fail the build.
	SeverityError Severity = iota
	// SeverityWarning indicates a significant issue that may cause problems.
	SeverityWarning
	// SeverityInfo indicates a suggestion or best practice recommendation.
	SeverityInfo
	// SeverityStyle indicates a style/formatting preference.
	SeverityStyle

	// SeverityOff disables the rule completely.
	// Placed after other severities to avoid zero-value confusion.
	SeverityOff
)

func ParseSeverity

func ParseSeverity(s string) (Severity, error)

ParseSeverity parses a severity string into a Severity value.

func (Severity) IsAtLeast

func (s Severity) IsAtLeast(threshold Severity) bool

IsAtLeast returns true if s is at least as severe as threshold.

func (Severity) IsMoreSevereThan

func (s Severity) IsMoreSevereThan(other Severity) bool

IsMoreSevereThan returns true if s is more severe than other.

func (Severity) MarshalJSON

func (s Severity) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Severity) String

func (s Severity) String() string

String returns the string representation of the severity.

func (*Severity) UnmarshalJSON

func (s *Severity) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. Pointer receiver required by json.Unmarshaler interface.

type SuggestedFix

type SuggestedFix struct {
	// Description explains what this fix does.
	Description string `json:"description"`

	// Edits contains the actual text replacements to apply.
	// May be empty if NeedsResolve is true (populated by resolver).
	Edits []TextEdit `json:"edits,omitempty"`

	// Safety indicates how reliable this fix is.
	// Default (zero value) is FixSafe.
	Safety FixSafety `json:"safety,omitzero"`

	// IsPreferred marks this as the recommended fix when alternatives exist.
	IsPreferred bool `json:"isPreferred,omitzero"`

	// NeedsResolve indicates this fix requires async resolution.
	// When true, Edits is empty and ResolverID specifies which resolver to use.
	// Examples: fetching image digests, computing file checksums.
	NeedsResolve bool `json:"needsResolve,omitzero"`

	// ResolverID identifies which FixResolver should compute the edits.
	// Only used when NeedsResolve is true.
	ResolverID string `json:"resolverId,omitempty"`

	// ResolverData contains opaque data for the resolver.
	// Not serialized to JSON; used internally during fix application.
	ResolverData any `json:"-"`

	// ResolveErr captures resolver failures during fix application.
	// Not serialized to JSON; used internally for diagnostics.
	ResolveErr error `json:"-"`

	// Priority determines application order when multiple fixes exist.
	// Copied from rule's FixPriority. Lower = applied first.
	// Content fixes (priority 0) run before structural transforms (priority 100+).
	Priority int `json:"priority,omitzero"`
}

SuggestedFix represents a structured edit hint for auto-fix suggestions. It describes what text to replace and what to replace it with.

Fixes can be synchronous (Edits populated immediately) or asynchronous (NeedsResolve=true, edits computed later by a FixResolver).

type TextEdit

type TextEdit struct {
	// Location specifies where to apply the edit.
	Location Location `json:"location"`
	// NewText is the text to insert/replace with. Empty string means delete.
	NewText string `json:"newText"`
}

TextEdit represents a single text replacement in a file.

type Violation

type Violation struct {
	// Location specifies where the violation occurred.
	Location Location `json:"location"`

	// RuleCode is the unique identifier for the rule (e.g., "DL3006", "max-lines").
	RuleCode string `json:"rule"`

	// Message is a human-readable description of the issue.
	Message string `json:"message"`

	// Detail provides additional context (optional).
	Detail string `json:"detail,omitempty"`

	// Severity indicates how critical this violation is.
	Severity Severity `json:"severity"`

	// DocURL links to documentation about this rule (optional).
	DocURL string `json:"docUrl,omitempty"`

	// SourceCode is the source snippet where the violation occurred (optional).
	// Populated by post-processing; rules don't need to set this.
	SourceCode string `json:"sourceCode,omitempty"`

	// SuggestedFix provides a structured fix hint (optional).
	// Supports "auto-fix suggestion" without auto-applying.
	SuggestedFix *SuggestedFix `json:"suggestedFix,omitempty"`

	// StageIndex tracks which Dockerfile stage this violation belongs to.
	// Used internally for merging async results; not serialized.
	StageIndex int `json:"-"`
}

Violation represents a single linting violation. This extends BuildKit's subrequests/lint.Warning with:

  • Severity levels (BuildKit treats all as warnings)
  • Inline file path (BuildKit uses SourceIndex into separate Sources array)
  • SuggestedFix for auto-fix hints
  • SourceCode snippet

See: github.com/moby/buildkit/frontend/subrequests/lint.Warning

func NewViolation

func NewViolation(loc Location, ruleCode, message string, severity Severity) Violation

NewViolation creates a new violation with the minimum required fields.

func NewViolationFromBuildKitWarning

func NewViolationFromBuildKitWarning(
	file string,
	ruleName string,
	description string,
	url string,
	message string,
	location []parser.Range,
) Violation

NewViolationFromBuildKitWarning converts BuildKit linter callback parameters to our Violation type. This bridges BuildKit's linter.LintWarnFunc with our output schema.

Parameters match linter.LintWarnFunc: (rulename, description, url, fmtmsg, location) The rule code is automatically namespaced with "buildkit/" prefix.

func (Violation) File

func (v Violation) File() string

File returns the file path from the location.

func (Violation) Line

func (v Violation) Line() int

Line returns the starting line number (for backward compatibility).

func (Violation) WithDetail

func (v Violation) WithDetail(detail string) Violation

WithDetail adds a detail message to the violation.

func (Violation) WithDocURL

func (v Violation) WithDocURL(url string) Violation

WithDocURL adds a documentation URL to the violation.

func (Violation) WithSourceCode

func (v Violation) WithSourceCode(code string) Violation

WithSourceCode adds source code snippet to the violation.

func (Violation) WithSuggestedFix

func (v Violation) WithSuggestedFix(fix *SuggestedFix) Violation

WithSuggestedFix adds a fix suggestion to the violation.

Directories

Path Synopsis
Package all imports all rule packages to register them.
Package all imports all rule packages to register them.
Package asyncutil provides shared helpers for async rule implementations.
Package asyncutil provides shared helpers for async rule implementations.
Package buildkit provides metadata for BuildKit's built-in linter rules.
Package buildkit provides metadata for BuildKit's built-in linter rules.
fixes
Package fixes provides auto-fix enrichment for BuildKit linter rules.
Package fixes provides auto-fix enrichment for BuildKit linter rules.
Package configutil provides utilities for rule configuration resolution.
Package configutil provides utilities for rule configuration resolution.
Package hadolint implements hadolint-compatible linting rules for Dockerfiles.
Package hadolint implements hadolint-compatible linting rules for Dockerfiles.
Package tally implements tally-specific linting rules for Dockerfiles.
Package tally implements tally-specific linting rules for Dockerfiles.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL