From 965e3b180a4604633a3c1411b33ba32d1dd5995f Mon Sep 17 00:00:00 2001 From: Rodrigo Bernardi Date: Thu, 16 Jul 2026 10:42:12 -0300 Subject: [PATCH] fix(boss): preserve conditional compilation directives in DPK requires clause (issue #190) --- utils/dcp/dcp.go | 69 +++++++++++++++++++++++++++++++------------ utils/dcp/dcp_test.go | 43 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/utils/dcp/dcp.go b/utils/dcp/dcp.go index 8d2a6fc3..13b7c154 100644 --- a/utils/dcp/dcp.go +++ b/utils/dcp/dcp.go @@ -13,16 +13,12 @@ import ( "github.com/hashload/boss/internal/core/domain" "github.com/hashload/boss/pkg/consts" "github.com/hashload/boss/pkg/msg" - "github.com/hashload/boss/utils" "github.com/hashload/boss/utils/librarypath" "golang.org/x/text/encoding/charmap" "golang.org/x/text/transform" ) -var ( - reRequires = regexp.MustCompile(`(?m)^(requires)([\n\r \w,{}\\.]+)(;)`) - reWhitespace = regexp.MustCompile(`[\r\n ]+`) -) +var reRequires = regexp.MustCompile(`(?m)^(requires)([^;]+)(;)`) // InjectDpcs injects DCP dependencies into project files. func InjectDpcs(pkg *domain.Package, lock domain.PackageLock) { @@ -111,7 +107,8 @@ func getDcpString(dcps []string) string { return result[:len(result)-2] } -// injectDcps injects DCP dependencies into the file content. +// injectDcps injects DCP dependencies into file content, preserving +// original formatting, comments, and compiler conditionals. func injectDcps(filecontent string, dcps []string) (string, bool) { resultRegex := reRequires.FindAllStringSubmatch(filecontent, -1) if len(resultRegex) == 0 { @@ -120,26 +117,60 @@ func injectDcps(filecontent string, dcps []string) (string, bool) { resultRegexIndexes := reRequires.FindAllStringSubmatchIndex(filecontent, -1) - currentRequiresString := reWhitespace.ReplaceAllString(resultRegex[0][2], "") + // Group 2 (indices 4 and 5) is the body of the requires section + body := filecontent[resultRegexIndexes[0][4]:resultRegexIndexes[0][5]] + + // Find all existing boss-injected dependencies in the body. + // They are marked with {BOSS}. We match them as \b([\w\.\-]+)\{BOSS\} + reBossDep := regexp.MustCompile(`(?i)\b([\w\.\-]+)\{BOSS\}`) + bossDepsMatches := reBossDep.FindAllStringSubmatch(body, -1) + + existingBossDeps := make(map[string]bool) + for _, match := range bossDepsMatches { + existingBossDeps[strings.ToLower(match[1])] = true + } + + // Prepare a map of the target dcps (lowercase for case-insensitive comparison) + targetDcpsMap := make(map[string]bool) + for _, dcp := range dcps { + targetDcpsMap[strings.ToLower(filepath.Base(dcp))] = true + } - currentRequires := strings.Split(currentRequiresString, ",") + // 1. Remove old boss deps that are no longer in the target dcps list + modifiedBody := body + for oldDcp := range existingBossDeps { + if !targetDcpsMap[oldDcp] { + escapedDcp := regexp.QuoteMeta(oldDcp) + reRemove := regexp.MustCompile(`(?i)\s*\b` + escapedDcp + `\{BOSS\}\s*,?`) + modifiedBody = reRemove.ReplaceAllString(modifiedBody, "") + } + } - var resultBuilder strings.Builder - resultBuilder.WriteString(filecontent[:resultRegexIndexes[0][3]]) + // 2. Add new dcps that are not already present in the requires section + for _, dcp := range dcps { + dcpName := filepath.Base(dcp) + dcpNameLower := strings.ToLower(dcpName) - for _, value := range currentRequires { - if strings.Contains(value, CommentBoss) || utils.Contains(dcps, value) { + escapedDcp := regexp.QuoteMeta(dcpNameLower) + reCheck := regexp.MustCompile(`(?i)\b` + escapedDcp + `\b`) + if reCheck.MatchString(modifiedBody) { continue } - resultBuilder.WriteString("\n ") - resultBuilder.WriteString(value) - resultBuilder.WriteString(",") + + trimmed := strings.TrimSpace(modifiedBody) + if trimmed != "" && !strings.HasSuffix(trimmed, ",") { + modifiedBody = trimmed + ",\n " + dcpName + CommentBoss + } else { + if trimmed == "" { + modifiedBody = "\n " + dcpName + CommentBoss + } else { + modifiedBody = modifiedBody + "\n " + dcpName + CommentBoss + } + } } - resultBuilder.WriteString(getDcpString(dcps)) - resultBuilder.WriteString(";") - resultBuilder.WriteString(filecontent[resultRegexIndexes[0][7]:]) - return resultBuilder.String(), true + result := filecontent[:resultRegexIndexes[0][4]] + modifiedBody + filecontent[resultRegexIndexes[0][5]:] + return result, true } // processFile processes the file content to inject DCP dependencies. diff --git a/utils/dcp/dcp_test.go b/utils/dcp/dcp_test.go index 283b3d7b..d34afaa6 100644 --- a/utils/dcp/dcp_test.go +++ b/utils/dcp/dcp_test.go @@ -185,3 +185,46 @@ func TestGetRequiresList_NoDependencies(t *testing.T) { t.Errorf("getRequiresList() should return empty list for no deps, got %v", result) } } + +// TestInjectDcps_WithConditionals tests that injection preserves conditional directives and comments. +func TestInjectDcps_WithConditionals(t *testing.T) { + content := `package MyPackage; +requires + rtl, + vcl, + {$IFDEF MSWINDOWS} + designide, + {$ENDIF} + AnotherPkg; +contains + Unit1 in 'Unit1.pas'; +end.` + + dcps := []string{"newpkg"} + + result, changed := injectDcps(content, dcps) + + if !changed { + t.Error("injectDcps() should return true when requires section is modified") + } + + // The new pkg should be added + if !strings.Contains(result, "newpkg{BOSS}") { + t.Error("injectDcps() should add new dcp to result with Boss marker") + } + + // All original lines, including conditionals and comments, must be fully preserved + if !strings.Contains(result, "{$IFDEF MSWINDOWS}") { + t.Error("injectDcps() should preserve the opening conditional directive") + } + if !strings.Contains(result, "designide,") { + t.Error("injectDcps() should preserve the designide dependency") + } + if !strings.Contains(result, "{$ENDIF}") { + t.Error("injectDcps() should preserve the closing conditional directive") + } + if !strings.Contains(result, "AnotherPkg") { + t.Error("injectDcps() should preserve AnotherPkg dependency") + } +} +