From 3b36a48b7ce5a53a9366f3b31f94311f178e2553 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 12 Sep 2026 01:22:12 +0200 Subject: krino: matching — scan, ignore, conditions, extraction, duplicates, explain, dry run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/engine/engine.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 internal/engine/engine.go (limited to 'internal/engine/engine.go') diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..eec9a60 --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package engine is what every krino front end calls: it loads and compiles +// the configuration, matches files against rules, and (from plan 3) plans +// and applies actions. It returns data; front ends only render it. +package engine + +import ( + "os" + "time" + + "krino/internal/cond" + "krino/internal/config" + "krino/internal/extract" + "krino/internal/ignore" +) + +// Engine holds a loaded, compiled configuration: everything a front end +// needs to check, match and (from plan 3) act. +type Engine struct { + Config *config.Config + Dirs []*Dir + Extract *extract.Extractor + Now func() time.Time // time.Now; tests replace it + MainFile string +} + +// Dir is one configured directory, with its ignore matcher and rules +// compiled. +type Dir struct { + Name string + Root string // absolute + Conf *config.Dir + Settings config.Resolved // built-in, then defaults, then the directory + Ignore *ignore.Matcher + Rules []*Rule + + // ContentVariants is the distinct (IgnoreCase, Fold) pairs any of + // Rules' content tests evaluate under, in first-seen order. B2: when + // this holds exactly one variant, facts.Content releases a file's raw + // extracted text once that variant's normalised copy exists, since no + // other variant will ever be asked for; with more than one, both must + // stay memoised, as before. + ContentVariants []cond.Options +} + +// Rule is one directory's rule, with its condition compiled. +type Rule struct { + Name string + Conf *config.Rule + Settings config.Resolved // the rule's own settings over its directory's + Cond *cond.Cond +} + +// Load reads and compiles everything. Any problem anywhere returns a nil +// Engine and every diagnostic: krino never acts on a configuration it only +// partly understood. Duplicate names are ignored after their first use. +func Load(mainFile string, names ...string) (*Engine, []*config.Diag) { + cfg, errs := config.Load(mainFile, dedupeNames(names)...) + if cfg == nil { + return nil, errs + } + + var dirs []*Dir + for _, d := range cfg.Dirs { + dir := &Dir{ + Name: d.Name, + Root: d.Path, + Conf: d, + Settings: cfg.Resolved(d), + } + if m, err := ignore.New(d.Ignore); err != nil { + errs = append(errs, &config.Diag{File: d.File, Msg: err.Error()}) + } else { + dir.Ignore = m + } + for _, r := range d.Rules { + rs := r.Settings.Over(dir.Settings) + c, cerrs := cond.Compile(d.File, r.When, cond.Options{ + IgnoreCase: rs.Case == config.CaseIgnore, + Fold: rs.Fold, + }) + if len(cerrs) > 0 { + errs = append(errs, cerrs...) + continue + } + dir.Rules = append(dir.Rules, &Rule{Name: r.Name, Conf: r, Settings: rs, Cond: c}) + } + dir.ContentVariants = contentVariants(dir.Rules) + dirs = append(dirs, dir) + } + + if len(errs) > 0 { + return nil, errs + } + return &Engine{ + Config: cfg, + Dirs: dirs, + Extract: extract.New(), + Now: time.Now, + MainFile: mainFile, + }, nil +} + +// dedupeNames returns names with every repeat after its first occurrence +// removed, order preserved. +func dedupeNames(names []string) []string { + var out []string + seen := map[string]bool{} + for _, n := range names { + if seen[n] { + continue + } + seen[n] = true + out = append(out, n) + } + return out +} + +// contentVariants returns the distinct (IgnoreCase, Fold) pairs any of +// rules' content tests evaluate under, in first-seen order — B2's per-Dir +// ContentVariants. A rule whose condition has no content test at all +// (Cond.UsesContent false) never calls facts.Content, so its resolved +// case/fold settings contribute no variant here. +func contentVariants(rules []*Rule) []cond.Options { + var out []cond.Options + seen := map[cond.Options]bool{} + for _, r := range rules { + if !r.Cond.UsesContent { + continue + } + opt := cond.Options{IgnoreCase: r.Settings.Case == config.CaseIgnore, Fold: r.Settings.Fold} + if seen[opt] { + continue + } + seen[opt] = true + out = append(out, opt) + } + return out +} + +// Report is what Check reports: the files involved and each directory's +// state. +type Report struct { + MainFile string + LogFile string + Dirs []DirReport + Tools []extract.Tool +} + +// DirReport is one directory's state in a Report. +type DirReport struct { + Dir *Dir + Missing bool // the root is not a directory right now +} + +// Check reports the engine's configuration files, each directory's current +// state and the external tools found for content extraction. +func (e *Engine) Check() Report { + r := Report{ + MainFile: e.MainFile, + LogFile: e.Config.LogFile(), + Tools: e.Extract.Tools(), + } + for _, d := range e.Dirs { + fi, err := os.Stat(d.Root) + r.Dirs = append(r.Dirs, DirReport{Dir: d, Missing: err != nil || !fi.IsDir()}) + } + return r +} -- cgit v1.3