Create a New tally/* Rule
Implement a new custom tally rule (not BuildKit, not Hadolint) in the tally repository.
Input is $ARGUMENTS: a natural-language description of rule intent.
Step 0: Derive Rule Name From Description
Derive a concise kebab-case slug from $ARGUMENTS and use it consistently as <rule_slug>.
Rule naming requirements:
- •Prefer action-first patterns:
no-*,prefer-*,require-*,avoid-*. - •Keep it concise (typically 2-4 words).
- •Keep it descriptive and stable (avoid vague names like
best-practice-rule). - •Keep it unique across
internal/rules/tally/*.goanddocs/rules/tally/*.md. - •Final rule code is
tally/<rule_slug>.
Step 1: Define Rule Contract
Pick and lock these before coding:
- •Rule code:
tally/<rule_slug> - •Severity:
error,warning,info, orstyle - •Category:
security,correctness,maintainability,style, etc. - •Default behavior:
- •Enabled by default (
DefaultSeverity != off) or - •Off by default (
IsExperimental: trueand enabled via config)
- •Enabled by default (
- •Fix strategy:
- •No fix
- •Sync fix (
SuggestedFix.Edits) - •Async fix (
NeedsResolve: true)
- •Coordination strategy:
- •Decide precedence if this rule overlaps with existing rules
- •Decide whether to suppress this rule, suppress only its fix, or defer by priority
Step 2: Choose Code Location
Create the rule in:
- •
internal/rules/tally/<rule_slug_with_underscores>.go
If detection logic is reusable across rules, extract helpers into focused packages:
- •
internal/shell/count.go - •
internal/shell/file_creation.go - •
internal/runmount/runmount.go
Step 3: Implement Rule Skeleton
Use this structure:
package tally
import (
"github.com/tinovyatkin/tally/internal/rules"
)
type MyRule struct{}
func NewMyRule() *MyRule { return &MyRule{} }
func (r *MyRule) Metadata() rules.RuleMetadata {
return rules.RuleMetadata{
Code: rules.TallyRulePrefix + "<rule_slug>",
Name: "...",
Description: "...",
DocURL: "https://github.com/tinovyatkin/tally/blob/main/docs/rules/tally/<rule_slug>.md",
DefaultSeverity: rules.SeverityStyle,
Category: "style",
IsExperimental: false,
}
}
func (r *MyRule) Check(input rules.LintInput) []rules.Violation {
// Rule logic
return nil
}
func init() {
rules.Register(NewMyRule())
}
Notes:
- •Rules self-register via
init(); no central tally registry file exists. - •If the rule needs semantic context, use
input.Semantic.(*semantic.Model)with nil/type guards. - •If configurable, implement:
- •
Schema() map[string]any - •
DefaultConfig() any - •
ValidateConfig(config any) errorviaconfigutil.ValidateWithSchema - •
resolveConfig()usingconfigutil.Resolve(...)
- •
Step 3.5: Handle Cross-Rule Interactions Early
If your rule can target the same command region as another rule, define coordination explicitly during implementation.
Use these mechanisms:
- •Detection-time gating:
- •Use
input.IsRuleEnabled("<other_rule_code>")to avoid dual suggestions on the same construct.
- •Use
- •Fix-time precedence:
- •Use
RuleMetadata.FixPriorityto enforce deterministic ordering. - •Lower priority runs first (content edits), higher priority runs later (structural transforms).
- •Use
- •Scope partitioning:
- •Narrow one rule to patterns it owns (for example, pure file-creation vs general chained RUN transformation).
Add regression tests for overlap behavior in both involved rule test files when practical.
Step 4: Use Existing Analysis Primitives
Prefer existing infrastructure over ad-hoc parsing:
- •
internal/semanticfor stage/shell/variable context - •
internal/shellfor shell command parsing and command-shape detection - •
internal/sourcemapfor stable location/snippet handling - •
internal/runmountwhen mount-aware behavior matters
Do not use brittle string splitting/regex if semantic/shell helpers can model the behavior.
Step 4.5: Choose Sync vs Async Fix (Prefer Simpler)
Default to sync fixes. Use async fixes only when sync cannot be reliable.
Use sync fix when:
- •Edit locations are known during
Check(...). - •Replacement is local and deterministic.
- •No post-fix re-parse is needed.
Use async fix when:
- •Correct edits depend on content after other fixes apply.
- •A robust solution requires reparsing current file state.
- •External resolution is required (network, lookup, expensive deferred computation).
If async is required:
- •Set
NeedsResolve: true,ResolverID,ResolverData, andPriority. - •Implement/register resolver under
internal/fix/. - •Ensure resolver computes edits from current modified content.
- •Keep async scope minimal; avoid async when a sync edit can cover the case safely.
Step 5: Add Unit Tests
Create:
- •
internal/rules/tally/<rule_slug_with_underscores>_test.go
Follow existing pattern:
- •
Test...Metadata - •
Test...DefaultConfig(if configurable) - •
Test...ValidateConfig(if configurable) - •
Test...Checkwithtestutil.RunRuleTests - •
Test...CheckWithFixesfor fix-bearing rules
If you added helper packages, add dedicated tests there too (like internal/shell/*_test.go).
If rule coordination exists, add explicit overlap tests (for example: competing rules enabled simultaneously).
Coverage target:
- •Aim for >=85% coverage for each newly added rule/helper file.
- •Use package coverage as gate and review file/function gaps:
go test ./internal/rules/tally/... -coverprofile=/tmp/tally.cover go tool cover -func=/tmp/tally.cover
- •Add focused tests for any uncovered branches in new files until the target is met.
Step 6: Add Integration Coverage
- •
Create fixture:
- •
internal/integration/testdata/<rule_slug>/Dockerfile - •Optional:
.tally.tomlfor rule enablement/tuning
- •
- •
Add
TestCheckcase ininternal/integration/integration_test.go:- •Use
selectRules("tally/<rule_slug>")to isolate behavior
- •Use
- •
If the rule has fixes, add/extend
TestFixcase(s).- •Include at least one case where another relevant rule is also enabled.
- •Prefer realistic Dockerfile content instead of toy-only snippets.
- •
Update snapshots:
UPDATE_SNAPS=true go test ./internal/integration/...
Important: adding a new enabled rule can change rules_enabled values and the total-rules-enabled snapshot.
Step 6.5: Use Real-World Dockerfile Examples
When creating docs examples and integration fixtures, prefer life-like patterns from public repositories.
Recommended workflow (GitHub MCP):
- •Search GitHub code with a Dockerfile language filter:
- •Use
lang:Dockerfile(or GitHub equivalentlanguage:Dockerfile) plus rule-relevant keywords.
- •Use
- •Pull candidate files and extract representative snippets.
- •Adapt snippets minimally for deterministic tests (small, stable, focused on the behavior under test).
- •Avoid fully synthetic fixtures when a real-world pattern exists.
Step 7: Update Documentation
Update all of:
- •
docs/rules/tally/<rule_slug>.md(new rule page) - •
docs/rules/tally/index.md(tally rules table) - •
RULES.md- •tally summary table row
- •dedicated section for
tally/<rule_slug> - •namespace counts if changed
- •
README.mdsupported rules count/table when totals change - •
docs/index.mdtally rule count if shown
Step 8: Validate End-to-End
Run focused tests first, then broad checks:
go test ./internal/rules/tally/... -run <rule_slug_or_test_name_fragment> -v go test ./internal/integration/... -run <rule_slug_or_test_name_fragment> -v go test ./... make lint make cpd
If docs changed and zensical is available:
zensical build --clean
Step 9: Configuration + UX Checks
Confirm:
- •Rule can be enabled/disabled via
[rules] include/exclude - •Per-rule config in
[rules.tally.<rule_slug>]works - •Violation messages are explicit and actionable
- •
DocURLresolves to the new docs page - •Fix safety level is correct:
- •
FixSafe - •
FixSuggestion - •
FixUnsafe
- •
- •If fix overlaps with another rule, precedence is deterministic and tested.
- •Sync fix is used unless async is necessary for correctness.
Completion Checklist
- • Rule implemented in
internal/rules/tally/ - •
init()registration added - • Config schema/default/validation implemented (if configurable)
- • Unit tests added for rule behavior and config
- • Helper tests added for extracted utilities
- • New rule/helper files are covered at >=85%
- • Integration fixture +
TestCheckcase added - •
TestFixcases added for fix-capable rules - • Fixtures/docs examples are based on realistic Dockerfile patterns
- • Snapshots updated
- • Docs page + docs indexes updated
- •
RULES.mdandREADME.mdcounts/details updated - •
go test ./...,make lint, andmake cpdpass