The lint package provides static analysis for statekit state machines, detecting potential issues like unreachable states, dead ends, non-deterministic transitions, and other structural problems.
The lint package is included with statekit:
go get go.klarlabs.de/statekitImport the package:
import "go.klarlabs.de/statekit/lint"import (
"fmt"
"go.klarlabs.de/statekit"
"go.klarlabs.de/statekit/lint"
)
func main() {
machine, _ := statekit.NewMachine[Context]("order").
WithInitial("pending").
// ... state definitions
Build()
// Run lint analysis
result := lint.Lint(machine)
// Check for issues
if result.HasErrors() {
fmt.Println("Errors found:")
for _, d := range result.Errors() {
fmt.Printf(" %s\n", d)
}
}
if result.HasWarnings() {
fmt.Println("Warnings found:")
for _, d := range result.Warnings() {
fmt.Printf(" %s\n", d)
}
}
}| Rule | Severity | Description |
|---|---|---|
unreachable |
Warning | State cannot be reached from the initial state |
dead-end |
Warning | Non-final state has no outgoing transitions |
non-determinism |
Error | Multiple unguarded transitions for the same event |
compound-initial |
Error | Compound state has children but no initial state |
self-transition |
Info | Unguarded self-transition will re-run entry actions |
unused-action |
Info | Registered action is never referenced |
unused-guard |
Info | Registered guard is never referenced |
Detects states that cannot be reached from the initial state through any transition path.
machine, _ := statekit.NewMachine[struct{}]("example").
WithInitial("idle").
State("idle").On("START").Target("running").Done().
State("running").Done().
State("orphan").Done(). // Warning: unreachable
Build()Detects non-final states with no outgoing transitions (and no parent transitions that could handle events via bubbling).
machine, _ := statekit.NewMachine[struct{}]("example").
WithInitial("idle").
State("idle").On("GO").Target("stuck").Done().
State("stuck").Done(). // Warning: dead-end (not final, no transitions)
Build()Note: This check considers event bubbling - if a parent state has transitions, child states are not flagged as dead ends.
Detects states where the same event triggers multiple unguarded transitions.
machine, _ := statekit.NewMachine[struct{}]("example").
WithInitial("idle").
State("idle").
On("GO").Target("a"). // No guard
On("GO").Target("b"). // No guard - Error: non-determinism!
Done().
State("a").Final().Done().
State("b").Final().Done().
Build()To fix, add guards to make transitions mutually exclusive:
State("idle").
On("GO").Target("a").Guard("checkA").
On("GO").Target("b").Guard("checkB").
Done()Detects compound states that have children but no defined initial child state.
// Error: compound-initial
// Compound state "parent" has children but no initial stateWarns when a state has an unguarded self-transition and also has entry actions, since the entry actions will re-run on each self-transition.
machine, _ := statekit.NewMachine[struct{}]("example").
WithInitial("counting").
WithAction("increment", func(ctx *struct{}, e statekit.Event) {}).
State("counting").
OnEntry("increment").
On("COUNT").Target("counting"). // Info: will re-run "increment"
Done().
Build()Detects registered actions or guards that are never referenced in any state or transition.
| Level | Description |
|---|---|
Error |
Definite problem that will cause issues at runtime |
Warning |
Likely problem that should be reviewed |
Info |
Suggestion or observation |
Configure the linter to skip specific rules:
linter := lint.NewLinter().
Ignore(lint.RuleUnreachable).
Ignore(lint.RuleDeadEnd)
result := lint.CheckTyped(linter, machine)result := lint.Lint(machine)
// Check overall status
result.HasErrors() // true if any Error-level diagnostics
result.HasWarnings() // true if any Warning or Error level diagnostics
// Get filtered diagnostics
result.Errors() // []Diagnostic with SeverityError
result.Warnings() // []Diagnostic with SeverityWarning
// All diagnostics
result.Diagnostics // []Diagnostic (sorted by severity, then state)
// Human-readable summary
fmt.Println(result.String())
// Output:
// Machine "example": 1 error(s), 2 warning(s), 0 info(s)
// [error] idle: event "GO" has 2 unguarded transitions (non-determinism)
// [warning] orphan: state is unreachable from initial state (unreachable)
// [warning] stuck: non-final state has no outgoing transitions (dead-end)type Diagnostic struct {
Severity Severity // SeverityError, SeverityWarning, SeverityInfo
Rule string // Rule identifier (e.g., "unreachable")
State statekit.StateID // State where issue was found (empty for machine-level)
Event statekit.EventType // Event involved (for transition issues)
Message string // Human-readable description
}Use the linter in your test suite:
func TestMachine_Lint(t *testing.T) {
machine := buildMyMachine()
result := lint.Lint(machine)
if result.HasErrors() {
t.Fatalf("machine has lint errors: %v", result.Errors())
}
// Optionally fail on warnings too
if result.HasWarnings() {
t.Logf("machine has lint warnings: %v", result.Warnings())
}
}-
Run lint in tests: Add lint checks to your test suite to catch issues early.
-
Fix non-determinism errors: These indicate undefined behavior at runtime.
-
Review dead ends: Intentional dead ends should be marked as final states.
-
Remove unused actions/guards: Clean up registered but unused handlers.
-
Use guards for multi-target events: When the same event can go to different states, use guards to make the choice explicit.
Use these constants when configuring ignored rules:
lint.RuleUnreachable // "unreachable"
lint.RuleDeadEnd // "dead-end"
lint.RuleNonDeterminism // "non-determinism"
lint.RuleCompoundInitial // "compound-initial"
lint.RuleSelfTransition // "self-transition"
lint.RuleUnusedAction // "unused-action"
lint.RuleUnusedGuard // "unused-guard"Get all rule names:
rules := lint.AllRules() // []string