Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 50 additions & 19 deletions utils/dcp/dcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions utils/dcp/dcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Loading