config: resolve nested includes against the including file

fixes #3647
This commit is contained in:
bbedward committed 2026-10-01 18:06:50 -04:00
1 parent ea6c52b5e8
commit e61fa45444
8 files changed
+166 -62

No files matched your search

+30 -42
View File
@@ -104,8 +104,6 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
return result, err
}
targetRel := filepath.ToSlash(filepath.Join("dms", filename))
mainLua := filepath.Join(configDir, "hyprland.lua")
if _, err := os.Stat(mainLua); err == nil {
result.ConfigFormat = "lua"
@@ -124,7 +122,7 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
result.ReadOnly = true
}
processed := make(map[string]bool)
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
if hyprlandFindIncludeHyprlang(mainConf, targetAbs, processed) {
result.Included = true
return result, nil
}
@@ -167,22 +165,12 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
fullPath, err := resolveSourcePath(baseDir, strings.TrimSpace(parts[1]))
if err != nil {
continue
}
if hyprlandFindIncludeHyprlang(expanded, target, processed) {
if fullPath == target || hyprlandFindIncludeHyprlang(fullPath, target, processed) {
return true
}
}
@@ -205,8 +193,13 @@ func checkNiriInclude(filename string) (IncludeResult, error) {
return result, nil
}
targetAbs, err := filepath.Abs(targetPath)
if err != nil {
return result, err
}
processed := make(map[string]bool)
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
result.Included = niriFindInclude(mainConfig, targetAbs, processed)
return result, nil
}
@@ -248,17 +241,12 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
continue
}
includePath := trimmed[startQuote+1 : endQuote]
if matchesTarget(includePath, target) {
return true
fullPath, err := resolveSourcePath(baseDir, trimmed[startQuote+1:endQuote])
if err != nil {
continue
}
fullPath := includePath
if !filepath.IsAbs(includePath) {
fullPath = filepath.Join(baseDir, includePath)
}
if niriFindInclude(fullPath, target, processed) {
if fullPath == target || niriFindInclude(fullPath, target, processed) {
return true
}
}
@@ -284,8 +272,13 @@ func checkMangoWCInclude(filename string) (IncludeResult, error) {
return result, nil
}
targetAbs, err := filepath.Abs(targetPath)
if err != nil {
return result, err
}
processed := make(map[string]bool)
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
result.Included = mangowcFindInclude(mainConfig, targetAbs, processed)
return result, nil
}
@@ -323,22 +316,12 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
continue
}
sourcePath := strings.TrimSpace(parts[1])
if matchesTarget(sourcePath, target) {
return true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
fullPath, err := resolveSourcePath(baseDir, strings.TrimSpace(parts[1]))
if err != nil {
continue
}
if mangowcFindInclude(expanded, target, processed) {
if fullPath == target || mangowcFindInclude(fullPath, target, processed) {
return true
}
}
@@ -346,8 +329,13 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
return false
}
func matchesTarget(path, target string) bool {
path = strings.TrimPrefix(path, "./")
target = strings.TrimPrefix(target, "./")
return path == target || strings.HasSuffix(path, "/"+target)
func resolveSourcePath(baseDir, raw string) (string, error) {
expanded, err := utils.ExpandPath(raw)
if err != nil {
return "", err
}
if !filepath.IsAbs(expanded) {
expanded = filepath.Join(baseDir, expanded)
}
return filepath.Abs(expanded)
}
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func writeConfigFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestNiriFindIncludeNestedRelativeToIncludingFile(t *testing.T) {
dir := t.TempDir()
writeConfigFile(t, filepath.Join(dir, "config.kdl"), "include \"dms/dms.kdl\"\n")
writeConfigFile(t, filepath.Join(dir, "dms", "dms.kdl"), "include \"binds.kdl\"\ninclude \"./outputs.kdl\"\n")
for _, name := range []string{"binds.kdl", "outputs.kdl"} {
target := filepath.Join(dir, "dms", name)
if !niriFindInclude(filepath.Join(dir, "config.kdl"), target, map[string]bool{}) {
t.Errorf("%s included through dms/dms.kdl not detected", name)
}
}
if niriFindInclude(filepath.Join(dir, "config.kdl"), filepath.Join(dir, "dms", "layout.kdl"), map[string]bool{}) {
t.Error("layout.kdl reported included without an include line")
}
}
func TestMangowcFindIncludeNestedRelativeToIncludingFile(t *testing.T) {
dir := t.TempDir()
writeConfigFile(t, filepath.Join(dir, "config.conf"), "source=./dms/dms.conf\n")
writeConfigFile(t, filepath.Join(dir, "dms", "dms.conf"), "source = binds.conf\n")
if !mangowcFindInclude(filepath.Join(dir, "config.conf"), filepath.Join(dir, "dms", "binds.conf"), map[string]bool{}) {
t.Error("binds.conf sourced through dms/dms.conf not detected")
}
}
+3 -3
View File
@@ -49,12 +49,12 @@ func (w *Walker) Record(sourcePath string) bool {
}
func (w *Walker) Include(baseDir, sourcePath string, visit func(absPath string) error) bool {
matched := w.Record(sourcePath)
resolved, err := Resolve(baseDir, sourcePath)
if err != nil {
return matched
return w.Record(sourcePath)
}
matched := w.Record(resolved)
if visit == nil {
return matched
}
+19 -2
View File
@@ -3,9 +3,14 @@ package configfrag
import (
"os"
"path/filepath"
"strings"
"testing"
)
func isDMSBindsConf(path string) bool {
return strings.HasSuffix(path, "/dms/binds.conf")
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
@@ -76,7 +81,7 @@ func TestATildeSourceExpandsToHomeAndIsNotJoinedToTheBaseDir(t *testing.T) {
func TestTheDMSFragmentPositionIsItsOrdinalAmongIncludes(t *testing.T) {
dir := t.TempDir()
walker, _, record := recordingWalker(t, func(path string) bool { return path == "dms/binds.conf" })
walker, _, record := recordingWalker(t, isDMSBindsConf)
walker.Include(dir, "first.conf", record)
walker.Include(dir, "dms/binds.conf", record)
@@ -154,7 +159,7 @@ func TestAnAssignmentWithoutAnEqualsIsNotAnInclude(t *testing.T) {
func TestAnAssignmentTrimsSpaceAroundThePath(t *testing.T) {
dir := t.TempDir()
walker, visited, record := recordingWalker(t, func(path string) bool { return path == "dms/binds.conf" })
walker, visited, record := recordingWalker(t, isDMSBindsConf)
if !walker.IncludeAssignment(dir, "source = dms/binds.conf ", record) {
t.Error("the spaced mango form must still match the dms fragment")
@@ -192,3 +197,15 @@ func TestRecordCountsAnIncludeTheCallerResolvesItself(t *testing.T) {
t.Errorf("scan = %+v, want count 2, position 2, seen", scan)
}
}
func TestTheMatcherSeesTheSourceResolvedAgainstTheIncludingFile(t *testing.T) {
dir := t.TempDir()
walker, _, record := recordingWalker(t, isDMSBindsConf)
if !walker.Include(filepath.Join(dir, "dms"), "binds.conf", record) {
t.Error("binds.conf sourced from inside dms/ must match the dms fragment")
}
if walker.Include(dir, "binds.conf", record) {
t.Error("binds.conf sourced from the config root must not match")
}
}
@@ -414,21 +414,17 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
}
includePath := strings.Trim(node.Arguments[0].String(), "\"")
isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
fullPath := filepath.Join(baseDir, includePath)
if filepath.IsAbs(includePath) {
fullPath = includePath
}
isDMSInclude := strings.HasSuffix(fullPath, "/dms/binds.kdl")
p.includeCount++
if isDMSInclude {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.bindsBeforeDMS = len(p.bindMap)
}
fullPath := filepath.Join(baseDir, includePath)
if filepath.IsAbs(includePath) {
fullPath = includePath
}
if isDMSInclude {
p.dmsProcessed = true
}
@@ -864,3 +864,33 @@ func TestNiriParseActionWithProperties(t *testing.T) {
}
}
}
func TestNiriParseIncludeNestedDMSBinds(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0o755); err != nil {
t.Fatal(err)
}
files := map[string]string{
filepath.Join(tmpDir, "config.kdl"): "include \"dms/dms.kdl\"\n",
filepath.Join(subDir, "dms.kdl"): "include \"binds.kdl\"\n",
filepath.Join(subDir, "binds.kdl"): "binds {\n Mod+T { spawn \"kitty\"; }\n}\n",
}
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if !result.DMSBindsIncluded {
t.Error("dms/binds.kdl included via dms/dms.kdl not detected")
}
if len(result.Section.Keybinds) != 1 {
t.Errorf("expected 1 keybind from the nested include, got %d", len(result.Section.Keybinds))
}
}
@@ -182,7 +182,11 @@ func (p *NiriRulesParser) handleInclude(node *document.Node, baseDir string) {
}
includePath := strings.Trim(node.Arguments[0].String(), "\"")
isDMSInclude := includePath == "dms/windowrules.kdl" || strings.HasSuffix(includePath, "/dms/windowrules.kdl")
fullPath := filepath.Join(baseDir, includePath)
if filepath.IsAbs(includePath) {
fullPath = includePath
}
isDMSInclude := strings.HasSuffix(fullPath, "/dms/windowrules.kdl")
p.includeCount++
if isDMSInclude {
@@ -191,11 +195,6 @@ func (p *NiriRulesParser) handleInclude(node *document.Node, baseDir string) {
p.dmsProcessed = true
}
fullPath := filepath.Join(baseDir, includePath)
if filepath.IsAbs(includePath) {
fullPath = includePath
}
_ = p.parseFile(fullPath)
}
@@ -372,3 +372,33 @@ window-rule {
t.Error("edited rule should have open-floating false")
}
}
func TestNiriParseIncludeNestedDMSRules(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(subDir, 0o755); err != nil {
t.Fatal(err)
}
files := map[string]string{
filepath.Join(tmpDir, "config.kdl"): "include \"dms/dms.kdl\"\n",
filepath.Join(subDir, "dms.kdl"): "include \"windowrules.kdl\"\n",
filepath.Join(subDir, "windowrules.kdl"): "window-rule {\n match app-id=\"nested\"\n opacity 0.9\n}\n",
}
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
result, err := ParseNiriWindowRules(tmpDir)
if err != nil {
t.Fatalf("ParseNiriWindowRules failed: %v", err)
}
if !result.DMSRulesIncluded {
t.Error("dms/windowrules.kdl included via dms/dms.kdl not detected")
}
if len(result.Rules) != 1 {
t.Errorf("expected 1 rule from the nested include, got %d", len(result.Rules))
}
}