diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 10:56:21 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 10:56:21 +0200 |
| commit | 1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc (patch) | |
| tree | fb6d573ca8726c1b1d91ba273a284ba666cde3f6 /internal/engine/engine.go | |
| parent | ebdd7bb254a0f19f815a644d26ce232de7be0adb (diff) | |
| download | krino-1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc.tar.gz krino-1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc.zip | |
krino: duplicates are found, never deleted
A rule combining (duplicate) with a delete action is refused at load, and a
file that is a duplicate under any duplicate scope its directory uses gets no
delete step from any rule: the plan shows it skipped and the chain continues.
A failed duplicate check blocks the delete too. Tests cover (matched), another
rule's own condition, a test evaluation skipped, two scopes and a failed
check, each checking every copy is still on disk.
Diffstat (limited to 'internal/engine/engine.go')
| -rw-r--r-- | internal/engine/engine.go | 50 |
1 files changed, 50 insertions, 0 deletions
diff --git a/internal/engine/engine.go b/internal/engine/engine.go index ea61e8c..b5f2106 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -8,6 +8,7 @@ package engine import ( "fmt" "os" + "strings" "time" "krino/internal/cond" @@ -44,6 +45,12 @@ type Dir struct { // other variant will ever be asked for; with more than one, both must // stay memoised, as before. ContentVariants []cond.Options + + // DupScopes is every distinct directory list the duplicate tests of + // Rules use, in first-seen order, the plain (duplicate) as an empty + // list. Spec §5.5 rule 2 looks a file up under each of them before any + // rule may delete it. + DupScopes [][]string } // Rule is one directory's rule, with its condition compiled. @@ -90,9 +97,14 @@ func Load(mainFile string, names ...string) (*Engine, []*config.Diag) { errs = append(errs, diag) continue } + if diag := checkDuplicateDelete(d.File, r, c); diag != nil { + errs = append(errs, diag) + continue + } dir.Rules = append(dir.Rules, &Rule{Name: r.Name, Conf: r, Settings: rs, Cond: c}) } dir.ContentVariants = contentVariants(dir.Rules) + dir.DupScopes = dupScopes(dir.Rules) dirs = append(dirs, dir) } @@ -132,6 +144,23 @@ func checkCaptures(file string, r *config.Rule, c *cond.Cond) *config.Diag { return nil } +// checkDuplicateDelete refuses a rule that combines a duplicate test with a +// delete action (spec §4.5, §5.5): duplicates are found, never deleted. +// Cond.DupDirs records every duplicate test compiled, inside or and not +// too, so a test anywhere in the condition counts. It reports only the +// first delete action, so one config mistake yields one diagnostic. +func checkDuplicateDelete(file string, r *config.Rule, c *cond.Cond) *config.Diag { + if len(c.DupDirs) == 0 { + return nil + } + for _, a := range r.Actions { + if a.Kind == config.Delete || a.Kind == config.DeletePermanent { + return &config.Diag{File: file, Pos: a.Pos, Msg: fmt.Sprintf("rule %q: (duplicate) cannot be combined with (%s): duplicates are never deleted, move them aside instead", r.Name, a.Kind)} + } + } + return nil +} + // captureGroups renders a capture-group count with correct singular/plural. func captureGroups(n int) string { if n == 1 { @@ -177,6 +206,27 @@ func contentVariants(rules []*Rule) []cond.Options { return out } +// dupScopes returns the distinct Cond.DupDirs lists of rules, in first-seen +// order. Lists are compared as written, with their length in the key so +// (duplicate) and (duplicate "") stay apart; two spellings of one directory +// stay two entries, which costs a second lookup but never a wrong answer, +// since facts.Duplicate resolves and shares the index itself. +func dupScopes(rules []*Rule) [][]string { + var out [][]string + seen := map[string]bool{} + for _, r := range rules { + for _, dirs := range r.Cond.DupDirs { + key := fmt.Sprintf("%d\x00%s", len(dirs), strings.Join(dirs, "\x00")) + if seen[key] { + continue + } + seen[key] = true + out = append(out, dirs) + } + } + return out +} + // Report is what Check reports: the files involved and each directory's // state. type Report struct { |
