9.8 KiB
9.8 KiB
Linter Templates
Go linter patterns for mechanical architecture enforcement.
Dependency Direction Linter
Validates that package imports respect the layer hierarchy.
// scripts/lint-deps.go
//
// Validates that package dependencies follow the layer hierarchy.
// Each layer can only import from lower layers.
//
// Usage: go run scripts/lint-deps.go
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
)
// CUSTOMIZE: Set your module path
const modulePath = "your-module-path"
// CUSTOMIZE: Define your layer hierarchy (lower index = lower layer)
var layers = [][]string{
// Layer 0: No internal dependencies
{"core/types"},
// Layer 1: Depends on Layer 0
{"core/utils"},
// Layer 2: Depends on Layers 0-1
{"core/config", "core/logging"},
// Layer 3: Depends on Layers 0-2
{"core/business"},
// Layer 4: Depends on core/ but NOT each other
{"ui", "sdk", "integrations"},
// Layer 5: Can depend on everything
{"cmd"},
}
// CUSTOMIZE: Packages that must not import each other
var mutuallyExclusive = [][]string{
{"ui", "sdk", "integrations"},
}
type Violation struct {
File string
Package string
Imports string
Message string
}
func main() {
violations := checkDependencies()
if len(violations) == 0 {
fmt.Println("✓ All package dependencies follow the layer hierarchy")
os.Exit(0)
}
fmt.Printf("✗ Found %d dependency violations:\n\n", len(violations))
for _, v := range violations {
fmt.Printf("%s:\n Package: %s\n Imports: %s\n Error: %s\n\n",
v.File, v.Package, v.Imports, v.Message)
}
os.Exit(1)
}
func checkDependencies() []Violation {
var violations []Violation
layerMap := buildLayerMap()
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
if info != nil && info.IsDir() {
name := info.Name()
if strings.HasPrefix(name, ".") || name == "vendor" || name == "dist" {
return filepath.SkipDir
}
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
violations = append(violations, checkFile(path, layerMap)...)
return nil
})
return violations
}
func buildLayerMap() map[string]int {
m := make(map[string]int)
for idx, pkgs := range layers {
for _, pkg := range pkgs {
m[pkg] = idx
}
}
return m
}
func checkFile(path string, layerMap map[string]int) []Violation {
var violations []Violation
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
if err != nil {
return violations
}
dir := filepath.ToSlash(filepath.Dir(path))
dir = strings.TrimPrefix(dir, "./")
pkgLayer := findLayer(dir, layerMap)
if pkgLayer < 0 {
return violations
}
for _, imp := range node.Imports {
importPath := strings.Trim(imp.Path.Value, `"`)
if !strings.HasPrefix(importPath, modulePath) {
continue
}
relImport := strings.TrimPrefix(importPath, modulePath+"/")
importLayer := findLayer(relImport, layerMap)
if importLayer < 0 {
continue
}
if importLayer >= pkgLayer {
// Check if same base package (allowed)
if getBase(dir) == getBase(relImport) {
continue
}
violations = append(violations, Violation{
File: path, Package: dir, Imports: relImport,
Message: fmt.Sprintf("Layer %d cannot import layer %d", pkgLayer, importLayer),
})
}
}
return violations
}
func findLayer(pkg string, layerMap map[string]int) int {
if layer, ok := layerMap[pkg]; ok {
return layer
}
for key, layer := range layerMap {
if strings.HasPrefix(pkg, key+"/") {
return layer
}
}
return -1
}
func getBase(pkg string) string {
parts := strings.SplitN(pkg, "/", 3)
if len(parts) >= 2 {
return parts[0] + "/" + parts[1]
}
return parts[0]
}
Quality Linter
Validates golden principles like structured logging, file sizes, and naming.
// scripts/lint-quality.go
//
// Validates golden principles:
// - No raw log.Printf (use structured logging)
// - File size limits (max 1000 lines)
// - No hardcoded brand strings
//
// Usage: go run scripts/lint-quality.go
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
const maxFileLines = 1000
// CUSTOMIZE: Patterns to flag
var rawLogPatterns = []*regexp.Regexp{
regexp.MustCompile(`\blog\.Printf\b`),
regexp.MustCompile(`\blog\.Println\b`),
regexp.MustCompile(`\blog\.Fatalf\b`),
}
// CUSTOMIZE: Directories to skip
var skipDirs = map[string]bool{
".git": true, "dist": true, "vendor": true,
}
type Violation struct {
File string
Line int
Rule string
Message string
}
func main() {
var violations []Violation
walkGoFiles(func(path string, content []byte) {
if strings.HasSuffix(path, "_test.go") {
return
}
// Check structured logging
lines := strings.Split(string(content), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") {
continue
}
for _, pattern := range rawLogPatterns {
if pattern.MatchString(line) {
violations = append(violations, Violation{
File: path, Line: lineNum + 1,
Rule: "structured-logging",
Message: "Use structured logging instead of raw log calls",
})
}
}
}
// Check file size
scanner := bufio.NewScanner(strings.NewReader(string(content)))
lineCount := 0
for scanner.Scan() {
lineCount++
}
if lineCount > maxFileLines {
violations = append(violations, Violation{
File: path, Rule: "file-size",
Message: fmt.Sprintf("File has %d lines (max %d)", lineCount, maxFileLines),
})
}
})
if len(violations) == 0 {
fmt.Println("✓ All quality checks passed")
os.Exit(0)
}
fmt.Printf("✗ Found %d quality violations:\n\n", len(violations))
for _, v := range violations {
fmt.Printf("%s:%d [%s]: %s\n", v.File, v.Line, v.Rule, v.Message)
}
os.Exit(1)
}
func walkGoFiles(fn func(path string, content []byte)) {
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
if info != nil && info.IsDir() && skipDirs[filepath.Base(path)] {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return nil
}
fn(path, content)
return nil
})
}
Template Linter
Validates template files (.tpl) are valid and referenced.
// scripts/lint-prompts.go
//
// Validates template files: parseable, referenced, no orphans.
//
// Usage: go run scripts/lint-prompts.go
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
)
// CUSTOMIZE: Directories containing template files
var templateDirs = []string{"templates", "prompts"}
func main() {
var violations int
for _, dir := range templateDirs {
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() || !strings.HasSuffix(path, ".tpl") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
fmt.Printf("%s: cannot read: %v\n", path, err)
violations++
return nil
}
_, err = template.New(filepath.Base(path)).
Option("missingkey=zero").
Parse(string(content))
if err != nil {
fmt.Printf("%s: invalid template: %v\n", path, err)
violations++
}
return nil
})
}
// Check for orphan templates
embedPattern := regexp.MustCompile(`//go:embed\s+(\S+\.tpl)`)
tplFiles := make(map[string]bool)
for _, dir := range templateDirs {
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return nil
}
if !info.IsDir() && strings.HasSuffix(path, ".tpl") {
tplFiles[filepath.Base(path)] = false
}
return nil
})
}
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
content, _ := os.ReadFile(path)
for _, match := range embedPattern.FindAllSubmatch(content, -1) {
tplFiles[string(match[1])] = true
}
return nil
})
for name, referenced := range tplFiles {
if !referenced {
fmt.Printf("WARNING: %s may be orphaned\n", name)
}
}
if violations == 0 {
fmt.Printf("✓ All template files are valid\n")
os.Exit(0)
}
os.Exit(1)
}
Makefile Integration
# Architecture checks
.PHONY: lint-arch
lint-arch:
@echo "Checking architecture constraints..."
@go run scripts/lint-deps.go
@go run scripts/lint-prompts.go
@go run scripts/lint-quality.go
@echo "✓ Architecture checks passed"
# Combined lint
.PHONY: lint
lint: lint-arch
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
fi
Adaptation Notes
For TypeScript Projects
Read references/adapters/typescript.md before creating files. Prefer package-manager scripts and
Node-native harness linters over Go examples:
- Create
scripts/lint-deps.mjsor an equivalent Node script for layer/import checks. - Create
scripts/lint-quality.mjsfor project-specific quality checks. - Add npm/pnpm/yarn/bun scripts such as
lint:deps,lint:quality,lint:arch, andlint:harness. - Keep CI strict: include existing
lint,typecheck,test, andbuildgates when available. - Do not weaken CI because baseline commands are red; report those as pre-existing project debt.
For small projects, ESLint import restrictions may be enough:
// .eslintrc.js - import restrictions
module.exports = {
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['../ui/*'], message: 'Core cannot import UI' },
{ group: ['../cmd/*'], message: 'Core cannot import CLI' },
]
}]
}
};
For Python Projects
Use custom pylint checkers or ruff rules:
# scripts/lint_deps.py
import ast, sys, pathlib
LAYER_ORDER = {
'models': 0,
'utils': 1,
'services': 2,
'api': 3,
'cli': 4,
}
# ... validate imports against layer order