all repos

clerk @ a45934c

missing tooling for ledger/hledger
5 files changed, 71 insertions(+), 0 deletions(-)
linter: add account-depth rule
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-06-22 18:00:13 +0300
Authored at: 2026-06-17 17:09:52 +0300
Change ID: tknvvnqpruqqktszyzoslyuokwmotzmt
Parent: 50bbb41
M internal/linter/linter_test.go
···
        15
        15
         	"omitted-precision": {&OmittedPrecision{}},

      
        16
        16
         	"missing-commodity": {&MissingCommodity{}},

      
        17
        17
         	"missing-status":    {&MissingStatus{}},

      
        
        18
        +	"account-depth":     {&AccountDepthLimit{MaxDepth: 3}},

      
        18
        19
         }

      
        19
        20
         

      
        20
        21
         func TestLinter(t *testing.T) {

      
A internal/linter/rule_account_depth.go
···
        
        1
        +package linter

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +

      
        
        6
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +// AccountDepthLimit checks that account names don't exceed MaxDepth

      
        
        10
        +type AccountDepthLimit struct {

      
        
        11
        +	MaxDepth int

      
        
        12
        +}

      
        
        13
        +

      
        
        14
        +func (AccountDepthLimit) ID() RuleID          { return "account-depth" }

      
        
        15
        +func (AccountDepthLimit) Severity() Severity  { return SeverityWarning }

      
        
        16
        +func (AccountDepthLimit) Description() string { return "account name depth exceeds limit" }

      
        
        17
        +func (a *AccountDepthLimit) CheckEntry(entry ast.Entry) []Find {

      
        
        18
        +	var finds []Find

      
        
        19
        +	switch e := entry.(type) {

      
        
        20
        +	case *ast.Transaction:

      
        
        21
        +		for _, p := range e.Postings {

      
        
        22
        +			a.check(&finds, p.Account)

      
        
        23
        +		}

      
        
        24
        +	case *ast.PeriodicTransaction:

      
        
        25
        +		for _, p := range e.Postings {

      
        
        26
        +			a.check(&finds, p.Account)

      
        
        27
        +		}

      
        
        28
        +	case *ast.AutomatedTransaction:

      
        
        29
        +		for _, p := range e.Postings {

      
        
        30
        +			a.check(&finds, p.Account)

      
        
        31
        +		}

      
        
        32
        +	case *ast.AccountDirective:

      
        
        33
        +		a.check(&finds, e.Account)

      
        
        34
        +	case *ast.AliasDirective:

      
        
        35
        +		a.check(&finds, e.From)

      
        
        36
        +		a.check(&finds, e.To)

      
        
        37
        +	}

      
        
        38
        +	return finds

      
        
        39
        +}

      
        
        40
        +

      
        
        41
        +func (a *AccountDepthLimit) check(finds *[]Find, acc ast.Account) {

      
        
        42
        +	if depth := len(acc.Name); depth > a.MaxDepth {

      
        
        43
        +		*finds = append(*finds, Find{

      
        
        44
        +			Code:     a.ID(),

      
        
        45
        +			Severity: a.Severity(),

      
        
        46
        +			Message: fmt.Sprintf("account %q depth (%d) exceeds max allowed depth (%d)",

      
        
        47
        +				acc.String(), depth, a.MaxDepth),

      
        
        48
        +			Span: acc.Span,

      
        
        49
        +		})

      
        
        50
        +	}

      
        
        51
        +}

      
M internal/linter/rules.go
···
        30
        30
         	&OmittedPrecision{},

      
        31
        31
         	&MissingCommodity{},

      
        32
        32
         	&MissingStatus{},

      
        
        33
        +	&AccountDepthLimit{MaxDepth: 4},

      
        33
        34
         }

      
A internal/linter/testdata/account-depth.golden
···
        
        1
        +account-depth.journal:2:9: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3)

      
        
        2
        +account-depth.journal:3:9: account-depth: account "Expenses:Tax:Income:More:Deep:Too" depth (6) exceeds max allowed depth (3)

      
        
        3
        +account-depth.journal:5:19: account-depth: account "Expenses:Food:Restaurants:Pizza:Hawaiian" depth (5) exceeds max allowed depth (3)

      
        
        4
        +account-depth.journal:12:5: account-depth: account "Income:Job:Salary:Monthly:Base" depth (5) exceeds max allowed depth (3)

      
        
        5
        +account-depth.journal:13:5: account-depth: account "Assets:Checking:Deep:Sub" depth (4) exceeds max allowed depth (3)

      
A internal/linter/testdata/account-depth.input
···
        
        1
        +account Expenses:Food:Restaurants

      
        
        2
        +account Expenses:Food:Restaurants:Pizza:Hawaiian

      
        
        3
        +account Expenses:Tax:Income:More:Deep:Too

      
        
        4
        +

      
        
        5
        +alias Fast:Food = Expenses:Food:Restaurants:Pizza:Hawaiian

      
        
        6
        +

      
        
        7
        +2024-01-01 groceries

      
        
        8
        +    Expenses:Food  $10.00

      
        
        9
        +    Assets:Checking

      
        
        10
        +

      
        
        11
        +2024-01-02 salary

      
        
        12
        +    Income:Job:Salary:Monthly:Base  $1000

      
        
        13
        +    Assets:Checking:Deep:Sub