core

package
v0.4.3 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package core defines the rule interface and runtime for go-arch-guard.

External rule authors implement the Rule interface and pass instances into a RuleSet, which is executed by Run against a Context.

Example
package main

import (
	"fmt"

	"github.com/NamhaeSusan/go-arch-guard/core"
)

// noDBInHandler is a hand-written example of a custom rule. Real rules
// would inspect ctx.Pkgs(); this fake scans a hard-coded import list to
// keep the example hermetic.
type noDBInHandler struct {
	imports []importEdge
}

type importEdge struct {
	From, To, File string
	Line           int
}

func (r *noDBInHandler) Spec() core.RuleSpec {
	return core.RuleSpec{
		ID:              "team.no-db-in-handler",
		Description:     "handlers must not import database/sql",
		DefaultSeverity: core.Error,
		Violations: []core.ViolationSpec{
			{ID: "team.no-db-in-handler", Description: "handler imports database/sql", DefaultSeverity: core.Error},
		},
	}
}

func (r *noDBInHandler) Check(ctx *core.Context) []core.Violation {
	var out []core.Violation
	for _, edge := range r.imports {
		if edge.From == "internal/handler" && edge.To == "database/sql" {
			out = append(out, core.Violation{
				File:    edge.File,
				Line:    edge.Line,
				Rule:    "team.no-db-in-handler",
				Message: "handler must not import database/sql",
				Fix:     "move DB calls to internal/repo and inject through interface",
			})
		}
	}
	return out
}

func main() {
	arch := core.Architecture{
		Layers: core.LayerModel{
			Sublayers: []string{"handler", "core"},
			Direction: map[string][]string{
				"handler": {"core"},
				"core":    {},
			},
		},
	}
	ctx := core.NewContext(nil, "github.com/example/app", "/repo", arch, nil)

	rule := &noDBInHandler{
		imports: []importEdge{
			{From: "internal/handler", To: "database/sql", File: "internal/handler/users.go", Line: 7},
		},
	}

	violations := core.Run(ctx, core.RuleSet{}.With(rule))
	for _, v := range violations {
		fmt.Println(v)
	}
}
Output:
[ERROR] violation: handler must not import database/sql (file: internal/handler/users.go:7, rule: team.no-db-in-handler, fix: move DB calls to internal/repo and inject through interface)

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NormalizeMatchPath

func NormalizeMatchPath(path string) string

NormalizeMatchPath canonicalizes both exclude patterns and the file paths rules emit so they match consistently. The supported equivalences are:

  • Backslashes are converted to forward slashes (Windows-shell paste).
  • Leading "/" is trimmed (absolute-path fallback in analysisutil).
  • Leading "./" is trimmed (rule-emitted relative paths).
  • Trailing "/" is trimmed for non-recursive patterns; a recursive pattern keeps its "..." suffix intact since matchExcludePattern reads it.

As a result "internal/foo", "/internal/foo", "./internal/foo", "internal\\foo", and "internal/foo/" are all the same key.

func Validate

func Validate(a Architecture) error

Validate is the package-level form of Architecture.Validate. Presets that prefer the functional form (e.g. `core.Validate(arch)`) can call this.

Types

type Architecture

type Architecture struct {
	Layers    LayerModel  // layer vocabulary (paths and basenames)
	Layout    LayoutModel // internal/ directory topology
	Naming    NamingPolicy
	Structure StructurePolicy
}

Architecture is the team-defined description of a project's layering and naming conventions. Presets construct Architecture instances; rules read from Context.Arch(). The vocabulary that names layers lives on LayerModel — see its godoc for the Sublayers vs LayerDirNames split.

func (Architecture) Validate

func (a Architecture) Validate() error

Validate checks that every layer-referencing field names a layer present in Layers.Sublayers, that Direction has a key for every sublayer (no silent enforcement holes), that PortLayers ⊆ ContractLayers, that Sublayers entries are non-empty and unique, and that Direction is a DAG.

Validate is called by Run before any rule executes; presets MAY call it at construction time to fail fast.

A zero-value Architecture (no Sublayers, no Direction) IS accepted as valid — it represents "no layer policy." Pairing it with a non-empty RuleSet is generally a configuration mistake (no rule that reads Layers.Sublayers can fire) but Validate cannot detect this without inspecting the RuleSet, so it is tolerated.

Layout-level non-empty checks (e.g. "AppDir must be set when an app-aware rule is enabled") are NOT performed here because Architecture does not know which rules will run. Presets that bundle layout-aware rules are responsible for refusing to construct an Architecture whose Layout is missing required directories.

type Context

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

Context is the read-only view a Rule.Check sees. Fields are unexported to enforce immutability — rules access them through accessor methods. Run constructs Context once per invocation and shares it across rules.

func NewContext

func NewContext(pkgs []*packages.Package, module, root string, arch Architecture, exclude []string) *Context

NewContext builds a Context. Excludes are normalized (leading "./" stripped, OS path separators converted) at construction. The Architecture is deep-cloned so caller-side mutations to its maps and slices after this call cannot leak into the Context's view; Arch() also returns a defensive clone, but cloning here closes the construction-time window where a caller could otherwise mutate shared state before the first Arch() read.

func (*Context) Arch

func (c *Context) Arch() Architecture

Arch returns a defensive deep copy of the architecture so a rule that mutates returned slices/maps cannot corrupt later rules' view of the policy. The cost is small (Architecture is bounded in size) and it preserves the "single source of truth" guarantee on Layers.Sublayers when several rules read from the context concurrently in some future runner.

func (*Context) IsExcluded

func (c *Context) IsExcluded(path string) bool

IsExcluded reports whether path matches any configured exclude pattern. Patterns ending in "..." match the base directory and any descendant; other patterns require an exact match. Both pattern and path are normalized to forward slashes with leading "./" stripped.

func (*Context) Module

func (c *Context) Module() string

func (*Context) Pkgs

func (c *Context) Pkgs() []*packages.Package

Pkgs returns the loaded packages. Treat the returned *packages.Package values as read-only — mutation is undefined behavior.

The returned slice is a header copy: reslicing or appending to it cannot affect other rules. However, the *packages.Package values it points at are SHARED across rules — Go does not let us deep-clone them cheaply, and a true copy would re-walk the type system per rule. Mutating any field of a *packages.Package (Imports, Types, Syntax, Errors, …) is therefore a contract violation: rules MUST be pure functions of their input. Violating this corrupts later rules' view of the world and is undefined behavior under any future parallel runner.

func (*Context) Root

func (c *Context) Root() string

type LayerModel

type LayerModel struct {
	// Sublayers is the authoritative list of layer paths.
	Sublayers []string
	// Direction maps each Sublayer to the Sublayers it may import.
	Direction map[string][]string
	// PortLayers lists Sublayers that are pure-interface ports (e.g. repo).
	// Every entry must appear in Sublayers.
	PortLayers []string
	// ContractLayers lists Sublayers exposed as cross-domain contracts
	// (typically PortLayers ∪ svc-style layers). Every entry must appear in
	// Sublayers.
	ContractLayers []string
	// PkgRestricted marks Sublayers that the shared pkg/ tree must not
	// import from. Keys must appear in Sublayers.
	PkgRestricted map[string]bool
	// InternalTopLevel lists directory names allowed directly under
	// internal/. Keys are top-level dir names (e.g. "domain", "pkg") and
	// are NOT required to appear in Sublayers.
	InternalTopLevel map[string]bool
	// LayerDirNames is the set of layer basenames recognized by file and
	// directory placement rules. Keys are basenames, NOT full Sublayer
	// paths.
	LayerDirNames map[string]bool
}

LayerModel owns layer vocabulary. Two complementary fields name layers for different consumers:

  • Sublayers carries full layer paths ("core/repo", "core/svc", "handler"). This is authoritative for direction-aware rules, port/contract sublayer matching, and domain isolation. Direction, PortLayers, ContractLayers, PkgRestricted, and StructurePolicy.InterfacePatternExclude entries MUST reference values that appear here.

  • LayerDirNames carries basenames ("repo", "svc", "model"). File and directory placement rules use these to recognize a layer directory regardless of nesting depth. The basename "repo" recognizes both internal/<domain>/core/repo/... and a flat internal/repo/... layout.

The two are complementary, not redundant. A typical preset declares "core/repo" in Sublayers AND "repo" in LayerDirNames so both kinds of rule have the data they need. LayerDirNames entries deliberately do NOT have to appear in Sublayers — they are basename hints, not full paths.

type LayoutModel

type LayoutModel struct {
	// InternalRoot is the project-relative directory under which all
	// rule-managed packages live. Defaults to "internal" when empty;
	// cloneArchitecture normalizes the empty value at construction so
	// rules read this field directly without a per-call default check.
	InternalRoot     string
	DomainDir        string
	OrchestrationDir string
	SharedDir        string
	AppDir           string
	ServerDir        string
}

LayoutModel describes the package-root directory topology. Empty fields disable the corresponding classification (e.g. flat layouts leave DomainDir == "").

type NamingPolicy

type NamingPolicy struct {
	BannedPkgNames []string
	LegacyPkgNames []string
	AliasFileName  string
}

NamingPolicy carries naming-only conventions. Layer names are NOT duplicated here — rules read ctx.Arch().Layers.Sublayers.

type Rule

type Rule interface {
	// Spec returns the rule's metadata. Spec MAY reflect construction-time
	// configuration (e.g. severity passed via WithSeverity); it is not
	// required to be a pure function of the type.
	Spec() RuleSpec

	// Check inspects the context and returns zero or more violations.
	// The returned slice is owned by the caller; rules must not retain it.
	Check(ctx *Context) []Violation
}

Rule is the contract every architecture rule satisfies. Implementations MUST be pure: Check must not mutate Context and must be safe to call multiple times with the same Context. Run executes rules serially, but the contract is purity so future runners may parallelize.

type RuleSet

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

RuleSet is an immutable collection of rules plus a set of violation IDs to filter out at the runner. With and Without return copies, so chaining is safe and the original RuleSet is never mutated.

func NewRuleSet

func NewRuleSet(rules ...Rule) RuleSet

NewRuleSet seeds a RuleSet with the given rules. Equivalent to RuleSet{}.With(rules...).

func (RuleSet) IsViolationSkipped

func (rs RuleSet) IsViolationSkipped(id string) bool

IsViolationSkipped reports whether Without(...) was called for id.

func (RuleSet) Rules

func (rs RuleSet) Rules() []Rule

Rules returns the rules in registration order. The returned slice is a copy; callers may not mutate it.

func (RuleSet) With

func (rs RuleSet) With(rules ...Rule) RuleSet

With returns a new RuleSet with the given rules appended. nil rules are silently dropped so callers can compose conditional rule lists like rs.With(maybeRule()) without an explicit nil check at every site.

func (RuleSet) Without

func (rs RuleSet) Without(violationIDs ...string) RuleSet

Without returns a new RuleSet whose runner filters out violations whose Rule field matches any of the given violation-level IDs (e.g. "dependency.cross-domain"). IDs not present in the active rule set are rejected by Run; see WithSeverityOverride for the same guarantee.

type RuleSpec

type RuleSpec struct {
	ID              string          // rule-type ID, e.g. "dependency.isolation"
	Description     string          // single-line summary used by --list-rules
	DefaultSeverity Severity        // fallback for emitted violations not listed in Violations
	Violations      []ViolationSpec // declarative violation-ID catalog
}

RuleSpec is the static-ish metadata describing a rule type. A rule type emits Violations whose Rule field matches one of the IDs in Violations.

Single-ID rules may leave Violations empty and reuse RuleSpec.ID as the violation ID; multi-ID rules MUST populate Violations so callers can discover and override individual sub-IDs.

func (RuleSpec) ViolationIDs

func (s RuleSpec) ViolationIDs() []string

ViolationIDs returns the IDs declared in spec.Violations, in declaration order. For single-ID rules with an empty Violations slice, returns nil.

type RunOption

type RunOption func(*runOpts)

RunOption configures a single Run invocation.

func WithSeverityOverride

func WithSeverityOverride(violationID string, s Severity) RunOption

WithSeverityOverride sets the effective severity for a violation-level ID. When multiple overrides target the same ID, the last one passed to Run wins.

Example:

core.Run(ctx, rules,
    core.WithSeverityOverride("dependency.cross-domain", core.Warning))

type Severity

type Severity int

Severity classifies how loud a Violation is. Error blocks builds; Warning is advisory only.

const (
	Error Severity = iota
	Warning
)

func (Severity) String

func (s Severity) String() string

type StructurePolicy

type StructurePolicy struct {
	RequireAlias            bool
	RequireModel            bool
	ModelPath               string
	TypePatterns            []TypePattern
	InterfacePatternExclude map[string]bool // sublayer names; validated against Layers.Sublayers
}

StructurePolicy carries placement and structure conventions.

type TypePattern

type TypePattern struct {
	Dir           string
	FilePrefix    string
	TypeSuffix    string
	RequireMethod string
}

TypePattern is an AST-based naming/structure convention for a directory.

type Violation

type Violation struct {
	File              string
	Line              int
	Rule              string
	Message           string
	Fix               string
	DefaultSeverity   Severity
	EffectiveSeverity Severity
}

Violation is a single rule failure emitted by Rule.Check. The Rule field is the violation-level ID (e.g. "dependency.cross-domain"), not the rule-type ID. DefaultSeverity records what the rule declared in its ViolationSpec; EffectiveSeverity is what callers see after construction- and runtime-level overrides have been applied by Run.

func Run

func Run(ctx *Context, rules RuleSet, opts ...RunOption) []Violation

Run executes the RuleSet against the Context and returns violations.

Contract:

  • Architecture is validated before any rule runs; an invalid Architecture panics (presets MAY call Validate at construction time to fail earlier).
  • Run rejects unknown violation IDs passed to RuleSet.Without or WithSeverityOverride by panicking — these are caller-side errors and must be surfaced loudly. The set of known IDs is the union of every rule's RuleSpec.ViolationIDs() plus each rule's RuleSpec.ID itself. Single-ID rules with empty Violations declare implicitly through their RuleSpec.ID.
  • For each Violation a rule emits, Run validates Violation.Rule against THAT rule's own ID set. Unknown IDs (rule-author bugs) are replaced with "meta.unknown-violation-id" rather than panicked — a buggy rule should not crash the entire run. Emitted IDs starting with "meta." are exempt from this check: any rule may emit meta.* violations to surface environmental issues (e.g. "meta.no-matching-packages" when the project module cannot be resolved) without declaring them in its catalog.
  • Rules execute serially in registration order. Rule.Check must be pure; future runners may parallelize.
  • Effective severity precedence (highest wins): 1. WithSeverityOverride(violationID, ...) 2. RuleSpec.Violations[i].DefaultSeverity for matching ID 3. Warning, when the violation ID starts with "meta." (environmental meta.* violations should never block builds by accident) 4. RuleSpec.DefaultSeverity 5. Error
  • Violations are deduped by Rule field for any Rule starting with "meta.".
  • Final violations are sorted by (File, Line, Rule, Message) for deterministic output regardless of map iteration order inside rules.

func (Violation) String

func (v Violation) String() string

type ViolationSpec

type ViolationSpec struct {
	ID              string
	Description     string
	DefaultSeverity Severity
}

ViolationSpec describes one violation ID emitted by a rule type.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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