aboutsummaryrefslogtreecommitdiff
path: root/internal/engine/engine.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:56:21 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 10:56:21 +0200
commit1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc (patch)
treefb6d573ca8726c1b1d91ba273a284ba666cde3f6 /internal/engine/engine.go
parentebdd7bb254a0f19f815a644d26ce232de7be0adb (diff)
downloadkrino-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.go50
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 {