From e6e2e4f909e7a08ecce7cdaf6e488fa96239f930 Mon Sep 17 00:00:00 2001 From: Isaque Pinheiro Date: Thu, 25 Jun 2026 09:01:46 -0300 Subject: [PATCH] fix(boss): preserve conditional compilation directives in DPK requires clause (issue #190) --- .../secondary/repository/repository_test.go | 8 +++ utils/dcp/dcp.go | 65 +++++++++++++++---- utils/dcp/dcp_test.go | 43 ++++++++++++ 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/internal/adapters/secondary/repository/repository_test.go b/internal/adapters/secondary/repository/repository_test.go index 813a305b..6098cec2 100644 --- a/internal/adapters/secondary/repository/repository_test.go +++ b/internal/adapters/secondary/repository/repository_test.go @@ -6,6 +6,7 @@ import ( "errors" "io" "os" + "path/filepath" "testing" "time" @@ -27,6 +28,7 @@ func NewMockFileSystem() *MockFileSystem { } func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { + name = filepath.ToSlash(name) if data, ok := m.files[name]; ok { return data, nil } @@ -34,6 +36,7 @@ func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { } func (m *MockFileSystem) WriteFile(name string, data []byte, _ os.FileMode) error { + name = filepath.ToSlash(name) m.files[name] = data return nil } @@ -43,6 +46,7 @@ func (m *MockFileSystem) MkdirAll(_ string, _ os.FileMode) error { } func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { + name = filepath.ToSlash(name) if _, ok := m.files[name]; ok { //nolint:nilnil // Mock for testing return nil, nil @@ -51,6 +55,7 @@ func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { } func (m *MockFileSystem) Remove(name string) error { + name = filepath.ToSlash(name) delete(m.files, name) return nil } @@ -60,6 +65,8 @@ func (m *MockFileSystem) RemoveAll(_ string) error { } func (m *MockFileSystem) Rename(oldpath, newpath string) error { + oldpath = filepath.ToSlash(oldpath) + newpath = filepath.ToSlash(newpath) if data, ok := m.files[oldpath]; ok { m.files[newpath] = data delete(m.files, oldpath) @@ -82,6 +89,7 @@ func (m *MockFileSystem) Create(_ string) (io.WriteCloser, error) { } func (m *MockFileSystem) Exists(name string) bool { + name = filepath.ToSlash(name) _, ok := m.files[name] return ok } diff --git a/utils/dcp/dcp.go b/utils/dcp/dcp.go index 1edcfab1..355ba0ac 100644 --- a/utils/dcp/dcp.go +++ b/utils/dcp/dcp.go @@ -13,14 +13,13 @@ 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,{}\\.]+)(;)`) + reRequires = regexp.MustCompile(`(?m)^(requires)([^;]+)(;)`) reWhitespace = regexp.MustCompile(`[\r\n ]+`) ) @@ -106,7 +105,7 @@ func getDcpString(dcps []string) string { return dcpRequiresLine[:len(dcpRequiresLine)-2] } -// injectDcps injects DCP dependencies into the file content. +// injectDcps injects DCP dependencies into the file content while preserving original formatting, comments, and conditionals. func injectDcps(filecontent string, dcps []string) (string, bool) { resultRegex := reRequires.FindAllStringSubmatch(filecontent, -1) if len(resultRegex) == 0 { @@ -115,20 +114,64 @@ 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]] - currentRequires := strings.Split(currentRequiresString, ",") + // 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) - var result = filecontent[:resultRegexIndexes[0][3]] + 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 + } + + // 1. Remove old boss deps that are no longer in the target dcps list + modifiedBody := body + for oldDcp := range existingBossDeps { + if !targetDcpsMap[oldDcp] { + // Remove this dependency, its {BOSS} comment, and any trailing comma/whitespace + escapedDcp := regexp.QuoteMeta(oldDcp) + reRemove := regexp.MustCompile(`(?i)\s*\b` + escapedDcp + `\{BOSS\}\s*,?`) + modifiedBody = reRemove.ReplaceAllString(modifiedBody, "") + } + } + + // 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) + + // Check if it's already in the requires section (case-insensitive) + // We match it as a whole word: \b\b + escapedDcp := regexp.QuoteMeta(dcpNameLower) + reCheck := regexp.MustCompile(`(?i)\b` + escapedDcp + `\b`) + if reCheck.MatchString(modifiedBody) { + continue // Already exists (either user-defined or already injected), skip + } - for _, value := range currentRequires { - if strings.Contains(value, CommentBoss) || utils.Contains(dcps, value) { - continue + // Append the new dependency + 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 + } } - result += "\n " + value + "," } - result = result + getDcpString(dcps) + ";" + filecontent[resultRegexIndexes[0][7]:] + // Reconstruct the file content by replacing the body + result := filecontent[:resultRegexIndexes[0][4]] + modifiedBody + filecontent[resultRegexIndexes[0][5]:] return result, true } 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") + } +} +